<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>
<title>JRNitre&#039;s Blog - 电气工程</title>
<link>https://blog.atoery.cn/index.php/category/electrial/</link>
<atom:link href="https://blog.atoery.cn/index.php/feed/category/electrial/" rel="self" type="application/rss+xml" />
<language>zh-CN</language>
<description></description>
<lastBuildDate>Fri, 10 Oct 2025 12:35:00 +0800</lastBuildDate>
<pubDate>Fri, 10 Oct 2025 12:35:00 +0800</pubDate>
<item>
<title>无线通信基本单位 - dB</title>
<link>https://blog.atoery.cn/index.php/2025/10/10/164.html</link>
<guid>https://blog.atoery.cn/index.php/2025/10/10/164.html</guid>
<pubDate>Fri, 10 Oct 2025 12:35:00 +0800</pubDate>
<dc:creator>JRNitre</dc:creator>
<description><![CDATA[分贝（decibel）是表示两个相同单位的物理量数量比例的单位，主要用于度量声音强度或电信号的功率级别简单来说有以下 dB 与倍率的关系：3dB 功率增加 2 倍，+ 10dB 功率增加 10 ...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<p>分贝（decibel）是表示两个相同单位的物理量数量比例的单位，主要用于度量声音强度或电信号的功率级别</p><p>简单来说有以下 dB 与倍率的关系：</p><ul><li><ul><li>3dB 功率增加 2 倍，+ 10dB 功率增加 10 倍。</li></ul></li><li><ul><li>3dB 功率减小为原来的 1/2，- 10dB 功率减小为原来的 1/10</li></ul></li></ul><p>$L_{dB} = 10log_{10}(\frac{P_{1}}{P_{0}} )$</p><p><img src="https://blog.atoery.cn/usr/uploads/2025/10/135290524.jpg" alt="" title=""></p>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.atoery.cn/index.php/2025/10/10/164.html#comments</comments>
<wfw:commentRss>https://blog.atoery.cn/index.php/feed/category/electrial/</wfw:commentRss>
</item>
<item>
<title>[STM32] EXTI 外部中断的介绍与使用</title>
<link>https://blog.atoery.cn/index.php/2025/07/28/161.html</link>
<guid>https://blog.atoery.cn/index.php/2025/07/28/161.html</guid>
<pubDate>Mon, 28 Jul 2025 11:58:42 +0800</pubDate>
<dc:creator>JRNitre</dc:creator>
<description><![CDATA[1.0 简介EXTI（Extern Interrupt）外部中断 EXTI 可以监测指定 GPIO 口的电平信号，当其指定的 GPIO 口产生电平变化的时候，EXTI 立即向 NVIC 发出中断...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<h1>1.0 简介</h1><p>EXTI（Extern Interrupt）外部中断 </p><p><strong>EXTI 可以监测指定 GPIO 口的电平信号</strong>，当其指定的 GPIO 口产生电平变化的时候，EXTI 立即向 NVIC 发出<strong>中断申请</strong>，经过 NVIC 裁决后即可中断 CPU 主程序，使得 CPU 执行 EXTI 对应的中断程序。</p><blockquote>其中 EXTI 支持四种不同的触发方式</blockquote><ul><li><code>上升沿</code> 数字电平从低电平 0 变化到高电平 1 的一瞬间叫做<strong>上升沿</strong>。</li><li><code>下降沿</code> 数字电平从高电平 1 变化到低电平 0 的一瞬间叫做<strong>下降沿</strong>。</li><li><code>双边沿</code> 上升沿或者下降沿。</li><li><code>软件触发</code> 不需要硬件触发的外部中断，而由软件触发的中断。</li></ul><h2>1.1 EXTI 特性</h2><ul><li>EXTI 支持所有 GPIO 口，但是相同 Pin 的 IO 口不可以同时触发中断（比如<strong>PA0</strong>，<strong>PB0</strong>不能同时触发中断）。</li><li>EXTI 支持的通道数：<strong>16个GPIO_Pin</strong>、<strong>PVD输出</strong>、<strong>RTC时钟</strong>、<strong>USB唤醒</strong>、<strong>以太网唤醒</strong>（合计<strong>20</strong>个中断线路）。</li><li>触发相应方式：<strong>中断响应</strong>、<strong>事件相应</strong>（事件响应不会触发中断，而是触发别的外设操作）。</li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/07/2640590146.png" alt="" title=""></p><h1>2.0 EXIT 外部中断</h1><h2>2.1 开启RCC时钟</h2><p>由于<strong>NVIC</strong>和<strong>EXTI</strong>是默认开启时钟的外设，因此不需要开启这两个外设的时钟；需要开启的有<strong>GPIO时钟</strong>以及<strong>AFIO时钟</strong>。</p><pre><code>RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOx, ENABLE);   // GPIO
RCC_APB2PeriphClockCmd (RCC_APB2Periph_AFIO, ENABLE);    // AFIO</code></pre><h2>2.2 配置GPIO口工作模式</h2><p>由于使用EXTI外部中断，因此需要使目标 GPIO 口工作在上拉输入 (GPIO_Mode_IPU) 的模式下。</p><pre><code>GPIO_InitTypeDef GOIO_InitStrucuture;

GOIO_InitStrucuture.GPIO_Mode = GPIO_Mode_IPU;      // GPIO 上拉输入
GOIO_InitStrucuture.GPIO_Pin = GPIO_Pin_x;          // 配置需要使用的 Pin 口
GOIO_InitStrucuture.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_Init(GPIOB,&amp;GOIO_InitStrucuture);</code></pre><h2>2.3 配置 AFIO</h2><p>通过 AFIO 将需要使用的 GPIO 口连接到 EXTI。</p><pre><code class="lang-c">GPIO_EXTILineConfig (GPIO_PortSourceGPIOx, GPIO_PinSourcex);
// 将 GPIOx 连接至 AFIO 的 x 中断线路上</code></pre><blockquote>通过这个函数可以将指定的 GPIO 端口与指定的 EXTI 线路相连</blockquote><h2>2.4 配置EXIT</h2><p>选择中断触发方式。</p><pre><code>EXTI_InitTypeDef EXTI_InitStructure;                        // EXTI初始化结构体

EXTI_InitStructure.EXTI_Line = EXTI_Linex;                  // 选择需要配置的中断线，这个与配置AFIO时所配置的中断线相同
EXTI_InitStructure.EXTI_LineCmd = ENABLE;                   // 选择指定中断线的状态
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;         // 指定模式(EXTI_Mode_Interrupt : 中断模式)
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;     // 指定触发信号的有效边沿

EXTI_Init(&amp;EXTI_InitStructure);</code></pre><blockquote>EXTI 的几种触发模式 <code>.EXTI_Trigger</code></blockquote><ul><li><code>EXTI_Trigger_Rising</code> —— 上升沿触发</li><li><code>EXTI_Trigger_Falling</code> —— 下降沿触发</li><li><code>EXTI_Trigger_Rsing_Falling</code> —— 双边沿触发</li></ul><h2>2.5 配置 NVIC</h2><p>给中断分配一个合适的<strong>优先级</strong>。</p><pre><code>NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 
// 配置NCIV优先级分组（抢占优先级、响应优先级）
// 该函数在每一个芯片中只能使用一次

NVIC_TypeDef NVIC_InitStructure;                            // NCIV初始化结构体

NVIC_InitStructure.NCIV_IRQChannel = EXTI15_10_IRQn;        // 指定中断通道开启或者关闭
NVIC_InitStructure.NCIV_IRQChannelCmd = ENABLE;             // 指定通道使能还是失能（ENABLE,DISABLE）
NVIC_InitStructure.NCIV_IRQChannelPreemptionPriority =  1;  // 指定抢占优先级的优先级
NVIC_InitStructure.NCIV_IRQChannelSubPriority = 1;          // 指定响应优先级的优先级

NVIC_Init(&amp;NVIC_InitStructure);</code></pre><blockquote>NVIC优先级的几种分组</blockquote><table><thead><tr><th align="center">NVIC_PriorityGroup</th><th align="center">抢占优先级</th><th align="center">相应优先级</th></tr></thead><tbody><tr><td align="center">NVIC_PriorityGroup_0</td><td align="center">0</td><td align="center">0-15</td></tr><tr><td align="center">NVIC_PriorityGroup_1</td><td align="center">0-1</td><td align="center">0-7</td></tr><tr><td align="center">NVIC_PriorityGroup_2</td><td align="center">0-3</td><td align="center">0-3</td></tr><tr><td align="center">NVIC_PriorityGroup_3</td><td align="center">0-7</td><td align="center">0-1</td></tr><tr><td align="center">NVIC_PriorityGroup_4</td><td align="center">0-15</td><td align="center">0</td></tr></tbody></table><h2>2.5 EXTI中断函数</h2><p>在 EXTI 配置完毕后，当达到中断触发条件后就会自动执行中断函数中的代码。</p><pre><code>//中断函数
void EXTI15_10_IRQHandler(void){
    if (EXTI_GetITStatus(EXTI_Linex) == SET ) {      // 判断中断标志位，判断是否为想要触发中断的通道

        /* 
         * 需要执行的函数 
        */

        EXTI_ClearITPendingBit(EXTI_Linex);         // 清除中断标志位，防止程序重复申请中断造成死循环
    }
}</code></pre>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.atoery.cn/index.php/2025/07/28/161.html#comments</comments>
<wfw:commentRss>https://blog.atoery.cn/index.php/feed/category/electrial/</wfw:commentRss>
</item>
<item>
<title>[屏幕驱动] SPI 驱动 ST7735s 应用 &amp;amp; 寄存器初始化详解</title>
<link>https://blog.atoery.cn/index.php/2025/05/04/102.html</link>
<guid>https://blog.atoery.cn/index.php/2025/05/04/102.html</guid>
<pubDate>Sun, 04 May 2025 22:29:00 +0800</pubDate>
<dc:creator>JRNitre</dc:creator>
<description><![CDATA[1.0 ST7735s 芯片简介st7735s 132RGB x 162 点 262k 色，带帧存储器的单芯片 TFT 控制器/驱动器ST7735s 使用 SPI 通讯接口可以将显示数据存储到芯...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<h1>1.0 ST7735s 芯片简介</h1><blockquote>st7735s 132RGB x 162 点 262k 色，带帧存储器的单芯片 TFT 控制器/驱动器</blockquote><p>ST7735s 使用 SPI 通讯接口可以将显示数据存储到芯片内部的显示 RAM 中，其容量位 132 <em> 162 </em> 18 位。</p><p><strong>[分辨率]</strong></p><p>其支持的分辨率为 132Bits <em> 162Bits </em> RGB</p><p><strong>[显示模式]</strong></p><ul><li>全彩色 (Full Color)：262K，RGB=(666)</li><li>真彩色 (Color Reduce)：8色，RGB=(111)</li></ul><p><strong>[显示特性]</strong></p><ul><li>软件可编程色彩深度模式</li><li>局部窗口移动和数据滚动功能</li></ul><h1>2.0 电气连接&STM32引脚配置</h1><p>我这里使用的是一块集成了编码器的 1.8inc 屏幕：</p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/2100572721.jpg" alt="" title=""></p><table><thead><tr><th>引脚</th><th>功能</th></tr></thead><tbody><tr><td>BLK</td><td>背光信号，文本例程不包含背光控制</td></tr><tr><td>CS</td><td>SPI 片选</td></tr><tr><td>DC</td><td>数据模式选择信号，1 -&gt; 写命令/ 0 -&gt; 写数据</td></tr><tr><td>RST</td><td>复位信号</td></tr><tr><td>SDA</td><td>SPI 数据线，ST7735s 作从机，接主机的 MOSI</td></tr><tr><td>SCL</td><td>SPI 时钟线</td></tr><tr><td>VDD</td><td>供电引脚，兼容 5V&3.3V</td></tr><tr><td>GND</td><td>接地</td></tr></tbody></table><p>本文基于 STM32401CCU6 HAL 库，采用硬件 SPI + DMA 的方式发送数据。</p><ul><li>APB 总线时钟频率配置为 84MHz。</li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/1636212814.png" alt="" title=""></p><ul><li>使用 SPI1_CH1，PA5-&gt;SCK/PA6-&gt;SPI1_MOSI/PA7-&gt;SPI1_MISO，这里我没有实现硬件片选，IO 配置为推挽输出手动操纵片选信号。</li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/4127129507.png" alt="" title=""></p><ul><li>配置三个推挽输出引脚用于 ST7735s 的 DC/CS/RST 引脚，PC13 用于操控指示灯。</li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/624826587.png" alt="" title=""></p><ul><li>开启 DMA 连接到 SPI1_TX，这里 ST7735s 作为从机不会向主机发送信号，这里的 DMA 仅需要 TX 不需要 RX。</li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/2197267164.png" alt="" title=""></p><ul><li>使用外部时钟，开<strong>启 SWD 调试接口</strong>，开启 USART1 用于串口调试。</li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/3805529452.png" alt="" title=""><br><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/192165497.png" alt="" title=""><br><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/640031666.png" alt="" title=""></p><ul><li>上述配置完毕后 <code>GENERATE CODE</code> 生成代码，关于生成平台和 CubeMX 生成配置此处不赘述。</li></ul><h1>3.0 ST7735s 控制流程</h1><div class="mermaid">flowchart TD
    A[启动] --&gt; B
    B[复位] --&gt; C
    C[初始化、配置参数] --&gt; D
    D[控制现存]</div><h2>3.0.5 SPI 基本操作功能封装</h2><p>根据前表可知，ST7735s 通过 DC 引脚控制数据&命令模式，结合软件控制 CS 引脚将其封装成指令&数据发送函数。</p><ul><li>发送指令</li></ul><pre><code>void st7735s_sendCommand(uint8_t command) {
    st7735s_setNSS(GPIO_PIN_RESET);    // 拉低 CS 引脚，SPI 开始通讯
    HAL_GPIO_WritePin(ST7735S_DC_GRP, ST7735S_DC_PIN, GPIO_PIN_RESET);    // 将 DC 引脚置低代表传输的是指令
    HAL_SPI_Transmit(&amp;hspi1, &amp;command, 1, 0xFFFF);    // 调用硬件 SPI 发送数据
    st7735s_setNSS(GPIO_PIN_SET);    // 释放 CS 引脚，结束 SPI 通讯
}</code></pre><ul><li>发送数据</li></ul><pre><code>void st7735s_sendData(uint8_t data) {
    st7735s_setNSS(GPIO_PIN_RESET);    // 拉低 CS 引脚，SPI 开始通讯
    HAL_GPIO_WritePin(ST7735S_DC_GRP, ST7735S_DC_PIN, GPIO_PIN_SET);    // 将 DC 引脚置高代表传输的是数据
    HAL_SPI_Transmit(&amp;hspi1, &amp;data, 1, 0xFFFF);    // 调用硬件 SPI 发送数据
    st7735s_setNSS(GPIO_PIN_SET);    // 释放 CS 引脚，结束 SPI 通讯
}</code></pre><h2>3.1 复位信号</h2><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/3751028109.png" alt="" title=""></p><p>上表可知 Reset Pulse Duration(复位脉冲持续时间) 最小为 <strong>10us</strong>，Rest Cancel(复位取消) 最大时间为 <strong>5ms</strong>，并且复位后最好等待 <strong>120ms</strong></p><p>因此这里拉低 RST 引脚 <strong>1ms</strong> 后等待 <strong>120ms</strong> 再进行后续操作即可</p><pre><code>/* *.h */
#define ST7735S_RES_H       HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET)
#define ST7735S_RES_L       HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET)

/* *.c */
void st7735s_sendResetSignal(void) {
    ST7735S_RES_H;
    HAL_Delay(1);
    ST7735S_RES_L;
    HAL_Delay(120);
}</code></pre><h2>3.2 参数配置</h2><h3>3.2.1 <code>0x11</code> 退出睡眠模式</h3><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/3298228815.png" alt="" title=""></p><p>首先执行指令 <code>0x11</code> 退出睡眠模式，方便后续配置，值得注意的是当退出睡眠模式后，此指令不会再发生作用，如果想要使用则需要再次进入睡眠模式 <code>0x10</code> 实现。</p><p>当发送了指令退出睡眠模式后，需要等待 <code>120ms</code> 待电源电压和时钟电路稳定后再发送下一个命令&数据。</p><pre><code>st7735s_sendCommand(0x11);
HAL_Delay(120);</code></pre><h3>3.2.2 帧率控制</h3><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/2096963059.png" alt="" title=""></p><p>这里的 <code>0xB1</code> 到 <code>0xB3</code> 寄存器用于配置不同屏幕模式下的帧率，这里的三个寄存器配置方式一致，因此这里只给出 <code>0xB1</code> 的图片和配置参考，后两个寄存器参数与第一个一致：</p><ul><li><code>0xB1</code>：正常模式/全彩色彩</li><li><code>0xB2</code>：空闲模式/8 色色彩</li><li><code>0xB3</code>：部分模式/全彩色彩</li></ul><p>这个配置项由三个 parameter 组成，每个 parameter 配置一个参数，不过这里不太一样，下面会解释：</p><p><strong>RTNA</strong> 这个参数占该寄存器的低四位，在配置这个参数前我们先看手册中给出的信息可知，这三个参数作为一个公式的变量存在，最后计算得到当前模式下的刷新率：</p><p>$$
\frac{fosc}{(RTNA \times 2 + 40) \times (LINE + FPA + BPA + 2)}= FrameRate
$$</p><ul><li><code>FrameRate</code> 最终得到的刷新率</li><li><code>fosc</code> 这是一个固定值 <code>850kHz</code></li><li>FPA 和 BPA 的值大于 0</li></ul><p>RTNA、FPA、BPA 这三个值由我们配置寄存器控制，<code>LINE</code> 是每行像素数量，我的屏幕是 <code>160</code> (横向分辨率)</p><p>得到了上面的条件后在我们配置寄存器前先计算一下参数：</p><p>我们想将屏幕配置为 <code>60Hz</code> 根据公式可得：</p><p>$$
\frac{850000}{(RTNA \times 2 + 40) \times (160 + FPA + BPA + 2)}= 60
$$</p><p>整理可得</p><p>$$
14166.66 = (2 \times RTNA + 40) \times (162 \times FPA \times BPA)
$$</p><p>下面我们只需要找到三个参数满足上述上述式子并且 FPA 和 BPA 大于 0 即可，我们先假设 <code>RTNA = 5</code></p><p>$$
14166.66 = 50(162 \times FPA \times BPA)
$$</p><p>$$
FPA + BPA = 121.33
$$</p><p>现在的式子显而易见 FPA 取 <code>60</code> BPA 取 <code>61</code> 即可。</p><p>此时再回到寄存器表中 RTNA 取 <code>5</code> -&gt; <code>0101</code> -&gt; <code>0x05</code>，FPA 取 <code>60</code> -&gt; <code>111100</code> -&gt; <code>0x3C</code>，BPA 取 <code>61</code> -&gt; <code>111101</code> -&gt; <code>0x3D</code></p><p><code>0xB2</code> 和 <code>0xB3</code> 使用同样的参数进行配置。</p><pre><code>/* Frame Rate Control - 帧率控制 */
st7735s_sendCommand(0xB1);  // Frame Rate Control - In normal Mode/Full colors
st7735s_sendData(0x05);
st7735s_sendData(0x3C);
st7735s_sendData(0x3C);

st7735s_sendCommand(0xB2);  // Frame Rate Control - In Idle mode/8 - colors
st7735s_sendData(0x05);
st7735s_sendData(0x3C);
st7735s_sendData(0x3C);

st7735s_sendCommand(0xB3);  // Frame Rate Control - In Partial mdoe/full colors
st7735s_sendData(0x05);
st7735s_sendData(0x3C);
st7735s_sendData(0x3C);</code></pre><hr><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/1294078241.png" alt="" title=""></p><h3>3.2.3 <code>0xB4</code> 显示反转控制</h3><p>这个寄存器用于控制在各个模式下的像素反转方式，可以减少闪烁和提高显示质量；其有三个参数每个占一位：</p><ul><li><code>NLA</code>：正常模式/全彩色彩</li><li><code>NLB</code>：空闲模式/8 色色彩</li><li><code>NLC</code>：部分模式/全彩色彩</li></ul><p>每一位置 0 代表点反转，1 代表列反转。其中点反转是用于大部分情况，这里全部置 <code>0</code> -&gt; <code>0x00</code></p><pre><code>/* Display Inversion Control - 显示反转控制 */
st7735s_sendCommand(0xB4);
st7735s_sendData(0x00);</code></pre><h3>3.2.4 电源控制</h3><p><strong>[0xC0 Power Control 1 - 电源控制 1]</strong></p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/1183201702.png" alt="手册第 163 页" title="手册第 163 页"></p><p>0xC0 的寄存器手册如图，可见需要配置的参数比较多，该寄存器主要用于配置显示器的工作电压和模式，并且显而易见 <strong>C0H</strong> 列有五行，有三行参数可以配置，下面按照顺序介绍每行参数和每位的作用。</p><ul><li><strong>[1st Parameter]</strong></li></ul><p><strong>AVDD</strong> 首先是该寄存器的高三位配置参数：<code>AVDD</code> 这是一个三位字段，用于配置模拟电源电压，用为液晶显示器提供驱动电压，根据表可得其有 8 种组合，对应的电压值为 <code>4.5v</code> 到 <code>5.2v</code>(步长 <code>0.1v</code>)</p><p>至于如何配置该位需要取决于你的硬件设计和显示需求，如果需要更高的亮度和对比度可以选较高的值：<code>111</code> -&gt; <code>5.2v</code>。相反如果需要较低的功耗等则可以选择较低的电压。</p><p>不过此处我们采取一个中间值：<code>100</code> -&gt; <code>4.9v</code> 作为我们的配置。</p><p><strong>VRHP</strong> 占用了剩下的五位，其作用是正参考电压高电平，它影响到了显示器内部的电压调节器的工作范围，正确的设置该参数有助于确保色彩和对比度的正确。</p><p>首先根据手册可得，该位的配置分为了两个部分，首先 VRHP 的最高位用于配置 GVDD 的两种可选范围，我们也能看到当 VRHP 最高位为 1 时 GVDD 仅有六位有效位，其在范围上远不如为 0 时的范围（32 位）这种分段设计允许用户在较宽的范围内灵活调整 GVDD，同时保留了一个小范围的高电压的选项。</p><p>这里我们选用 GVDD 范围较宽的配置，VRHP 最高位置 <code>0</code></p><p>如上，VRHP 最高位为 0 后，低四位就有了 32 种组合，对应 GVDD 的范围为：<code>4.7v</code> 到 <code>3.15v</code>，步长为 <code>0.05v</code>，这里的配置与上一步配置 AVDD 时相同，如果需要更高的对比度或亮度等则可以配置更高的 GVDD 值反之亦然。</p><p>因此与 AVDD 配置时相同，我们选用一个中间值：<code>01000</code> -&gt; <code>4.3v</code> 作为我们的配置。</p><p>结合上述文字，VRHP 部分的配置就为：<code>001000</code>。</p><p><strong>到此</strong> 针对 1 parameter 的配置解析就完成了，不过在我们将数据写入到寄存器之前还有一个细节需要注意，前文我们提到 AVDD 需要三位用于配置，而在介绍 VRHP 的时候提到了通过 <code>VRHP[5]</code> 配置电压范围，这里的 <strong>5</strong> 很关键，这代表了这一个参数配置需要使用 <strong>6</strong> 位的空间？这明显不对，与 AVDD 的配置数据加起来明显超了！再次查表发现 <code>VRHP[5]</code> 被放在了 3rd parameter 的最低位中，同样后文要讲的 <code>VRHN[5]</code> 也在这配置，所以需要要将刚刚配置好的 VRHP <code>001000</code> 中的最高位摘取出 1 位，即 <code>01000</code> 与 AVDD 的配置位合并写入到第一个数据包中：<code>10001000</code> -&gt; <code>0x88</code></p><ul><li><strong>[2nd Parameter]</strong></li></ul><p><strong>VRHN</strong> 第二个数据包就比较简单了，仅有低 5 位用于配置 VRHN 与刚才讲到的 VRHP 相反，用于配置负参考电压高电平，该参数的影响前文也提到了，若想拥有用户需要的显示效果则需要正确配置这两个参数。</p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/2643264143.png" alt="" title=""></p><p>VRHN 的配置方式与 VRHP 的配置方式完全相同，这里就不再赘述了，不过值得注意的是它的 <code>VRHN[5]</code> 同样在第三个数据包中 (<em>在第三个数据包中还会提的</em>)</p><p>同样的我们采用中间值作为我们的配置：<code>01000</code> -&gt; <code>4.3v</code> 作为我们的配置。</p><p><strong>到此</strong> 第二个数据包就配置完成了，根据手册高三位写 <code>0</code> 低五位写 <code>01000</code> 即为 <code>00001000</code> -&gt; <code>0x08</code></p><ul><li><strong>[3rd Parameter]</strong></li></ul><p><strong>MODE</strong> 这是一个两位字段，占用寄存器高两位进行配置，用于控制电源模式，其有四种合。</p><p>其中表中标注了 <code>01</code>和<code>11</code> 该两位作为保留位并不建议使用<em>Don't use this setting!</em>。</p><p>那么此时仅剩两个配置项：<code>00</code> -&gt; 2X 模式和 <code>10</code> -&gt; AUTO 模式</p><ul><li><code>00</code> 可以将倍压电路设置工作在 2 倍增压模式。</li><li><code>10</code> 可以自动调整倍压电路的工作状态，根据负载动态优化功耗和性能。</li></ul><p>可见既然提供了 AUTO 模式，无脑配置为 AUTO 也可以适应绝大多数应用场景。</p><p><strong>到此</strong> 我们配置好了 <code>0xC0</code> 的所有参数，并且将我们上述提到的 VRHP 和 VRHN 的范围控制位和 MDOE 配置位打包写入即可完成配置，根据手册可得 MODE 写入到高两位，另外两个参数 (均为 0) 写入低两位，其余位根据手册要求 <code>[D5 - D2]</code> -&gt; <code>0001</code>。</p><p>最终得到 <code>10000100</code> -&gt; <code>0x84</code>，结合上面的配置，针对 <code>0xC0</code> 的配置数据包即为 <code>0x88</code>、<code>0x08</code>、<code>0x84</code></p><p><strong>[0xC1 Power Control 2 - 电源控制 2]</strong></p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/2621668011.png" alt="手册第 165 页" title="手册第 165 页"></p><p>0xC1 同样是电源配置寄存器，不过它需要发送的数据包还是比较少的 (发送一个就行)，首先介绍一些该寄存器的功能，它主要用于控制 VGH 和 VGL 的供电电平，这些电压直接影响到显示器的驱动能力和功耗。</p><ul><li><code>VGH</code>：正高压，用于驱动液晶单元的开启状态。</li><li><code>VGL</code>：负低压，用于驱动液晶单元的关闭状态。</li></ul><p>下面我们优先介绍位于 <strong>D3</strong> 和 <strong>D2</strong> 位的 VGHBT 这个参数直接影响 VGH 的输出，VGH 的值通常会带来更高的对比度和更快的相应速度，但是可能会增加功耗。</p><p>这是一个两位的参数，共有四种配置选项：</p><ul><li>00 -&gt; $2 \times AVDD + VGH25 - 0.5$</li><li>01 -&gt; $3 \times AVDD - 0.5$</li><li>10 -&gt; $3 \times AVDD + VGH25 - 0.5$</li><li>11 -&gt; 不推荐使用，保留用于测试</li></ul><p>可以看到关于 VGH 的值有三种可用的配置公式，其中涉及到 AVDD 和本寄存器中可以配置 VGH25。</p><p>在我们的配置中，还是选择一个平衡的方案进行配置，这里选用计算较为简单的 $3 \times AVDD - 0.5$ 方案 -&gt; <code>01</code> 根据前文的配置可以知道 AVDD 我们配置为了 <code>4.9v</code> 我们已知了所有的参数，但是现在并不要着急计算该式子的值，我们继续往下看寄存器的配置。</p><p><strong>VGLSEL</strong> 该参数同样有四种配置选项，其不用计算直接控制 VGL 的输出：</p><ul><li>00 -&gt; -7.5</li><li>01 -&gt; -10</li><li>10 -&gt; -12.5</li><li>11 -&gt; -13</li></ul><p>一样的理念，采用 <code>-10v</code> -&gt; <code>01</code> 作为一个平衡的选择。</p><p>现在我们已经有了计划好的 VGH 和 VGL 我们还需要注意一件事情，手册中提到：</p><blockquote>$VGH - VGL \le 32V$ 这个条件限制了 VGH 和 VGL 之间的差值不能超过 32V 这一限制可以保护液晶显示器中的电子元件。</blockquote><p>根据上述的限制我们计算一下刚才选择的配置是否合理。</p><p>$$ 
VGH = 3 \times 4.9 - 0.5 = 14.2V
$$</p><p>$$
VGL = -10V
$$</p><p>将二者相减可得 <code>24.2V</code> 满足限制条件。</p><p>现在我们配置好了两个参数，还有一个参数 VGH25 并未配置，虽然它会在 VGHBT 中参与运算，但是本次我们并未选用，不过该位置也要配置一个参数防止未来切换配置或者硬件中存在以来该值的位置。</p><p>对照手册将其设置位一个平衡值 <code>2.2</code> -&gt; <code>01</code> 即可。</p><p>综上所述根据手册的要求将 VGH25 置于高两位，VGHB 和 VGLSEL 按照顺序置于最低位，其余位置 <code>0</code> 即可 <code>01000101</code> -&gt; <code>0x69</code></p><p><strong>[0xC2 Power Control 3 - 电源控制 3]</strong></p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/430326715.png" alt="手册第 167 页" title="手册第 167 页"></p><p>与 Power Control 1 寄存器类似，它由两个数据包配置，一共控制三个参数。</p><ul><li><strong>[1st Parameter]</strong></li></ul><p><strong>DCA</strong> 首先，我们先看第一个 parameter 的高两位，是 <em>DCA9</em> 和 <em>DCA8</em> 如果此时关注一下第二个 parameter 就能发现还有 <em>DCA7</em> 到 <em>DCA6</em> 很显然这个参数的配置有一点不一样！不过在配置这个参数之前需要了解这一位是做什么的：DCA 用于将升压电路的升压周期设置为正常模式或全彩模式，具体来说它通过调整升压时钟 (BCLK) 的分频系数来控制升压电路的工作频率，关于这个参数配置我们需要重点关注一下手册中提供的表格。</p><p>首先从位于第一个 parameter 高两位的 <code>DCA[9:8]</code> 看起，这一位用于配置整体的分频系数的范围，它决定了升压电路的基本工作频率范围，在该参数选定后后续的参数用于调节每一个部分的分频比例。</p><p><code>DCA[9:8]</code> 有四种选择，对应了四种范围，如何判断范围呢？这张表需要以行为单位看，比如 <code>DCA[9:8]</code> = <code>00</code> 时 <code>DCA[7:0]</code> 的可选项就已经配置好了 <code>BLCK/3</code>、<code>BLCK/1</code>、<code>BLCK/1</code>、<code>BLCK/1</code>，根据表可得：</p><ul><li><code>BLCK/1</code> -&gt; <code>00</code></li><li><code>BLCK/3</code> -&gt; <code>01</code></li><li><code>BLCK/2</code> -&gt; <code>10</code></li><li><code>BLCK/4</code> -&gt; <code>11</code></li></ul><p><code>BLCK/3</code>、<code>BLCK/1</code>、<code>BLCK/1</code>、<code>BLCK/1</code> -&gt; <code>01_00_00_00</code></p><p>在确定好了后续可以选择的范围之后我们着重关心的就是 <code>DCA[9:8]</code> 的选择了，此时众多的分频频率到底是如何影响最终的显示效果的？前面我们在配置 VGH 和 VGL 的时候看到这两个参数配置好后往往需要 4/5V 以上的电压，这就依赖升压电路来提供所需电压：</p><ul><li>较高的分频值就意味着更长的升压周期，通常会降低功耗但是可能会导致升压效率的下降。</li><li>较低的分频值就意味着更快的升压周期，通常能提供更高的升压效率，但是可能会增加功耗。</li></ul><p>了解了上面的信息后，对于参数的配置就豁然开朗了，我们还是选择平衡的配置选项，首先将分频范围限制在 <code>BCLK/1</code> 到 <code>BLCK/3</code> 之间，对应 <code>DCA[9:8]</code> -&gt; <code>00</code></p><p>在这种范围限制下表格给出了如下分配分频配置：</p><ul><li><code>DCA[7:6]</code> -&gt; <code>BCLK/3</code> -&gt; <code>01</code></li><li><code>DCA[5:4]</code> -&gt; <code>BCLK/1</code> -&gt; <code>00</code></li><li><code>DCA[3:2]</code> -&gt; <code>BCLK/1</code> -&gt; <code>00</code></li><li><code>DCA[1:0]</code> -&gt; <code>BCLK/1</code> -&gt; <code>00</code></li></ul><p>综上所述针对 <code>DCA[9:0]</code> 的配置应该为 <code>0001000000</code> 由于我们先构建第一个 parameter 因此只抽取 DCA[9:8] -&gt; <code>00</code></p><p><strong>AP</strong> 这个参数占用了寄存器的低三位，共有 8 种组合，用于配置运算放大器中的电流量。运算放大器在液晶显示器中用于信号放大和电压调节，通过调整运算放大器的电流量可以调整显示效果、功耗和响应速度。</p><ul><li><code>000</code> -&gt; 运算放大器停止工作</li><li><code>001</code> -&gt; 小电流</li><li><code>010</code> -&gt; 中低电流</li><li><code>011</code> -&gt; 中电流</li><li><code>100</code> -&gt; 中高电流</li><li><code>101</code> -&gt; 大电流</li><li><code>110</code> -&gt; 保留/不使用</li><li><code>111</code> -&gt; 保留/不使用</li></ul><p>根据上述的数据显而易见，最后两种组合为保留组合并且如果关闭了运算放大器可能会导致显示器无法正常工作，根据文字叙述我们采用中等电流 <code>011</code> 即可。</p><p><strong>SPA</strong> 手册中同样提供了这个参数的表格，可见他跟 AP 的配置完全一样，它的作用是控制源极驱动电路中的运算放大器电流量，虽然与 AP 名字上有所不同，但是功能上大抵相同这里不再赘述直接使用 AP 的配置 <code>011</code></p><p>至此第一个数据包就构建完毕了由两位 DAC 参数 <code>00</code> 加三位 SPA 和 两位 AP 参数构成 <code>00011011</code> -&gt; <code>0x1B</code></p><ul><li><strong>[2nd Parameter]</strong></li></ul><p>根据一个数据包构建时的分析来看，这第二个数据包就很简单了，将剩余的分频配置写入即可 <code>01000000</code> -&gt; <code>0x40</code></p><p><strong>[0xC3 Power Control 4 - 电源控制 4]</strong></p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/2134994651.png" alt="手册第 169 页" title="手册第 169 页"></p><p>这个部分的配置整体上与 <code>0xC2</code> 的配置相同 <code>0x1B</code> 和 <code>0x40</code></p><p><strong>[0xC4 Power Control 5 - 电源控制 5]</strong></p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/2736546615.png" alt="手册第 171 页" title="手册第 171 页"></p><p>同样的这里的配置比较无趣，与上面相同 <code>0x1B</code> 和 <code>0x40</code></p><p><strong>[0xC5 VCOM Control 1]</strong></p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/1488410731.png" alt="手册第 173 页" title="手册第 173 页"></p><p>VCOM 控制，该寄存器仅配置一个参数，VCOM 电压，VCOM 是显示器中公共极电压，其对显示效果有重要影响具体会影响闪烁、对比度和色彩均匀性。</p><ul><li><strong>闪烁</strong>：过高的 VCOM 电压会导致屏幕闪烁，过低会导致图像暗淡或出现色彩偏差。</li><li><strong>对比度</strong>：适当的 VCOM 电压会增强图像对比度，使颜色更加鲜明。</li></ul><p>手册提供了详细的配置项和电压的对照表，这里选用位于中间的 <code>-1.2v</code> -&gt; <code>011111</code> -&gt; <code>0x1F</code></p><p>由于该配置项占低位因此空余位置全部写 0 该寄存器配置为 <code>0x1F</code> 即可。</p><p><strong>[0xC5 VCOM Offset Control]</strong></p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/195281719.png" alt="手册第 175 页" title="手册第 175 页"></p><p>这是电源配置部分的最后一个寄存器，用于配置 VCOM 偏移量，微调 VCOM 电压，功能上与上一个寄存器相同，或者说这两个寄存器配置的都是同一个参数，这个参数分为两个部分；此外手册中提到如果需要使用这个参数需要将位于 <code>0xD9</code> 的 <code>VMF_EN</code> 位置 <code>1</code></p><ul><li><code>VMF[4]</code>：用于控制偏移方向 <code>0</code> 正向偏移，<code>1</code> 负向偏移。</li><li><code>VMF[3:0]</code>：配置具体的偏移量，从 <code>+16d</code> 到 <code>-15d</code></li></ul><p>由于这里是用于微调 VCOM 参数的，这里根据需要开启和配置参数进行调整，本例中就不开启这个参数了。</p><p><strong>[电源配置 - 代码]</strong></p><p>到这里电源部分就配置完毕了，代码部分如下：</p><pre><code>/* Power Contrl - 电源配置 */
st7735s_sendCommand(0xC0);  // Power Control 1
st7735s_sendData(0x88);
st7735s_sendData(0x08);
st7735s_sendData(0x84);

st7735s_sendCommand(0xC1);  // Power Control 2
st7735s_sendData(0x69);

st7735s_sendCommand(0xC2);  // Power Control 3
st7735s_sendData(0x1B);
st7735s_sendData(0x40);

st7735s_sendCommand(0xC3);  // Power Control 4
st7735s_sendData(0x1B);
st7735s_sendData(0x40);

st7735s_sendCommand(0xC4);  // Power Control 5
st7735s_sendData(0x1B);
st7735s_sendData(0x40);

st7735s_sendCommand(0xC5);  // VCOM Voltage
st7735s_sendData(0x1F);</code></pre><h3>3.2.5 <code>0x36</code> 配置显存数据访问方式</h3><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/1996493775.png" alt="" title=""></p><p><strong>[地址顺序配置 - 高三位]</strong></p><ul><li><p><code>MY</code>：行地址顺序 - <code>如果希望图像上下反转此位置高</code></p><p>用于控制行地址顺序是否反转 <code>MY</code> 置高则行地址顺序将被反转（即从最后一行开始到第一行）</p></li><li><p><code>MX</code>：列地址顺序 - <code>如果希望图像左右反转此位置高</code></p><p>这一位用于控制列地址是否反转其功能与 <code>MY</code> 位功能一致不再赘述。</p></li><li><p><code>MV</code>：行列地址顺序交换 - <code>如果希望图像旋转 90°/270° 此位置高</code></p><p>当此位被置高时，原本的行地址将作为列地址使用，列地址将作为行地址使用；通常的表现时屏幕内容向左或者向右旋转了 90°</p></li></ul><p>这三位搭配使用可以用于配置屏幕内容的方向，以便适应不同的硬件布局等；本例中不需要特殊布局全部置 <code>0</code></p><p><strong>[<code>ML</code> 垂直刷新方向配置]</strong></p><p>此位用于配置屏幕的垂直刷新方向，默认（0）位从上到下刷新屏幕；配置为 1 后变为从下到上刷新屏幕；此处使用默认刷新方向配置为 <code>0</code></p><p><strong>[<code>RGB&amp;BGR</code> 颜色选择开关控制]</strong></p><p>此位配置位低使用 <code>RGB</code> 模式，配置为高则为 <code>BGR</code> 模式，此处使用 RGB 模式配置为 <code>0</code> ；<strong>注意，屏幕色彩如果出现问题则可以优先检查程序中使用的 RGB 模式与配置的模式相同</strong>。</p><p><strong>[<code>MH</code> 水平刷新方向配置]</strong></p><p>此位作用与 ML 位相同，配置水平刷新方向，配置为 0 则为从左向右刷新，配置为 1 则为从右向左刷新，此处使用默认配置为 <code>0</code></p><p>由于我打算竖着用这块屏幕所以将其上下左右翻转 <code>0xC0</code></p><pre><code>/* Memory Data Access Control - 显存数据访问方式 */
st7735s_sendCommand(0x36);
st7735s_sendData(0xC0);</code></pre><h3>3.2.6 Gamma 矫正</h3><p><strong>[正 Gamma 矫正]</strong></p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/634556800.png" alt="" title=""></p><p>这个配置这个指令所需要的 parameter 比较多，足足有 16 个，但是仔细看一看给出的寄存器表格就可以发现实际上一共配置了三个参数:</p><ul><li><code>VRF0P[5:0]</code> - 6 位二进制值，用于调整高电平可变电阻 VRHP，较大的值可以提高最高灰度级的亮度，但可能会导致过曝。</li><li><code>PK0P[5:0] ~ PK9P[5:0]</code> - 这些参数用于配置不同灰度级别上的表现，可以精细调整这里的参数用于获得更好的显示效果。</li><li><code>SELVOP[5:0] ~ SELV63P[5:0]</code> - 6 位二进制值，用于选择不同灰度级的电压，影响颜色显示的准确性和均匀性，这里可以参照标准 Gamma 曲线进行设置。</li><li><code>VOS0P[5:0]</code> - 6 位二进制值，用于调整低电平可变电阻 VRLP，较大的值可以提高最低灰度级的亮度，但可能导致黑场不纯。</li></ul><p><strong>[负 Gamma 矫正]</strong></p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/3053999477.png" alt="" title=""></p><p>由于负 Gamma 矫正配置的是当像素电平较低时的参数，因此与正 Gamma 矫正使用同样的参数。</p><p>综上 Gamma 配置部分我直接采用了别人验证好的一套参数，代码如下：</p><pre><code>/* Gamma Correction Characteristics Setting - Gamma 矫正设置 */
st7735s_sendCommand(0xE0);  // 正极性 Gamma 校正
st7735s_sendData(0x20);     // VRF0P = 0x20
st7735s_sendData(0x00);     // SELV0P = 0x00
st7735s_sendData(0x02);     // SELV1P = 0x02
st7735s_sendData(0x04);     // SELV2P = 0x04
st7735s_sendData(0x08);     // SELV3P = 0x08
st7735s_sendData(0x0E);     // SELV4P = 0x0E
st7735s_sendData(0x12);     // SELV5P = 0x12
st7735s_sendData(0x1A);     // SELV6P = 0x1A
st7735s_sendData(0x24);     // SELV7P = 0x24
st7735s_sendData(0x2A);     // SELV8P = 0x2A
st7735s_sendData(0x30);     // SELV9P = 0x30
st7735s_sendData(0x36);     // SELV10P = 0x36
st7735s_sendData(0x3C);     // SELV11P = 0x3C
st7735s_sendData(0x3F);     // SELV12P = 0x3F
st7735s_sendData(0x00);     // SELV13P = 0x00
st7735s_sendData(0x00);     // SELV14P = 0x00

st7735s_sendCommand(0xE1);  // 负极性 Gamma 校正
st7735s_sendData(0x04);     // VRF0N
st7735s_sendData(0x16);     // SELV0N
st7735s_sendData(0x06);     // SELV1N
st7735s_sendData(0x0D);     // SELV2N
st7735s_sendData(0x2D);     // SELV3N
st7735s_sendData(0x26);     // SELV4N
st7735s_sendData(0x23);     // SELV5N
st7735s_sendData(0x27);     // SELV6N
st7735s_sendData(0x27);     // SELV7N
st7735s_sendData(0x25);     // SELV8N
st7735s_sendData(0x2D);     // SELV9N
st7735s_sendData(0x3B);     // SELV10N
st7735s_sendData(0x02);     // SELV11N
st7735s_sendData(0x03);     // SELV12N
st7735s_sendData(0x06);     // SELV13N
st7735s_sendData(0x13);     // SELV14N</code></pre><h3>3.2.7 <code>0x3A</code> 接口像素格式</h3><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/1273188015.png" alt="" title=""></p><p>这个寄存器用于配置每位像素使用的颜色深度，这会影响显示颜色的质量和数据传输效率，这个寄存器只有一个参数 <code>IFPF</code> 根据表可得：</p><ul><li><code>011</code> -&gt; 12-bit</li><li><code>101</code> -&gt; 16-bit</li><li><code>110</code> -&gt; 18-bit</li><li><code>111</code> -&gt; 未使用</li></ul><p>这里使用最常用的 16bit 色彩深度 <code>101</code> -&gt; <code>0x05</code></p><pre><code>/* Interface Pixel Format - 接口像素格式 */
st7735s_sendCommand(0x3A);
st7735s_sendData(0x05);</code></pre><h3>3.2.8 <code>0x29</code> 开启显示</h3><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/3414708297.png" alt="" title=""></p><p>到了这里 ST7735s 的初始化配置流程就结束了，最后发送 <code>0x29</code> 命令即可开启显示。</p><h2>3.3 屏幕坐标位置控制</h2><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/1288236000.png" alt="" title=""></p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/1288236000.png" alt="" title=""></p><p>由于我使用的屏幕分辨率为 128 * 160 因此在这个屏幕中表示坐标仅需要两个八位数据即可，同理如果需要表示一个范围直接使用四个八位数据即可。在 ST7735s 中使用 <code>0x2A</code> 和 <code>0x2B</code> 分别表示 x 范围和 y 范围。由于我们只用到了每个寄存器的低八位进行传输，因此两个寄存器不使用的部分填充 0 即可。</p><pre><code>st7735s_sendCommand(0x2A);  // Cloumn Address Set
st7735s_sendData(0x00);
st7735s_sendData(xStart);
st7735s_sendData(0x00);
st7735s_sendData(xEnd);

st7735s_sendCommand(0x2B);  // Row Address Set
st7735s_sendData(0x00);
st7735s_sendData(yStart);
st7735s_sendData(0x00);
st7735s_sendData(yEnd);</code></pre><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/1123216493.png" alt="" title=""></p><p>坐标范围配置好后通过发送 <code>0x2C</code> 指令后即可开始发送颜色数据，对坐标范围内的颜色进行配置。</p><pre><code>void st7735s_write_address(uint8_t xStart, uint8_t xEnd, uint8_t yStart, uint8_t yEnd) {
    st7735s_sendCommand(0x2A);  // Cloumn Address Set
    st7735s_sendData(0x00);
    st7735s_sendData(xStart);
    st7735s_sendData(0x00);
    st7735s_sendData(xEnd);

    st7735s_sendCommand(0x2B);  // Row Address Set
    st7735s_sendData(0x00);
    st7735s_sendData(yStart);
    st7735s_sendData(0x00);
    st7735s_sendData(yEnd);

    st7735s_sendCommand(0x2C);
}</code></pre><h2>3.4 全屏颜色填充</h2><blockquote>从这里开始初始化的工作完成了，下面有一些简单的图形绘制示例展示，展示一些绘制方面的基本原理。</blockquote><p>简单实现全屏颜色填充的逻辑比较简单，先发送需要填充颜色的区域后再循环向区域写入需要的颜色数据即可。</p><p>前面配置时提到了，我在这里将颜色深度配置为了 16bit 因此我们需要向其中传入一个 16bit 的颜色数据；这就需要一个通过 SPI 总线发送 16bit 数据的函数：</p><pre><code>void st7735s_sendu16Data(uint16_t data) {
    // 将 16bit 数据拆分为两个 8bit 数据用于发送
    uint8_t dataH = (uint8_t)(data &gt;&gt; 8);
    uint8_t dataL = (uint8_t)(data &amp; 0xFF);


    ST7735S_NSS_L;  // 拉低片选 - 开始通讯
    ST7735S_DC_H;   // 拉高 DC 代表的是数据
    // 先发送高字节，再发送低字节
    HAL_SPI_Transmit(&amp;hspi1, &amp;dataH, 1, 0xFFFF);
    HAL_SPI_Transmit(&amp;hspi1, &amp;dataL, 1, 0xFFFF);
    ST7735S_NSS_H;  // 拉高片选 - 结束通讯
}</code></pre><p>然后根据上面描述的流程，先设置范围，再循环填充即可：</p><pre><code>// 我选用了一个淡绿色作为测试 -&gt; 0x8426

void st7735s_RefreshAll(uint16_t rgb) {
    st7735s_write_address(0, 127, 0, 159);      // 设置显示范围
    for(uint16_t j = 0; j &lt; 160; j++) {         // 两个 for 遍历这个范围发送颜色数据
        for(uint16_t i = 0; i &lt; 128; i++) {
            st7735s_sendu16Data(rgb);
        }
    }
}</code></pre><p><strong>[显示效果]</strong></p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/3098287647.jpg" alt="" title=""></p><h2>3.5 区域颜色填充</h2><p>区域填充的原理与全屏一样，只不过是设置计算好的范围后再填充。</p><pre><code>void st7735s_drawBlock(uint8_t x, uint8_t y, uint8_t len, uint16_t color) {
    st7735s_write_address(x, x + len - 1, y, y + len - 1);
    for (int i = 0; i &lt; len; i++) {
        for (int j = 0; j &lt; len; j++) {
            st7735s_sendu16Data(color);
        }
    }
}</code></pre><p><strong>[显示效果]</strong></p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/05/2045616909.jpg" alt="" title=""></p><h2>3.7 画点</h2><p>画点函数非常简单，简单视作绘制一个边长为 1 像素的正方形即可。</p><pre><code>void st7735s_drawPoint(uint8_t x, uint8_t y, uint16_t color) {
    st7735s_write_address(x, x, y, y );
    st7735s_sendu16Data(color);
}</code></pre><h2>3.8 显示图片</h2><blockquote>待编写</blockquote><h2>3.9 LVGL 显示测试</h2><blockquote>待编写</blockquote><h1>END 参考与引用</h1><p><strong>[参考地址]</strong></p><ul><li><a href="https://blog.csdn.net/m0_59825000/article/details/129495765">CSDN - ST7735S应用笔记</a></li><li><a href="http://mashirospace.cn:4607/usr/uploads/2025/04/2833970739.pdf">ST7735SDataSheet.PDF</a></li></ul><blockquote>本文部分内容由 Ai 辅助生成</blockquote>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.atoery.cn/index.php/2025/05/04/102.html#comments</comments>
<wfw:commentRss>https://blog.atoery.cn/index.php/feed/category/electrial/</wfw:commentRss>
</item>
<item>
<title>[嵌入式] SPI 总线协议与实现</title>
<link>https://blog.atoery.cn/index.php/2025/04/21/98.html</link>
<guid>https://blog.atoery.cn/index.php/2025/04/21/98.html</guid>
<pubDate>Mon, 21 Apr 2025 22:05:00 +0800</pubDate>
<dc:creator>JRNitre</dc:creator>
<description><![CDATA[1.0 SPI 简介[软件模拟 SPI 库]SPI (Serial Peripheral Interface - 串行外设接口) 是 Motorola 开发的一种由四根通信线构成的一主多从通用数...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<h1>1.0 SPI 简介</h1><p><strong>[软件模拟 SPI 库]</strong><br></p><p>SPI (<em>Serial Peripheral Interface - 串行外设接口</em>) 是 Motorola 开发的一种由四根通信线构成的一主多从通用数据总线。</p><p>[SPI 总线具有如下特性]</p><ul><li>同步&全双工的通讯模式</li><li>支持总线挂载多设备</li></ul><h1>2.0 SPI 物理层</h1><p>SPI 总线由四根线构成：</p><ul><li><strong>SCK(Serial Clock)</strong>：时钟，用于提供时钟时序，数据的收发在时钟信号的上升沿或下降沿实现</li><li><strong>MOSI(Master Output Slave Input)</strong>：主机输出从机输入</li><li><strong>MISO(Master Input Slave Output)</strong>：主机输入从机输出</li><li><strong>SS(Slave Select)</strong> ：片选</li></ul><blockquote>值得注意的是，这里的名称并非标准，对于不同引脚名称的描述可能不同。</blockquote><p><strong>[一主多从]</strong></p><p>SPI 通讯仅支持一主机多从机的通讯方式，不支持多主机多从机的通讯方式。</p><p><strong>[SPI 片选]</strong></p><ul><li>主机的数据输出连接到从机的数据输入</li><li>从机的数据输出连接到主机的数据输入</li><li>从机的片选控制引脚连接到主机负责控制片选的引脚上</li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/2414901253.png" alt="" title=""></p><p><strong>[通讯引脚 IO 配置]</strong></p><p>SPI 的输出引脚均配置为<strong>推挽输出</strong>；输入引脚配置为<strong>浮空</strong>或者<strong>上拉输入</strong>。</p><h1>3.0 SPI 协议层</h1><h2>3.1 数据传输原理</h2><p>SPI 的主机与从机中，均包含一个串行移位寄存器，该位移寄存器每来一个时钟信号移位寄存器会向左移动一位。</p><p>主机和从机中的移位寄存器的时钟由主机提供，同时该时钟也通过 SCK 引脚进行输出。</p><p>而主机移位寄存器移出的数据通过 MOSI 引脚传输至从机，而从机移位寄存器移出的数据通过 MISO 引脚传输至主机。</p><p>通过如上过程，仅需要主机时钟驱动即可实现数据在两个通信单位之间传输。</p><p>因此，SPI 通讯的核心思想是通过时钟驱动的位移寄存器将数据在主机和从机之间进行置换。</p><h2>3.2 SS 片选引脚</h2><p>SPI 通过单独的片选引脚选择不同的从机设备，需要与谁通信就将对应 SS 引脚拉低电平状态即可。</p><p>同一时间仅能选择一个从机，如果多从机被同时选择可能会产生数据冲突问题。</p><h2>3.3 SPI 时序基本单元</h2><h3>3.3.1 起始与终止信号</h3><ul><li>起始信号：SS(片选) 从高电平切换到低电平</li><li>结束信号：SS(片选) 从低电平切换到高电平</li></ul><p>因此在数据传输的过程总，通讯单元之间的 SS 引脚需要始终保持低电平。</p><h3>3.3.2 数据位的传输</h3><p>SPI 通讯为了兼容不同芯片等之间的通讯，其对于时钟驱动的数据读写方式并未做规定，因此针对不同的模式 SPI 有 4 中不同的工作模式。</p><h3>3.3.3 交换一个字节 (模式 0)</h3><ul><li>CPOL = 0：空闲状态时，SCK 为低电平</li><li>CPHA = 0：SCK 第一个边沿移入数据，第二个边沿移出数据</li></ul><p>值得注意的是在这种模式下，在 SCK 的第一个时钟边沿到来前通讯双方就要移除数据，也就是说在 SS 产生下降沿的时候就要将数据移出。</p><p>在实际使用过程中，模式 0 最为常用，后续基于代码实现中以模式 0 实现。</p><h3>3.3.4 交换一个字节 (模式 1)</h3><ul><li>CPOL = 0：空闲状态时，SCK 为低电平</li><li>CPHA = 1：SCK 第一个边沿移出数据，第二个边沿移入数据</li></ul><h3>3.3.5 交换一个字节 (模式 2&3)</h3><p>模式 2 和模式 3 仅是相对于模式 0 和模式 1 将其 SCK 信号取反，其它并无差别。</p><h1>4.0 SPI 代码实现</h1><h2>4.1 起始信号与结束信号</h2><p>SPI 的实现方式对比 I2C 简单粗暴，所谓开始&结束信号只需要拉低&高 SS 片选引脚即可：</p><pre><code>void spiStartSignal(void) {
    spIO_SS(0);
}

void spiStopSignal(void) {
    spIO_SS(1);
}</code></pre><p>值得注意的是，如果是以模式 0 和模式 2 工作的话，在起始信号发出后 MOSI 就要马上将需要发送的数据搬到 MOSI 引脚上；因此这里调用 MOSI 的函数中是没有用于控制通讯速率的延迟的：</p><pre><code>void spIO_SS(uint8_t bitValue) {    // SPI SS 片选信号
    if(bitValue != 0) {
        global_spiConfigure.IO_SS_H();
    }else{
        global_spiConfigure.IO_SS_L();
    }
}</code></pre><h3>4.2 交换一个字节数据</h3><p>根据上述对 SPI 的描述，实现交换字节的逻辑也比较简单：</p><pre><code>uint8_t spiSwapByte(uint8_t byte) {
    uint8_t recByte = 0x00;

    // 循环发送&amp;读取数据
    for(int i = 0; i &lt; 8; i++) {
        spIO_MOSI(byte &amp; (0x80 &gt;&gt; i));
        // SCK 上升沿移入(读取)数据
        spIO_SCK(1);
        if(spIO_GetBitValue() == 1) {
            recByte |= (0x80 &gt;&gt; i);
        }
        // SCK 下降沿移出(写入)数据
        spIO_SCK(0);
    }
    return recByte;
}</code></pre><ul><li>此处根据模式 0 来看，由于前边 SS 引脚已经发送了一个起始信号，因此这里 MOSI 需要马上发送第一位数据；<code>byte &amp; (0x80 &gt;&gt; i)</code> 这里通过掩码的方式逐次取出指定的数据位用于发送。</li><li>驱动 SCK 来到第一个边沿</li><li>从 MISO 引脚读取一个字节数据并存储到缓存 (<code>recByte</code>) 中</li><li>驱动 SCK 来到第二个边沿开始发送数据，一个循环结束。</li></ul><p><strong>[代码优化]</strong></p><p>针对上述代码，如果我们根据 SPI 硬件层面实现原理来看可以优化代码如下：</p><pre><code>uint8_t spiSwapByte(uint8_t byte) {
    // 循环发送&amp;读取数据
    for(int i = 0; i &lt; 8; i++) {
        // 发送最高位
        spIO_MOSI(byte &amp; 0x80);
        // SCK 上升沿移入(读取)数据
        spIO_SCK(1);
        byte &lt;&lt;= 1;
        if(spIO_GetBitValue() == 1) {
            // 将接收到的数据存至 byte 的最低位
            byte |= 0x01;
        }
        // SCK 下降沿移出(写入)数据
        spIO_SCK(0);
    }
    return byte;
}</code></pre><p>这其中与之前不同的是每次发送的都是 <code>byte</code> 的最高位，与 SPI 通讯主机中的位移寄存器工作原理相同，数据位被发送出去后，将 <code>byte</code> 整体左移一位，低位空出来的位置正好可以用于存储从机传来的数据。</p><p>因此在数据读取边沿将 <code>byte</code> 或等于 <code>0x01</code> 这样就可以将接收到的数据存储在 <code>byte</code> 的最低为，就像位移寄存器一样。</p><p>这样编程可以节省一个缓存变量，节约空间。</p><blockquote>此外，不同的模式仅有奇数边沿读取还是偶数边沿读，以及时钟的正反相的区别，因此针对模式 0 的发送函数，对代码语句的简单排序即可得到其它模式下的发送函数。</blockquote><pre><code>// Mode 1 模式
uint8_t spiSwapByte_mode1(uint8_t byte) {
    uint8_t recByte = 0x00;

    // 循环发送&amp;读取数据
    for(int i = 0; i &lt; 8; i++) {
        spIO_SCK(1);
        spIO_MOSI(byte &amp; (0x80 &gt;&gt; i));
        spIO_SCK(0);
        if(spIO_GetBitValue() == 1) {
            recByte |= (0x80 &gt;&gt; i);
        }
    }
    return recByte;
}</code></pre><h1>END 参考与声明</h1><p><strong>[参考]</strong></p><ul><li><a href="https://www.bilibili.com/video/BV1th411z7sn?spm_id_from=333.788.videopod.episodes&vd_source=ba723374214ddeba5614ef26265f47e0&p=36">STM32入门教程-2023版 细致讲解 中文字幕</a></li></ul>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.atoery.cn/index.php/2025/04/21/98.html#comments</comments>
<wfw:commentRss>https://blog.atoery.cn/index.php/feed/category/electrial/</wfw:commentRss>
</item>
<item>
<title>[嵌入式] STM32 在 FreeRTOS 下实现 us 级别延时</title>
<link>https://blog.atoery.cn/index.php/2025/04/19/100.html</link>
<guid>https://blog.atoery.cn/index.php/2025/04/19/100.html</guid>
<pubDate>Sat, 19 Apr 2025 18:57:48 +0800</pubDate>
<dc:creator>JRNitre</dc:creator>
<description><![CDATA[1.0 前言与概述最近在编写软件实现的 I2C 和 SPI 协议库，需要稳定的 1us 延时用作心跳函数。在 FreeRTOS 下编写 us 延时逻辑不太一样，故在文章中作记录。与裸机实现 us...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<h1>1.0 前言与概述</h1><p>最近在编写软件实现的 I2C 和 SPI 协议库，需要稳定的 1us 延时用作心跳函数。在 FreeRTOS 下编写 us 延时逻辑不太一样，故在文章中作记录。</p><p>与裸机实现 us 延时方法相近，使用滴答定时器通过比较数值即可实现延时。</p><p>在 RTOS 环境下实现思路如下：</p><ol><li>计算指定延时时长所需要的计数器的值</li><li>关闭系统任务调度 - 防止延时器件被任务调度打断影响计数精度</li><li>获取当前重装载值</li><li>获取开始延时寄存器里面的计数值</li><li>不断获取当前计数器的值</li><li>如果当前计数器值大于设定的计数器的值，退出延时</li><li>释放系统任务调度</li></ol><h1>2.0 实现代码</h1><pre><code>void delay_us(uint32_t us) {
  volatile uint32_t tcnt = 0;
  uint32_t reload = SysTick-&gt;LOAD;
  uint32_t ticks = us * (SystemCoreClock / 1000000);

  vTaskSuspendAll();  // 阻止 OS 调度
  volatile uint32_t told = SysTick-&gt;VAL;
  while(1) {
    volatile uint32_t tnow = SysTick-&gt;VAL;
    if(tnow != told) {
      if(tnow&lt;told) {
        tcnt += told - tnow;
      }else {
        tcnt += reload - tnow + told;
      }
      told = tnow;
      if(tcnt &gt;= ticks) {
        break;
      }
    }
  }
  xTaskResumeAll(); // 恢复 OS 调度
}</code></pre><h2>3.0 实现过程分析</h2><h3>3.1 初始化部分</h3><ul><li><code>uint32_t tcnt = 0;</code>：用于累计已经经过的时间</li><li><code>uint32_t reload = SysTick-&gt;LOAD;</code>：获取 SysTick 的重装载值，这是定时器的最大值</li><li><code>uint32_t ticks = us * (SystemCoreClock / 1000000);</code>：计算需要延时的 SysTick 的值</li></ul><h3>3.2 暂停任务调度</h3><p>通过 FreeRTOS 提供的 <code>vTaskSupendAll();</code> 函数，暂停操作系统的任务调度，防止在延时期间发生任务切换，影响延时精度。</p><h3>3.3 获取初始计数器值</h3><p><code>uint32_t told = SysTick-&gt;VAL;</code>：获取 SysTick 定时器当前计数值</p><h3>3.4 主循环</h3><p>主循环中会一直循环，直到时间累计到目标值 <code>ticks</code></p><h3>3.4.1 获取当前计数器值</h3><p><code>uint32_t tnow = SysTick-&gt;VAL;</code>：获取当前 SysTick 计数器的值</p><h3>3.4.2 判断数值是否发生变化</h3><p><code>if(tnow != told)</code>：如果 tnow 和 told 的值不等，则 SysTick 的值发生了变化，更新累计时间 tcnt</p><h3>3.4.3 计算时间差</h3><p><code>if(tnow &lt; told)</code>：如果 tnow 小于 told，则说明计数器并未溢出，累加差值 <code>tcnt = told - tnow</code>；否则说明计数器发生了溢出，<code>reload - tnow</code> 得到 tnow 到 0 的剩余时间，再减去 told 计算出 reload 到 told 的时间。</p><h3>3.4.4 更新旧计数值</h3><p><code>told = tnow</code> 更新 told 的值，便于下次比较</p><h3>3.4.5 判断是否达到目标延时</h3><p>如果累计时间 tcnt 大于或等于 ticks 则达到了目标延时时长，退出循环。</p><h3>3.4.6 恢复任务调度</h3><p>FreeRTOS 提供的 <code>xTaskResumeAll();</code> 函数可以恢复操作系统的任务调度</p><h1>4.0 上述代码存在的问题</h1><p>上述的代码中，可以轻松的实现 us 级别的延时功能，但是存在一些问题；程序中为了保证延时的精度，在延时函数的核心功能代码中关闭了系统的任务调度。这会对其它线程的任务运行产生干扰。</p><p>因为在系统的任务调度被暂停后，所有的任务切换都会被禁止，相当于回到了裸机状态，直到调度器被恢复。</p><p>这种行为可能会对其它线程中要求实时性的任务产生负面影响，尤其是多任务系统。</p><h1>4.1 另一种实现 us 延时的方法</h1><p>通过 STM32 的硬件外设 TIM 定时器可以实现无需关闭任务调度实现 us 延时。</p><h2>4.2 实现代码</h2><pre><code>void tim_delay_us (uint32_t us) {
  uint32_t start = __HAL_TIM_GET_COUNTER(&amp;htim2);  // 获取当前计数值
  uint32_t elapsed = 0;

  while (elapsed &lt; us) {
    uint32_t now = __HAL_TIM_GET_COUNTER(&amp;htim2);  // 获取当前计数值
    if (now &gt;= start) {
      elapsed = now - start;  // 正常情况
    } else {
      elapsed = 0xFFFFFFFF - start + now;  // 处理溢出
    }
  }
}</code></pre><p>通过硬件定时器的实现可在不占用调度器的情况下实现同样的延时效果。</p><h1>END 参考与声明</h1><p><strong>[参考文章]</strong></p><ul><li><a href="https://blog.csdn.net/weixin_44098974/article/details/134464328">STM32在FreeRTOS下的us延时</a></li></ul><blockquote>本文部分内容和代码由 Ai 辅助生成</blockquote>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.atoery.cn/index.php/2025/04/19/100.html#comments</comments>
<wfw:commentRss>https://blog.atoery.cn/index.php/feed/category/electrial/</wfw:commentRss>
</item>
<item>
<title>[FreeRTOS] 堆和栈的基本概念&amp;amp;在FreeRTOS中的使用</title>
<link>https://blog.atoery.cn/index.php/2025/04/16/92.html</link>
<guid>https://blog.atoery.cn/index.php/2025/04/16/92.html</guid>
<pubDate>Wed, 16 Apr 2025 21:41:29 +0800</pubDate>
<dc:creator>JRNitre</dc:creator>
<description><![CDATA[1.0 堆1.1 基本概念堆的本质是在 RAM 中的一段连续的内存区域，开发者可以主动申请一些内存块，其中与栈不同的是，堆的声明周期是完全由程序员进行控制的。1.2 堆的管理机制动态性碎片化风险...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<h1>1.0 堆</h1><h1>1.1 基本概念</h1><p>堆的本质是在 RAM 中的一段连续的内存区域，开发者可以主动申请一些内存块，其中与栈不同的是，堆的声明周期是完全由程序员进行控制的。</p><h1>1.2 堆的管理机制</h1><ul><li>动态性</li><li>碎片化风险</li><li>手动管理</li></ul><h1>1.3 例子</h1><p>在 C/C++ 中，定义一个变量时，其存储位置取决于其定义位置上下文：</p><ul><li>如果在函数内定义，则其存储在栈中。</li><li>若在全局或静态作用域定义，则存储在静态存储区。</li></ul><p>真正的堆内存必须通过动态分配函数进行显示申请。</p><pre><code>char heap_buf[1024];    // 若在函数内进行申请，则存储在栈中，函数结束后自动释放</code></pre><p>-</p><pre><code>char* heap_buf = (char*)malloc(1024);   // 显式申请堆内存
</code></pre><h1>2.0 栈</h1><h2>2.1 基本概念</h2><p>栈是一种先进后出的数据结构，其基本操作通常包括：压栈和弹栈。</p><h2>2.2 栈的特点</h2><ul><li>后进先出 (LIFO)</li><li>高效性</li><li>有限容量 (固定大小栈)</li><li>自动管理：在嵌入式系统中，函数调用时使用的栈由编译器和处理器自动管理，无需程序员手动干预。</li></ul><h1>3.0 RTOS 如何使用栈</h1><h1>END 参考 & 声明</h1><p><strong>[参考]</strong></p><ul><li>FreeRTOS入门与工程实践 --由浅入深带你学习FreeRTOS（FreeRTOS教程 基于STM32，以实际项目为导向） -&gt; <em>BV1Jw411i7Fz</em></li></ul><blockquote>本文部分内容由 Ai 辅助生成</blockquote>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.atoery.cn/index.php/2025/04/16/92.html#comments</comments>
<wfw:commentRss>https://blog.atoery.cn/index.php/feed/category/electrial/</wfw:commentRss>
</item>
<item>
<title>[ARM 架构] 浅入 - 简单汇编指令</title>
<link>https://blog.atoery.cn/index.php/2025/04/16/91.html</link>
<guid>https://blog.atoery.cn/index.php/2025/04/16/91.html</guid>
<pubDate>Wed, 16 Apr 2025 20:32:00 +0800</pubDate>
<dc:creator>JRNitre</dc:creator>
<description><![CDATA[ARM 架构是一种精简指令集计算机，其一些基本的汇编指令如下：读内存 LOADLDR R0, [R1, #4 ;    读地址 “R1 + 4&quot;，将从 R1 读取的 4 个字节存入 R...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<p>ARM 架构是一种精简指令集计算机，其一些基本的汇编指令如下：</p><ul><li>读内存 <code>LOAD</code></li></ul><pre><code>LDR R0, [R1, #4 ;    读地址 “R1 + 4&quot;，将从 R1 读取的 4 个字节存入 R0</code></pre><ul><li>写内存 <code>STORE</code></li></ul><pre><code>STR R0, [R1, #4] ;    将 R0 的数据 4 个字节写入地址 R1</code></pre><ul><li>加减 <code>ADD&amp;SUB</code></li></ul><pre><code>ADD R0, R1, R2 ;    R0 = R1 + R2
ADD R0, R1, #1 ;    R0 = R1 + 1
SUB R0, R1, R2 ;    R0 = R1 - R2
SUB R0, R1, #1 ;    R0 = R1 - 1</code></pre><ul><li>比较 <code>CMP</code></li></ul><pre><code>CMP R0, R1 ;    比较 R0 与 R1 的值，将结果保存到 PSR[程序状态寄存去] 中</code></pre><ul><li>跳转 <code>B&amp;BL</code></li></ul><pre><code>B main ;    直接跳转
BL main ;    先把返回地址保存到 LR 寄存器后再跳转</code></pre><blockquote>未来我深入学习 ARM 架构或者汇编等后，这部分可能会继续更新~</blockquote>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.atoery.cn/index.php/2025/04/16/91.html#comments</comments>
<wfw:commentRss>https://blog.atoery.cn/index.php/feed/category/electrial/</wfw:commentRss>
</item>
<item>
<title>[FreeRTOS] 学习记录 - 创建一个多任务程序</title>
<link>https://blog.atoery.cn/index.php/2025/04/14/90.html</link>
<guid>https://blog.atoery.cn/index.php/2025/04/14/90.html</guid>
<pubDate>Mon, 14 Apr 2025 22:44:58 +0800</pubDate>
<dc:creator>JRNitre</dc:creator>
<description><![CDATA[HAL 库版本在使用 CubeMX 初始化的 FreeRTOS 工程后，可以选择默认创建一个线程（名称可配置），位于 freertos.c 中：/* creation of mainTask *...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<h1>HAL 库版本</h1><p>在使用 CubeMX 初始化的 FreeRTOS 工程后，可以选择默认创建一个线程（名称可配置），位于 <code>freertos.c</code> 中：</p><pre><code>/* creation of mainTask */
mainTaskHandle = osThreadNew(StartDefaultTask, NULL, &amp;mainTask_attributes);
/* USER CODE BEGIN RTOS_THREADS */</code></pre><p>创建一个新的任务，则需要使用 <code>xTaskCreate</code> 函数，这个函数的声明如下：</p><pre><code>BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
                        const char * const pcName,
                        const configSTACK_DEPTH_TYPE usStackDepth,
                        void * const pvParameters,
                        UBaseType_t uxPrioity,
                        TaskHandle_t * const pxCreateTask );</code></pre><ul><li><code>TaskFunction_t pxTaskCode</code>：指定的运行函数</li><li><code>const char * const pcName</code>：名字</li><li><code>const configSTACK_DEPTH_TYPE usStackDepth</code>：栈的深度</li><li><code>void * const pvParameters</code>：参数</li><li><code>UBaseType_t uxPrioity</code>：优先级</li><li><code>TaskHandle_t * const pxCreateTask</code>：句柄</li></ul><p>创建一个任务用于串口发送数据：</p><pre><code>xTaskCreate(usartTask, &quot;usartTask&quot;, 128, NULL, osPriorityNormal, NULL);</code></pre><pre><code>void usartTask(void *argument){
        while(1){
            printf(&quot;usart task is run!\n&quot;);
            HAL_Delay(500);
    }
}</code></pre><h1>标准库版本</h1>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.atoery.cn/index.php/2025/04/14/90.html#comments</comments>
<wfw:commentRss>https://blog.atoery.cn/index.php/feed/category/electrial/</wfw:commentRss>
</item>
<item>
<title>[STM32] EXTI 外部中断</title>
<link>https://blog.atoery.cn/index.php/2025/04/10/80.html</link>
<guid>https://blog.atoery.cn/index.php/2025/04/10/80.html</guid>
<pubDate>Thu, 10 Apr 2025 16:12:00 +0800</pubDate>
<dc:creator>JRNitre</dc:creator>
<description><![CDATA[1.0 EXTI 简介EXTI（Extern Interrupt）外部中断EXTI 可以监测指定 GPIO 口的电平信号，当其指定的 GPIO 口产生电平变化的时候，EXTI 立即向 NVIC ...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<h1>1.0 EXTI 简介</h1><p>EXTI（Extern Interrupt）外部中断</p><p>EXTI 可以监测指定 GPIO 口的电平信号，当其指定的 GPIO 口产生电平变化的时候，EXTI 立即向 NVIC 发出中断申请，经过 NVIC 裁决后即可中断 CPU 主程序，使得 CPU 执行 EXTI 对应的中断程序。</p><blockquote>其中 EXTI 支持四种不同的触发方式。</blockquote><ul><li>上升沿：数字电平从低电平 0 变化到高电平 1 的一瞬间叫做上升沿。</li><li>下降沿：数字电平从高电平 1 变化到低电平 0 的一瞬间叫做下降沿。</li><li>双边沿：上升沿和下降沿。</li><li>软件触发：不需要硬件触发的外部中断，而由软件触发的中断。</li></ul><h1>2.0 EXTI 特性</h1><ul><li>EXTI 支持所有 GPIO 口，但是相同 Pin 的 IO 口不可以同时触发中断（比如PA0，PB0不能同时触发中断）。</li><li>EXTI 支持的通道数：16个GPIO_Pin、PVD输出、RTC时钟、USB唤醒、以太网唤醒（合计20个中断线路）。</li><li>触发相应方式：中断响应、事件相应（事件响应不会触发中断，而是触发别的外设操作）。</li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/04/1675659071.png" alt="" title=""></p><h1>3.0 STM32 标准库使用 EXTI</h1><p>这里以 <code>STM32F103C8T6</code> 作为示例。</p><h2>3.1 使能 RCC 时钟</h2><p>由于NVIC和EXTI是默认开启时钟的外设，因此不需要开启这两个外设的时钟；需要开启的有GPIO时钟以及AFIO时钟。</p><pre><code>RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOx, ENABLE);   // GPIO
RCC_APB2PeriphClockCmd (RCC_APB2Periph_AFIO, ENABLE);    // AFIO</code></pre><h2>3.2 配置 GPIO 工作模式</h2><p>由于使用EXTI外部中断，因此需要使目标 GPIO 口工作在上拉输入 (<em>GPIO_Mode_IPU</em>) 的模式下。</p><pre><code>GPIO_InitTypeDef GOIO_InitStrucuture;

GOIO_InitStrucuture.GPIO_Mode = GPIO_Mode_IPU;      // GPIO 上拉输入
GOIO_InitStrucuture.GPIO_Pin = GPIO_Pin_x;          // 配置需要使用的 Pin 口
GOIO_InitStrucuture.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_Init(GPIOB,&amp;GOIO_InitStrucuture);</code></pre><h2>3.3 配置 AFIO</h2><p>通过 AFIO 将需要使用的 GPIO 口连接到 EXTI。</p><pre><code>GPIO_EXTILineConfig (GPIO_PortSourceGPIOx, GPIO_PinSourcex);</code></pre><h2>3.4 配置 EXTI</h2><p>选择中断触发方式。</p><pre><code>EXTI_InitTypeDef EXTI_InitStructure;                        // EXTI初始化结构体

EXTI_InitStructure.EXTI_Line = EXTI_Linex;                  // 选择需要配置的中断线，这个与配置AFIO时所配置的中断线相同
EXTI_InitStructure.EXTI_LineCmd = ENABLE;                   // 选择指定中断线的状态
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;         // 指定模式(EXTI_Mode_Interrupt : 中断模式)
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;     // 指定触发信号的有效边沿

EXTI_Init(&amp;EXTI_InitStructure);</code></pre><blockquote>EXTI 有如下三种触发设置</blockquote><ol><li><code>EXTI_Trigger_Rising</code> —— 上升沿触发</li><li><code>EXTI_Trigger_Falling</code> —— 下降沿触发</li><li><code>EXTI_Trigger_Rsing_Falling</code> —— 双边沿触发</li></ol><h2>3.5 配置 NVIC</h2><p>为中断分配一个合适的优先级。</p><pre><code>NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 
// 配置NCIV优先级分组（抢占优先级、响应优先级）
// 该函数在每一个芯片中只能使用一次

NVIC_TypeDef NVIC_InitStructure;                            // NCIV初始化结构体

NVIC_InitStructure.NCIV_IRQChannel = EXTI15_10_IRQn;        // 指定中断通道开启或者关闭
NVIC_InitStructure.NCIV_IRQChannelCmd = ENABLE;             // 指定通道使能还是失能（ENABLE,DISABLE）
NVIC_InitStructure.NCIV_IRQChannelPreemptionPriority =  1;  // 指定抢占优先级的优先级
NVIC_InitStructure.NCIV_IRQChannelSubPriority = 1;          // 指定响应优先级的优先级

NVIC_Init(&amp;NVIC_InitStructure);</code></pre><blockquote>NVIC 优先级的几种分组</blockquote><table><thead><tr><th>NVIC_PriorityGroup</th><th>抢占优先级</th><th>相应优先级</th></tr></thead><tbody><tr><td>NVIC_PriorityGroup_0</td><td>0</td><td>0~15</td></tr><tr><td>NVIC_PriorityGroup_1</td><td>0~1</td><td>0~7</td></tr><tr><td>NVIC_PriorityGroup_2</td><td>0~3</td><td>0~3</td></tr><tr><td>NVIC_PriorityGroup_3</td><td>0~7</td><td>0~1</td></tr><tr><td>NVIC_PriorityGroup_4</td><td>0~15</td><td>0</td></tr></tbody></table><h2>3.6 EXTI 中断函数</h2><p>在 EXTI 配置完毕后，当达到中断触发条件后就会自动执行中断函数中的代码。</p><pre><code>
//中断函数
void EXTI15_10_IRQHandler(void){
    if (EXTI_GetITStatus(EXTI_Linex) == SET ) {      // 判断中断标志位，判断是否为想要触发中断的通道

        /* 
         * 需要执行的函数 
        */

        EXTI_ClearITPendingBit(EXTI_Linex);         // 清除中断标志位，防止程序重复申请中断造成死循环
    }
}</code></pre>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.atoery.cn/index.php/2025/04/10/80.html#comments</comments>
<wfw:commentRss>https://blog.atoery.cn/index.php/feed/category/electrial/</wfw:commentRss>
</item>
<item>
<title>[嵌入式] I2C 总线协议与实现</title>
<link>https://blog.atoery.cn/index.php/2025/04/02/71.html</link>
<guid>https://blog.atoery.cn/index.php/2025/04/02/71.html</guid>
<pubDate>Wed, 02 Apr 2025 10:18:00 +0800</pubDate>
<dc:creator>JRNitre</dc:creator>
<description><![CDATA[1.0 I2C 简介本文章的参考代码已上传 Git 仓库：JRNitre/softwareI2CI2C&IIC (Inter-Integrated Circuit - 集成电路总线) 是由 NX...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<h1>1.0 I2C 简介</h1><p>本文章的参考代码已上传 Git 仓库：<a href="http://mashirospace.cn:8418/JRNitre/softwareI2C">JRNitre/softwareI2C</a></p><p>I2C&IIC (<em>Inter-Integrated Circuit - 集成电路总线</em>) 是由 NXP (原 Phihips) 在八十年代初开发的一种多主机通用数据总线，主要用于近距离、低速的芯片之间通信；标准情况下最高传输速率为 100Kbps，快速模式下 400Kbps, 高速模式下 3.4Mbps。</p><p>其是一种两线式串行总线，顾名思义其由两根线完成数据通信；一根是数据线 SDA (Serial Data Line)  另一根是时钟线 SCL (Serial Clock Line) 主设备控制时钟频率来决定 I2C 的通信波特率。</p><p>[I2C 总线具有如下特性]</p><ul><li>传输的任意时刻仅能有一个主机。</li><li>同步通信</li><li>半双工通信</li><li>带数据应答</li><li>支持总线挂载多设备（一主多从、多主多从）</li></ul><h1>2.0 I2C 物理层</h1><p>I2C 总线有两条线构成数据传输总线：</p><ul><li>SCL：时钟线，用于主机控制数据发送的时序</li><li>SDA：数据传输线，用于传输数据</li></ul><blockquote>I2C 是多主从架构，每个设备都有唯一的通讯地址，理论上可以连接 127 个从设备。</blockquote><p>I2C 的两条总线在空闲状态时默认为高电平，因此在 I2C 总线的电路设计中，两根总线需要上拉至通讯 VCC 电平中。</p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/03/2207136780.png" alt="" title=""></p><p>因此，在使用单片机进行 I2C 通讯时，通讯引脚使用开漏输出对总线电平状态进行控制，引脚内部由 MOS 管控制对地导通 MOS 管关断总线上拉至 VCC; MOS 导通时总线通过 MOS 管至地，电平状态为低。</p><h1>3.0 I2C 协议层</h1><p>通过规定好的协议，按照一定规则操纵时钟线和数据线，即可实现主机与从机之间的数据交换，I2C 的大致通信过程如下：</p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/03/4112326226.png" alt="" title=""></p><h2>3.1 寻址方式</h2><p>主机发送起始信号开始通讯后，必须先发送一个字节的数据用于寻址；其中高七位为地址数据，最后一位为后续字节传输方向。</p><ul><li>传输方向位为 0：主机 -&gt; 从机</li><li>传输方向位为 1：从机 -&gt; 主机</li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/03/3835967431.png" alt="" title=""></p><h2>3.2 基本时序单元</h2><h3>3.2.1 起始信号与停止信号</h3><ul><li>起始信号 (Start)：<code>当 SCL 为高电平时， SDA 从高电平向低电平跳变，代表开始传输数据</code></li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/03/2599483978.png" alt="" title=""></p><ul><li>结束信号 (Stop)：<code>当 SCL 为高电平时， SDA 从低电平向高电平跳变，代表数据传输结束</code></li></ul><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/03/2880638438.png" alt="" title=""></p><p>其中，起始与终止信号均由通讯主机发出，起始信号发出后总线处于占用状态；而停止信号发出后总线被释放处于空闲状态。</p><p>停止信号的发出有两种：</p><ul><li><strong>主机停止发送</strong>：发送停止信号</li><li><strong>从机停止接收，未向主机发送应答信号</strong>：此时主机发送停止信号结束通讯</li></ul><h3>3.2.2 应答信号</h3><p>在发送数据的过程中，所有地址或者数据都以 <code>8bit</code> 为单位进行传输，如果接收端正确的接收了 8bit 的数据，则回复一个 bit 的 <code>0</code> 作为应答信号 (ACK) 如果数据接收不正确或者接收端不再接收数据，则不回复总线状态为一个 bit <code>1</code> 的信号作为非应答信号 (NACK)。</p><p>因此 I2C 的一帧数据帧通常有 9 位。</p><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/03/3548671479.png" alt="" title=""></p><h2>3.3 数据传输</h2><ul><li><strong>数据有效性</strong>：I2C 协议规定，在信号传输的过程中，在 SCL 为高电平时，SDA 的状态必须稳定，不允许产生电平跳变；只有在 SCL 为低电平的时候 SDA 的电平状态才可以变化。</li></ul><h3>3.3.1 I2C 发送一个字节</h3><p>基于上述基本时序单元可知，I2C 发送一个字节顺序如下：</p><ol><li>发送起始条件</li><li>发送从机设备地址</li><li>发送一位方向位</li><li>接收从机应答</li><li>发送有效数据</li><li>接收从机应答</li><li>循环 5，6 步骤，直到数据发送完毕或者无从机应答</li><li>发送结束条件</li></ol><p><img src="http://mashirospace.cn:4607/usr/uploads/2025/03/891409240.png" alt="" title=""></p><h3>3.3.2 I2C 读取一个字节</h3><h2>4.0 I2C 代码实现</h2><h3>4.1 基本功能单元的封装</h3><h4>4.1.1 GPIO 工作模式配置</h4><p>由于 I2C 在工作时空闲状态为高电平，因此 SCL 和 SDA 引脚需要配置为开漏输出模式对总线进行控制。</p><pre><code>// 使能需要使用的 GPIO 所在的总线时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;

GPIO_InitStruct.GPIO_Mode        = GPIO_Mode_OUT;    // 输出模式
GPIO_InitStruct.GPIO_OType    = GPIO_OType_OD;    // 开漏
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;    

GPIO_InitStruct.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN;
GPIO_Init(I2C_GROUP,&amp;GPIO_InitStruct);</code></pre><h4>4.1.2 配置 TIM 定时器</h4><p>在使用 I2C 协议发送数据时，每个电平状态之间使用延时函数进行延时，从而对 I2C 的通信速度进行控制，这里使用 TIM2 定时器作为延时函数的基本实现。</p><p>在本文编写时，使用的 MCU 是 <code>STM32F401CCU6</code> 其默认时钟频率为 <code>84Mhz</code> 供给至 AB1 总线部分的时钟频率也为 <code>84Mhz</code>。</p><p>而根据上文可知，I2C 的标准通信速度为 <code>100Kbps</code> 由此可知每比特数据之间的传输间隔为：</p><p>$10us = \frac{1}{100000} $</p><p>对于 I2C 协议数据采样发生在高或低电平的中点，因此两次电平状态的时间间隔为 <code>5us</code>。</p><p>//</p><p>综上我们需要实现一个精度是 <code>us</code> 级别的延时函数，根据计数器时钟周期计算公式：</p><p>$T_{count} = \frac{(PSC + 1)  * (ARR + 1)}{f_{clock}} $</p><p>计算可知 PSC 应该等于 63 然后 <code>ARR = 延时时间 - 1</code> 得到 ARR = 4。</p><pre><code>RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

TIM_TimeBaseStructure.TIM_Period = 0xFFFFFFFF;
TIM_TimeBaseStructure.TIM_Prescaler = 83;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &amp;TIM_TimeBaseStructure);

TIM_Cmd(TIM2, ENABLE);</code></pre><h4>4.1.3 电平操纵函数</h4><p>将引脚操作函数和延时函数封装到一起，用于控制总线产生 0 或 1</p><pre><code>void i2cCon_SCL(uint8_t BitValue){
    GPIO_WriteBit(I2C_GROUP, I2C_SCL_PIN, (BitAction)(BitValue));
    i2cDelay_us(i2cSpeedDelay);
}

void i2cCon_SDA(uint8_t BitValue){
    GPIO_WriteBit(I2C_GROUP, I2C_SDA_PIN, (BitAction)(BitValue));
    i2cDelay_us(i2cSpeedDelay);
}</code></pre><h4>4.1.4 开始信号与结束信号</h4><p>基于上面封装的函数和 I2C 通信原理，组装信号时序：</p><pre><code>
void i2cSignal_Start(void){
    i2cCon_SDA(1);
    i2cCon_SCL(1);
    i2cCon_SDA(0);
    i2cCon_SCL(0);
}

void i2cSignal_Stop(void){
    i2cCon_SDA(0);
    i2cCon_SCL(1);
    i2cCon_SDA(1);
}
</code></pre><h3>4.2 数据收发</h3><h4>4.2.1 发送一个 bit 数据</h4><pre><code>void i2cSend_Byte(uint8_t byte){
    for(int i = 0; i &lt; 8; i++){
        i2cCon_SDA(!!(byte &amp; (0x80 &gt;&gt; i)));
        i2cCon_SCL(1);
        i2cCon_SCL(0);
    }
}</code></pre><h4>4.2.2 接收一个 bit 数据</h4><pre><code>uint8_t i2cReceive_Byte(void){
    uint8_t rByte = 0x00;
    i2cCon_SDA(1);
    
    for(int i = 0; i &lt; 8; i++){
        i2cCon_SCL(1);
        
        if(i2cReceive_SDA() == 1){
            rByte |= (0x80 &gt;&gt; i);
        }
        
        i2cCon_SCL(0);
    }
    
    return rByte;
}</code></pre><h3>4.3 应答信号的发送与处理</h3><h4>4.3.1 发送应答信号</h4><pre><code>void i2cSend_ACK(uint8_t ackBit){
    i2cCon_SDA(ackBit);
    i2cCon_SCL(1);
    i2cCon_SCL(0);
}</code></pre><h4>4.3.2 检查应答信号</h4><pre><code>uint8_t i2cReceive_ACK(void){
    i2cCon_SDA(1);
    i2cCon_SCL(1);
    uint8_t ackBit = i2cReceive_SDA();
    i2cCon_SCL(0);
    return ackBit;
}</code></pre>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.atoery.cn/index.php/2025/04/02/71.html#comments</comments>
<wfw:commentRss>https://blog.atoery.cn/index.php/feed/category/electrial/</wfw:commentRss>
</item>
</channel>
</rss>