<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0"><channel><title>浮云落笔</title><link>https://www.flor.cn</link><atom:link href="https://www.flor.cn/feed.xml" rel="self" type="application/rss+xml"/><description>浮云落笔皆成记,一盏流年似旧时</description><generator>Halo v2.27.2</generator><language>zh-cn</language><image><url>https://www.flor.cn/upload/logo.png</url><title>浮云落笔</title><link>https://www.flor.cn</link></image><lastBuildDate>Fri, 13 Nov 2026 23:45:42 GMT</lastBuildDate><item><title><![CDATA[I2C 实验记录与原理复习]]></title><link>https://www.flor.cn/archives/i2c-experiment-notes</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=I2C%20%E5%AE%9E%E9%AA%8C%E8%AE%B0%E5%BD%95%E4%B8%8E%E5%8E%9F%E7%90%86%E5%A4%8D%E4%B9%A0&amp;url=/archives/i2c-experiment-notes" width="1" height="1" alt="" style="opacity:0;">
<h2 id="%E6%A0%B8%E5%BF%83%E7%BB%93%E8%AE%BA">核心结论</h2>
<p>I2C 学习可以归纳为三层：</p>
<pre><code class="language-text">电气层：SCL、SDA、开漏输出、上拉电阻
协议层：START、设备地址、数据、ACK、STOP
器件层：寄存器地址、寄存器数据、芯片手册定义
</code></pre>
<p>最重要的一句话：</p>
<blockquote>
 <p><strong>I2C 只负责传输字节；设备地址决定访问哪个芯片，寄存器地址决定访问芯片内部哪个位置，芯片手册决定读回来的字节代表什么。</strong></p>
</blockquote>
<hr>
<h2 id="%E4%B8%80%E3%80%81I2C%20%E7%9A%84%E7%A1%AC%E4%BB%B6%E7%BB%93%E6%9E%84">一、I2C 的硬件结构</h2>
<p>I2C 使用两根线：</p>
<table>
 <thead>
  <tr>
   <th>信号</th>
   <th>英文全称</th>
   <th>作用</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>SCL</td>
   <td>Serial Clock，串行时钟</td>
   <td>主机产生时钟</td>
  </tr>
  <tr>
   <td>SDA</td>
   <td>Serial Data，串行数据</td>
   <td>传输地址和数据</td>
  </tr>
 </tbody>
</table>
<p>本次实验连接：</p>
<table>
 <thead>
  <tr>
   <th>项目</th>
   <th>配置</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>STM32 SCL</td>
   <td>PB6</td>
  </tr>
  <tr>
   <td>STM32 SDA</td>
   <td>PB7</td>
  </tr>
  <tr>
   <td>电平</td>
   <td>3.3V</td>
  </tr>
  <tr>
   <td>OLED</td>
   <td>SSD1306，7 位地址 <code>0x3C</code></td>
  </tr>
  <tr>
   <td>传感器</td>
   <td>MPU6050，AD0 接地，7 位地址 <code>0x68</code></td>
  </tr>
  <tr>
   <td>必要连接</td>
   <td>电源正极、电源地、SCL、SDA</td>
  </tr>
 </tbody>
</table>
<p>STM32、OLED 和 MPU6050 必须共地。否则即使模块电源灯亮，也不代表通信线路真正连通。</p>
<hr>
<h2 id="%E4%BA%8C%E3%80%81%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81%E5%BC%80%E6%BC%8F%E8%BE%93%E5%87%BA%E5%92%8C%E4%B8%8A%E6%8B%89%E7%94%B5%E9%98%BB">二、为什么需要开漏输出和上拉电阻</h2>
<p>I2C 的 SCL、SDA 都是开漏结构：</p>
<ul>
 <li>设备可以主动拉低总线</li>
 <li>设备不能主动输出高电平</li>
 <li>设备释放总线后，由上拉电阻把线拉高</li>
</ul>
<p>结构可以表示为：</p>
<pre><code class="language-text">3.3V
  │
上拉电阻
  │
SCL 或 SDA ───── 多个设备的开漏引脚共同连接
</code></pre>
<p>这样允许多个设备共享总线。任何一个设备拉低，总线就为低；所有设备都释放，总线才为高。</p>
<p>当前软件 I2C 将 PB6、PB7 配置为开漏输出，通过 GPIO 控制电平。</p>
<hr>
<h2 id="%E4%B8%89%E3%80%81SCL%20%E5%92%8C%20SDA%20%E7%9A%84%E6%97%B6%E5%BA%8F%E8%A7%84%E5%88%99">三、SCL 和 SDA 的时序规则</h2>
<p>最核心的规则：</p>
<blockquote>
 <p><strong>SDA 只能在 SCL 低电平期间改变，在 SCL 高电平期间必须保持稳定。</strong></p>
</blockquote>
<p>主机在 SCL 高电平期间读取 SDA：</p>
<pre><code class="language-text">SCL       __/‾‾\__/‾‾\__/‾‾\__
             ↑     ↑     ↑
采样        读取   读取   读取

SDA       ‾‾‾‾‾‾‾‾‾\________
          稳定保持    在 SCL 低电平期间变化
</code></pre>
<p>每条 I2C 消息都由 START 开始、STOP 结束：</p>
<table>
 <thead>
  <tr>
   <th>条件</th>
   <th>动作</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>START</td>
   <td>SCL 为高时，SDA 从高变低</td>
  </tr>
  <tr>
   <td>STOP</td>
   <td>SCL 为高时，SDA 从低变高</td>
  </tr>
 </tbody>
</table>
<p>普通数据位不能在高电平期间随意变化，否则可能被误认为 START 或 STOP。</p>
<hr>
<h2 id="%E5%9B%9B%E3%80%81%E8%AE%BE%E5%A4%87%E5%9C%B0%E5%9D%80%E4%B8%8E%E8%AF%BB%E5%86%99%E6%96%B9%E5%90%91">四、设备地址与读写方向</h2>
<p>I2C 芯片通常使用 7 位地址。实际发送 8 位：前 7 位选择设备，最低位表示方向。</p>
<pre><code class="language-text">bit7～bit1：7 位设备地址
bit0：0 = 写，1 = 读
</code></pre>
<p>MPU6050 的 7 位地址是 <code>0x68</code>：</p>
<pre><code class="language-text">0x68 = 1101000

写地址：1101000 0 = 0xD0
读地址：1101000 1 = 0xD1
</code></pre>
<p>这里需要准确理解：</p>
<ul>
 <li><code>0xD0</code> 不是单独的“写命令”</li>
 <li><code>0xD0</code> 表示“访问 0x68 这个设备，本次方向为写”</li>
 <li><code>0xD1</code> 表示“访问 0x68 这个设备，本次方向为读”</li>
</ul>
<p>OLED 的 7 位地址是 <code>0x3C</code>：</p>
<pre><code class="language-text">0x3C &lt;&lt; 1 = 0x78
</code></pre>
<p>所以串口打印的 <code>0x78</code> 与扫描得到的 <code>0x3C</code> 是同一个 OLED 地址，只是一个使用 8 位写法，一个使用 7 位写法。</p>
<hr>
<h2 id="%E4%BA%94%E3%80%81I2C%20%E6%80%8E%E6%A0%B7%E4%BC%A0%E8%BE%93%E4%B8%80%E4%B8%AA%E5%AD%97%E8%8A%82">五、I2C 怎样传输一个字节</h2>
<p>每个数据字节包含 8 位，采用高位先发：</p>
<pre><code class="language-text">bit7 → bit6 → bit5 → bit4 → bit3 → bit2 → bit1 → bit0
</code></pre>
<p>发送完 8 位后，还有第 9 个时钟，用于 ACK：</p>
<table>
 <thead>
  <tr>
   <th>ACK 状态</th>
   <th>SDA 电平</th>
   <th>含义</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>ACK</td>
   <td>接收方拉低</td>
   <td>收到，可以继续</td>
  </tr>
  <tr>
   <td>NACK</td>
   <td>SDA 保持高</td>
   <td>不再接收，结束读取</td>
  </tr>
 </tbody>
</table>
<p>读取多个字节时：</p>
<ul>
 <li>非最后一个数据字节：主机发送 ACK，表示继续读</li>
 <li>最后一个数据字节：主机发送 NACK，表示不再继续</li>
</ul>
<p>因此“读取一个字节”实际上也需要 <strong>9 个时钟</strong>：8 个数据时钟加 1 个 ACK 时钟。</p>
<p><code>0x6B = 0110 1011</code> 只是教学示例。I2C 协议只规定如何传输字节，不规定固定字节内容。</p>
<hr>
<h2 id="%E5%85%AD%E3%80%81%E5%86%99%E5%AF%84%E5%AD%98%E5%99%A8%E7%9A%84%E5%AE%8C%E6%95%B4%E6%B5%81%E7%A8%8B">六、写寄存器的完整流程</h2>
<p>以 MPU6050 将 <code>PWR_MGMT_1</code> 写成 <code>0x01</code> 为例：</p>
<pre><code class="language-text">START
  → 0xD0 + ACK       设备地址，方向为写
  → 0x6B + ACK       选择 PWR_MGMT_1 寄存器
  → 0x01 + ACK       写入新值
STOP
</code></pre>
<p>对应关系：</p>
<table>
 <thead>
  <tr>
   <th>总线内容</th>
   <th>意义</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td><code>0xD0</code></td>
   <td>选择 MPU6050，并声明准备写</td>
  </tr>
  <tr>
   <td><code>0x6B</code></td>
   <td>选择芯片内部寄存器</td>
  </tr>
  <tr>
   <td><code>0x01</code></td>
   <td>写入该寄存器的新值</td>
  </tr>
 </tbody>
</table>
<p>普通配置寄存器写入新值后，会覆盖原来的值：</p>
<pre><code class="language-text">原值：PWR_MGMT_1 = 0x40
写入：PWR_MGMT_1 ← 0x01
结果：PWR_MGMT_1 = 0x01
</code></pre>
<p>芯片随后根据新值改变工作方式。特殊命令寄存器也可能采用“写 1 触发动作”等规则，必须查看具体芯片手册。</p>
<hr>
<h2 id="%E4%B8%83%E3%80%81%E8%AF%BB%E5%AF%84%E5%AD%98%E5%99%A8%E7%9A%84%E5%AE%8C%E6%95%B4%E6%B5%81%E7%A8%8B">七、读寄存器的完整流程</h2>
<p>读取 MPU6050 的 <code>WHO_AM_I</code>：</p>
<pre><code class="language-text">START
  → 0xD0 + ACK       设备地址，方向为写
  → 0x75 + ACK       选择 WHO_AM_I 寄存器
  → 重复 START
  → 0xD1 + ACK       设备地址，方向改为读
  → 芯片返回一个字节
  → 主机 NACK
STOP
</code></pre>
<p>这里最容易混淆的是：“读取为什么一开始用了写地址？”</p>
<p>原因是读取分为两个阶段：</p>
<pre><code class="language-text">第一阶段：先选中芯片，并设置内部寄存器指针
第二阶段：重新以读方向访问同一芯片，读取指针所指寄存器的内容
</code></pre>
<p>第一阶段并不写入新的配置值，只发送寄存器地址。但从总线层面看，寄存器地址本身也是一个数据字节，因此此时 SDA 上当然有数据。</p>
<p>可以对比为：</p>
<pre><code class="language-text">写配置：设备写地址 + 寄存器地址 + 新值
读数据：设备写地址 + 寄存器地址 + 重复START + 设备读地址 + 返回数据
</code></pre>
<p>不能把“读操作”理解成只发送 <code>0xD1</code>。必须先告诉芯片从哪个寄存器开始读取。</p>
<hr>
<h2 id="%E5%85%AB%E3%80%81%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BD%BF%E7%94%A8%E9%87%8D%E5%A4%8D%20START">八、为什么使用重复 START</h2>
<p>读取过程中：</p>
<pre><code class="language-text">D0 → 寄存器地址 → 重复START → D1
</code></pre>
<p>重复 START 的作用是：</p>
<ul>
 <li>不发送 STOP</li>
 <li>不释放总线</li>
 <li>直接改变设备访问方向</li>
 <li>保持之前设置的寄存器指针</li>
</ul>
<p>如果中间先发送 STOP，部分器件可能退出当前访问状态，后续读地址就不一定从刚才指定的寄存器开始。</p>
<hr>
<h2 id="%E4%B9%9D%E3%80%81%E5%AF%84%E5%AD%98%E5%99%A8%E5%9C%B0%E5%9D%80%E3%80%81%E5%AF%84%E5%AD%98%E5%99%A8%E5%92%8C%E6%95%B0%E6%8D%AE%E7%9A%84%E5%85%B3%E7%B3%BB">九、寄存器地址、寄存器和数据的关系</h2>
<p>可以把芯片内部想成一排小存储盒：</p>
<table>
 <thead>
  <tr>
   <th>概念</th>
   <th>例子</th>
   <th>类比</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>设备地址</td>
   <td><code>0x68</code></td>
   <td>哪栋楼</td>
  </tr>
  <tr>
   <td>读写方向</td>
   <td><code>0xD0/0xD1</code></td>
   <td>来写东西还是取东西</td>
  </tr>
  <tr>
   <td>寄存器地址</td>
   <td><code>0x75</code></td>
   <td>楼里的房间号</td>
  </tr>
  <tr>
   <td>寄存器</td>
   <td>芯片内部存储单元</td>
   <td>房间</td>
  </tr>
  <tr>
   <td>寄存器值</td>
   <td><code>0x68</code></td>
   <td>房间里当前保存的内容</td>
  </tr>
 </tbody>
</table>
<p>重要区别：</p>
<ul>
 <li><strong>寄存器地址</strong>：选择操作哪个存储位置</li>
 <li><strong>寄存器值</strong>：该位置里面保存的数据</li>
 <li>写入普通配置寄存器时，新值会覆盖旧值</li>
</ul>
<hr>
<h2 id="%E5%8D%81%E3%80%81MPU6050%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%AE%9E%E9%AA%8C">十、MPU6050 初始化实验</h2>
<p>程序先读取 <code>WHO_AM_I</code>：</p>
<pre><code class="language-text">寄存器地址：0x75
返回内容：0x68
</code></pre>
<p>确认芯片身份后，写入五个配置寄存器：</p>
<table>
 <thead>
  <tr>
   <th>寄存器</th>
   <th style="text-align: right;">写入值</th>
   <th>控制效果</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td><code>PWR_MGMT_1</code></td>
   <td style="text-align: right;"><code>0x01</code></td>
   <td>退出睡眠，选择时钟源</td>
  </tr>
  <tr>
   <td><code>CONFIG</code></td>
   <td style="text-align: right;"><code>0x03</code></td>
   <td>开启约 44Hz 低通滤波</td>
  </tr>
  <tr>
   <td><code>SMPLRT_DIV</code></td>
   <td style="text-align: right;"><code>0x04</code></td>
   <td>内部 1kHz 分频后约 200Hz</td>
  </tr>
  <tr>
   <td><code>GYRO_CONFIG</code></td>
   <td style="text-align: right;"><code>0x00</code></td>
   <td>陀螺仪量程 ±250°/s</td>
  </tr>
  <tr>
   <td><code>ACCEL_CONFIG</code></td>
   <td style="text-align: right;"><code>0x00</code></td>
   <td>加速度计量程 ±2g</td>
  </tr>
 </tbody>
</table>
<p>这说明“写数据控制芯片”的本质：</p>
<blockquote>
 <p>向控制寄存器写入不同值，芯片会改变采样率、量程、滤波、电源状态等行为。</p>
</blockquote>
<hr>
<h2 id="%E5%8D%81%E4%B8%80%E3%80%81MPU6050%20%E8%BF%9E%E7%BB%AD%E8%AF%BB%E5%8F%96%2014%20%E5%AD%97%E8%8A%82">十一、MPU6050 连续读取 14 字节</h2>
<p>程序设置起始寄存器 <code>0x3B</code>，连续读取 14 字节。MPU6050 读取一个寄存器后，内部地址会自动递增。</p>
<p>数据顺序固定为：</p>
<table>
 <thead>
  <tr>
   <th>字节位置</th>
   <th>寄存器</th>
   <th>含义</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>1～2</td>
   <td><code>0x3B/0x3C</code></td>
   <td>X 轴加速度高/低字节</td>
  </tr>
  <tr>
   <td>3～4</td>
   <td><code>0x3D/0x3E</code></td>
   <td>Y 轴加速度高/低字节</td>
  </tr>
  <tr>
   <td>5～6</td>
   <td><code>0x3F/0x40</code></td>
   <td>Z 轴加速度高/低字节</td>
  </tr>
  <tr>
   <td>7～8</td>
   <td><code>0x41/0x42</code></td>
   <td>温度高/低字节</td>
  </tr>
  <tr>
   <td>9～10</td>
   <td><code>0x43/0x44</code></td>
   <td>X 轴角速度高/低字节</td>
  </tr>
  <tr>
   <td>11～12</td>
   <td><code>0x45/0x46</code></td>
   <td>Y 轴角速度高/低字节</td>
  </tr>
  <tr>
   <td>13～14</td>
   <td><code>0x47/0x48</code></td>
   <td>Z 轴角速度高/低字节</td>
  </tr>
 </tbody>
</table>
<p>MPU6050 使用大端格式，高字节在前：</p>
<pre><code class="language-text">高字节：0x45
低字节：0x16
合并：0x45 &lt;&lt; 8 | 0x16 = 0x4516
</code></pre>
<p>不能把 <code>0x45</code> 和 <code>0x16</code> 直接相加。</p>
<p>传感器数据是有符号 16 位数。例如：</p>
<pre><code class="language-text">0xF2B0 → -3408
</code></pre>
<p>物理量换算公式：</p>
<table>
 <thead>
  <tr>
   <th>数据类型</th>
   <th>换算公式</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>加速度</td>
   <td>原始值 ÷ 16384，单位 g</td>
  </tr>
  <tr>
   <td>陀螺仪</td>
   <td>原始值 ÷ 131，单位 °/s</td>
  </tr>
  <tr>
   <td>温度</td>
   <td>原始值 ÷ 340 + 36.53，单位 ℃</td>
  </tr>
 </tbody>
</table>
<p>例如：</p>
<pre><code class="language-text">0x4516 = 17686
17686 ÷ 16384 ≈ 1.079g
</code></pre>
<p>当前实际读取帧示例：</p>
<pre><code class="language-text">[RAW] 45 16 F2 B0 F3 90 F4 10 FF 00 00 5B 00 7C
</code></pre>
<p>拆分后：</p>
<pre><code class="language-text">AX   = 45 16
AY   = F2 B0
AZ   = F3 90
TEMP = F4 10
GX   = FF 00
GY   = 00 5B
GZ   = 00 7C
</code></pre>
<hr>
<h2 id="%E5%8D%81%E4%BA%8C%E3%80%81SSD1306%20OLED%20%E7%9A%84%E6%8E%A7%E5%88%B6%E6%96%B9%E5%BC%8F">十二、SSD1306 OLED 的控制方式</h2>
<p>OLED 同样使用“设备地址加后续字节”的结构，但 SSD1306 的第二个字节是控制字节，而不是普通寄存器地址。</p>
<table>
 <thead>
  <tr>
   <th>控制字节</th>
   <th>后续字节含义</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td><code>0x00</code></td>
   <td>OLED 命令</td>
  </tr>
  <tr>
   <td><code>0x40</code></td>
   <td>OLED 显存数据</td>
  </tr>
 </tbody>
</table>
<p>发送命令：</p>
<pre><code class="language-text">START + 0x78 + 0x00 + 命令字节 + STOP
</code></pre>
<p>发送像素数据：</p>
<pre><code class="language-text">START + 0x78 + 0x40 + 显存数据 + STOP
</code></pre>
<p>因此，不同器件虽然都使用 I2C，设备地址后面的内容需要按各自手册解释。</p>
<p>SSD1306 中：</p>
<ul>
 <li>命令控制屏幕参数和显示模式</li>
 <li>显存数据决定哪些像素点亮</li>
 <li>程序先修改本地显存缓冲区，再整屏发送给 OLED</li>
</ul>
<hr>
<h2 id="%E5%8D%81%E4%B8%89%E3%80%81%E6%9C%AC%E6%AC%A1%E5%AE%9E%E9%AA%8C%E5%AE%8C%E6%88%90%E7%9A%84%E5%86%85%E5%AE%B9">十三、本次实验完成的内容</h2>
<p>实验工程：<code>i2c_oled</code></p>
<p>完成内容：</p>
<ol>
 <li>使用 GPIO 实现软件 I2C。</li>
 <li>扫描总线并识别 OLED 和 MPU6050。</li>
 <li>读取 MPU6050 的 <code>WHO_AM_I</code>。</li>
 <li>写入配置寄存器并启动 MPU6050。</li>
 <li>连续读取 14 字节传感器原始数据。</li>
 <li>将数据换算成加速度、温度和角速度。</li>
 <li>通过串口输出原始帧和物理量。</li>
 <li>在 OLED 中显示 MPU6050 温度。</li>
 <li>温度页固定显示，每秒更新一次。</li>
 <li>验证 OLED 与 MPU6050 可以同时挂在一条 I2C 总线上。</li>
</ol>
<p>上电后的关键串口输出：</p>
<pre><code class="language-text">ACK: 0x3C
ACK: 0x68
WHO_AM_I=0x68
TEMP 27.7 C
[RAW] 45 16 F2 B0 F3 90 F4 10 FF 00 00 5B 00 7C
</code></pre>
<p>该结果说明：</p>
<ul>
 <li>地址扫描成功</li>
 <li>两个设备都能回应 ACK</li>
 <li>MPU6050 身份识别正确</li>
 <li>寄存器读写成功</li>
 <li>14 字节数据读取成功</li>
 <li>OLED 与传感器并联通信正常</li>
</ul>
<hr>
<h2 id="%E5%8D%81%E5%9B%9B%E3%80%81%E6%9C%AC%E6%AC%A1%E5%AE%9E%E9%AA%8C%E9%81%87%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98">十四、本次实验遇到的问题</h2>
<p>并联 OLED 和 MPU6050 后，曾经只扫描到 OLED，MPU6050 没有 ACK。检查软件时序、降低速度和尝试交换逻辑引脚都没有解决，最终重新连接模块线路后恢复。</p>
<p>这个故障提供了几个重要经验：</p>
<ol>
 <li>模块电源灯亮不代表 SCL/SDA 已经真正接通。</li>
 <li>扫描不到地址时，应优先检查接线、共地、供电和上拉。</li>
 <li><code>0x68</code> 没有 ACK，不一定是程序错误。</li>
 <li>总线通信失败应先验证最基础的地址 ACK，再分析寄存器内容。</li>
 <li>两设备并联后，要保证每个模块的 SCL 都接总线 SCL，SDA 都接总线 SDA。</li>
</ol>
<hr>
<h2 id="%E5%8D%81%E4%BA%94%E3%80%81%E8%BD%AF%E4%BB%B6%20I2C%20%E4%B8%8E%E7%A1%AC%E4%BB%B6%20I2C">十五、软件 I2C 与硬件 I2C</h2>
<table>
 <thead>
  <tr>
   <th>项目</th>
   <th>软件 I2C</th>
   <th>硬件 I2C</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>时序产生</td>
   <td>GPIO 手动翻转</td>
   <td>STM32 I2C 外设自动产生</td>
  </tr>
  <tr>
   <td>CPU 占用</td>
   <td>较高</td>
   <td>较低</td>
  </tr>
  <tr>
   <td>引脚灵活度</td>
   <td>高</td>
   <td>受外设和复用引脚限制</td>
  </tr>
  <tr>
   <td>时序理解</td>
   <td>最直观</td>
   <td>部分过程被硬件封装</td>
  </tr>
  <tr>
   <td>适合场景</td>
   <td>学习协议、故障诊断、低速设备</td>
   <td>产品开发、高速或多设备通信</td>
  </tr>
 </tbody>
</table>
<p>当前实验采用软件 I2C，速度设置为保守低速，优先保证时序容易观察和模块兼容。</p>
<p>软件 I2C 的核心实现包括：</p>
<ul>
 <li>START：SCL 高时拉低 SDA</li>
 <li>发送字节：每次先准备 SDA，再产生 SCL 高电平</li>
 <li>第 9 拍释放 SDA，读取 ACK</li>
 <li>接收字节：主机释放 SDA，从机驱动 8 位数据</li>
 <li>最后一字节发送 NACK</li>
 <li>读寄存器使用重复 START</li>
 <li>STOP：SCL 高时释放 SDA</li>
</ul>
<p>代码中还包含总线恢复逻辑：如果初始化时发现 SDA 被某个从机持续拉低，则最多发送 9 个 SCL 脉冲，再发送 STOP，让设备释放总线。</p>
<hr>
<h2 id="%E5%8D%81%E5%85%AD%E3%80%81I2C%E3%80%81SPI%20%E4%B8%8E%E5%9C%BA%E6%99%AF%E9%80%89%E6%8B%A9">十六、I2C、SPI 与场景选择</h2>
<table>
 <thead>
  <tr>
   <th>总线</th>
   <th style="text-align: right;">线数</th>
   <th>特点</th>
   <th>常见设备</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>I2C</td>
   <td style="text-align: right;">2 根公共线</td>
   <td>节省引脚，多设备共享，需要地址和上拉</td>
   <td>OLED、传感器、EEPROM</td>
  </tr>
  <tr>
   <td>SPI</td>
   <td style="text-align: right;">通常 4 根</td>
   <td>速度高，片选线随设备增加</td>
   <td>Flash、显示屏、无线模块</td>
  </tr>
 </tbody>
</table>
<p>简单选择原则：</p>
<ul>
 <li>设备多、速度要求一般、希望节省引脚：优先考虑 I2C</li>
 <li>数据量大、刷新速度快：优先考虑 SPI</li>
 <li>协议学习和时序诊断：软件 I2C 很直观</li>
 <li>正式产品中追求稳定和降低 CPU 占用：通常使用硬件 I2C</li>
</ul>
<hr>
<h2 id="%E5%8D%81%E4%B8%83%E3%80%81%E5%AD%A6%E4%B9%A0%E6%96%B0%20I2C%20%E8%8A%AF%E7%89%87%E7%9A%84%E6%A3%80%E6%9F%A5%E8%A1%A8">十七、学习新 I2C 芯片的检查表</h2>
<p>拿到新器件后依次确认：</p>
<ol>
 <li>7 位设备地址是多少？</li>
 <li>地址引脚是否会改变设备地址？</li>
 <li>SCL/SDA 应接在哪里，是否需要上拉？</li>
 <li>控制寄存器有哪些？</li>
 <li>每个控制值代表什么功能？</li>
 <li>数据寄存器从哪个地址开始？</li>
 <li>一次需要读取几个字节？</li>
 <li>连续读取时地址是否自动递增？</li>
 <li>数据是高字节在前还是低字节在前？</li>
 <li>是否是有符号数，需要什么换算公式？</li>
 <li>写操作后能否回读同一寄存器确认配置？</li>
</ol>
<hr>
<h2 id="%E5%8D%81%E5%85%AB%E3%80%81%E6%9C%80%E7%BB%88%E5%BF%85%E9%A1%BB%E8%AE%B0%E7%89%A2%E7%9A%84%E5%9B%9B%E4%B8%AA%E6%A8%A1%E5%9E%8B">十八、最终必须记牢的四个模型</h2>
<h3 id="1.%20%E4%B8%80%E4%B8%AA%E8%AE%BE%E5%A4%87%E5%9C%B0%E5%9D%80">1. 一个设备地址</h3>
<pre><code class="language-text">7 位设备地址 + 读写方向位
</code></pre>
<h3 id="2.%20%E4%B8%80%E6%AC%A1%E5%86%99%E6%93%8D%E4%BD%9C">2. 一次写操作</h3>
<pre><code class="language-text">设备写地址 → 寄存器地址 → 新值
</code></pre>
<h3 id="3.%20%E4%B8%80%E6%AC%A1%E8%AF%BB%E6%93%8D%E4%BD%9C">3. 一次读操作</h3>
<pre><code class="language-text">设备写地址 → 寄存器地址 → 重复START → 设备读地址 → 返回数据
</code></pre>
<p>读操作前半段不是写入配置值，而是设置内部寄存器指针。</p>
<h3 id="4.%20%E6%95%B0%E6%8D%AE%E5%90%AB%E4%B9%89">4. 数据含义</h3>
<pre><code class="language-text">寄存器地址 + 芯片手册 + 配置量程 → 字节含义和物理量
</code></pre>
<hr>
<h2 id="%E5%A4%8D%E4%B9%A0%E6%97%B6%E5%8F%AA%E9%9C%80%E5%9B%9E%E7%AD%94%E8%BF%99%E4%BA%94%E4%B8%AA%E9%97%AE%E9%A2%98">复习时只需回答这五个问题</h2>
<ol>
 <li>SDA 为什么只能在 SCL 低电平期间变化？</li>
 <li>为什么 I2C 总线需要上拉电阻？</li>
 <li><code>0xD0</code> 和 <code>0xD1</code> 分别表示什么？</li>
 <li>读取寄存器时为什么先发写地址，再发读地址？</li>
 <li>读回一组字节后，怎样判断每个字节代表什么数据？</li>
</ol>
<p>如果这五个问题都能用自己的话回答，就说明 I2C 的协议、寄存器模型和实验流程已经形成完整理解。</p>]]></description><guid isPermaLink="false">/archives/i2c-experiment-notes</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Fri, 13 Nov 2026 13:07:19 GMT</pubDate></item><item><title><![CDATA[MCP6071 运算放大器用法详解：从开环比较器到三大反馈电路]]></title><link>https://www.flor.cn/archives/mcp6071-opamp-usage</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=MCP6071%20%E8%BF%90%E7%AE%97%E6%94%BE%E5%A4%A7%E5%99%A8%E7%94%A8%E6%B3%95%E8%AF%A6%E8%A7%A3%EF%BC%9A%E4%BB%8E%E5%BC%80%E7%8E%AF%E6%AF%94%E8%BE%83%E5%99%A8%E5%88%B0%E4%B8%89%E5%A4%A7%E5%8F%8D%E9%A6%88%E7%94%B5%E8%B7%AF&amp;url=/archives/mcp6071-opamp-usage" width="1" height="1" alt="" style="opacity:0;">
<h2 id="%E5%86%99%E5%9C%A8%E5%89%8D%E9%9D%A2">写在前面</h2>
<p>MCP6071 是 Microchip（微芯科技）推出的单通道 CMOS 运算放大器，主打<strong>低功耗 + 高精度</strong>：</p>
<ul>
 <li>供电范围 <strong>1.8V ~ 6.0V</strong>（单节锂电池即可工作）</li>
 <li>静态电流仅 <strong>110µA</strong>（典型值）</li>
 <li>输入失调电压 <strong>±150µV</strong>（最大值）</li>
 <li>输入偏置电流低至 <strong>1pA</strong>（CMOS 输入，几乎不取电流）</li>
 <li><strong>轨到轨输入输出</strong>（Rail-to-Rail I/O，动态范围吃满）</li>
 <li>增益带宽积 <strong>1.2MHz</strong>，单位增益稳定</li>
</ul>
<p>同一颗芯片，<strong>接法不同就是完全不同的电路</strong>。本文按四种用法逐一拆解，每种都遵循"看图 → 推导 → 什么时候用"的思路。</p>
<h2 id="%E4%B8%80%E3%80%81%E5%BC%80%E7%8E%AF%E4%BD%BF%E7%94%A8%EF%BC%9A%E6%AF%94%E8%BE%83%E5%99%A8">一、开环使用：比较器</h2>
<svg aria-label="运放开环输出两种状态" role="img" style="max-width:100%;height:auto;display:block;margin:14px auto;border-radius:10px" viewbox="0 0 500 300">
 <rect fill="#0f1923" height="300" width="500" x="0" y="0"></rect> <line stroke="#e04040" stroke-width="2.5" x1="90" x2="420" y1="40" y2="40"></line> <text fill="#e04040" font-size="12" x="100" y="32">VDD</text> <line stroke="#4040e0" stroke-width="2.5" x1="90" x2="420" y1="255" y2="255"></line> <text fill="#7070ff" font-size="12" x="100" y="274">VSS</text> <line stroke="#4caf50" stroke-width="2.5" x1="70" x2="140" y1="105" y2="105"></line> <text fill="#4caf50" font-size="12" text-anchor="middle" x="60" y="98">V⁺</text> <line stroke="#ff5252" stroke-width="2.5" x1="70" x2="140" y1="195" y2="195"></line> <text fill="#ff5252" font-size="12" text-anchor="middle" x="60" y="212">V⁻</text> <polygon fill="#16293a" points="140,75 140,225 290,150" stroke="#00d4ff" stroke-width="2.5"></polygon> <text fill="#00d4ff" font-size="14" x="152" y="109">+</text> <text fill="#00d4ff" font-size="14" x="152" y="199">−</text> <text fill="#9fd8e8" font-size="9.5" text-anchor="middle" x="215" y="153">增益 ≈ 50万倍</text> <line stroke="#ff9f43" stroke-width="2.5" x1="290" x2="345" y1="150" y2="150"></line> <text fill="#ff9f43" font-size="12" x="300" y="140">VOUT</text> <line stroke="#ff9f43" stroke-width="2.5" x1="375" x2="375" y1="150" y2="98"></line> <polygon fill="#ff9f43" points="375,90 369,104 381,104"></polygon> <text fill="#4caf50" font-size="10.5" x="386" y="102">V⁺ &gt; V⁻</text> <text fill="#d7e6ee" font-size="10.5" x="386" y="117">→ 顶到 VDD（高）</text> <line stroke="#ff9f43" stroke-width="2.5" x1="375" x2="375" y1="150" y2="202"></line> <polygon fill="#ff9f43" points="375,210 369,196 381,196"></polygon> <text fill="#ff5252" font-size="10.5" x="386" y="200">V⁺ &lt; V⁻</text> <text fill="#d7e6ee" font-size="10.5" x="386" y="215">→ 压到 VSS（低）</text> <text fill="#cfe3ee" font-size="11.5" text-anchor="middle" x="250" y="290">VOUT = A × (V⁺ − V⁻)　A ≈ 500000　→　微小差值即饱和</text>
</svg>
<p>运放的开环增益极大（MCP6071 约 115dB，即 50 万倍）：</p>
<p><strong>VOUT = A × (V⁺ − V⁻)</strong></p>
<p>输入端哪怕只差 1µV，输出就会被放大 0.5V——任何微小差值都会让输出<strong>撞到电源轨（饱和）</strong>。所以开环使用时输出只有两种状态：</p>
<ul>
 <li>V⁺ &gt; V⁻ → 输出顶到 <strong>VDD（高电平）</strong></li>
 <li>V⁺ &lt; V⁻ → 输出压到 <strong>VSS（低电平）</strong></li>
</ul>
<svg aria-label="过零检测正弦转方波" role="img" style="max-width:100%;height:auto;display:block;margin:14px auto;border-radius:10px" viewbox="0 0 500 260">
 <rect fill="#0f1923" height="260" width="500" x="0" y="0"></rect> <line stroke="#5a7a8a" stroke-width="2" x1="60" x2="482" y1="220" y2="220"></line> <polygon fill="#5a7a8a" points="490,220 476,214 476,226"></polygon> <line stroke="#5a7a8a" stroke-width="2" x1="60" x2="60" y1="220" y2="22"></line> <polygon fill="#5a7a8a" points="60,14 54,28 66,28"></polygon> <text fill="#7f9aa8" font-size="12" text-anchor="middle" x="480" y="238">t</text> <text fill="#7f9aa8" font-size="12" text-anchor="middle" x="48" y="26">V</text> <line stroke="#8aa6b3" stroke-dasharray="6 5" stroke-width="1.4" x1="60" x2="470" y1="120" y2="120"></line> <text fill="#8aa6b3" font-size="10" text-anchor="end" x="52" y="124">0V</text> <line stroke="#3d5a6a" stroke-dasharray="4 4" stroke-width="1" x1="142" x2="142" y1="30" y2="215"></line><line stroke="#3d5a6a" stroke-dasharray="4 4" stroke-width="1" x1="224" x2="224" y1="30" y2="215"></line><line stroke="#3d5a6a" stroke-dasharray="4 4" stroke-width="1" x1="306" x2="306" y1="30" y2="215"></line><line stroke="#3d5a6a" stroke-dasharray="4 4" stroke-width="1" x1="388" x2="388" y1="30" y2="215"></line> <path d="M60.00,120.00 L61.71,116.08 L63.42,112.17 L65.13,108.29 L66.83,104.47 L68.54,100.71 L70.25,97.04 L71.96,93.46 L73.67,90.00 L75.38,86.67 L77.08,83.47 L78.79,80.44 L80.50,77.57 L82.21,74.89 L83.92,72.40 L85.63,70.11 L87.33,68.04 L89.04,66.19 L90.75,64.57 L92.46,63.18 L94.17,62.04 L95.88,61.15 L97.58,60.51 L99.29,60.13 L101.00,60.00 L102.71,60.13 L104.42,60.51 L106.13,61.15 L107.83,62.04 L109.54,63.18 L111.25,64.57 L112.96,66.19 L114.67,68.04 L116.38,70.11 L118.08,72.40 L119.79,74.89 L121.50,77.57 L123.21,80.44 L124.92,83.47 L126.63,86.67 L128.33,90.00 L130.04,93.46 L131.75,97.04 L133.46,100.71 L135.17,104.47 L136.88,108.29 L138.58,112.17 L140.29,116.08 L142.00,120.00 L143.71,123.92 L145.42,127.83 L147.13,131.71 L148.83,135.53 L150.54,139.29 L152.25,142.96 L153.96,146.54 L155.67,150.00 L157.38,153.33 L159.08,156.53 L160.79,159.56 L162.50,162.43 L164.21,165.11 L165.92,167.60 L167.63,169.89 L169.33,171.96 L171.04,173.81 L172.75,175.43 L174.46,176.82 L176.17,177.96 L177.88,178.85 L179.58,179.49 L181.29,179.87 L183.00,180.00 L184.71,179.87 L186.42,179.49 L188.13,178.85 L189.83,177.96 L191.54,176.82 L193.25,175.43 L194.96,173.81 L196.67,171.96 L198.38,169.89 L200.08,167.60 L201.79,165.11 L203.50,162.43 L205.21,159.56 L206.92,156.53 L208.63,153.33 L210.33,150.00 L212.04,146.54 L213.75,142.96 L215.46,139.29 L217.17,135.53 L218.88,131.71 L220.58,127.83 L222.29,123.92 L224.00,120.00 L225.71,116.08 L227.42,112.17 L229.13,108.29 L230.83,104.47 L232.54,100.71 L234.25,97.04 L235.96,93.46 L237.67,90.00 L239.38,86.67 L241.08,83.47 L242.79,80.44 L244.50,77.57 L246.21,74.89 L247.92,72.40 L249.63,70.11 L251.33,68.04 L253.04,66.19 L254.75,64.57 L256.46,63.18 L258.17,62.04 L259.88,61.15 L261.58,60.51 L263.29,60.13 L265.00,60.00 L266.71,60.13 L268.42,60.51 L270.13,61.15 L271.83,62.04 L273.54,63.18 L275.25,64.57 L276.96,66.19 L278.67,68.04 L280.38,70.11 L282.08,72.40 L283.79,74.89 L285.50,77.57 L287.21,80.44 L288.92,83.47 L290.63,86.67 L292.33,90.00 L294.04,93.46 L295.75,97.04 L297.46,100.71 L299.17,104.47 L300.88,108.29 L302.58,112.17 L304.29,116.08 L306.00,120.00 L307.71,123.92 L309.42,127.83 L311.13,131.71 L312.83,135.53 L314.54,139.29 L316.25,142.96 L317.96,146.54 L319.67,150.00 L321.38,153.33 L323.08,156.53 L324.79,159.56 L326.50,162.43 L328.21,165.11 L329.92,167.60 L331.63,169.89 L333.33,171.96 L335.04,173.81 L336.75,175.43 L338.46,176.82 L340.17,177.96 L341.88,178.85 L343.58,179.49 L345.29,179.87 L347.00,180.00 L348.71,179.87 L350.42,179.49 L352.13,178.85 L353.83,177.96 L355.54,176.82 L357.25,175.43 L358.96,173.81 L360.67,171.96 L362.38,169.89 L364.08,167.60 L365.79,165.11 L367.50,162.43 L369.21,159.56 L370.92,156.53 L372.63,153.33 L374.33,150.00 L376.04,146.54 L377.75,142.96 L379.46,139.29 L381.17,135.53 L382.88,131.71 L384.58,127.83 L386.29,123.92 L388.00,120.00 L389.71,116.08 L391.42,112.17 L393.13,108.29 L394.83,104.47 L396.54,100.71 L398.25,97.04 L399.96,93.46 L401.67,90.00 L403.38,86.67 L405.08,83.47 L406.79,80.44 L408.50,77.57 L410.21,74.89 L411.92,72.40 L413.63,70.11 L415.33,68.04 L417.04,66.19 L418.75,64.57 L420.46,63.18 L422.17,62.04 L423.88,61.15 L425.58,60.51 L427.29,60.13 L429.00,60.00 L430.71,60.13 L432.42,60.51 L434.13,61.15 L435.83,62.04 L437.54,63.18 L439.25,64.57 L440.96,66.19 L442.67,68.04 L444.38,70.11 L446.08,72.40 L447.79,74.89 L449.50,77.57 L451.21,80.44 L452.92,83.47 L454.63,86.67 L456.33,90.00 L458.04,93.46 L459.75,97.04 L461.46,100.71 L463.17,104.47 L464.88,108.29 L466.58,112.17 L468.29,116.08 L470.00,120.00" fill="none" stroke="#4caf50" stroke-linejoin="round" stroke-width="2.5"></path> <path d="M60,70 H142.00 V170 H224.00 V70 H306.00 V170 H388.00 V70 H470.00" fill="none" stroke="#ff9f43" stroke-linejoin="miter" stroke-width="2.5"></path> <text fill="#cfe3ee" font-size="10.5" text-anchor="middle" x="142" y="26">输入过零 → 输出翻转</text> <line opacity="0.9" stroke="#e8b86d" stroke-dasharray="3 3" stroke-width="1.2" x1="142" x2="142" y1="30" y2="215"></line> <line stroke="#4caf50" stroke-width="2.5" x1="300" x2="330" y1="245" y2="245"></line> <text fill="#8aa6b3" font-size="11" x="336" y="249">输入 VIN</text> <line stroke="#ff9f43" stroke-width="2.5" x1="400" x2="430" y1="245" y2="245"></line> <text fill="#8aa6b3" font-size="11" x="436" y="249">输出 VOUT</text>
</svg>
<p>这正是<strong>比较器（Comparator）</strong>的行为。典型应用是<strong>过零检测</strong>：正弦波接 V⁺、地接 V⁻，输入每次过零输出就翻转，正弦波被"整形"成方波。</p>
<p>⚠️ 注意：运放开环当比较器属于"勉强能用"——响应慢、无迟滞易抖动。正经的比较器应用请选专用芯片（如 LM393）。</p>
<h2 id="%E4%BA%8C%E3%80%81%E7%94%B5%E5%8E%8B%E8%B7%9F%E9%9A%8F%E5%99%A8">二、电压跟随器</h2>
<svg style="max-width:100%;height:auto;display:block;margin:14px auto;border-radius:10px" viewbox="0 0 460 320" xmlns="http://www.w3.org/2000/svg">
 <rect fill="#0f1923" height="320" width="460" x="0" y="0"></rect> <text fill="#4caf50" font-size="14" x="42" y="105">VIN</text> <line stroke="#4caf50" stroke-width="2.5" x1="100" x2="180" y1="100" y2="100"></line> <line stroke="#00d4ff" stroke-width="2.5" x1="150" x2="180" y1="160" y2="160"></line> <polygon fill="rgba(0,212,255,0.07)" points="180,70 180,190 300,130" stroke="#00d4ff" stroke-width="2.5"></polygon> <text fill="#e0e0e0" font-size="17" font-weight="bold" x="190" y="106">+</text> <text fill="#e0e0e0" font-size="17" font-weight="bold" x="190" y="166">−</text> <line stroke="#00d4ff" stroke-width="2.5" x1="300" x2="380" y1="130" y2="130"></line> <text fill="#ff9f43" font-size="14" x="388" y="135">VOUT</text> <polyline fill="none" points="150,160 150,240 340,240 340,130" stroke="#ff9f43" stroke-width="2.5"></polyline> <circle cx="150" cy="160" fill="#ff9f43" r="4"></circle> <circle cx="340" cy="130" fill="#ff9f43" r="4"></circle> <text fill="#e0e0e0" font-size="15" text-anchor="middle" x="230" y="298">VOUT = VIN</text>
</svg>
<p>把输出<strong>直接连回反相输入端（−）</strong>，就得到最简单的负反馈电路。100% 的负反馈让 V⁻ 始终等于 V⁺，而 V⁻ 就是 VOUT，所以：</p>
<p><strong>VOUT = VIN（增益精确等于 1）</strong></p>
<p>它不放大电压，价值在于<strong>阻抗变换</strong>：</p>
<ul>
 <li>输入阻抗极高（偏置电流仅 1pA，几乎不取电流）</li>
 <li>输出阻抗极低（带负载后电压不跌）</li>
</ul>
<p>任何信号源都有内阻，直连负载会被分压衰减。中间加一级跟随器，源内阻被输入端的"无穷大阻抗"架空，负载面对的则是"零内阻理想源"——<strong>信号无损传递</strong>。</p>
<p><strong>典型场景</strong>：高阻传感器（光电二极管、pH 电极、高阻分压网络）接 ADC 采样前的缓冲。</p>
<h2 id="%E4%B8%89%E3%80%81%E5%90%8C%E7%9B%B8%E6%94%BE%E5%A4%A7%E5%99%A8">三、同相放大器</h2>
<svg style="max-width:100%;height:auto;display:block;margin:14px auto;border-radius:10px" viewbox="0 0 460 320" xmlns="http://www.w3.org/2000/svg">
 <rect fill="#0f1923" height="320" width="460" x="0" y="0"></rect> <text fill="#4caf50" font-size="14" x="98" y="85">VIN</text> <line stroke="#4caf50" stroke-width="2.5" x1="140" x2="230" y1="90" y2="90"></line> <polygon fill="rgba(0,212,255,0.07)" points="230,60 230,180 350,120" stroke="#00d4ff" stroke-width="2.5"></polygon> <text fill="#e0e0e0" font-size="17" font-weight="bold" x="240" y="96">+</text> <text fill="#e0e0e0" font-size="17" font-weight="bold" x="240" y="156">−</text> <line stroke="#e04040" stroke-width="2.5" x1="150" x2="230" y1="150" y2="150"></line> <circle cx="150" cy="150" fill="#ffdd33" r="4.5"></circle> <text fill="#ffdd33" font-size="13" text-anchor="middle" x="140" y="136">V⁻</text> <line stroke="#e0e0e0" stroke-width="2.5" x1="150" x2="110" y1="150" y2="150"></line> <line stroke="#e0e0e0" stroke-width="2.5" x1="110" x2="110" y1="150" y2="168"></line> <rect fill="#0f1923" height="44" stroke="#e0e0e0" stroke-width="2.5" width="18" x="101" y="168"></rect> <line stroke="#e0e0e0" stroke-width="2.5" x1="110" x2="110" y1="212" y2="235"></line> <text fill="#e0e0e0" font-size="13" text-anchor="end" x="94" y="194">R1</text> <line stroke="#e0e0e0" stroke-width="2.5" x1="98" x2="122" y1="235" y2="235"></line> <line stroke="#e0e0e0" stroke-width="2.5" x1="102" x2="118" y1="241" y2="241"></line> <line stroke="#e0e0e0" stroke-width="2.5" x1="106" x2="112" y1="247" y2="247"></line> <text fill="#e0e0e0" font-size="11" text-anchor="middle" x="110" y="266">GND</text> <line stroke="#00d4ff" stroke-width="2.5" x1="350" x2="400" y1="120" y2="120"></line> <text fill="#e0e0e0" font-size="14" x="406" y="125">VOUT</text> <line stroke="#ff9f43" stroke-width="2.5" x1="380" x2="380" y1="120" y2="270"></line> <line stroke="#ff9f43" stroke-width="2.5" x1="380" x2="307" y1="270" y2="270"></line> <rect fill="#0f1923" height="18" stroke="#ff9f43" stroke-width="2.5" width="44" x="263" y="261"></rect> <line stroke="#ff9f43" stroke-width="2.5" x1="263" x2="190" y1="270" y2="270"></line> <line stroke="#ff9f43" stroke-width="2.5" x1="190" x2="190" y1="270" y2="150"></line> <line stroke="#ff9f43" stroke-width="2.5" x1="190" x2="150" y1="150" y2="150"></line> <circle cx="380" cy="120" fill="#ff9f43" r="4"></circle> <text fill="#ff9f43" font-size="13" text-anchor="middle" x="285" y="254">Rf</text> <text fill="#e0e0e0" font-size="15" text-anchor="middle" x="230" y="296">VOUT = VIN × (1 + Rf / R1)</text> <text fill="#8fa3b8" font-size="12.5" text-anchor="middle" x="230" y="314">例：Rf = 9kΩ，R1 = 1kΩ → 增益 = 10 倍</text>
</svg>
<p>在跟随器基础上，把反馈路径改成 <strong>Rf / R1 两个电阻分压</strong>，就能精确控制增益。信号从同相端（+）进入，R1 从 V⁻ 节点接地，Rf 从输出反馈到 V⁻ 节点。</p>
<p>用两条黄金法则推导——<strong>虚短</strong>（V⁻ = V⁺ = VIN）+ <strong>虚断</strong>（输入端不取电流，流过 R1 和 Rf 的电流相等），三行解出：</p>
<p><strong>VOUT = VIN × (1 + Rf / R1)</strong></p>
<p>例：Rf = 9kΩ、R1 = 1kΩ → 增益 10 倍。</p>
<ul>
 <li>增益只取决于<strong>电阻比例</strong>，与运放自身参数无关 → 1% 电阻即 1% 精度</li>
 <li>输入阻抗极高 → 不打扰信号源</li>
 <li>增益 ≥ 1（只能放大，不能衰减）</li>
</ul>
<p><strong>典型场景</strong>：热电偶、传感器桥等弱电压信号的精密放大。</p>
<h2 id="%E5%9B%9B%E3%80%81%E5%8F%8D%E7%9B%B8%E6%94%BE%E5%A4%A7%E5%99%A8">四、反相放大器</h2>
<svg class="fig" style="max-width:100%;height:auto;display:block;margin:14px auto;border-radius:10px" viewbox="0 0 500 340" xmlns="http://www.w3.org/2000/svg">
 <rect fill="#0f1923" height="340" width="500" x="0" y="0"></rect> <line stroke="#e04040" stroke-width="2.5" x1="170" x2="330" y1="50" y2="50"></line> <text fill="#e04040" font-size="13" text-anchor="start" x="338" y="55">VDD</text> <line stroke="#4040e0" stroke-width="2.5" x1="170" x2="330" y1="305" y2="305"></line> <text fill="#4040e0" font-size="13" text-anchor="start" x="338" y="310">VSS</text> <polygon fill="none" points="200,100 200,240 300,170" stroke="#00d4ff" stroke-linejoin="round" stroke-width="2.5"></polygon> <text fill="#00d4ff" font-size="14" text-anchor="middle" x="238" y="175">运放</text> <polyline fill="none" points="200,120 160,120 160,168" stroke="#4caf50" stroke-width="2.5"></polyline> <line stroke="#4caf50" stroke-width="2.5" x1="145" x2="175" y1="168" y2="168"></line> <line stroke="#4caf50" stroke-width="2.5" x1="150" x2="170" y1="175" y2="175"></line> <line stroke="#4caf50" stroke-width="2.5" x1="155" x2="165" y1="182" y2="182"></line> <text fill="#4caf50" font-size="12" text-anchor="middle" x="95" y="140">V⁺ 接地 = 0V</text> <line stroke="#4caf50" stroke-width="2.5" x1="30" x2="70" y1="220" y2="220"></line> <circle cx="30" cy="220" fill="#4caf50" r="4"></circle> <text fill="#4caf50" font-size="12" text-anchor="middle" x="30" y="208">VIN</text> <rect fill="#4caf50" fill-opacity="0.28" height="18" stroke="#4caf50" stroke-width="2.5" width="40" x="70" y="211"></rect> <text fill="#4caf50" font-size="12" text-anchor="middle" x="90" y="200">R1=10kΩ</text> <line stroke="#4caf50" stroke-width="2.5" x1="110" x2="150" y1="220" y2="220"></line> <line stroke="#4caf50" stroke-width="2.5" x1="150" x2="200" y1="220" y2="220"></line> <circle cx="150" cy="220" fill="#ffd54f" r="5"></circle> <text fill="#ffd54f" font-size="12" text-anchor="middle" x="90" y="248">虚地 V⁻ ≈ 0V</text> <line stroke="#ff9f43" stroke-width="2.5" x1="300" x2="360" y1="170" y2="170"></line> <circle cx="360" cy="170" fill="#ff9f43" r="4"></circle> <text fill="#ff9f43" font-size="12" text-anchor="start" x="368" y="166">VOUT</text> <polyline fill="none" points="360,170 360,290 150,290 150,220" stroke="#ff9f43" stroke-width="2.5"></polyline> <rect fill="#ff9f43" fill-opacity="0.28" height="18" stroke="#ff9f43" stroke-width="2.5" width="40" x="235" y="281"></rect> <text fill="#ff9f43" font-size="12" text-anchor="middle" x="255" y="272">Rf=100kΩ</text> <polygon fill="#ff9f43" points="125,215 135,220 125,225"></polygon> <polygon fill="#ff9f43" points="145,250 155,250 150,260"></polygon> <polygon fill="#ff9f43" points="295,285 305,290 295,295"></polygon> <polygon fill="#ff9f43" points="355,235 365,235 360,225"></polygon> <text fill="#ff9f43" font-size="12" text-anchor="middle" x="255" y="332">电流全部流过 Rf</text> <text fill="#00d4ff" font-size="15" text-anchor="middle" x="250" y="22">VOUT = −VIN × (Rf / R1)</text> <text fill="#4caf50" font-size="12" text-anchor="middle" x="250" y="42">例：Rf=100kΩ, R1=10kΩ → 增益 = −10</text>
</svg>
<p>信号换了个门：从<strong>反相端（−）经 R1 进入，+ 端直接接地</strong>。这一换，电路性格全变。</p>
<p>+ 端接地（0V）+ 虚短 → <strong>V⁻ ≈ 0V 但没真接地</strong>，这个节点叫<strong>虚地</strong>——电压钉死在 0V，专门"吸收"电流。虚断保证电流进不了运放，所以信号电流的路径是唯一的：</p>
<p>VIN → R1 → 虚地点 → Rf → VOUT</p>
<p>R1 两端压差就是 VIN（虚地是 0V），电流 = VIN / R1；这股电流流过 Rf 产生压降，输出 = 0 − I × Rf：</p>
<p><strong>VOUT = −VIN × (Rf / R1)</strong></p>
<svg class="fig" style="max-width:100%;height:auto;display:block;margin:14px auto;border-radius:10px" viewbox="0 0 500 260" xmlns="http://www.w3.org/2000/svg">
 <rect fill="#0f1923" height="260" width="500" x="0" y="0"></rect>
 <defs>
  <marker id="ax" markerheight="7" markerwidth="7" orient="auto-start-reverse" refx="8" refy="5" viewbox="0 0 10 10">
   <path d="M 0 0 L 10 5 L 0 10 z" fill="#5a7a8a"></path>
  </marker>
 </defs>
 <line marker-end="url(#ax)" stroke="#5a7a8a" stroke-width="2.5" x1="60" x2="470" y1="210" y2="210"></line> <line marker-end="url(#ax)" stroke="#5a7a8a" stroke-width="2.5" x1="60" x2="60" y1="210" y2="25"></line> <text fill="#5a7a8a" font-size="14" text-anchor="middle" x="478" y="214">t</text> <text fill="#5a7a8a" font-size="14" text-anchor="end" x="52" y="22">电压</text> <path d="M 70.0,115.0 L 71.5,113.2 L 73.0,111.4 L 74.5,109.6 L 76.0,107.8 L 77.5,106.1 L 79.0,104.4 L 80.5,102.7 L 82.0,101.1 L 83.5,99.5 L 85.0,98.0 L 86.5,96.5 L 88.0,95.1 L 89.5,93.8 L 91.0,92.5 L 92.5,91.4 L 94.0,90.3 L 95.5,89.3 L 97.0,88.4 L 98.5,87.6 L 100.0,86.9 L 101.5,86.4 L 103.0,85.9 L 104.5,85.5 L 106.0,85.2 L 107.5,85.1 L 109.0,85.0 L 110.5,85.1 L 112.0,85.2 L 113.5,85.5 L 115.0,85.9 L 116.5,86.4 L 118.0,86.9 L 119.5,87.6 L 121.0,88.4 L 122.5,89.3 L 124.0,90.3 L 125.5,91.4 L 127.0,92.5 L 128.5,93.8 L 130.0,95.1 L 131.5,96.5 L 133.0,98.0 L 134.5,99.5 L 136.0,101.1 L 137.5,102.7 L 139.0,104.4 L 140.5,106.1 L 142.0,107.8 L 143.5,109.6 L 145.0,111.4 L 146.5,113.2 L 148.0,115.0 L 149.5,116.8 L 151.0,118.6 L 152.5,120.4 L 154.0,122.2 L 155.5,123.9 L 157.0,125.6 L 158.5,127.3 L 160.0,128.9 L 161.5,130.5 L 163.0,132.0 L 164.5,133.5 L 166.0,134.9 L 167.5,136.2 L 169.0,137.5 L 170.5,138.6 L 172.0,139.7 L 173.5,140.7 L 175.0,141.6 L 176.5,142.4 L 178.0,143.1 L 179.5,143.6 L 181.0,144.1 L 182.5,144.5 L 184.0,144.8 L 185.5,144.9 L 187.0,145.0 L 188.5,144.9 L 190.0,144.8 L 191.5,144.5 L 193.0,144.1 L 194.5,143.6 L 196.0,143.1 L 197.5,142.4 L 199.0,141.6 L 200.5,140.7 L 202.0,139.7 L 203.5,138.6 L 205.0,137.5 L 206.5,136.2 L 208.0,134.9 L 209.5,133.5 L 211.0,132.0 L 212.5,130.5 L 214.0,128.9 L 215.5,127.3 L 217.0,125.6 L 218.5,123.9 L 220.0,122.2 L 221.5,120.4 L 223.0,118.6 L 224.5,116.8 L 226.0,115.0 L 227.5,113.2 L 229.0,111.4 L 230.5,109.6 L 232.0,107.8 L 233.5,106.1 L 235.0,104.4 L 236.5,102.7 L 238.0,101.1 L 239.5,99.5 L 241.0,98.0 L 242.5,96.5 L 244.0,95.1 L 245.5,93.8 L 247.0,92.5 L 248.5,91.4 L 250.0,90.3 L 251.5,89.3 L 253.0,88.4 L 254.5,87.6 L 256.0,86.9 L 257.5,86.4 L 259.0,85.9 L 260.5,85.5 L 262.0,85.2 L 263.5,85.1 L 265.0,85.0 L 266.5,85.1 L 268.0,85.2 L 269.5,85.5 L 271.0,85.9 L 272.5,86.4 L 274.0,86.9 L 275.5,87.6 L 277.0,88.4 L 278.5,89.3 L 280.0,90.3 L 281.5,91.4 L 283.0,92.5 L 284.5,93.8 L 286.0,95.1 L 287.5,96.5 L 289.0,98.0 L 290.5,99.5 L 292.0,101.1 L 293.5,102.7 L 295.0,104.4 L 296.5,106.1 L 298.0,107.8 L 299.5,109.6 L 301.0,111.4 L 302.5,113.2 L 304.0,115.0 L 305.5,116.8 L 307.0,118.6 L 308.5,120.4 L 310.0,122.2 L 311.5,123.9 L 313.0,125.6 L 314.5,127.3 L 316.0,128.9 L 317.5,130.5 L 319.0,132.0 L 320.5,133.5 L 322.0,134.9 L 323.5,136.2 L 325.0,137.5 L 326.5,138.6 L 328.0,139.7 L 329.5,140.7 L 331.0,141.6 L 332.5,142.4 L 334.0,143.1 L 335.5,143.6 L 337.0,144.1 L 338.5,144.5 L 340.0,144.8 L 341.5,144.9 L 343.0,145.0 L 344.5,144.9 L 346.0,144.8 L 347.5,144.5 L 349.0,144.1 L 350.5,143.6 L 352.0,143.1 L 353.5,142.4 L 355.0,141.6 L 356.5,140.7 L 358.0,139.7 L 359.5,138.6 L 361.0,137.5 L 362.5,136.2 L 364.0,134.9 L 365.5,133.5 L 367.0,132.0 L 368.5,130.5 L 370.0,128.9 L 371.5,127.3 L 373.0,125.6 L 374.5,123.9 L 376.0,122.2 L 377.5,120.4 L 379.0,118.6 L 380.5,116.8 L 382.0,115.0 L 383.5,113.2 L 385.0,111.4 L 386.5,109.6 L 388.0,107.8 L 389.5,106.1 L 391.0,104.4 L 392.5,102.7 L 394.0,101.1 L 395.5,99.5 L 397.0,98.0 L 398.5,96.5 L 400.0,95.1 L 401.5,93.8 L 403.0,92.5 L 404.5,91.4 L 406.0,90.3 L 407.5,89.3 L 409.0,88.4 L 410.5,87.6 L 412.0,86.9 L 413.5,86.4 L 415.0,85.9 L 416.5,85.5 L 418.0,85.2 L 419.5,85.1 L 421.0,85.0 L 422.5,85.1 L 424.0,85.2 L 425.5,85.5 L 427.0,85.9 L 428.5,86.4 L 430.0,86.9 L 431.5,87.6 L 433.0,88.4 L 434.5,89.3 L 436.0,90.3 L 437.5,91.4 L 439.0,92.5 L 440.5,93.8 L 442.0,95.1 L 443.5,96.5 L 445.0,98.0 L 446.5,99.5 L 448.0,101.1 L 449.5,102.7 L 451.0,104.4 L 452.5,106.1 L 454.0,107.8 L 455.5,109.6 L 457.0,111.4 L 458.5,113.2 L 460.0,115.0" fill="none" stroke="#4caf50" stroke-linejoin="round" stroke-width="2.5"></path> <path d="M 70.0,115.0 L 71.5,118.6 L 73.0,122.2 L 74.5,125.8 L 76.0,129.4 L 77.5,132.9 L 79.0,136.3 L 80.5,139.6 L 82.0,142.9 L 83.5,146.0 L 85.0,149.1 L 86.5,152.0 L 88.0,154.8 L 89.5,157.4 L 91.0,159.9 L 92.5,162.2 L 94.0,164.4 L 95.5,166.3 L 97.0,168.1 L 98.5,169.7 L 100.0,171.1 L 101.5,172.3 L 103.0,173.3 L 104.5,174.0 L 106.0,174.6 L 107.5,174.9 L 109.0,175.0 L 110.5,174.9 L 112.0,174.6 L 113.5,174.0 L 115.0,173.3 L 116.5,172.3 L 118.0,171.1 L 119.5,169.7 L 121.0,168.1 L 122.5,166.3 L 124.0,164.4 L 125.5,162.2 L 127.0,159.9 L 128.5,157.4 L 130.0,154.8 L 131.5,152.0 L 133.0,149.1 L 134.5,146.0 L 136.0,142.9 L 137.5,139.6 L 139.0,136.3 L 140.5,132.9 L 142.0,129.4 L 143.5,125.8 L 145.0,122.2 L 146.5,118.6 L 148.0,115.0 L 149.5,111.4 L 151.0,107.8 L 152.5,104.2 L 154.0,100.6 L 155.5,97.1 L 157.0,93.7 L 158.5,90.4 L 160.0,87.1 L 161.5,84.0 L 163.0,80.9 L 164.5,78.0 L 166.0,75.2 L 167.5,72.6 L 169.0,70.1 L 170.5,67.8 L 172.0,65.6 L 173.5,63.7 L 175.0,61.9 L 176.5,60.3 L 178.0,58.9 L 179.5,57.7 L 181.0,56.7 L 182.5,56.0 L 184.0,55.4 L 185.5,55.1 L 187.0,55.0 L 188.5,55.1 L 190.0,55.4 L 191.5,56.0 L 193.0,56.7 L 194.5,57.7 L 196.0,58.9 L 197.5,60.3 L 199.0,61.9 L 200.5,63.7 L 202.0,65.6 L 203.5,67.8 L 205.0,70.1 L 206.5,72.6 L 208.0,75.2 L 209.5,78.0 L 211.0,80.9 L 212.5,84.0 L 214.0,87.1 L 215.5,90.4 L 217.0,93.7 L 218.5,97.1 L 220.0,100.6 L 221.5,104.2 L 223.0,107.8 L 224.5,111.4 L 226.0,115.0 L 227.5,118.6 L 229.0,122.2 L 230.5,125.8 L 232.0,129.4 L 233.5,132.9 L 235.0,136.3 L 236.5,139.6 L 238.0,142.9 L 239.5,146.0 L 241.0,149.1 L 242.5,152.0 L 244.0,154.8 L 245.5,157.4 L 247.0,159.9 L 248.5,162.2 L 250.0,164.4 L 251.5,166.3 L 253.0,168.1 L 254.5,169.7 L 256.0,171.1 L 257.5,172.3 L 259.0,173.3 L 260.5,174.0 L 262.0,174.6 L 263.5,174.9 L 265.0,175.0 L 266.5,174.9 L 268.0,174.6 L 269.5,174.0 L 271.0,173.3 L 272.5,172.3 L 274.0,171.1 L 275.5,169.7 L 277.0,168.1 L 278.5,166.3 L 280.0,164.4 L 281.5,162.2 L 283.0,159.9 L 284.5,157.4 L 286.0,154.8 L 287.5,152.0 L 289.0,149.1 L 290.5,146.0 L 292.0,142.9 L 293.5,139.6 L 295.0,136.3 L 296.5,132.9 L 298.0,129.4 L 299.5,125.8 L 301.0,122.2 L 302.5,118.6 L 304.0,115.0 L 305.5,111.4 L 307.0,107.8 L 308.5,104.2 L 310.0,100.6 L 311.5,97.1 L 313.0,93.7 L 314.5,90.4 L 316.0,87.1 L 317.5,84.0 L 319.0,80.9 L 320.5,78.0 L 322.0,75.2 L 323.5,72.6 L 325.0,70.1 L 326.5,67.8 L 328.0,65.6 L 329.5,63.7 L 331.0,61.9 L 332.5,60.3 L 334.0,58.9 L 335.5,57.7 L 337.0,56.7 L 338.5,56.0 L 340.0,55.4 L 341.5,55.1 L 343.0,55.0 L 344.5,55.1 L 346.0,55.4 L 347.5,56.0 L 349.0,56.7 L 350.5,57.7 L 352.0,58.9 L 353.5,60.3 L 355.0,61.9 L 356.5,63.7 L 358.0,65.6 L 359.5,67.8 L 361.0,70.1 L 362.5,72.6 L 364.0,75.2 L 365.5,78.0 L 367.0,80.9 L 368.5,84.0 L 370.0,87.1 L 371.5,90.4 L 373.0,93.7 L 374.5,97.1 L 376.0,100.6 L 377.5,104.2 L 379.0,107.8 L 380.5,111.4 L 382.0,115.0 L 383.5,118.6 L 385.0,122.2 L 386.5,125.8 L 388.0,129.4 L 389.5,132.9 L 391.0,136.3 L 392.5,139.6 L 394.0,142.9 L 395.5,146.0 L 397.0,149.1 L 398.5,152.0 L 400.0,154.8 L 401.5,157.4 L 403.0,159.9 L 404.5,162.2 L 406.0,164.4 L 407.5,166.3 L 409.0,168.1 L 410.5,169.7 L 412.0,171.1 L 413.5,172.3 L 415.0,173.3 L 416.5,174.0 L 418.0,174.6 L 419.5,174.9 L 421.0,175.0 L 422.5,174.9 L 424.0,174.6 L 425.5,174.0 L 427.0,173.3 L 428.5,172.3 L 430.0,171.1 L 431.5,169.7 L 433.0,168.1 L 434.5,166.3 L 436.0,164.4 L 437.5,162.2 L 439.0,159.9 L 440.5,157.4 L 442.0,154.8 L 443.5,152.0 L 445.0,149.1 L 446.5,146.0 L 448.0,142.9 L 449.5,139.6 L 451.0,136.3 L 452.5,132.9 L 454.0,129.4 L 455.5,125.8 L 457.0,122.2 L 458.5,118.6 L 460.0,115.0" fill="none" stroke="#ff9f43" stroke-linejoin="round" stroke-width="2.5"></path> <line stroke="#ffd54f" stroke-dasharray="4 4" stroke-width="1.5" x1="109.0" x2="109.0" y1="85.0" y2="175.0"></line> <text fill="#ffd54f" font-size="11" text-anchor="middle" x="109.0" y="34">输入到峰，输出在谷</text> <line stroke="#4caf50" stroke-width="2.5" x1="75" x2="105" y1="235" y2="235"></line> <text fill="#4caf50" font-size="12" text-anchor="start" x="112" y="239">输入 VIN</text> <line stroke="#ff9f43" stroke-width="2.5" x1="215" x2="245" y1="235" y2="235"></line> <text fill="#ff9f43" font-size="12" text-anchor="start" x="252" y="239">输出 VOUT</text>
</svg>
<p>例：Rf = 100kΩ、R1 = 10kΩ → 增益 −10。负号代表输出与输入<strong>相位差 180°</strong>（输入涨输出跌，波形上下镜像）。</p>
<ul>
 <li>增益可任意（含 &lt; 1 的衰减）</li>
 <li>无共模误差（+ 端永远接地）</li>
 <li>虚地是电流型传感器（光电二极管 TIA）的天然接口</li>
 <li>代价：输入阻抗只有 R1</li>
</ul>
<h2 id="%E4%BA%94%E3%80%81%E5%B7%AE%E5%88%86%E6%94%BE%E5%A4%A7%E5%99%A8">五、差分放大器</h2>
<svg aria-label="差分放大器电路" class="fig" role="img" style="max-width:100%;height:auto;display:block;margin:14px auto;border-radius:10px" viewbox="0 0 520 340" xmlns="http://www.w3.org/2000/svg">
 <rect fill="#0f1923" height="340" width="520" x="0" y="0"></rect> <line stroke="#e04040" stroke-width="2.5" x1="270" x2="410" y1="60" y2="60"></line> <text fill="#e04040" font-size="13" text-anchor="start" x="416" y="65">VDD</text> <line stroke="#e04040" stroke-width="2.5" x1="300" x2="300" y1="60" y2="145"></line> <line stroke="#4040e0" stroke-width="2.5" x1="270" x2="410" y1="300" y2="300"></line> <text fill="#4040e0" font-size="13" text-anchor="start" x="416" y="305">VSS</text> <line stroke="#4040e0" stroke-width="2.5" x1="300" x2="300" y1="300" y2="215"></line> <polygon fill="none" points="250,110 250,250 350,180" stroke="#00d4ff" stroke-linejoin="round" stroke-width="2.5"></polygon> <text fill="#00d4ff" font-size="15" text-anchor="middle" x="262" y="141">+</text> <text fill="#00d4ff" font-size="15" text-anchor="middle" x="262" y="232">−</text> <text fill="#00d4ff" font-size="12" text-anchor="middle" x="286" y="185">运放</text> <circle cx="40" cy="135" fill="#4caf50" r="4"></circle> <line stroke="#4caf50" stroke-width="2.5" x1="40" x2="112" y1="135" y2="135"></line> <rect fill="none" height="20" stroke="#4caf50" stroke-width="2.5" width="44" x="112" y="125"></rect> <line stroke="#4caf50" stroke-width="2.5" x1="156" x2="250" y1="135" y2="135"></line> <text fill="#4caf50" font-size="13" text-anchor="middle" x="40" y="120">V2</text> <text fill="#4caf50" font-size="12" text-anchor="middle" x="134" y="117">R3=10kΩ</text> <circle cx="200" cy="135" fill="#4caf50" r="4"></circle> <line stroke="#4caf50" stroke-width="2.5" x1="200" x2="200" y1="135" y2="180"></line> <rect fill="none" height="20" stroke="#4caf50" stroke-width="2.5" width="44" x="178" y="180"></rect> <line stroke="#4caf50" stroke-width="2.5" x1="200" x2="200" y1="200" y2="219"></line> <path d="M200,219 A6,6 0 0 1 200,231" fill="none" stroke="#4caf50" stroke-width="2.5"></path> <line stroke="#4caf50" stroke-width="2.5" x1="200" x2="200" y1="231" y2="262"></line> <line stroke="#4caf50" stroke-width="2.5" x1="186" x2="214" y1="262" y2="262"></line> <line stroke="#4caf50" stroke-width="2.5" x1="190" x2="210" y1="268" y2="268"></line> <line stroke="#4caf50" stroke-width="2.5" x1="194" x2="206" y1="274" y2="274"></line> <text fill="#4caf50" font-size="12" text-anchor="start" x="228" y="194">R4=100kΩ</text> <circle cx="40" cy="225" fill="#ff5252" r="4"></circle> <line stroke="#ff5252" stroke-width="2.5" x1="40" x2="112" y1="225" y2="225"></line> <rect fill="none" height="20" stroke="#ff5252" stroke-width="2.5" width="44" x="112" y="215"></rect> <line stroke="#ff5252" stroke-width="2.5" x1="156" x2="250" y1="225" y2="225"></line> <text fill="#ff5252" font-size="13" text-anchor="middle" x="40" y="248">V1</text> <text fill="#ff5252" font-size="12" text-anchor="middle" x="134" y="254">R1=10kΩ</text> <path d="M350,180 L455,180 L455,308 L175,308 L175,225" fill="none" stroke="#ff9f43" stroke-linejoin="round" stroke-width="2.5"></path> <rect fill="none" height="20" stroke="#ff9f43" stroke-width="2.5" width="48" x="210" y="298"></rect> <text fill="#ff9f43" font-size="12" text-anchor="middle" x="234" y="293">Rf=100kΩ</text> <circle cx="175" cy="225" fill="#ff9f43" r="4"></circle> <text fill="#ff9f43" font-size="13" text-anchor="middle" x="405" y="172">VOUT</text> <text fill="#00d4ff" font-size="15" text-anchor="middle" x="260" y="20">VOUT = (Rf / R1) × (V2 − V1)</text> <text fill="#8aa4b2" font-size="12" text-anchor="middle" x="260" y="40">条件：Rf / R1 = R4 / R3</text> <text fill="#4caf50" font-size="12" text-anchor="middle" x="260" y="336">例：Rf=100kΩ, R1=10kΩ → 增益 10 ｜ 放大差值，抑制共模</text>
</svg>
<p>差分放大器只放大两个信号的<strong>差值</strong>，无视它们的共同部分：<strong>VOUT = 增益 × (V2 − V1)</strong>。</p>
<p>为什么需要它？信号经长线传输会捡到环境噪声（工频、电磁干扰），而两根挨着的线捡到的噪声几乎一样——这叫<strong>共模信号</strong>。差分结构让两线<strong>相减</strong>：相同的噪声互相抵消，只剩信号差。</p>
<p>电路是前面两招的组合拳：<strong>V1 经 R1 进 − 端</strong>（反相半，Rf 反馈），<strong>V2 经 R3 进 + 端</strong>（同相半，R4 接地），四电阻对称匹配 <strong>Rf/R1 = R4/R3</strong>。</p>
<p>用叠加法推导：只看 V1 时就是反相放大器（VOUT₁ = −(Rf/R1)·V1）；只看 V2 时先经 R3/R4 分压到 + 端再同相放大（VOUT₂ = (Rf/R1)·V2）；两路叠加：</p>
<p><strong>VOUT = (Rf / R1) × (V2 − V1)</strong></p>
<p>数字验证（共模消失）：设两线各带 0.95V 共模噪声、信号差 0.1V——V2=1.05V、V1=0.95V，VOUT = 10 × 0.10 = 1V，0.95V 噪声凭空消失。</p>
<svg aria-label="共模抑制" class="fig" role="img" style="max-width:100%;height:auto;display:block;margin:14px auto;border-radius:10px" viewbox="0 0 500 300" xmlns="http://www.w3.org/2000/svg">
 <rect fill="#0f1923" height="300" width="500" x="0" y="0"></rect> <line stroke="#5a7a8a" stroke-width="2.5" x1="55" x2="475" y1="95" y2="95"></line> <text fill="#5a7a8a" font-size="13" text-anchor="start" x="481" y="99">t</text> <text fill="#8aa4b2" font-size="12" text-anchor="middle" x="265" y="52">两输入各自带相同噪声</text> <path d="M 60.0,98.5 L 63.4,94.5 L 66.8,90.6 L 70.2,86.7 L 73.7,82.9 L 77.1,79.2 L 80.5,75.7 L 83.9,79.4 L 87.3,76.3 L 90.8,73.4 L 94.2,70.8 L 97.6,68.4 L 101.0,66.3 L 104.4,64.5 L 107.8,70.0 L 111.2,68.8 L 114.7,67.9 L 118.1,67.3 L 121.5,67.0 L 124.9,67.0 L 128.3,67.3 L 131.8,74.8 L 135.2,75.6 L 138.6,76.6 L 142.0,77.8 L 145.4,79.2 L 148.8,80.8 L 152.2,89.5 L 155.7,91.3 L 159.1,93.1 L 162.5,95.0 L 165.9,96.9 L 169.3,98.7 L 172.8,100.5 L 176.2,109.2 L 179.6,110.7 L 183.0,112.2 L 186.4,113.4 L 189.8,114.4 L 193.2,115.2 L 196.7,122.7 L 200.1,123.0 L 203.5,123.0 L 206.9,122.7 L 210.3,122.1 L 213.8,121.2 L 217.2,120.0 L 220.6,125.5 L 224.0,123.7 L 227.4,121.6 L 230.8,119.2 L 234.2,116.6 L 237.7,113.7 L 241.1,110.6 L 244.5,114.3 L 247.9,110.8 L 251.3,107.1 L 254.8,103.3 L 258.2,99.4 L 261.6,95.5 L 265.0,98.5 L 268.4,94.5 L 271.8,90.6 L 275.2,86.7 L 278.7,82.9 L 282.1,79.2 L 285.5,75.7 L 288.9,79.4 L 292.3,76.3 L 295.8,73.4 L 299.2,70.8 L 302.6,68.4 L 306.0,66.3 L 309.4,64.5 L 312.8,70.0 L 316.2,68.8 L 319.7,67.9 L 323.1,67.3 L 326.5,67.0 L 329.9,67.0 L 333.3,74.3 L 336.8,74.8 L 340.2,75.6 L 343.6,76.6 L 347.0,77.8 L 350.4,79.2 L 353.8,80.8 L 357.2,89.5 L 360.7,91.3 L 364.1,93.1 L 367.5,95.0 L 370.9,96.9 L 374.3,98.7 L 377.8,100.5 L 381.2,109.2 L 384.6,110.7 L 388.0,112.2 L 391.4,113.4 L 394.8,114.4 L 398.2,115.2 L 401.7,122.7 L 405.1,123.0 L 408.5,123.0 L 411.9,122.7 L 415.3,122.1 L 418.8,121.2 L 422.2,120.0 L 425.6,125.5 L 429.0,123.7 L 432.4,121.6 L 435.8,119.2 L 439.2,116.6 L 442.7,113.7 L 446.1,110.6 L 449.5,114.3 L 452.9,110.8 L 456.3,107.1 L 459.8,103.3 L 463.2,99.4 L 466.6,95.5 L 470.0,98.5" fill="none" stroke="#4caf50" stroke-linejoin="round" stroke-width="2.5"></path> <path d="M 60.0,98.5 L 63.4,100.4 L 66.8,102.2 L 70.2,104.0 L 73.7,105.7 L 77.1,107.2 L 80.5,108.7 L 83.9,116.9 L 87.3,117.9 L 90.8,118.7 L 94.2,119.2 L 97.6,119.5 L 101.0,119.5 L 104.4,119.2 L 107.8,125.6 L 111.2,124.8 L 114.7,123.5 L 118.1,122.0 L 121.5,120.2 L 124.9,118.1 L 128.3,115.7 L 131.8,120.1 L 135.2,117.2 L 138.6,114.1 L 142.0,110.8 L 145.4,107.2 L 148.8,103.6 L 152.2,106.8 L 155.7,102.9 L 159.1,99.0 L 162.5,95.0 L 165.9,91.0 L 169.3,87.1 L 172.8,83.2 L 176.2,86.4 L 179.6,82.8 L 183.0,79.2 L 186.4,75.9 L 189.8,72.8 L 193.2,69.9 L 196.7,74.3 L 200.1,71.9 L 203.5,69.8 L 206.9,68.0 L 210.3,66.5 L 213.8,65.2 L 217.2,64.4 L 220.6,70.8 L 224.0,70.5 L 227.4,70.5 L 230.8,70.8 L 234.2,71.3 L 237.7,72.1 L 241.1,73.1 L 244.5,81.3 L 247.9,82.7 L 251.3,84.3 L 254.8,86.0 L 258.2,87.8 L 261.6,89.6 L 265.0,98.5 L 268.4,100.4 L 271.8,102.2 L 275.2,104.0 L 278.7,105.7 L 282.1,107.3 L 285.5,108.7 L 288.9,116.9 L 292.3,117.9 L 295.8,118.7 L 299.2,119.2 L 302.6,119.5 L 306.0,119.5 L 309.4,119.2 L 312.8,125.6 L 316.2,124.8 L 319.7,123.5 L 323.1,122.0 L 326.5,120.2 L 329.9,118.1 L 333.3,122.7 L 336.8,120.1 L 340.2,117.2 L 343.6,114.1 L 347.0,110.8 L 350.4,107.2 L 353.8,103.6 L 357.2,106.8 L 360.7,102.9 L 364.1,99.0 L 367.5,95.0 L 370.9,91.0 L 374.3,87.1 L 377.8,83.2 L 381.2,86.4 L 384.6,82.8 L 388.0,79.2 L 391.4,75.9 L 394.8,72.8 L 398.2,69.9 L 401.7,74.3 L 405.1,71.9 L 408.5,69.8 L 411.9,68.0 L 415.3,66.5 L 418.8,65.2 L 422.2,64.4 L 425.6,70.8 L 429.0,70.5 L 432.4,70.5 L 435.8,70.8 L 439.2,71.3 L 442.7,72.1 L 446.1,73.1 L 449.5,81.3 L 452.9,82.8 L 456.3,84.3 L 459.8,86.0 L 463.2,87.8 L 466.6,89.6 L 470.0,98.5" fill="none" stroke="#ff5252" stroke-linejoin="round" stroke-width="2.5"></path> <line stroke="#4caf50" stroke-width="2.5" x1="70" x2="100" y1="28" y2="28"></line> <text fill="#4caf50" font-size="12" text-anchor="start" x="106" y="32">V2</text> <line stroke="#ff5252" stroke-width="2.5" x1="250" x2="280" y1="28" y2="28"></line> <text fill="#ff5252" font-size="12" text-anchor="start" x="286" y="32">V1</text> <line stroke="#5a7a8a" stroke-width="2.5" x1="55" x2="475" y1="240" y2="240"></line> <text fill="#5a7a8a" font-size="13" text-anchor="start" x="481" y="244">t</text> <text fill="#ff9f43" font-size="12" text-anchor="middle" x="265" y="170">输出：只剩差值，噪声抵消</text> <path d="M 60.0,240.0 L 63.4,234.6 L 66.8,229.2 L 70.2,223.9 L 73.7,218.8 L 77.1,214.0 L 80.5,209.4 L 83.9,205.2 L 87.3,201.4 L 90.8,197.9 L 94.2,195.0 L 97.6,192.5 L 101.0,190.5 L 104.4,189.1 L 107.8,188.3 L 111.2,188.0 L 114.7,188.3 L 118.1,189.1 L 121.5,190.5 L 124.9,192.5 L 128.3,195.0 L 131.8,197.9 L 135.2,201.4 L 138.6,205.2 L 142.0,209.4 L 145.4,214.0 L 148.8,218.8 L 152.2,223.9 L 155.7,229.2 L 159.1,234.6 L 162.5,240.0 L 165.9,245.4 L 169.3,250.8 L 172.8,256.1 L 176.2,261.2 L 179.6,266.0 L 183.0,270.6 L 186.4,274.8 L 189.8,278.6 L 193.2,282.1 L 196.7,285.0 L 200.1,287.5 L 203.5,289.5 L 206.9,290.9 L 210.3,291.7 L 213.8,292.0 L 217.2,291.7 L 220.6,290.9 L 224.0,289.5 L 227.4,287.5 L 230.8,285.0 L 234.2,282.1 L 237.7,278.6 L 241.1,274.8 L 244.5,270.6 L 247.9,266.0 L 251.3,261.2 L 254.8,256.1 L 258.2,250.8 L 261.6,245.4 L 265.0,240.0 L 268.4,234.6 L 271.8,229.2 L 275.2,223.9 L 278.7,218.8 L 282.1,214.0 L 285.5,209.4 L 288.9,205.2 L 292.3,201.4 L 295.8,197.9 L 299.2,195.0 L 302.6,192.5 L 306.0,190.5 L 309.4,189.1 L 312.8,188.3 L 316.2,188.0 L 319.7,188.3 L 323.1,189.1 L 326.5,190.5 L 329.9,192.5 L 333.3,195.0 L 336.8,197.9 L 340.2,201.4 L 343.6,205.2 L 347.0,209.4 L 350.4,214.0 L 353.8,218.8 L 357.2,223.9 L 360.7,229.2 L 364.1,234.6 L 367.5,240.0 L 370.9,245.4 L 374.3,250.8 L 377.8,256.1 L 381.2,261.2 L 384.6,266.0 L 388.0,270.6 L 391.4,274.8 L 394.8,278.6 L 398.2,282.1 L 401.7,285.0 L 405.1,287.5 L 408.5,289.5 L 411.9,290.9 L 415.3,291.7 L 418.8,292.0 L 422.2,291.7 L 425.6,290.9 L 429.0,289.5 L 432.4,287.5 L 435.8,285.0 L 439.2,282.1 L 442.7,278.6 L 446.1,274.8 L 449.5,270.6 L 452.9,266.0 L 456.3,261.2 L 459.8,256.1 L 463.2,250.8 L 466.6,245.4 L 470.0,240.0" fill="none" stroke="#ff9f43" stroke-linejoin="round" stroke-width="2.5"></path> <line stroke="#ff9f43" stroke-width="2.5" x1="70" x2="100" y1="288" y2="288"></line> <text fill="#ff9f43" font-size="12" text-anchor="start" x="106" y="292">VOUT ×10</text>
</svg>
<p>局限：输入阻抗只有 R1/R3（10kΩ，高阻电桥带不动）；电阻必须严格匹配（1% 电阻实际 CMRR 约 50dB，芯片自身 89dB 被电阻拖累）；改增益要同时换两个电阻保持对称。高精度场景升级为<strong>仪表放大器</strong>（两跟随器缓冲 + 差分核心，三运放结构）。</p>
<p><strong>典型场景</strong>：称重/压力传感器电桥、热电偶、心电 ECG、工业长线接收端。</p>
<h2 id="%E7%BB%93%E8%AF%AD">结语</h2>
<p>以上是 MCP6071 的五种基础用法：<strong>开环（比较器）→ 电压跟随器 → 同相放大器 → 反相放大器 → 差分放大器</strong>。所有用法都是同一个原理的变形：负反馈 + 虚短虚断。</p>
<p>更多应用——<strong>跨阻放大器（TIA 光电检测）、有源滤波器、回转器</strong>——后续学习后持续补充。</p>]]></description><guid isPermaLink="false">/archives/mcp6071-opamp-usage</guid><dc:creator>flor</dc:creator><category>模电</category><pubDate>Fri, 9 Oct 2026 14:05:30 GMT</pubDate></item><item><title><![CDATA[STM32 DMA 数据搬运实验记录：让 CPU 卸下搬运工的活]]></title><link>https://www.flor.cn/archives/dma-usart-experiment-4</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=STM32%20DMA%20%E6%95%B0%E6%8D%AE%E6%90%AC%E8%BF%90%E5%AE%9E%E9%AA%8C%E8%AE%B0%E5%BD%95%EF%BC%9A%E8%AE%A9%20CPU%20%E5%8D%B8%E4%B8%8B%E6%90%AC%E8%BF%90%E5%B7%A5%E7%9A%84%E6%B4%BB&amp;url=/archives/dma-usart-experiment-4" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p>平台：STM32F103ZET6 @ 72MHz · 工程名 usart_dma · USART1（PA9=TX / PA10=RX）@ 115200 前置知识：USART 中断接收 + 环形缓冲区 + 协议帧（模块 2） 实验日期：2026-10-04 · 两个实验全部验收通过</p>
</blockquote>
<h2 id="%E4%B8%80%E3%80%81%E5%AE%9E%E9%AA%8C%E8%83%8C%E6%99%AF%EF%BC%9Acpu-%E5%BD%93%E6%90%AC%E8%BF%90%E5%B7%A5%E7%9A%84%E8%BE%9B%E9%85%B8">一、实验背景：CPU 当搬运工的辛酸</h2>
<p>在学习 DMA 之前，我的串口架构是这样的：每来一个字节，硬件敲门（RXNE 中断）→ CPU 放下手头的活 → 进中断 → 从 DR 寄存器搬一个字节进缓冲区 → 返回。115200 波特率连续收数据 = <strong>每秒 11520 次中断</strong>，CPU 的日程表被"搬字节"切碎。</p>
<p>发送侧更惨：<code>HAL_UART_Transmit</code> 是阻塞式的——发 420 字节，CPU 在函数里干等约 37ms，一步不能离开。</p>
<p><strong>DMA（Direct Memory Access，直接内存访问）= 芯片里一个独立的数据搬运处理器</strong>。它挂在 AHB 总线（主干道）上，能自己访问内存和外设。CPU 是经理，DMA 是搬运工：<strong>经理填一张搬运单（源地址、目的地址、数量），搬运工自己搬，搬完敲门汇报</strong>。期间经理想干嘛干嘛。</p>
<h2 id="%E4%BA%8C%E3%80%81dma-%E5%8E%9F%E7%90%86%E8%A6%81%E7%82%B9">二、DMA 原理要点</h2>
<h3 id="%E6%90%AC%E8%BF%90%E5%8D%95%E5%85%AD%E8%A6%81%E7%B4%A0">搬运单六要素</h3>
<p>配置一次 DMA 传输 = 填这张单子：</p>
<table>
 <thead>
  <tr>
   <th>字段</th>
   <th>含义</th>
   <th>串口发送的例子</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>源地址</td>
   <td>从哪搬</td>
   <td>内存里那个字符串数组</td>
  </tr>
  <tr>
   <td>目的地址</td>
   <td>搬到哪</td>
   <td>USART1 的 DR 寄存器（固定不变）</td>
  </tr>
  <tr>
   <td>数量</td>
   <td>搬多少个</td>
   <td>字符串长度</td>
  </tr>
  <tr>
   <td>数据宽度</td>
   <td>每件多大</td>
   <td>Byte（半字/字用于 16/32 位数据）</td>
  </tr>
  <tr>
   <td>方向</td>
   <td>P2M / M2P / M2M</td>
   <td>M2P（内存→外设）</td>
  </tr>
  <tr>
   <td>增量</td>
   <td>每搬一件地址走不走</td>
   <td>内存地址 +1（数组递增），外设地址不动（永远是 DR）</td>
  </tr>
 </tbody>
</table>
<h3 id="%E8%AF%B7%E6%B1%82%EF%BC%88request%EF%BC%89%EF%BC%9A%E6%90%AC%E8%BF%90%E7%9A%84%E8%8A%82%E5%A5%8F%E6%8E%A7%E5%88%B6%E5%99%A8">请求（Request）：搬运的节奏控制器</h3>
<p>DMA 不是一口气把数据砸进外设，而是<strong>盯着外设的"空了"信号</strong>——串口每发完一字节，TXE 标志变空 → 硬件向 DMA 发一个请求 → DMA 搬一字节进 DR。<strong>传输速率被外设节流</strong>（115200 的节奏），DMA 只是随叫随到。</p>
<h3 id="f1-%E7%9A%84%E9%80%9A%E9%81%93%E6%98%A0%E5%B0%84%E6%98%AF%E5%9B%BA%E5%AE%9A%E6%AD%BB%E7%9A%84">F1 的通道映射是固定死的</h3>
<p>DMA1 有 7 个通道、DMA2 有 5 个，每个通道同一时刻只能服务一个外设，<strong>对应关系查参考手册的映射表，不能自由组合</strong>：USART1_TX 固定走 DMA1 通道 4、USART1_RX 固定走 DMA1 通道 5。（F4 以后的 Stream/Request 才可以任意组合。）</p>
<h3 id="%E5%85%AD%E9%97%AE%E9%80%9F%E7%AD%94">六问速答</h3>
<p>挂哪条总线：<strong>AHB</strong>（第一个住 AHB 楼层的外设，这是它能"直接访问内存"的结构原因）；门控开关：RCC_AHBENR 的 DMA1EN；两种模式：Normal（搬一批停）和 Circular（循环，流式场景）。</p>
<h2 id="%E4%B8%89%E3%80%81%E5%AE%9E%E9%AA%8C-4.1%EF%BC%9A%E9%98%BB%E5%A1%9E-vs-dma-%E5%BD%93%E9%9D%A2%E5%AF%B9%E5%86%B3">三、实验 4.1：阻塞 vs DMA 当面对决</h2>
<h3 id="%E5%AE%9E%E9%AA%8C%E8%AE%BE%E8%AE%A1%EF%BC%88%E5%90%AB%E4%B8%80%E6%AC%A1%E9%87%8D%E8%A6%81%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%BF%AE%E6%AD%A3%EF%BC%89">实验设计（含一次重要的设计修正）</h3>
<p>同一个 420 字节的字符串，两种发法，<strong>用 CPU 计数器当裁判</strong>。</p>
<p>最初的设计是：阻塞发送后打印计数（=0），DMA 发送期间 CPU 数数（=几十万）。但我自己发现了这个设计的漏洞：<strong>阻塞版的"计数=0"其实是"压根没人数"，不是"想数但数不动"，没有说服力</strong>。</p>
<p>关键洞察：<strong>你恰恰没法在阻塞期间加计数</strong>——CPU 被关在 <code>HAL_UART_Transmit</code> 函数里出不来，一行计数代码都执行不了，"加不进计数"本身就是"阻塞"的定义。</p>
<p>所以要严谨地展示"CPU 被占死"，得请一个<strong>不受阻塞影响的观察者</strong>——中断。现成的是 SysTick（每 1ms 中断一次，主循环卡死它照跑），用它计时，主循环计数器量工作量，两个维度一起打印：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">char msg[512];
int n = 0;
for (int i = 0; i &lt; 10; i++)                                   // 填 10 行 ≈ 420 字节
    n += sprintf(&amp;msg[n], "DMA搬运演示第%d行-0123456789ABCDEFG\r\n", i);
while (1)
{
    // ① 阻塞发送：SysTick 计时（中断观察者，主循环卡死它照跑）
    uint32_t t0 = HAL_GetTick();
    cpu_count = 0;                                // 想数数？看看数不数得起来
    HAL_UART_Transmit(&amp;huart1, (uint8_t *)msg, n, 100);
    // 此刻 cpu_count 必然还是 0——CPU 在函数里出不来
    printf("&gt;&gt;&gt; 阻塞发送 %d 字节：耗时 %lu ms，CPU 计数 = %lu（想数也数不了）\r\n\r\n",
           n, (unsigned long)(HAL_GetTick() - t0), (unsigned long)cpu_count);

    HAL_Delay(1000);

    // ② DMA 发送：CPU 扔下地址就走，期间数数
    uint32_t t1 = HAL_GetTick();
    cpu_count = 0;
    HAL_UART_Transmit_DMA(&amp;huart1, (uint8_t *)msg, n);   // 立即返回！
    while (huart1.gState != HAL_UART_STATE_READY)        // DMA 还在搬
        cpu_count++;                                     // CPU 闲着数圈
    printf("&gt;&gt;&gt; DMA 发送 %d 字节：耗时 %lu ms，CPU 计数 = %lu（全程自由）\r\n\r\n",
           n, (unsigned long)(HAL_GetTick() - t1), (unsigned long)cpu_count);

    HAL_Delay(1000);
}
</code></pre>
</shiki-code>
<h3 id="cubemx-%E9%85%8D%E7%BD%AE">CubeMX 配置</h3>
<p>USART1 → DMA Settings → Add → <strong>USART1_TX</strong>：Channel=DMA1_Channel4（自动）、Direction=Memory to Peripheral、Mode=<strong>Normal</strong>、Memory Increment ✅、Peripheral Increment ❌、Data Width=Byte。NVIC 勾 <strong>DMA1 channel 4</strong> 和 <strong>USART1</strong> 两个中断。</p>
<h3 id="%E5%AE%9E%E6%B5%8B%E6%95%B0%E6%8D%AE%E4%B8%8E%E7%BB%93%E8%AE%BA">实测数据与结论</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>&gt;&gt;&gt; 阻塞发送 420 字节：耗时 38 ms，CPU 计数 = 0（想数也数不了）
&gt;&gt;&gt; DMA 发送 420 字节：耗时 38 ms，CPU 计数 = 101437（全程自由）
</code></pre>
</shiki-code>
<p><strong>两行对比的完整解读</strong>：耗时几乎一样（都被 115200 波特率定死——<strong>DMA 买不回速度</strong>）；差别全在 CPU 计数——<strong>DMA 买回的是 CPU 那 38ms 的自由时间</strong>。一个维度证"速度没变"，一个维度证"占用天差地别"。</p>
<h3 id="%E6%96%B9%E6%B3%95%E8%AE%BA%E6%B2%89%E6%B7%80">方法论沉淀</h3>
<p><strong>要证明"某段代码被卡死"，不能用它自己计数（它数不了），要用不受它影响的观察者（中断/定时器）</strong>。以后调试"主循环是不是被某行代码拖死了"，同一招：SysTick 打点前后对比。</p>
<h2 id="%E5%9B%9B%E3%80%81%E5%AE%9E%E9%AA%8C-4.2%EF%BC%9Adma-%E6%8E%A5%E6%94%B6-%2B-idle-%E7%A9%BA%E9%97%B2%E6%96%AD%E5%B8%A7">四、实验 4.2：DMA 接收 + IDLE 空闲断帧</h2>
<h3 id="%E9%97%AE%E9%A2%98%EF%BC%9Adma-%E5%8F%AA%E7%AE%A1%E5%9F%8B%E5%A4%B4%E6%90%AC%EF%BC%8C%E6%80%8E%E4%B9%88%E7%9F%A5%E9%81%93%22%E4%B8%80%E5%B8%A7%E7%BB%93%E6%9D%9F%E4%BA%86%22%EF%BC%9F">问题：DMA 只管埋头搬，怎么知道"一帧结束了"？</h3>
<p>DMA 接收时字节悄无声息地进缓冲区，CPU 睡着了——谁来叫醒它？答案在 UART 硬件里：<strong>IDLE 空闲检测</strong>。</p>
<h3 id="idle%EF%BC%9A%E7%A1%AC%E4%BB%B6%E8%87%AA%E5%B8%A6%E7%9A%84%22%E6%96%AD%E5%8F%A5%E5%99%A8%22">IDLE：硬件自带的"断句器"</h3>
<p>串口有个天然规律：<strong>一帧数据一口气发完</strong>（字节间隔 &lt; 1 字符时间），<strong>帧与帧之间有静默</strong>。UART 硬件的 IDLE 标志：<strong>收到数据后总线静默满一个字符时间（115200 下约 87µs），IDLE 置位</strong>——这是硬件替你数的"句号"。</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>AA 55 02 01 01 02 ──────静默 87µs────── 下一帧...
└───────────────┘ └── IDLE 中断! ──┘
 DMA 逐字节默默搬
                  ↑ CPU 只在这里醒一次
</code></pre>
</shiki-code>
<p><strong>整个接收过程：6 个字节 = 0 次中断；帧结束 = 1 次 IDLE 中断</strong>——中断数量从"字节数"降到"帧数"，这就是 DMA 高速接收的全部秘密。</p>
<h3 id="%E4%B8%8D%E5%AE%9A%E9%95%BF%E7%9A%84%E9%AD%94%E6%B3%95">不定长的魔法</h3>
<p>IDLE 中断里 CPU 醒来后的第一问：这帧多长？<strong>DMA 的计数器就是答案</strong>：</p>
<blockquote>
 <p>本次到达字节数 Size = 缓冲区大小 − DMA 剩余计数（CNDTR）</p>
</blockquote>
<p>发 6 字节算 6，发 60 字节算 60——<strong>不用事先约定长度，来多少算多少</strong>，这就是"不定长接收"。HAL 已把这整套打包成 <code>HAL_UARTEx_ReceiveToIdle_DMA</code>，事件回调直接给算好的 Size。</p>
<h3 id="%E6%9E%B6%E6%9E%84%EF%BC%9A%E4%B8%80%E5%B8%A7%E6%95%B0%E6%8D%AE%E7%9A%84%E5%AE%8C%E6%95%B4%E6%97%85%E7%A8%8B">架构：一帧数据的完整旅程</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>PC 串口助手(HEX)
   │  AA 55 02 01 01 02
   ▼
PA10(RX) ──► USART1 DR ──► [DMA1_CH5 硬件搬运] ──► rxDmaBuf[0..5]
   │              每字节：RXNE→硬件DMA请求→搬一字节→CNDTR减一（0中断）
   ├────────── 87µs ──────────┐
   ▼                          ▼
静默满 → IDLE 置位 ──► IDLE 中断（每帧仅 1 次）
              │ Size = 64 − CNDTR = 6
              ▼
   HAL_UARTEx_RxEventCallback(huart, Size)
              │
              ▼
   frame_parse(rxDmaBuf, Size)   ← 协议层，与模块2.3一字未改
              │ 五状态机推进
              ▼
   frame_exec：LED 点亮 + ACK 经 DMA 回发
              │
              ▼
   重新武装 ReceiveToIdle_DMA ──► 待命等下一帧
</code></pre>
</shiki-code>
<h3 id="%E6%A0%B8%E5%BF%83%E4%BB%A3%E7%A0%81">核心代码</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">// 启动（USER CODE 2）：
HAL_UARTEx_ReceiveToIdle_DMA(&amp;huart1, rxDmaBuf, RXBUF_SIZE);

// IDLE 事件回调（USER CODE 4）：
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart-&gt;Instance == USART1 &amp;&amp;
        huart-&gt;RxEventType == HAL_UART_RXEVENT_IDLE) {
        frame_parse(rxDmaBuf, Size);                        // Size=本次字节数(不定长)
        HAL_UARTEx_ReceiveToIdle_DMA(huart, rxDmaBuf, RXBUF_SIZE);  // 重新武装
    }
}
</code></pre>
</shiki-code>
<p>（<code>frame_parse</code>/<code>frame_exec</code> 是模块 2.3 的五状态机解析器<strong>原封不动</strong>搬过来，仅 ACK 回发从阻塞改成 <code>HAL_UART_Transmit_DMA</code>。）</p>
<h3 id="%E9%AA%8C%E6%94%B6%EF%BC%9A%E4%BA%94%E8%BF%9E%E6%B5%8B%E8%AF%95%EF%BC%88%E4%B8%B2%E5%8F%A3%E5%8A%A9%E6%89%8B-hex-%E6%A8%A1%E5%BC%8F%EF%BC%89">验收：五连测试（串口助手 HEX 模式）</h3>
<table>
 <thead>
  <tr>
   <th>测试</th>
   <th>发送</th>
   <th>预期</th>
   <th>结果</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>开灯</td>
   <td><code>AA 55 02 01 01 02</code></td>
   <td>LED 亮 + 回传同帧</td>
   <td>✅</td>
  </tr>
  <tr>
   <td>关灯</td>
   <td><code>AA 55 02 01 00 03</code></td>
   <td>LED 灭 + 回传同帧</td>
   <td>✅</td>
  </tr>
  <tr>
   <td>坏 CRC</td>
   <td><code>AA 55 02 01 01 99</code></td>
   <td>无动作无回传</td>
   <td>✅</td>
  </tr>
  <tr>
   <td>垃圾+好帧</td>
   <td><code>12 34 56 AA 55 02 01 01 02</code></td>
   <td>重新同步，灯亮+回帧</td>
   <td>✅</td>
  </tr>
  <tr>
   <td>双 AA</td>
   <td><code>AA AA 55 02 01 01 02</code></td>
   <td>第二个 AA 识别为真帧头</td>
   <td>✅</td>
  </tr>
 </tbody>
</table>
<p><strong>最有价值的发现：协议层一行没改，五连全过</strong>——搬运层从"每字节中断"换装成"DMA 批量"，解析器毫无感觉。<strong>分层解耦的活证据：换一层，其他层无感</strong>。</p>
<h3 id="%E4%B8%8E%E6%A8%A1%E5%9D%97-2-%E7%9A%84%E8%B4%A6%E6%9C%AC%E5%AF%B9%E6%AF%94%EF%BC%88%E6%AF%8F%E5%B8%A7-6-%E5%AD%97%E8%8A%82%EF%BC%89">与模块 2 的账本对比（每帧 6 字节）</h3>
<table>
 <thead>
  <tr>
   <th></th>
   <th>2.2/2.3（RXNE 每字节中断）</th>
   <th>4.2（DMA+IDLE）</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>RX 中断次数</td>
   <td>6</td>
   <td><strong>1</strong></td>
  </tr>
  <tr>
   <td>CPU 搬字节数</td>
   <td>6</td>
   <td><strong>0</strong></td>
  </tr>
  <tr>
   <td>TX 回 ACK</td>
   <td>阻塞（CPU 等）</td>
   <td>DMA（CPU 走人）</td>
  </tr>
  <tr>
   <td>满速连续流</td>
   <td>11520 中断/秒</td>
   <td>11520÷帧长 次/秒</td>
  </tr>
 </tbody>
</table>
<h2 id="%E4%BA%94%E3%80%81%E8%B8%A9%E5%9D%91%E5%AE%9E%E5%BD%95%EF%BC%9Amx_dma_init-%E7%BC%BA%E5%A4%B1%EF%BC%88%E6%9C%AC%E6%AC%A1%E6%9C%80%E5%80%BC%E9%92%B1%E7%9A%84%E6%95%99%E8%AE%AD%EF%BC%89">五、踩坑实录：MX_DMA_Init 缺失（本次最值钱的教训）</h2>
<h3 id="%E7%8E%B0%E8%B1%A1">现象</h3>
<p>烧录后串口只打了一轮"阻塞发送…计数=0"+10 行数据，然后<strong>永久冻结</strong>。用调试器读 CPU 计数变量：<strong>单调增长到 840 万、从不归零</strong>——程序卡死在 DMA 等待循环里。</p>
<h3 id="%E8%AF%8A%E6%96%AD%E9%93%BE%EF%BC%88gdb-%E7%AC%A6%E5%8F%B7%E7%BA%A7%E8%B0%83%E8%AF%95%EF%BC%8C%E5%85%A8%E7%A8%8B%E8%BF%9C%E7%A8%8B%EF%BC%89">诊断链（gdb 符号级调试，全程远程）</h3>
<table>
 <thead>
  <tr>
   <th>步骤</th>
   <th>读数</th>
   <th>推理</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td><code>p huart1.gState</code></td>
   <td>0x21 = BUSY_TX</td>
   <td>卡在 DMA 发送等待 ✓</td>
  </tr>
  <tr>
   <td><code>p huart1.TxXferCount</code></td>
   <td>420 = 全量</td>
   <td>一个字节都没发出去</td>
  </tr>
  <tr>
   <td>DMA1 所有寄存器</td>
   <td><strong>全 0</strong></td>
   <td>DMA 没通电——配置写进没通电的寄存器=石沉大海</td>
  </tr>
  <tr>
   <td>NVIC ISER0</td>
   <td>0（CH4 中断没使能）</td>
   <td>dma.c 里的使能代码<strong>从没执行过</strong></td>
  </tr>
  <tr>
   <td>NVIC ISER1</td>
   <td>bit5=1（USART1 使能了）</td>
   <td>usart.c 的 MspInit 跑了</td>
  </tr>
  <tr>
   <td>查 main.c 初始化序列</td>
   <td>只有 GPIO→USART1</td>
   <td><strong>根本没有 <code>MX_DMA_Init()</code> 调用！</strong></td>
  </tr>
 </tbody>
</table>
<h3 id="%E6%A0%B9%E5%9B%A0">根因</h3>
<p><strong>CubeMX 给已有工程加 DMA 后重新生成，偶尔不往 main.c 插 <code>MX_DMA_Init()</code> 调用</strong>——dma.c 里时钟使能、中断使能都生成得好好的，但没人调用它们，DMA 房间没通电。症状极具迷惑性：编译零错误、<code>HAL_UART_Transmit_DMA</code> 返回 OK、然后永远等不到完成。</p>
<h3 id="%E4%BF%AE%E5%A4%8D">修复</h3>
<p>main.c 补两处（<code>MX_DMA_Init</code> 必须在 USART 之前——MspInit 要写 DMA 寄存器）：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">#include "dma.h"
...
MX_GPIO_Init();
MX_DMA_Init();              // DMA 时钟+中断使能
MX_USART1_UART_Init();
</code></pre>
</shiki-code>
<p>修复后复测：<code>gState</code> = 0x20（READY，正常循环）、<code>cpu_count</code> = 101437（DMA 期间 CPU 自由计数）——复活。</p>
<h3 id="%E9%98%B2%E5%A4%8D%E5%8F%91">防复发</h3>
<p><strong>以后每次 CubeMX 重新生成，git diff 里专门盯一眼 <code>MX_DMA_Init()</code> 还在不在</strong>——被吃掉就补回来。</p>
<h3 id="%E6%AC%A1%E7%BA%A7%E7%9F%A5%E8%AF%86%E7%82%B9">次级知识点</h3>
<p>HAL 的 DMA 发送完成走<strong>两步</strong>：DMA 中断（数据搬完进 DR）→ <strong>USART TC 中断</strong>（最后一位移出移位寄存器）→ gState=READY。所以 <strong>USART1 的 NVIC 也必须勾</strong>，只勾 DMA 通道中断不够。</p>
<h2 id="%E5%85%AD%E3%80%81%E8%B0%83%E8%AF%95%E6%89%8B%E6%B3%95%E6%B2%89%E6%B7%80%EF%BC%88%E6%9C%AC%E6%A8%A1%E5%9D%97%E6%96%B0%E5%A2%9E%EF%BC%89">六、调试手法沉淀（本模块新增）</h2>
<ul>
 <li><strong>gdb 符号级读句柄</strong>：<code>p/x huart1.gState</code>、<code>p/x huart1.TxXferCount</code>——比猜寄存器位准得多</li>
 <li><strong>NVIC ISER 读中断使能实况</strong>：ISER0/ISER1 对照 IRQ 号（DMA1_CH4=14、USART1=37）</li>
 <li><strong>"没通电"的识别</strong>：外设寄存器全零 + 配置写入无效 = 时钟没开</li>
 <li><strong>证明卡死用独立观察者</strong>：SysTick/定时器打点，别让被卡死的代码自己数</li>
</ul>
<h2 id="%E4%B8%83%E3%80%81%E4%B8%80%E5%8F%A5%E8%AF%9D%E7%9F%A5%E8%AF%86%E7%82%B9%E6%B8%85%E5%8D%95">七、一句话知识点清单</h2>
<ul>
 <li>DMA=独立搬运工：CPU 填单（源/目的/数量/宽度/方向/增量），DMA 搬完敲门</li>
 <li>DMA1/DMA2 挂 AHB，门控 RCC_AHBENR——第一个 AHB 楼层外设</li>
 <li>F1 通道映射固定：USART1_TX=CH4、RX=CH5，查表不可选</li>
 <li>请求节流：速率被外设定死——<strong>DMA 买的是 CPU 自由时间，不是速度</strong></li>
 <li>IDLE=硬件断句器：静默 1 字符时间置位，帧边界的物理本质</li>
 <li>不定长 Size = 缓冲区大小 − CNDTR</li>
 <li><code>HAL_UARTEx_ReceiveToIdle_DMA</code> + <code>RxEventCallback</code>；Normal 模式 IDLE 后必须 re-arm</li>
 <li>DMA TX 完成两步走：DMA 中断 + USART TC 中断——两个 NVIC 都要勾</li>
 <li><code>MX_DMA_Init</code> 必须在用 DMA 的外设之前调用</li>
 <li>CubeMX 偶发不插 MX_DMA_Init——每次生成 git diff 盯梢</li>
 <li>分层解耦：搬运层升级，协议层免疫（2.3 解析器零改动上 DMA）</li>
 <li>证明卡死要用不受影响的观察者（SysTick），不能让卡死者自己数</li>
</ul>
<hr>
<p><em>模块 4 实验记录 · 2026-10-04 · STM32F103ZET6 最小系统板 · 工程名 usart_dma</em></p>]]></description><guid isPermaLink="false">/archives/dma-usart-experiment-4</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Sun, 4 Oct 2026 03:00:29 GMT</pubDate></item><item><title><![CDATA[STM32 定时器测量：输入捕获、编码器接口与高级定时器实验全记录]]></title><link>https://www.flor.cn/archives/stm32-timer-3b-input-capture-encoder-advanced</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=STM32%20%E5%AE%9A%E6%97%B6%E5%99%A8%E6%B5%8B%E9%87%8F%EF%BC%9A%E8%BE%93%E5%85%A5%E6%8D%95%E8%8E%B7%E3%80%81%E7%BC%96%E7%A0%81%E5%99%A8%E6%8E%A5%E5%8F%A3%E4%B8%8E%E9%AB%98%E7%BA%A7%E5%AE%9A%E6%97%B6%E5%99%A8%E5%AE%9E%E9%AA%8C%E5%85%A8%E8%AE%B0%E5%BD%95&amp;url=/archives/stm32-timer-3b-input-capture-encoder-advanced" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p>平台：STM32F103ZET6 @ 72MHz · 工程名 pwm_measure（一个工程跑完三个实验）· 串口 115200（UTF-8）
  <br>
  前置知识：模块 3A（时基、输出比较、PWM 呼吸灯）</p>
</blockquote>
<h2 id="%E5%BC%95%E8%A8%80%EF%BC%9A%E4%BB%8E%E2%80%9C%E4%BA%A7%E7%94%9F%E4%BF%A1%E5%8F%B7%E2%80%9D%E5%88%B0%E2%80%9C%E6%B5%8B%E9%87%8F%E4%BF%A1%E5%8F%B7%E2%80%9D">引言：从“产生信号”到“测量信号”</h2>
<p>模块 3A 让定时器<strong>输出</strong>信号（PWM 点灯），模块 3B 把方向反过来——用定时器<strong>测量</strong>外部信号：测一个 PWM 波形的周期和占空比、数旋钮转了多少格、给电机驱动补上安全机制。三个实验共用同一个工程 <code>pwm_measure</code>，一个实验叠一个实验，最后三套功能在同一块板上和平共处——这也是真实项目的形态。</p>
<hr>
<h2 id="%E4%B8%80%E3%80%813b.1-%E8%BE%93%E5%85%A5%E6%8D%95%E8%8E%B7%EF%BC%9A%E9%80%A0%E4%B8%80%E5%8F%B0-pwm-%E6%B5%8B%E9%87%8F%E4%BB%AA">一、3B.1 输入捕获：造一台 PWM 测量仪</h2>
<h3 id="1.1-%E5%AE%9E%E9%AA%8C%E7%9B%AE%E6%A0%87">1.1 实验目标</h3>
<p>测一个未知信号的<strong>周期</strong>和<strong>占空比</strong>，串口每秒打印结果——这就是一台“PWM 测量仪”。它是很多应用的地基：测转速（编码器脉冲）、超声波回波计时、红外遥控解码、频率计。</p>
<h3 id="1.2-%E5%8E%9F%E7%90%86%EF%BC%9A%E7%BB%99%E5%AE%9A%E6%97%B6%E5%99%A8%E8%A3%85%E2%80%9C%E7%A1%AC%E4%BB%B6%E5%BF%AB%E9%97%A8%E2%80%9D">1.2 原理：给定时器装“硬件快门”</h3>
<p>朴素思路是“中断+软件时间戳”，但 CPU 响应中断有微秒级抖动（可能被别的事耽误），测出来的时刻不准。</p>
<p><strong>输入捕获（IC）= 硬件快门</strong>：捕获通道盯着引脚，设定的边沿（上升/下降）一到达，<strong>硬件瞬间把当前 CNT 的值拍进 CCR</strong>——零延迟、零抖动，然后才置标志、发中断。你在回调里取走的不是“现在几点”，而是“边沿到达那一刻的照片”。</p>
<p>测量公式：</p>
<ul>
 <li><strong>周期</strong> = 两次上升沿快照之差</li>
 <li><strong>高电平时长</strong> = 上升沿快照 − 下降沿快照</li>
 <li><strong>占空比</strong> = 高电平时长 ÷ 周期</li>
</ul>
<p><strong>F1 的硬件限制</strong>：捕获极性只有上升/下降两档，<strong>没有双沿捕获</strong>（双沿是 F2/F4 之后新系列才加的，CubeMX 里找不到 Both Edges 不是操作问题，是芯片能力）。想要双沿效果 → <strong>双通道分工</strong>：CH2 守上升沿测周期、CH3 守下降沿测脉宽——这本来就是仪器级的标准做法。</p>
<p><strong>16 位回绕</strong>：CNT 数到 65535 会归零。只要信号周期小于定时器溢出周期，用 <strong><code>(uint16_t)(now - last)</code></strong> 的 16 位减法就能自动处理回绕（数学上 mod 65536）。</p>
<h3 id="1.3-%E5%AE%9E%E9%AA%8C%E8%AE%BE%E8%AE%A1%EF%BC%9A%E4%BF%A1%E5%8F%B7%E6%BA%90-%2B-%E6%B5%8B%E9%87%8F%E4%BB%AA%EF%BC%8C%E8%87%AA%E5%B7%B1%E6%B5%8B%E8%87%AA%E5%B7%B1">1.3 实验设计：信号源 + 测量仪，自己测自己</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>TIM3_CH3 ──PB0──┬──→ PA1 (TIM2_CH2, 上升沿捕获) → 测周期
                │ 两根杜邦线
                └──→ PA2 (TIM2_CH3, 下降沿捕获) → 测高电平时长
</code></pre>
</shiki-code>
<p>三个设计决策：</p>
<ol>
 <li><strong>信号源用已知参数</strong>：TIM3 输出 1Hz、50% PWM（PSC=7199、ARR=9999、Pulse=5000）。测量仪读数必须精确等于已知答案——这叫<strong>自校准</strong>：用已知校准未知，是仪器思维的第一课</li>
 <li><strong>测量仪必须用另一个定时器</strong>：TIM3 的 CNT 正被自己的 PWM 用着，相位完全同步——上升沿永远发生在 CNT 同一位置，周期会测成 0（混叠）。信号源和测量仪必须<strong>时钟独立</strong></li>
 <li><strong>分辨率与量程的取舍</strong>：测量仪 TIM2 用 PSC=7199（每格 0.1ms）+ ARR=65535（量程 6.55 秒）——覆盖 1 秒信号绰绰有余，回绕减法也成立</li>
</ol>
<h3 id="1.4-cubemx-%E9%85%8D%E7%BD%AE">1.4 CubeMX 配置</h3>
<table>
 <thead>
  <tr>
   <th>项</th>
   <th>配置</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>TIM3（信号源）</td>
   <td>Clock Source=Internal；Channel3=<strong>PWM Generation CH3</strong>；PSC=<strong>7199</strong>、ARR=<strong>9999</strong>、Pulse=<strong>5000</strong></td>
  </tr>
  <tr>
   <td>TIM2（测量仪）</td>
   <td>Channel2=IC Direct（<strong>Rising Edge</strong>）；Channel3=IC Direct（<strong>Falling Edge</strong>）；PSC=<strong>7199</strong>、Counter Period=<strong>65535</strong></td>
  </tr>
  <tr>
   <td>USART1</td>
   <td>Asynchronous，115200（打印结果用）</td>
  </tr>
  <tr>
   <td>NVIC</td>
   <td><strong>TIM2 global interrupt</strong> 打勾（TIM3 不勾——PWM 零中断）</td>
  </tr>
  <tr>
   <td>硬件</td>
   <td>杜邦线两根：PB0→PA1、PB0→PA2</td>
  </tr>
 </tbody>
</table>
<h3 id="1.5-%E6%A0%B8%E5%BF%83%E4%BB%A3%E7%A0%81">1.5 核心代码</h3>
<p>启动（USER CODE 2）：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">HAL_TIM_PWM_Start(&amp;htim3, TIM_CHANNEL_3); // 信号源：PB0 输出 1Hz 50% PWM
HAL_TIM_IC_Start_IT(&amp;htim2, TIM_CHANNEL_2); // CH2 上升沿：测周期
HAL_TIM_IC_Start_IT(&amp;htim2, TIM_CHANNEL_3); // CH3 下降沿：测高电平时长
</code></pre>
</shiki-code>
<p>捕获回调（USER CODE 4）：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim){
    if(htim-&gt;Instance != TIM2) return;
    if(htim-&gt;Channel == HAL_TIM_ACTIVE_CHANNEL_2){ // 上升沿 → 周期
        uint16_t now = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
        period = (uint16_t)(now - cap_rise); // 两次上升沿之差(16位减法自动处理回绕)
        cap_rise = now;
        ready = 1; // 一整周期凑齐
    }else if(htim-&gt;Channel == HAL_TIM_ACTIVE_CHANNEL_3){ // 下降沿 → 高电平时长
        uint16_t now = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_3);
        width = (uint16_t)(now - cap_rise); // 上升→下降之差
    }
}
</code></pre>
</shiki-code>
<p>主循环每秒打印 <code>周期/高/占空比</code> 三件套（printf 重定向用模块 2 的 <code>__io_putchar</code> 老配方，记得 Includes 区加 <code>#include &lt;stdio.h&gt;</code>）。</p>
<h3 id="1.6-%E7%8E%B0%E8%B1%A1%E4%B8%8E%E9%AA%8C%E8%AF%81">1.6 现象与验证</h3>
<ul>
 <li>串口每秒一行：<code>周期=1000 ms 高=500 ms 占空比=50%</code>——和信号源已知值精确吻合，自校准通过</li>
 <li>周期在 999/1000 之间偶尔跳变——<strong>末位量化误差</strong>：信号边沿恰好落在 0.1ms 刻度线附近，捕获有时差一拍。0.1ms 分辨率测 1 秒信号理论精度 ±0.01%，真实仪器的末位跳变就是这么来的</li>
 <li><strong>实时跟踪验收</strong>：用调试器现场把信号源的 CCR3 从 5000 改成 2500（一条 <code>mww</code> 命令，不重新烧录），测量仪下一秒立刻显示 <code>高=250 ms 占空比=25%</code>——仪器追上了活的信号变化</li>
</ul>
<h3 id="1.7-%E8%B8%A9%E5%9D%91%E5%AE%9E%E5%BD%95%EF%BC%88%E8%BF%99%E8%8A%82%E8%B8%A9%E5%BE%97%E6%9C%80%E5%AF%86%E9%9B%86%EF%BC%89">1.7 踩坑实录（这节踩得最密集）</h3>
<ol>
 <li><strong>F1 无双沿捕获</strong>：CubeMX 里 Polarity 只有 Rising/Falling——换双通道分工</li>
 <li><strong>主循环代码放错区被吃</strong>：代码写在 <code>USER CODE END WHILE</code> 和 <code>BEGIN 3</code> 之间的<strong>缝隙</strong>（模板自留地），CubeMX 重新生成时被还原——加 USART1 重新生成后 printf 循环凭空消失。正确位置：<code>BEGIN 3</code> 之后</li>
 <li><strong>TIM2 PSC 填成 72</strong>：tick 变成约 1µs，1 秒信号跨 15 次 16 位回绕，减法只救一次——周期打印成 326ms 的乱数（实测变量 period=3261，铁证）</li>
 <li><strong>CH3 极性忘改 Falling</strong>：两个通道都守上升沿，CH2/CH3 在同一个上升沿同时触发，CH2 先算周期更新 cap_rise，CH3 随后算 width=now−cap_rise=<strong>0</strong>——占空比永远 0%（实测 width=0）</li>
 <li><strong>USART1 没配</strong>：<code>huart1</code> 未声明，编译直接报错</li>
 <li><strong>串口中文乱码</strong>：固件 printf 是 UTF-8、串口助手默认 GBK——数字和 % 是 ASCII 两边一样所以能认出，中文全花。解法：助手切 UTF-8</li>
</ol>
<p><strong>排障方法论</strong>：变量不对时用调试器直接读运行中的变量（<code>nm</code> 查地址 + <code>mdw</code> 读值）——period=3261、width=0 两个值一出来，两个配置 bug 当场定位，比瞎猜快一百倍。</p>
<hr>
<h2 id="%E4%BA%8C%E3%80%813b.2-%E7%BC%96%E7%A0%81%E5%99%A8%E6%8E%A5%E5%8F%A3%EF%BC%9A%E8%AE%A9%E7%A1%AC%E4%BB%B6%E8%87%AA%E5%B7%B1%E6%95%B0%E6%97%8B%E9%92%AE">二、3B.2 编码器接口：让硬件自己数旋钮</h2>
<h3 id="2.1-%E5%AE%9E%E9%AA%8C%E7%9B%AE%E6%A0%87">2.1 实验目标</h3>
<p>读取旋转编码器的<strong>位置</strong>（累计计数）、<strong>方向</strong>（顺/逆时针）、<strong>速度</strong>（单位时间计数变化），串口实时打印。</p>
<h3 id="2.2-%E5%8E%9F%E7%90%86%EF%BC%9Aab-%E6%AD%A3%E4%BA%A4-%2B-%E7%A1%AC%E4%BB%B6%E8%A7%A3%E7%A0%81%E5%99%A8">2.2 原理：AB 正交 + 硬件解码器</h3>
<p>旋转编码器（EC11）转起来输出两路方波，玄机在<strong>相位差 90°</strong>：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>顺时针（A 超前 B 90°）：
A: ─┐ ┌─┐ ┌─
    └─┘ └─┘ └
B: ──┐ ┌─┐
     └─┘ └
逆时针（B 超前 A 90°）
</code></pre>
</shiki-code>
<ul>
 <li><strong>方向</strong>藏在“谁超前谁”里</li>
 <li><strong>位置</strong>藏在累计计数里（每圈 N 个脉冲，EC11 常见 20 个）</li>
 <li><strong>速度</strong> = 单位时间的计数变化</li>
</ul>
<p><strong>软件解码的痛</strong>：A、B 各接一个 EXTI 中断数边沿判相位——每圈几百次中断、机械触点抖动产生假边沿、判相位容易错。</p>
<p><strong>硬件编码器接口</strong>：定时器内置的<strong>正交解码器</strong>——A 接 CH1、B 接 CH2，硬件实时盯着两相位关系，<strong>每个有效边沿自动让 CNT +1 或 −1</strong>（顺时针加、逆时针减，方向判断零成本）。你随时读 CNT 就知道位置，<strong>零中断、零 CPU</strong>。这是“把数数交给硬件”的第三次登场（时基→PWM→编码器）。</p>
<p>三个关键机制：</p>
<ul>
 <li><strong>四倍频（x4）</strong>：Encoder Mode 选 "TI1 and TI2" = A、B 的<strong>每个边沿</strong>都计数 → 分辨率 ×4（20 脉冲/圈 = 80 计数/圈），转一格恰好 ±4——“四倍频”名字即来于此</li>
 <li><strong>PSC 失效</strong>：编码器模式下 CNT 的心跳来自<strong>外部边沿</strong>而不是内部时钟——预分频器形同虚设（不用设）</li>
 <li><strong>16 位双向回绕</strong>：顺转加到 65535 回绕到 0，逆转减到 0 回绕到 65535。读位置差用 <strong><code>(int16_t)(now - last)</code></strong>——有符号 16 位减法自动处理双向回绕，还白送一个符号（正=顺、负=逆）</li>
</ul>
<h3 id="2.3-%E5%AE%9E%E9%AA%8C%E8%AE%BE%E8%AE%A1">2.3 实验设计</h3>
<p>TIM4 编码器模式 + 主循环每 200ms 采样一次 CNT 算速度。<strong>引脚重映射实战</strong>：TIM4 编码器默认脚是 PB6/PB7，但配置时把引脚点到了 <strong>PD12/PD13</strong>（TIM4 重映射后的位置）——同一对通道的两个住址，CubeMX 自动生成 <code>__HAL_AFIO_REMAP_TIM4_ENABLE()</code>，和 PB6/PB7 完全等效。这无意中把 F1 的“引脚搬家”（AFIO 重映射）功能用了一遍。</p>
<p>编码器模块五脚（GND/VCC/A/B/C）：<strong>VCC→3.3V</strong>（别接 5V，模块上拉电平要和 STM32 一致）、GND→GND、A→PD12、B→PD13、<strong>C 是按键脚</strong>（按下导通，本实验不接，留给 3B.3 当刹车按钮）。</p>
<h3 id="2.4-cubemx-%E9%85%8D%E7%BD%AE">2.4 CubeMX 配置</h3>
<table>
 <thead>
  <tr>
   <th>项</th>
   <th>配置</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>TIM4</td>
   <td>Combined Channels → <strong>Encoder Mode</strong></td>
  </tr>
  <tr>
   <td>Encoder Mode</td>
   <td><strong>Encoder Mode TI1 and TI2</strong>（x4 四倍频）</td>
  </tr>
  <tr>
   <td>Input Filter（两通道）</td>
   <td><strong>15</strong>（机械触点抖动大，硬件滤波）</td>
  </tr>
  <tr>
   <td>Counter Period</td>
   <td><strong>65535</strong>（满量程，16 位回绕成立）</td>
  </tr>
  <tr>
   <td>引脚 GPIO</td>
   <td>PD12/PD13 设 <strong>Pull-up</strong>（编码器是开关输出，必须上拉；带板模块有板载上拉则双上拉无害）</td>
  </tr>
 </tbody>
</table>
<h3 id="2.5-%E6%A0%B8%E5%BF%83%E4%BB%A3%E7%A0%81">2.5 核心代码</h3>
<p>启动只要一行（无需中断）：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">HAL_TIM_Encoder_Start(&amp;htim4, TIM_CHANNEL_ALL); // 启动编码器接口: 硬件开始自动计数
</code></pre>
</shiki-code>
<p>主循环每 200ms 采样：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">int16_t now = (int16_t)__HAL_TIM_GET_COUNTER(&amp;htim4); // 读当前位置
int16_t speed = (int16_t)(now - enc_last); // 差值: 正=顺 负=逆 0=静止
enc_last = now;
printf("位置=%d 速度=%d %s\r\n", now, speed,
        speed&gt;0 ? "顺时针" : (speed&lt;0 ? "逆时针" : "静止"));
</code></pre>
</shiki-code>
<h3 id="2.6-%E7%8E%B0%E8%B1%A1%E4%B8%8E%E9%AA%8C%E8%AF%81">2.6 现象与验证</h3>
<p>串口每 200ms 一行，转动旋钮时：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>位置=-25 速度=1 顺时针
位置=-16 速度=8 顺时针
位置=-8 速度=0 静止
位置=-26 速度=-18 逆时针
</code></pre>
</shiki-code>
<p>调试器协同采样（云端定时读 CNT，用户同时转旋钮）：<code>+7 → −13 → +23 → −12 → +21 → −22 → +8 → +17 → +7</code>——<strong>双向计数实锤</strong>，数值在正负之间摆动对应来回旋转，停下归零。A/B 接反不会坏任何东西，只是方向反过来——对调两根线即可，编码器接线的日常。</p>
<h3 id="2.7-%E8%B8%A9%E5%9D%91%E5%AE%9E%E5%BD%95">2.7 踩坑实录</h3>
<ol>
 <li><strong>编码器没接就查代码</strong>：CNT 六次采样全 0，排查半天——其实是编码器根本还没接线。先确认物理连接再查软件</li>
 <li><strong>引脚住址困惑</strong>：资料说 TIM4 是 PD12/13，教程说 PB6/7——两个都对，一个是重映射后的住址。跟着固件实际配置接（看 CubeMX 引脚分配或生成代码里的 REMAP 宏）</li>
 <li><strong>打印刷屏</strong>：主循环 <code>HAL_Delay(10)</code> 没按设计改成 200——每秒 100 行串口刷成瀑布。采样周期决定打印节奏，200ms 才能看清</li>
</ol>
<hr>
<h2 id="%E4%B8%89%E3%80%813b.3-%E9%AB%98%E7%BA%A7%E5%AE%9A%E6%97%B6%E5%99%A8-tim1%EF%BC%9A%E4%BA%92%E8%A1%A5%E3%80%81%E6%AD%BB%E5%8C%BA%E4%B8%8E%E5%88%B9%E8%BD%A6">三、3B.3 高级定时器 TIM1：互补、死区与刹车</h2>
<h3 id="3.1-%E5%AE%9E%E9%AA%8C%E7%9B%AE%E6%A0%87">3.1 实验目标</h3>
<p>输出一对<strong>互补 PWM</strong>（驱动半桥上下管），插入<strong>死区时间</strong>（防直通），接上<strong>刹车输入</strong>（硬件急停）——电机控制的看家本领三件套。</p>
<h3 id="3.2-%E5%8E%9F%E7%90%86%EF%BC%9A%E4%B8%BA%E5%8D%8A%E6%A1%A5%E8%80%8C%E7%94%9F%E7%9A%84%E5%AE%89%E5%85%A8%E9%93%BE">3.2 原理：为半桥而生的安全链</h3>
<p>半桥是电机驱动的最小单元：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>     ──┬── 电源
 [上管] ← TIM1_CH1 (PE9)
       ├──── 中点 → 接电机
 [下管] ← TIM1_CH1N(PE8)
     ──┴── GND
</code></pre>
</shiki-code>
<p>上下管<strong>绝不能同时导通</strong>——同时导通=电源对地短路（直通），MOS 管瞬间烧毁。互补 PWM 保证逻辑上不同时通，<strong>但 MOS 管关断比开通慢</strong>——切换瞬间上管没关干净、下管已经开通，微秒级直通照样炸管。</p>
<p>三个硬件安全机制：</p>
<ul>
 <li><strong>互补输出</strong>：每个通道两根引脚（CHx/CHxN），输出极性相反，正好驱动上下两管</li>
 <li><strong>死区时间（DTG）</strong>：每次切换插入一段“双管全关”的空档，<strong>硬件按 tDTS（72MHz，13.9ns 步进）自动插入</strong>，不受 PSC 影响，软件零参与</li>
 <li><strong>刹车（BKIN）</strong>：一个引脚触发时<strong>硬件直接清 MOE 总闸</strong>（主输出使能），所有输出瞬间进安全态，不等软件反应——过流保护、急停按钮的最后一道保险丝</li>
</ul>
<h3 id="3.3-%E5%AE%9E%E9%AA%8C%E8%AE%BE%E8%AE%A1%EF%BC%9A%E7%AC%AC%E4%B8%89%E6%AC%A1%E2%80%9C%E8%87%AA%E5%B7%B1%E6%B5%8B%E8%87%AA%E5%B7%B1%E2%80%9D">3.3 实验设计：第三次“自己测自己”</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>TIM1_CH1(PE9) ──┬──→ PA1 (捕获仪CH2, 上升沿→周期)
                └──→ PA2 (捕获仪CH3, 下降沿→脉宽)
TIM1_CH1N(PE8) ──→ 空着（调试器采样电平验证互补性）
编码器 C 键 ──→ PE15 (BKIN 刹车)
</code></pre>
</shiki-code>
<p><strong>实验参数 vs 工程参数的取舍</strong>（重要思维）：真实电机用 20kHz PWM + 几百 ns 死区，但捕获仪最快 1µs 分辨率测不到——所以故意<strong>放慢 PWM（100Hz）、加大死区（14µs）</strong>让仪器能看见。实验参数为可测性服务，工程参数为效率服务，别混为一谈。</p>
<p>参数计算：</p>
<ul>
 <li>TIM1：PSC=71（1µs 粒度）、ARR=9999（10ms 周期=100Hz）、Pulse=5000（50%）、<strong>DTG=255（0xFF 编码 ≈ 14µs 死区）</strong></li>
 <li>捕获仪 TIM2：PSC 从 7199 改成 <strong>71</strong>（分辨率从 0.1ms 提到 1µs）</li>
 <li><strong>预期读数</strong>：周期=10000µs、高=<strong>4986µs</strong>——理想 5000 被死区啃掉 14 → <strong>死区被直接测出来</strong>；占空比 49%（死区吃掉 1% 占空比）</li>
</ul>
<p><strong>引脚重映射</strong>：TIM1 默认脚在板上全被占（CH1N=PA7 是 W25Q16、CH2=PA9 是串口、CH3=PA10 是串口、CH4=PA11 是 USB、BKIN=PA6 是 W25Q16）——<strong>全重映射到 PE 口</strong>：CH1=PE9、CH1N=PE8、BKIN=PE15（动手前先确认板上排针有引出）。</p>
<h3 id="3.4-cubemx-%E9%85%8D%E7%BD%AE">3.4 CubeMX 配置</h3>
<table>
 <thead>
  <tr>
   <th>项</th>
   <th>配置</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>TIM1 → Channel1</td>
   <td><strong>PWM Generation CH1 CH1N</strong>（互补模式）</td>
  </tr>
  <tr>
   <td>引脚（点击分配）</td>
   <td>PE9→TIM1_CH1、PE8→TIM1_CH1N、<strong>PE15→TIM1_BKIN</strong>（先分配 BKIN，刹车选项才出现）</td>
  </tr>
  <tr>
   <td>TIM1 时基</td>
   <td>PSC=<strong>71</strong>、ARR=<strong>9999</strong>、Pulse=<strong>5000</strong></td>
  </tr>
  <tr>
   <td>Break and Dead Time 分组</td>
   <td><strong>Dead Time=255</strong>、BRK Polarity=<strong>Low</strong>（按下=低=刹车）、Automatic Output State=<strong>Enable</strong>（松开自动恢复）、OSSI=<strong>Enable</strong>（刹车时引脚驱动到确定电平）</td>
  </tr>
  <tr>
   <td>PE15 GPIO</td>
   <td><strong>Pull-up</strong>（关键！见踩坑）</td>
  </tr>
  <tr>
   <td>TIM2</td>
   <td>Prescaler 7199→<strong>71</strong>（捕获仪提分辨率）</td>
  </tr>
  <tr>
   <td>接线</td>
   <td>PE9→PA1、PE9→PA2（从 PB0 挪线）、编码器 C 键→PE15</td>
  </tr>
 </tbody>
</table>
<p>刹车选项含义速查：<strong>BRK State</strong> 刹车电路电源开关；<strong>BRK Polarity</strong> 什么电平触发（按键按下接地→Low）；<strong>Automatic Output State</strong> 刹车消失后谁合闸（Enable=硬件自动，真实故障场景用 Disable 等软件确认）；<strong>OSSI/OSSR</strong> 输出关闭时引脚浮空还是驱动到确定电平（电机场景必开，防栅极浮空误触发）；<strong>Lock</strong> 给安全配置上锁防篡改。</p>
<h3 id="3.5-%E6%A0%B8%E5%BF%83%E4%BB%A3%E7%A0%81">3.5 核心代码</h3>
<p>互补输出要开<strong>两个</strong>开关（USER CODE 2）：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">HAL_TIM_PWM_Start(&amp;htim1, TIM_CHANNEL_1); // CH1 主输出(顺带开MOE总闸)
HAL_TIMEx_PWMN_Start(&amp;htim1, TIM_CHANNEL_1); // CH1N 互补输出(注意是Ex扩展函数)
</code></pre>
</shiki-code>
<p>printf 换微秒版（tick 已是 1µs，不用再除）：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">printf("周期=%u us 高=%u us 占空比=%u%%\r\n",
        period, width, width*100/period);
</code></pre>
</shiki-code>
<h3 id="3.6-%E7%8E%B0%E8%B1%A1%E4%B8%8E%E9%AA%8C%E8%AF%81">3.6 现象与验证</h3>
<ul>
 <li>串口：<code>周期=10000 us 高=4986 us 占空比=49%</code>——<strong>5000−4986=14µs 就是死区实测</strong>，与 DTG=255 的理论值精确命中</li>
 <li><strong>互补性铁证</strong>（调试器十次采样 PE8/PE9 电平）：<code>PE9=1,PE8=0 → PE8=1,PE9=0 → …</code> <strong>10/10 严格单高</strong>——两脚永不同时为高，互补+死区成立</li>
 <li><strong>刹车实测</strong>：按住编码器旋钮（C 键把 PE15 拉低）→ PWM 立即停止、打印冻结；松开 → AOE 自动合闸、打印恢复滚动</li>
</ul>
<h3 id="3.7-%E8%B8%A9%E5%9D%91%E5%AE%9E%E5%BD%95%EF%BC%9Abkin-%E6%82%AC%E7%A9%BA%E5%BC%95%E5%8F%91%E7%9A%84%E2%80%9C%E5%85%A8%E9%93%BE%E8%B7%AF%E5%81%87%E6%95%85%E9%9A%9C%E2%80%9D%EF%BC%88%E6%9C%AC%E6%A8%A1%E5%9D%97%E6%9C%80%E7%B2%BE%E5%BD%A9%E7%9A%84%E4%B8%80%E6%A1%88%EF%BC%89">3.7 踩坑实录：BKIN 悬空引发的“全链路假故障”（本模块最精彩的一案）</h3>
<p><strong>现象</strong>：烧录后测量值全 0、串口没有测量数据、输出引脚电平乱跳。</p>
<p><strong>排查链</strong>（每一步都有寄存器证据）：</p>
<ol>
 <li>读测量变量：period=0、width=0 → 捕获仪没收到边沿 → PE9 上没有波形</li>
 <li>读 TIM1_BDTR=0x54FF：死区/刹车/自动恢复配置全对，但 <strong>MOE=0——总闸被拉掉了</strong></li>
 <li>读 PE15 电平：<strong>低</strong> → 刹车处于触发状态（配的是低有效）</li>
 <li>因果链闭合：PE15 低 → 刹车 → 硬件清 MOE → 输出禁 → 无波形 → 测量 0。PE15 电平乱跳还导致刹车反复触发/AOE 反复合闸，MOE 振荡，输出时有时无</li>
 <li>查生成代码：PE15 是 <code>GPIO_MODE_INPUT + GPIO_NOPULL</code>——<strong>悬空</strong>！</li>
 <li>修复（配上拉）后仍读低 → 读 CRH（模式=上拉输入 ✓）+ ODR（拉的方向=上 ✓）+ IDR（实际电平=低 ✗）三连——内部上拉生效但读低 = <strong>外部有东西硬拉地</strong></li>
 <li><strong>判决性实验</strong>：拔掉 PE15 上的线 → 立刻变高、MOE=1、测量值 10000/4986 全部出现——实锤是那根线接着地（模块端插错孔）</li>
</ol>
<p><strong>教训</strong>：刹车电路对电平极度敏感，<strong>BKIN 必须接确定电平（上拉），绝不悬空</strong>——这本身就是刹车威力的最好反面教材。排查手法沉淀：CRH/ODR/IDR 三连读（配置生效了吗+拉对方向了吗+实际电平是多少），内部上拉打不过外部直连地，拔线是最终的二分判决。</p>
<p>另一个坑：<strong>frp 调试偶发“全零假象”</strong>——openocd 连接后芯片停在复位态没放行，所有寄存器读复位值 0、RAM 是陈旧值，看起来像全面故障。解法：<code>monitor reset halt</code> + <code>monitor resume</code> 序列。<strong>读数全零时先怀疑目标没在跑，再怀疑代码。</strong></p>
<hr>
<h2 id="%E7%BB%93%E8%AF%AD%EF%BC%9A%E4%B8%89%E4%B8%AA%E5%AE%9E%E9%AA%8C%E4%B8%B2%E6%88%90%E7%9A%84%E4%B8%80%E6%9D%A1%E7%BA%BF">结语：三个实验串成的一条线</h2>
<p>三个实验其实是同一条思路的三次递进：</p>
<ul>
 <li><strong>3B.1</strong>：硬件快门记时刻 → 测周期测脉宽（测量时间）</li>
 <li><strong>3B.2</strong>：硬件解码器数边沿 → 测位置测方向（测量旋转）</li>
 <li><strong>3B.3</strong>：硬件死区刹车保安全 → 互补输出驱动半桥（输出+保护）</li>
</ul>
<p>加上 3A 的时基/PWM，F103 的定时器全家桶至此没有死角。调试手法也攒出一套：<code>nm</code> 查变量地址 + <code>mdw</code> 读运行中的值、<code>compare-sections</code> 鉴别固件身份、<code>mww</code> 不重烧录现场改参数、协同采样（人转旋钮+云端读寄存器）——没有示波器的环境下，调试器就是你的眼睛。</p>]]></description><guid isPermaLink="false">/archives/stm32-timer-3b-input-capture-encoder-advanced</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Fri, 2 Oct 2026 13:27:39 GMT</pubDate></item><item><title><![CDATA[STM32 定时器实验全解：三阶段完整代码与思路（时基 → 输出比较 → PWM 呼吸灯）]]></title><link>https://www.flor.cn/archives/stm32-timer-3stage-timbase-oc-pwm</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=STM32%20%E5%AE%9A%E6%97%B6%E5%99%A8%E5%AE%9E%E9%AA%8C%E5%85%A8%E8%A7%A3%EF%BC%9A%E4%B8%89%E9%98%B6%E6%AE%B5%E5%AE%8C%E6%95%B4%E4%BB%A3%E7%A0%81%E4%B8%8E%E6%80%9D%E8%B7%AF%EF%BC%88%E6%97%B6%E5%9F%BA%20%E2%86%92%20%E8%BE%93%E5%87%BA%E6%AF%94%E8%BE%83%20%E2%86%92%20PWM%20%E5%91%BC%E5%90%B8%E7%81%AF%EF%BC%89&amp;url=/archives/stm32-timer-3stage-timbase-oc-pwm" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p>平台：STM32F103ZET6 @ 72MHz · LED=PB5（低电平点亮）· 工程名 tim_led 三个阶段是同一条思路的递进：<strong>每一步都把上一课软件干的活移交给硬件</strong></p>
</blockquote>
<h2 id="%E9%98%B6%E6%AE%B5%E4%B8%80%EF%BC%9A%E6%97%B6%E5%9F%BA%E2%80%94%E2%80%941ms-%E5%AE%9A%E6%97%B6%EF%BC%8Cled-1hz-%E9%97%AA%E7%83%81%EF%BC%88%E8%BD%AF%E4%BB%B6%E8%AE%A1%E6%95%B0%E7%89%88%EF%BC%89">阶段一：时基——1ms 定时，LED 1Hz 闪烁（软件计数版）</h2>
<h3 id="%E6%80%9D%E8%B7%AF">思路</h3>
<p>先解决"HAL_Delay 会阻塞 CPU"的问题：让定时器硬件自己数数，数满 1ms 敲一次门（中断），CPU 平时自由，只在被敲门时干一小点活。LED 要 1Hz 闪（亮 500ms 灭 500ms），但定时器只会每 1ms 响一次铃——<strong>"500ms"这个大时间尺度由软件在回调里数数实现</strong>：攒 500 个 1ms 就翻转一次引脚。</p>
<h3 id="cubemx-%E9%85%8D%E7%BD%AE">CubeMX 配置</h3>
<table>
 <thead>
  <tr>
   <th>项</th>
   <th>值</th>
   <th>算法</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>TIM2 Clock Source</td>
   <td>Internal Clock</td>
   <td>用内部 72MHz</td>
  </tr>
  <tr>
   <td>Prescaler (PSC)</td>
   <td>71</td>
   <td>72M ÷ (71+1) = 1MHz，每格 <strong>1µs</strong></td>
  </tr>
  <tr>
   <td>Counter Period (ARR)</td>
   <td>999</td>
   <td>1000 格 × 1µs = <strong>1ms</strong> 溢出一次</td>
  </tr>
  <tr>
   <td>NVIC</td>
   <td>TIM2 global interrupt 打勾</td>
   <td>放行中断线</td>
  </tr>
 </tbody>
</table>
<p>注意 +1 陷阱：PSC/ARR 都从 0 数起，所以 71 是"72 档"、999 是"1000 格"。TIM2 挂 APB1（名义 36M），但定时器走 ×2 后门实际吃 72MHz。</p>
<h3 id="%E5%AE%8C%E6%95%B4%E4%BB%A3%E7%A0%81">完整代码</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&amp;htim2);   // 拧发条：CEN=1 开始数数，UIE=1 数满允许敲门
/* USER CODE END 2 */
</code></pre>
</shiki-code>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">/* USER CODE BEGIN 4 */
// TIM2 每 1ms 进来一次（中断上下文：快进快出，只做加减和翻转）
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim-&gt;Instance == TIM2) {
        static uint16_t ms = 0;              // static：跨中断保留进度
        if (++ms &gt;= 500) {                   // 攒满 500 个 1ms
            ms = 0;
            HAL_GPIO_TogglePin(led1_GPIO_Port, led1_Pin);  // 翻转 → 1Hz 闪烁
        }
    }
}
/* USER CODE END 4 */
</code></pre>
</shiki-code>
<h3 id="%E9%80%90%E8%A1%8C%E8%AE%B2%E8%A7%A3">逐行讲解</h3>
<ul>
 <li><strong><code>HAL_TIM_Base_Start_IT(&amp;htim2)</code></strong>：CubeMX 的 <code>MX_TIM2_Init</code> 只负责"装填参数"（PSC、ARR 写进寄存器），此时计数器是静止的（CEN=0）。这一行干两件事：<strong>CEN=1</strong>（开始数）+ <strong>UIE=1</strong>（允许 Update 事件触发中断）。不调它，配置再对也是死表。</li>
 <li><strong><code>if (htim-&gt;Instance == TIM2)</code></strong>：<code>HAL_TIM_PeriodElapsedCallback</code> 是全局弱符号回调——将来你开 TIM3、TIM4，它们的 Update 事件也进这同一个函数。不判断是谁，就会串台。</li>
 <li><strong><code>static uint16_t ms = 0</code></strong>：本阶段的灵魂。普通局部变量每次函数返回就消失，<code>static</code> 让它<strong>活过函数返回</strong>——每次中断进来，ms 都是"上次的值"。没有 static，ms 永远从 0 数起，永远到不了 500，灯永远不闪。</li>
 <li><strong><code>if (++ms &gt;= 500)</code></strong>：前缀自增（先加再比）。500 个 1ms = 500ms。</li>
 <li><strong><code>HAL_GPIO_TogglePin</code></strong>：翻转 PB5。亮 500ms、灭 500ms，1Hz。</li>
 <li><strong>中断纪律</strong>：回调里只做加减和写引脚——中断上下文必须快进快出（拖久了下一次 1ms 到点就堆积）。</li>
</ul>
<h3 id="%E7%8E%B0%E8%B1%A1%E4%B8%8E%E6%9C%AC%E8%B4%A8">现象与本质</h3>
<p>LED 1Hz 闪烁。本质：<strong>硬件管"1ms 这个小周期"，软件数"500 次这个大尺度"</strong>。缺点也明显：每秒 1000 次中断，只为数一个数。</p>
<hr>
<h2 id="%E9%98%B6%E6%AE%B5%E4%BA%8C%EF%BC%9A%E8%BE%93%E5%87%BA%E6%AF%94%E8%BE%83%E2%80%94%E2%80%94%E5%8F%8C%E9%97%A8%E6%A7%9B%E5%AE%9A%E7%82%B9%E4%BA%AE%E7%81%AD%EF%BC%88%E9%9B%B6%E8%AE%A1%E6%95%B0%E7%89%88%EF%BC%89">阶段二：输出比较——双门槛定点亮灭（零计数版）</h2>
<h3 id="%E6%80%9D%E8%B7%AF">思路</h3>
<p>阶段一里"500ms"靠软件数——能不能让硬件直接在 500ms 处响铃？<strong>输出比较（OC）就是给 CNT 再加一道门槛 CCR</strong>：CNT 数到 CCR 的瞬间，Compare 事件爆发。于是周期里有两道硬件门槛：</p>
<ul>
 <li><strong>Update 事件</strong>（CNT 溢出归零，t=0s）→ 点亮</li>
 <li><strong>Compare 事件</strong>（CNT 数到 CCR1，t=0.5s）→ 熄灭</li>
</ul>
<p>回调里<strong>一个计数器都不需要</strong>——两道门槛把时间点定死，软件只负责"听到铃声做动作"。这就是 PWM 的手工版。</p>
<h3 id="cubemx-%E9%85%8D%E7%BD%AE">CubeMX 配置</h3>
<table>
 <thead>
  <tr>
   <th>项</th>
   <th>值</th>
   <th>算法</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>TIM2 Channel1</td>
   <td><strong>Output Compare No Output</strong></td>
   <td>只比较不占引脚（TIM2_CH1=PA0 是 WKUP 按键脚，不能碰）</td>
  </tr>
  <tr>
   <td>Prescaler (PSC)</td>
   <td>7199</td>
   <td>72M ÷ 7200 = 10kHz，每格 <strong>0.1ms</strong></td>
  </tr>
  <tr>
   <td>Counter Period (ARR)</td>
   <td>9999</td>
   <td>10000 格 = <strong>1s 周期</strong></td>
  </tr>
  <tr>
   <td>Pulse（CCR1 的界面名）</td>
   <td>5000</td>
   <td>第 5000 格 = <strong>500ms</strong> 处响铃</td>
  </tr>
 </tbody>
</table>
<p>为什么 PSC 从 71 改成 7199？ARR 是 16 位（最大 65535），1µs 粒度最多数 65.5ms——<strong>凑 1 秒周期必须先降频</strong>。这就是 (PSC, ARR) 的因式分解自由度：先定粒度，再凑周期。</p>
<h3 id="%E5%AE%8C%E6%95%B4%E4%BB%A3%E7%A0%81">完整代码</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&amp;htim2);              // 数数 + Update中断（周期起点事件）
HAL_TIM_OC_Start_IT(&amp;htim2, TIM_CHANNEL_1);  // 再开CH1比较中断（CCR门槛事件）
/* USER CODE END 2 */
</code></pre>
</shiki-code>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">/* USER CODE BEGIN 4 */
// Update事件：CNT溢出归零的瞬间 = 周期起点 → 点亮
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim-&gt;Instance == TIM2)
        HAL_GPIO_WritePin(led1_GPIO_Port, led1_Pin, GPIO_PIN_RESET);  // 低电平=亮
}

// Compare事件：CNT数到CCR1的瞬间 → 熄灭
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim-&gt;Instance == TIM2 &amp;&amp; htim-&gt;Channel == HAL_TIM_ACTIVE_CHANNEL_1)
        HAL_GPIO_WritePin(led1_GPIO_Port, led1_Pin, GPIO_PIN_SET);    // 高电平=灭
}
/* USER CODE END 4 */
</code></pre>
</shiki-code>
<h3 id="%E9%80%90%E8%A1%8C%E8%AE%B2%E8%A7%A3">逐行讲解</h3>
<ul>
 <li><strong>两声令</strong>：<code>HAL_TIM_Base_Start_IT</code> 开 Update 中断许可（UIE=1）；<code>HAL_TIM_OC_Start_IT</code> 开 CH1 的 Compare 中断许可（CC1IE=1）+ 通道使能。两个 Start 各管各的许可灯，缺一个就少一种铃声。</li>
 <li><strong>一个入口，两个回调</strong>：两种事件走<strong>同一个</strong> <code>TIM2_IRQHandler</code>，HAL 在 <code>HAL_TIM_IRQHandler</code> 里查标志分家——UIF 走 <code>PeriodElapsedCallback</code>，CC1IF 走 <code>OC_DelayElapsedCallback</code>。</li>
 <li><strong>OC 回调多判一个 <code>htim-&gt;Channel</code></strong>：4 个通道共用一个 OC 回调，<code>Channel</code> 告诉你是哪个通道在响（这里必须确认是 CH1）。</li>
 <li><strong>两个回调名字都是弱符号</strong>：名字一字不差才构成覆盖。笔误一个字母 = 函数成孤儿 = 编译零警告、现象纹丝不动（真实踩过：<code>HLA_TIM_PeriodElapsedClallback</code>）。防御：从 HAL 源码抄、用 VSCode 补全敲、写完 grep 核对。</li>
 <li><strong>写引脚用 SET/RESET 而不是 Toggle</strong>：亮灭动作分别绑定在两道门槛上，语义清晰——这正是 PWM"起点置高、比较处置低"的雏形。</li>
</ul>
<h3 id="%E7%8E%B0%E8%B1%A1%E4%B8%8E%E6%9C%AC%E8%B4%A8">现象与本质</h3>
<p>现象和阶段一一样是 1Hz 闪烁，但内核换了引擎：<strong>中断从 1000 次/秒降到 2 次/秒</strong>（CPU 占用差 500 倍），定时精度从"受中断延迟影响"变成"硬件门槛精准到格"。</p>
<hr>
<h2 id="%E9%98%B6%E6%AE%B5%E4%B8%89%EF%BC%9Apwm%E2%80%94%E2%80%94%E7%A1%AC%E4%BB%B6%E6%96%B9%E6%B3%A2-%2B-%E5%91%BC%E5%90%B8%E7%81%AF%EF%BC%88%E6%9C%80%E7%BB%88%E7%89%88%EF%BC%89">阶段三：PWM——硬件方波 + 呼吸灯（最终版）</h2>
<h3 id="%E6%80%9D%E8%B7%AF">思路</h3>
<p>两个硬件事实决定了方案：</p>
<ol>
 <li><strong>硬件 PWM 的规则</strong>：通道配成 PWM 模式后，硬件自动执行"<strong>CNT &lt; CCR → 引脚高，CNT ≥ CCR → 引脚低</strong>"——零中断零 CPU。用 TIM3_CH3（PB0）做硬件 PWM 演示（PA6/PA7 被板载 Flash 占用，选脚先查板载外设）。</li>
 <li><strong>LED 在 PB5——不属于任何定时器通道</strong>，硬件 PWM 够不着它 → 呼吸灯必须<strong>软件 PWM</strong>。</li>
</ol>
<p>软件 PWM 的架构是<strong>载波与包络分层</strong>：</p>
<ul>
 <li><strong>载波层</strong>：阶段二的双门槛机器，把周期从 1s 缩到 10ms（100Hz）——这个频率不能慢，慢了眼睛看见闪烁</li>
 <li><strong>包络层</strong>：Update 回调每个载波周期推一下占空比 duty（±1，在 0↔100 折返），用 <code>__HAL_TIM_SET_COMPARE</code> 动态写进 CCR1</li>
 <li>眼睛+LED 是低通滤波器：100Hz 快速亮灭只留<strong>平均亮度</strong>，正比占空比 → duty 扫 0↔100，灯就呼吸</li>
</ul>
<h3 id="cubemx-%E9%85%8D%E7%BD%AE">CubeMX 配置</h3>
<table>
 <thead>
  <tr>
   <th>项</th>
   <th>值</th>
   <th>算法</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>TIM2 Prescaler</td>
   <td>7199（不变）</td>
   <td>每格 0.1ms</td>
  </tr>
  <tr>
   <td>TIM2 Counter Period</td>
   <td><strong>99</strong></td>
   <td>100 格 × 0.1ms = <strong>10ms = 100Hz 载波</strong></td>
  </tr>
  <tr>
   <td>TIM2 Pulse</td>
   <td>0</td>
   <td>代码每周期重写，初值无所谓</td>
  </tr>
  <tr>
   <td>TIM3 Clock Source</td>
   <td>Internal Clock</td>
   <td></td>
  </tr>
  <tr>
   <td>TIM3 Channel3</td>
   <td><strong>PWM Generation CH3</strong></td>
   <td>引脚自动分配 PB0</td>
  </tr>
  <tr>
   <td>TIM3 PSC / ARR / Pulse</td>
   <td>7199 / 9999 / 5000</td>
   <td><strong>1Hz、50% 占空比</strong>（人眼级慢动作，方便调试器采样验证）</td>
  </tr>
  <tr>
   <td>TIM3 NVIC</td>
   <td><strong>不勾</strong></td>
   <td>硬件 PWM 零中断——和 TIM2 软件版的本质区别</td>
  </tr>
 </tbody>
</table>
<p>关键刻度对齐：<strong>duty 是 0~100 的百分数，直接写进 CCR1，所以 ARR 必须=99</strong>——在 ARR=99 的世界里 100/100=100%，在 ARR=9999 的世界里 100/10000 只有 1%（真实踩过：PSC/ARR 改反，呼吸在 1% 亮度里扫，肉眼等于没亮）。<strong>占空比是比值，duty 的尺子必须和 ARR 同刻度</strong>。</p>
<h3 id="%E5%AE%8C%E6%95%B4%E4%BB%A3%E7%A0%81">完整代码</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">/* USER CODE BEGIN PV */
uint8_t duty = 0;     // 当前占空比 0~100（呼吸的"肺活量"）
int8_t  dir  = 1;     // 呼吸方向：+1 渐亮，-1 渐暗
/* USER CODE END PV */
</code></pre>
</shiki-code>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&amp;htim2);              // TIM2：呼吸灯的软件PWM机器（阶段二原样）
HAL_TIM_OC_Start_IT(&amp;htim2, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&amp;htim3, TIM_CHANNEL_3);   // TIM3：PB0 硬件PWM，零中断
/* USER CODE END 2 */
</code></pre>
</shiki-code>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">/* USER CODE BEGIN 4 */
// Update事件 = 每个PWM周期(10ms)的起点：推进呼吸坡度，装填本周期占空比
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
  if(htim-&gt;Instance == TIM2){
    duty += dir;                                        // 坡度每周期 ±1
    if(duty &gt;= 100){ duty = 100; dir = -1; }            // 亮到头，折返渐暗
    if(duty &lt;= 0)  { duty = 0;   dir = +1; }             // 暗到头，折返渐亮
    __HAL_TIM_SET_COMPARE(&amp;htim2, TIM_CHANNEL_1, duty); // 门槛=本周期占空比
    if(duty == 0)
      HAL_GPIO_WritePin(led1_GPIO_Port,led1_Pin,GPIO_PIN_SET);   // 0%：直接灭
    else
      HAL_GPIO_WritePin(led1_GPIO_Port,led1_Pin,GPIO_PIN_RESET); // &gt;0%：先亮，等Compare来灭
  }
}

// Compare事件 = CNT数到duty处：熄灭（与阶段二一字不差）
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim){
  if(htim-&gt;Instance == TIM2 &amp;&amp; htim-&gt;Channel == HAL_TIM_ACTIVE_CHANNEL_1){
    HAL_GPIO_WritePin(led1_GPIO_Port,led1_Pin,GPIO_PIN_SET);
  }
}
/* USER CODE END 4 */
</code></pre>
</shiki-code>
<h3 id="%E9%80%90%E8%A1%8C%E8%AE%B2%E8%A7%A3">逐行讲解</h3>
<ul>
 <li><strong>PV 区两个变量</strong>：<code>duty</code> 是当前占空比（呼吸的"肺活量"），<code>dir</code> 是方向（+1 渐亮 / −1 渐暗）。两个变量合起来就是一个微型呼吸状态机。</li>
 <li><strong><code>HAL_TIM_PWM_Start(&amp;htim3, TIM_CHANNEL_3)</code></strong>：硬件 PWM 的启动只要一行，连 NVIC 都不用配——波形完全由硬件按"CNT&lt;CCR3 输出高"的规则自己画，CPU 零参与。</li>
 <li><strong><code>duty += dir</code> + 两个折返 if</strong>：三角波坡度。duty 到 100 掉头向下、到 0 掉头向上，0↔100 来回扫。呼吸节奏 = 200 步 × 10ms = <strong>2 秒一个呼吸</strong>。</li>
 <li><strong><code>__HAL_TIM_SET_COMPARE(&amp;htim2, TIM_CHANNEL_1, duty)</code></strong>：呼吸的全部秘密——CCR 不是死的，<strong>运行中随时可改</strong>。这个宏把新 duty 写进 CCR1，本周期后半段 Compare 事件就按新门槛触发。</li>
 <li><strong><code>if (duty == 0)</code> 特判</strong>：duty=0 时 CCR1=0，Compare 事件和 Update 在 CNT=0 <strong>同一瞬间</strong>爆发，而 HAL 先处理 Compare 后处理 Update——不特判，0% 会变成常亮（阶段二调试时那个"常亮 bug"的缩小版）。所以 0% 时 Update 回调里直接写灭，不指望 Compare。</li>
 <li><strong><code>else</code> 分支先亮</strong>：&gt;0% 时周期起点先点亮，熄灭的活儿留给 Compare 事件在"数到 duty"处干——亮多久完全由 duty 说了算。</li>
 <li><strong>Compare 回调</strong>：与阶段二一字未改——这就是分层的红利，载波机器不用动，只换包络逻辑。</li>
</ul>
<h3 id="%E7%8E%B0%E8%B1%A1%E4%B8%8E%E6%9C%AC%E8%B4%A8">现象与本质</h3>
<ul>
 <li><strong>PB5 呼吸灯</strong>：约 2 秒一个渐亮渐暗循环，亮度平滑变化</li>
 <li><strong>PB0 硬件 PWM</strong>：1Hz、50% 方波（调试器实时采样引脚电平验证：6 次采样 0/1 完美交替，PSC/ARR/CCR 寄存器读数 7199/9999/5000 与计算一致）</li>
</ul>
<hr>
<h2 id="%E4%B8%89%E9%98%B6%E6%AE%B5%E6%BC%94%E8%BF%9B%E6%80%BB%E8%A7%88">三阶段演进总览</h2>
<table>
 <thead>
  <tr>
   <th></th>
   <th>阶段一（时基）</th>
   <th>阶段二（输出比较）</th>
   <th>阶段三（PWM 呼吸灯）</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>事件源</td>
   <td>Update × 1</td>
   <td>Update + Compare</td>
   <td>同阶段二 + 动态改 CCR</td>
  </tr>
  <tr>
   <td>回调里干的事</td>
   <td>数 500 个 ms</td>
   <td>直接写引脚，零计数</td>
   <td>推坡度 + 写引脚</td>
  </tr>
  <tr>
   <td>中断频率</td>
   <td>1000 次/秒</td>
   <td>2 次/秒</td>
   <td>200 次/秒（载波）+ 0（硬件 PWM）</td>
  </tr>
  <tr>
   <td>定时精度</td>
   <td>受中断延迟影响</td>
   <td>硬件门槛，精准到格</td>
   <td>同左，占空比运行中可调</td>
  </tr>
  <tr>
   <td>本质</td>
   <td>硬件数周期，软件数精细</td>
   <td>硬件记两个时间点</td>
   <td>载波/包络分层，CPU 只管节奏</td>
  </tr>
 </tbody>
</table>
<p><strong>一句话总结整个模块</strong>：定时器的学习史就是一部"移交史"——把"等"从 CPU 移交给硬件（阶段一），把"数"移交给门槛（阶段二），把"开关引脚"移交给比较器（阶段三的硬件 PWM）；CPU 最后只剩一件体面的活：决定呼吸的节奏。</p>]]></description><guid isPermaLink="false">/archives/stm32-timer-3stage-timbase-oc-pwm</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Thu, 24 Sep 2026 06:44:05 GMT</pubDate></item><item><title><![CDATA[串口协议帧设计]]></title><link>https://www.flor.cn/archives/uart-frame-protocol</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=%E4%B8%B2%E5%8F%A3%E5%8D%8F%E8%AE%AE%E5%B8%A7%E8%AE%BE%E8%AE%A1&amp;url=/archives/uart-frame-protocol" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p>平台：STM32F103ZET6 @ 72MHz · USART1（PA9=TX / PA10=RX）· 115200-8-N-1 · LED=PB5 低电平点亮 前置知识：USART 中断接收 + 环形缓冲区</p>
</blockquote>
<h2 id="%E4%B8%80%E3%80%81%E8%A6%81%E8%A7%A3%E5%86%B3%E7%9A%84%E9%97%AE%E9%A2%98">一、要解决的问题</h2>
<p>printf 重定向解决了"怎么发"，中断接收 + 环形缓冲区解决了"怎么收"。但那两课搬运的都只是<strong>字节</strong>，而真实项目需要的是<strong>命令</strong>——PC 发一条指令，板子解析、执行、回确认。</p>
<p>核心矛盾在这里：<strong>串口是一条无边界字节流</strong>。PC 连续发来 12 个字节，串口本身不会告诉你这是"两条 6 字节命令"，还是"一条 12 字节命令"，还是"一堆垃圾 + 一条命令"。噪声、丢字节、粘包、劈包，在串口世界里都是家常便饭。</p>
<p>所以这个课题分两半：</p>
<ol>
 <li><strong>设计帧格式</strong>——给字节流加上结构和边界</li>
 <li><strong>写解析器</strong>——从字节流里可靠地"切"出完整帧，且坏帧不能影响好帧</li>
</ol>
<h2 id="%E4%BA%8C%E3%80%81%E5%B8%A7%E6%A0%BC%E5%BC%8F%E8%AE%BE%E8%AE%A1">二、帧格式设计</h2>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>┌──────┬──────┬──────┬─────────────┬──────────┐
│ 0xAA │ 0x55 │ len  │  data[len]  │  XOR CRC │
└──────┴──────┴──────┴─────────────┴──────────┘
 帧头1   帧头2   长度      数据域         校验
</code></pre>
</shiki-code>
<table>
 <thead>
  <tr>
   <th>字段</th>
   <th>长度</th>
   <th>作用</th>
   <th>设计理由</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>帧头 0xAA 0x55</td>
   <td>2 字节</td>
   <td>标记帧的起点</td>
   <td>双字节帧头，随机数据误命中概率降到 1/65536</td>
  </tr>
  <tr>
   <td>len</td>
   <td>1 字节</td>
   <td>数据域长度</td>
   <td><strong>断帧依据</strong>：收满 len 个就知道数据域结束了</td>
  </tr>
  <tr>
   <td>数据域</td>
   <td>len 字节</td>
   <td>命令码 + 参数</td>
   <td>业务载荷，本例 <code>[CMD][参数...]</code></td>
  </tr>
  <tr>
   <td>CRC</td>
   <td>1 字节</td>
   <td>异或校验</td>
   <td>检错（不是纠错），坏帧整帧丢弃</td>
  </tr>
 </tbody>
</table>
<p><strong>开灯帧逐字节拆解</strong>：<code>AA 55 02 01 01 02</code></p>
<ul>
 <li><code>AA 55</code>：帧头</li>
 <li><code>02</code>：数据域长度为 2</li>
 <li><code>01 01</code>：CMD=0x01（LED 控制），参数=1（亮）</li>
 <li><code>02</code>：校验 = <code>02 ^ 01 ^ 01</code>（<strong>长度字节也参与异或</strong>，这是收发双方的约定）</li>
</ul>
<h3 id="%E4%B8%A4%E7%A7%8D%E6%96%AD%E5%B8%A7%E6%96%B9%E6%A1%88%E5%AF%B9%E6%AF%94">两种断帧方案对比</h3>
<table>
 <thead>
  <tr>
   <th>方案</th>
   <th>原理</th>
   <th>优缺点</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td><strong>长度断帧</strong>（本例）</td>
   <td>帧头后跟长度，收满即断</td>
   <td>不依赖发送方时序习惯，帧劈开也能拼回；代价是多一个长度字段</td>
  </tr>
  <tr>
   <td>IDLE 空闲断帧</td>
   <td>总线空闲（<code>HAL_UARTEx_ReceiveToIdle_IT</code>）即一帧结束</td>
   <td>省掉长度字段；但依赖"发送方一口气发完一帧"，劈包后无依据</td>
  </tr>
 </tbody>
</table>
<h2 id="%E4%B8%89%E3%80%81%E8%BF%90%E8%A1%8C%E6%80%9D%E8%B7%AF%EF%BC%9A%E4%B8%80%E5%8F%B0%E4%B8%A4%E7%BA%A7%E8%8A%82%E5%A5%8F%E7%9A%84%E6%B5%81%E6%B0%B4%E7%BA%BF">三、运行思路：一台两级节奏的流水线</h2>
<p>整台机器是<strong>两条流水线 + 一个中转仓库</strong>：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>PC串口助手(HEX) ──AA 55 02 01 01 02──► CH340 ──► PA10(RX引脚)
                                              │ 每到 1 字节: RXNE→NVIC→中断
                                              ▼
           ┌── 中断上下文【生产者·快进快出】──────────┐
           │  RxCpltCallback:                          │
           │    rxbuf[rxHead]=字节; rxHead++;           │
           │    接力挂单(下一个字节继续中断收)            │
           └──────────────┬─────────────────────────────┘
                          ▼
                 〖环形缓冲区 rxbuf[128]〗←时间解耦的中转仓库
                          │
           ┌── 主循环【消费者·10ms 醒一次】─────────────┐
           │  rb_read() 批量取 → frame_parse()          │
           │  状态机: HDR1→HDR2→LEN→DATA→CRC            │
           └──────────────┬─────────────────────────────┘
                          ▼ CRC 通过(唯一合法入口)
                      frame_exec()
                     ┌─────┴──────┐
                     ▼            ▼
               PB5 拉低→LED亮   组ACK帧→HAL_UART_Transmit
                                   →PA9(TX)→CH340→PC显示
</code></pre>
</shiki-code>
<p>分层的本质是<strong>三种节奏的解耦</strong>：</p>
<ul>
 <li><strong>字节节奏（86.8µs/字节）</strong>：115200 波特率 8N1，每字节 10 位。这个节奏由硬件决定，用<strong>中断</strong>接——轮询要么丢字节要么烧 CPU</li>
 <li><strong>帧节奏（10ms）</strong>：主循环每 10ms 醒一次，把仓库里现有字节一次性搬空喂给解析器。这个值就是<strong>命令响应延迟上限</strong></li>
 <li><strong>中间的环形缓冲区</strong>：两种节奏差两个数量级，仓库负责削峰填谷。head 只有中断写、tail 只有主循环写——每个变量单一写者，加 volatile 就够，不用关中断</li>
</ul>
<p>为什么解析不放中断里做？<strong>中断必须快进快出</strong>——解析器最坏要跑完一整帧状态机，还会调用阻塞发送回 ACK，这些都不能在中断上下文里干。<strong>收字节的归中断，动脑子的归主循环。</strong></p>
<h2 id="%E5%9B%9B%E3%80%81%E7%8A%B6%E6%80%81%E6%9C%BA%E8%A7%A3%E6%9E%90%E5%99%A8%EF%BC%9A%E4%BB%A3%E7%A0%81%E4%B8%8E%E9%80%90%E6%AE%B5%E8%AF%B4%E6%98%8E">四、状态机解析器：代码与逐段说明</h2>
<p>五个状态就是帧格式五个区段的镜像：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>  状态     等什么      成功去哪     失败去哪
  S_HDR1   0xAA        S_HDR2      原地不动（垃圾字节直接无视）
  S_HDR2   0x55        S_LEN       见AA留下（可能是真帧头），其他回S_HDR1
  S_LEN    1~32        S_DATA      非法长度回S_HDR1（这道闸防住 data[] 溢出）
  S_DATA   收满len个   S_CRC       无失败分支（来者不拒）
  S_CRC    校验字节    通过→执行   无论过不过都回S_HDR1（丢弃式恢复）
</code></pre>
</shiki-code>
<h3 id="%E5%B8%A7%E6%89%A7%E8%A1%8C%EF%BC%9A%E5%90%88%E6%B3%95%E5%B8%A7%E7%9A%84%E5%94%AF%E4%B8%80%E5%87%BA%E5%8F%A3">帧执行：合法帧的唯一出口</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">#define FRAME_HDR1 0xAA   // 帧头第1字节：在无边界的字节流里"认出"帧的起跑线
#define FRAME_HDR2 0x55   // 帧头第2字节：双字节帧头，大幅降低随机数据误同步的概率
#define FRAME_MAX  32     // 数据域最大长度：决定 data[] 大小和长度字节的合法上限

// 谁能调用我：只有 frame_parse 的 S_CRC 分支（五关全过才轮到我）
// 参数：p=数据域首地址  len=数据域长度
// 职责：解释数据域含义并执行 + 组确认帧回传（闭环）
void frame_exec(uint8_t *p, uint8_t len)
{
    // 数据域约定格式：[命令码][参数...]
    // len&gt;=2：至少要有"命令码+参数"两个字节，命令才有意义
    if (len &gt;= 2 &amp;&amp; p[0] == 0x01) {              // CMD=0x01：LED 控制
        // 板子 LED 共阳接法（3.3V→LED→510Ω→PB5），低电平才亮
        // 参数非0 → 写 RESET(低电平) 点亮；参数0 → 写 SET(高电平) 熄灭
        HAL_GPIO_WritePin(led1_GPIO_Port, led1_Pin,
                          p[1] ? GPIO_PIN_RESET : GPIO_PIN_SET);

        // ---- 组确认帧（ACK）：把收到的帧原样回传 ----
        // 发送方收到一模一样的帧 = "命令已落地"的闭环确认
        uint8_t ack[4 + FRAME_MAX];              // 栈上缓冲：4+32=36字节，装得下最长合法帧
        ack[0] = FRAME_HDR1;                     // 帧头1
        ack[1] = FRAME_HDR2;                     // 帧头2
        ack[2] = len;                            // 长度
        uint8_t crc = len;                       // 校验初值=长度字节（与发送方的约定）
        for (uint8_t i = 0; i &lt; len; i++) {
            ack[3 + i] = p[i];                   // 数据域原样拷贝
            crc ^= p[i];                         // 边拷边异或，一趟干两件事
        }
        ack[3 + len] = crc;                      // 校验字节收尾

        // 阻塞式发送：这是主循环上下文（不是中断），等得起
        // 6字节@115200 约0.5ms发完，100ms 超时只是保险丝
        HAL_UART_Transmit(&amp;huart1, ack, 4 + len, 100);
    }
    // 其他命令码：静默忽略（帧是合法的，只是本机不认识这个命令）
}
</code></pre>
</shiki-code>
<h3 id="%E7%8A%B6%E6%80%81%E6%9C%BA%E4%B8%BB%E4%BD%93">状态机主体</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">void frame_parse(uint8_t *buf, uint16_t n)
{
    typedef enum { S_HDR1, S_HDR2, S_LEN, S_DATA, S_CRC } PState;
    static PState st = S_HDR1;      // static：跨调用保留 → 帧劈成两半也能拼回
    static uint8_t data[FRAME_MAX]; // static：半成品帧的暂存区（每次清零就拼不回了）
    static uint8_t idx = 0;         // 已收数据字节数
    static uint8_t len = 0;         // 本帧数据域长度
    static uint8_t crc = 0;         // 边收边算的校验值

    for (uint16_t i = 0; i &lt; n; i++) {         // 这批字节逐个喂进状态机
        uint8_t b = buf[i];
        switch (st) {

        case S_HDR1:                            // 状态1：扫帧头第1字节
            if (b == FRAME_HDR1) st = S_HDR2;   // 见AA → 前进一步
            // 不是AA：原地不动（重新同步的第1道闸）
            break;

        case S_HDR2:                            // 状态2：等帧头第2字节
            if (b == FRAME_HDR2)      st = S_LEN;  // AA 55 → 帧头成立
            else if (b == FRAME_HDR1) st = S_HDR2; // AA AA：这个AA可能是真帧头，
                                                   // 留在原地再等一个55
            else                      st = S_HDR1; // 前面那个AA是假帧头，回炉重扫
            break;

        case S_LEN:                             // 状态3：长度字节
            if (b &gt; 0 &amp;&amp; b &lt;= FRAME_MAX) {      // 合法区间 1~32（data[] 装得下的范围）
                len = b;                       // 记住本帧数据域长度
                idx = 0;                       // 新帧新起点：计数器清零
                crc = b;                       // 校验初值=长度字节（约定）
                st = S_DATA;
            }
            else st = S_HDR1;                   // 0或&gt;32 → 非法，丢弃重新同步
            break;

        case S_DATA:                            // 状态4：收数据域
            data[idx++] = b;                    // 存入半成品区，计数+1
            crc ^= b;                           // 边收边异或，收完校验值也算完
            if (idx == len) st = S_CRC;          // 收满 → 去校验关
            break;

        case S_CRC:                             // 状态5：对校验字节
            if (crc == b) frame_exec(data, len);// 算出的==送来的 → 合法帧，执行！
            st = S_HDR1;                        // 无论过不过都回起点：坏帧不回ACK，
            break;                              // 发送方超时重发即可（丢弃式恢复）
        }
    }
}
</code></pre>
</shiki-code>
<h3 id="%E4%B8%BB%E5%BE%AA%E7%8E%AF%EF%BC%88%E6%B6%88%E8%B4%B9%E8%80%85%E4%BE%A7%EF%BC%89">主循环（消费者侧）</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">uint8_t tmp[RXBUF_SIZE];                    // 主循环的提货筐：一次最多搬128字节
while (1)
{
    uint16_t n = rb_read(tmp, sizeof(tmp));  // 消费者：把环形缓冲区现有字节一次搬空
    if (n) frame_parse(tmp, n);             // 有货就整批喂给状态机
    HAL_Delay(10);                          // 10ms轮询节奏 = 命令响应延迟上限
}
</code></pre>
</shiki-code>
<h3 id="%E4%B8%89%E4%B8%AA%E5%85%B3%E9%94%AE%E6%9C%BA%E5%88%B6">三个关键机制</h3>
<ol>
 <li><strong>static 跨调用记忆</strong>：一帧 6 字节可能这批来 3 个、下批来 3 个（发送方分两次发、或主循环轮询切在中间）。状态机的进度存在 static 变量里，函数返回也不丢——帧劈开也能拼回。这是它和"一次性处理完整帧"思路的本质区别</li>
 <li><strong>AA AA 55 分支</strong>：收到 <code>AA AA 55</code> 时，第一个 AA 之后等来的是又一个 AA——它可能是真帧头。所以留在 S_HDR2 原地再等一个 55，而不是回 S_HDR1。没有这个分支，<code>AA AA 55 ...</code> 开头的帧会丢</li>
 <li><strong>丢弃式错误恢复</strong>：CRC 不过，整帧丢弃、回 S_HDR1、不回任何东西。实现最简，且坏帧不回 ACK 本身就是信号——发送方超时没等到确认，重发即可</li>
</ol>
<h2 id="%E4%BA%94%E3%80%81%E9%AA%8C%E6%94%B6%E4%B8%8E%E8%B8%A9%E5%9D%91">五、验收与踩坑</h2>
<p>验收五连（串口助手 HEX 模式）：①开灯 <code>AA 55 02 01 01 02</code>→灯亮+回同帧；②关灯 <code>AA 55 02 01 00 03</code>→灯灭+回同帧；③坏CRC <code>AA 55 02 01 01 99</code>→彻底沉默；④垃圾前缀 <code>12 34 56</code>+好帧→重新同步成功；⑤双AA <code>AA AA 55 02 01 01 02</code>→第二个AA当真帧头。①②验常规通路，③④⑤专考状态机的防御分支。</p>
<p>实战踩过两个坑，都值得记一辈子：</p>
<ul>
 <li><strong><code>HAL_Delay</code> 单位是毫秒</strong>：把 10ms 写成 <code>HAL_Delay(10000)</code>，编译零警告、逻辑全对，但命令响应延迟变成 0~10 秒随机。这类 bug 最阴险：<strong>不报错，只让系统慢得莫名其妙</strong></li>
 <li><strong>长度判断 off-by-one</strong>：<code>b &lt; FRAME_MAX</code> 会把 len=32 的满帧也拒掉（<code>data[32]</code> 下标 0~31，len=32 完全合法），正确写法 <code>b &lt;= FRAME_MAX</code>。教训：<strong>边界值必须专门测</strong>——len=0、1、31、32、33 各来一发</li>
</ul>
<p>另一个强烈推荐的手法：<strong>主机侧仿真测试</strong>——上真机前把解析器原样搬到 PC，HAL 调用换成记录桩（<code>HAL_GPIO_WritePin</code> 写个 led 变量、<code>HAL_UART_Transmit</code> 把字节拷进内存），本机 gcc 毫秒级跑完全部测试向量。不碰硬件、可回归、出错定位在逻辑层而不是"板子抽风"。</p>
<h2 id="%E5%85%AD%E3%80%81%E8%A6%81%E7%82%B9%E9%80%9F%E8%AE%B0">六、要点速记</h2>
<ul>
 <li>串口是无边界字节流，协议帧 = 自己造边界（帧头+长度+校验）</li>
 <li>双字节帧头把误同步概率降到 1/65536；长度断帧不依赖发送方时序</li>
 <li>状态机五状态 = 帧格式五区段的镜像；<code>static</code> 让帧劈开也能拼回</li>
 <li><code>AA AA 55</code>：S_HDR2 见 AA 留原地；长度闸门（1~32）防数组溢出</li>
 <li>丢弃式恢复：坏帧不回 ACK，靠发送方超时重发；ACK 原样回传 = 闭环确认</li>
 <li>收字节的归中断（快进快出），动脑子的归主循环；环形缓冲区是两种节奏之间的仓库</li>
 <li>10ms 轮询 = 命令响应延迟上限；<code>HAL_Delay</code> 单位是毫秒</li>
 <li>上真机前，主机侧仿真先跑一遍测试向量</li>
</ul>]]></description><guid isPermaLink="false">/archives/uart-frame-protocol</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Mon, 21 Sep 2026 03:35:28 GMT</pubDate></item><item><title><![CDATA[数制与逻辑门速查]]></title><link>https://www.flor.cn/archives/digital-logic-quickref</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=%E6%95%B0%E5%88%B6%E4%B8%8E%E9%80%BB%E8%BE%91%E9%97%A8%E9%80%9F%E6%9F%A5&amp;url=/archives/digital-logic-quickref" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p>内容来源：数电教材第一章 + STC8A8K64D4 手册，全部图表重绘 用途：数制与逻辑门速查卡片</p>
</blockquote>
<h2 id="%E4%B8%80%E3%80%81%E6%95%B0%E5%88%B6%E5%AF%B9%E7%85%A7%E8%A1%A8">一、数制对照表</h2>
<p><img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fdata%2Fphoto%2Fshuzhi-table.png&amp;size=m" srcset="https://www.flor.cn/upload/data/photo/shuzhi-table.png?width=1600 1600w, https://www.flor.cn/upload/data/photo/shuzhi-table.png?width=800 800w, https://www.flor.cn/upload/data/photo/shuzhi-table.png?width=400 400w, https://www.flor.cn/upload/data/photo/shuzhi-table.png?width=1200 1200w" style="max-width:100%"></p>
<p>三个要点：</p>
<ul>
 <li><strong>十六进制是二进制的速记法</strong>：16 = 2⁴，一位十六进制恰好对应四位二进制——8/16/32 位二进制整齐地写成 2/4/8 位十六进制，书写程序十分简便</li>
 <li><strong>两个关键行</strong>：十进制 8 → 八进制 10（逢八进位，没有 8 这个数字）；十进制 10 → 十六进制 A（从 10 开始用字母 A~F）</li>
 <li><strong>最大值公式</strong>：n 位 b 进制的最大值 = bⁿ − 1。1111B = 15、7777O = 4095、FFFFH = 65535——嵌入式里 0xFF（8位满量程）、0xFFF（12位ADC满量程）、0xFFFF（16位上限）全是这一族</li>
</ul>
<h2 id="%E4%BA%8C%E3%80%81%E5%A5%87%E5%81%B6%E6%A0%A1%E9%AA%8C%E7%A0%81">二、奇偶校验码</h2>
<p>数据在存取、运算和传送过程中难免出错（1 错成 0、0 错成 1）。奇偶校验码 = 信息位 + 1 位校验位，让整个码字中 1 的个数凑成奇数（奇校验）或偶数（偶校验）。</p>
<ul>
 <li><strong>能抓</strong>：任何 1 位翻转（1 的个数 ±1，奇偶性破坏立刻露馅）</li>
 <li><strong>抓不住</strong>：同时翻 2 位（个数 ±2 或不变，奇偶性完好——天生盲区）</li>
</ul>
<p>嵌入式对应：串口配置里的 <code>UART_PARITY_EVEN / ODD / NONE</code> 就是它。代价是每字节多 1 位（12.5% 开销），且只能发现不能纠正，所以正经链路升级用 CRC。</p>
<h2 id="%E4%B8%89%E3%80%81%E4%B8%89%E7%A7%8D%E5%9F%BA%E6%9C%AC%E9%80%BB%E8%BE%91%E8%BF%90%E7%AE%97">三、三种基本逻辑运算</h2>
<p><img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fdata%2Fphoto%2Flogic-basic-gates.png&amp;size=m" srcset="https://www.flor.cn/upload/data/photo/logic-basic-gates.png?width=1600 1600w, https://www.flor.cn/upload/data/photo/logic-basic-gates.png?width=800 800w, https://www.flor.cn/upload/data/photo/logic-basic-gates.png?width=400 400w, https://www.flor.cn/upload/data/photo/logic-basic-gates.png?width=1200 1200w" style="max-width:100%"></p>
<p>真值表不用死记：与 = 乘法（0 乘啥都 0）、或 = 加法（但 1+1=1）、非 = 取反。</p>
<p>对应 C 语言的位操作：</p>
<ul>
 <li>置位：<code>P1 |= 0x10</code>（或）</li>
 <li>清位：<code>P1 &amp;= ~0x20</code>（与 + 非）</li>
 <li>取反：<code>P1 = ~P1</code>（非）</li>
</ul>
<h2 id="%E5%9B%9B%E3%80%81%E7%BB%84%E5%90%88%E9%97%A8%EF%BC%9A%E4%B8%8E%E9%9D%9E%E3%80%81%E6%88%96%E9%9D%9E%E3%80%81%E4%B8%8E%E6%88%96%E9%9D%9E">四、组合门：与非、或非、与或非</h2>
<p><img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fdata%2Fphoto%2Flogic-composite-gates.png&amp;size=m" srcset="https://www.flor.cn/upload/data/photo/logic-composite-gates.png?width=1600 1600w, https://www.flor.cn/upload/data/photo/logic-composite-gates.png?width=800 800w, https://www.flor.cn/upload/data/photo/logic-composite-gates.png?width=400 400w, https://www.flor.cn/upload/data/photo/logic-composite-gates.png?width=1200 1200w" style="max-width:100%"></p>
<ul>
 <li><strong>与非门是万能门</strong>：只用与非门一种就能拼出全部逻辑（非 = 与非(A,A)；或 = 与非(A′,B′)）。74LS00（四与非门）曾是地球上产量最大的芯片，NAND Flash 之名即源于此</li>
 <li><strong>德摩根律</strong>：(A·B)′ = A′ + B′——与非门输出端的小圆圈挪到输入端就变成或门。圆圈可以在输入/输出之间"漂移"，这是看懂和化简逻辑电路图的核心技巧</li>
 <li><strong>与或非</strong>：Y = (A·B + C·D)′，16 行真值表仅 7 行为 0（AB=11 或 CD=11 时）。典型用途：地址译码、比较器（74HC51）</li>
</ul>
<h2 id="%E4%BA%94%E3%80%81%E5%BC%82%E6%88%96%E4%B8%8E%E5%90%8C%E6%88%96">五、异或与同或</h2>
<p><img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fdata%2Fphoto%2Fxor-xnor.png&amp;size=m" srcset="https://www.flor.cn/upload/data/photo/xor-xnor.png?width=1600 1600w, https://www.flor.cn/upload/data/photo/xor-xnor.png?width=800 800w, https://www.flor.cn/upload/data/photo/xor-xnor.png?width=400 400w, https://www.flor.cn/upload/data/photo/xor-xnor.png?width=1200 1200w" style="max-width:100%"></p>
<p>异或（不同为 1）是比较器，同或（相同为 1）是相等判断器，两者互为取反。</p>
<p>异或的四个神技（嵌入式天天用）：</p>
<ol>
 <li><strong>翻转位</strong>：<code>P1 ^= 0x01</code>——按一下翻一次</li>
 <li><strong>求奇偶</strong>：把数据位全部异或 = 偶校验位（奇偶校验本质上就是异或数奇偶）</li>
 <li><strong>免临时变量交换</strong>：<code>a^=b; b^=a; a^=b;</code>（因为 A⊕A=0、A⊕0=A）</li>
 <li><strong>半加器</strong>：A⊕B = 不带进位的和，A·B = 进位——CPU 加法器的地基</li>
</ol>
<p>状态监测经典用法：<code>changed = new ^ old</code>——一行异或，变了的位全亮出来。</p>
<h2 id="%E5%85%AD%E3%80%81%E4%B8%80%E9%A1%B5%E6%80%BB%E8%A7%88">六、一页总览</h2>
<table>
 <thead>
  <tr>
   <th>运算</th>
   <th>公式</th>
   <th>口诀</th>
   <th>C 语言</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>与</td>
   <td>Y = A·B</td>
   <td>全 1 才 1</td>
   <td><code>a &amp; b</code></td>
  </tr>
  <tr>
   <td>或</td>
   <td>Y = A+B</td>
   <td>有 1 就 1</td>
   <td><code>a \| b</code></td>
  </tr>
  <tr>
   <td>非</td>
   <td>Y = A′</td>
   <td>0 变 1</td>
   <td><code>~a</code></td>
  </tr>
  <tr>
   <td>与非</td>
   <td>Y = (A·B)′</td>
   <td>有 0 出 1，全 1 才 0</td>
   <td><code>!(a &amp; b)</code></td>
  </tr>
  <tr>
   <td>或非</td>
   <td>Y = (A+B)′</td>
   <td>全 0 出 1，有 1 就 0</td>
   <td><code>!(a \| b)</code></td>
  </tr>
  <tr>
   <td>与或非</td>
   <td>Y = (A·B+C·D)′</td>
   <td>任一组全 1 则 0</td>
   <td><code>!((a&amp;&amp;b)\|\|(c&amp;&amp;d))</code></td>
  </tr>
  <tr>
   <td>异或</td>
   <td>Y = A⊕B</td>
   <td>不同为 1</td>
   <td><code>a ^ b</code></td>
  </tr>
  <tr>
   <td>同或</td>
   <td>Y = A⊙B</td>
   <td>相同为 1</td>
   <td><code>!(a ^ b)</code></td>
  </tr>
 </tbody>
</table>
<p><strong>一句话收尾：与或非是地基，与非是万能砖，异或是比较器——八种运算，一张表带走。</strong></p>]]></description><guid isPermaLink="false">/archives/digital-logic-quickref</guid><dc:creator>flor</dc:creator><category>数电</category><pubDate>Sat, 19 Sep 2026 09:48:35 GMT</pubDate></item><item><title><![CDATA[环形缓冲区详解]]></title><link>https://www.flor.cn/archives/embedded-ring-buffer</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=%E7%8E%AF%E5%BD%A2%E7%BC%93%E5%86%B2%E5%8C%BA%E8%AF%A6%E8%A7%A3&amp;url=/archives/embedded-ring-buffer" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p>适用场景：串口/传感器/键盘等"数据碎片化到达"场景的裸机开发 阅读基础：了解 C 语言、中断的基本概念</p>
</blockquote>
<h2 id="0.-%E5%BC%95%E5%AD%90%EF%BC%9A%E4%B8%A4%E4%B8%AA%E4%B8%96%E7%95%8C%E7%9A%84%E8%8A%82%E5%A5%8F%E5%B7%AE">0. 引子：两个世界的节奏差</h2>
<p>串口中断接收有一个根本矛盾：</p>
<ul>
 <li>
  <p><strong>中断世界</strong>：字节什么时候来，由对方（发送方）决定——随时可能来，也可能一秒钟来一百个</p>
 </li>
 <li>
  <p><strong>主循环世界</strong>：什么时候有空处理，由程序逻辑决定——可能正在刷屏、算滤波、睡觉</p>
 </li>
</ul>
<p>115200 波特率下每个字节间隔约 87µs，72MHz 的 CPU 在这 87µs 里能执行约 6000 条指令。如果让 CPU 死盯串口（轮询），这 6000 条指令全浪费；如果主循环去干别的，字节来了没人管，下一个字节一到就把上一个挤丢（硬件溢出 ORE）。</p>
<p><strong>环形缓冲区就是两个世界的"时差消化器"</strong>：中断是快递员（只管把包裹塞进快递柜，几微秒走人），主循环是收件人（有空时一次性取走所有包裹慢慢拆），缓冲区是快递柜，丢包计数器是登记簿</p>
<h2 id="1.-%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%AF%22%E7%8E%AF%E5%BD%A2%22">1. 为什么是"环形"</h2>
<p>普通数组当缓冲区有两条死路：</p>
<ul>
 <li>
  <p>写到头就停 → 空间浪费一半</p>
 </li>
 <li>
  <p>写到头搬回开头 → 每次搬移 O(n)，CPU 干无用功</p>
 </li>
</ul>
<p>环形的答案：<strong>两个下标各自前进、互不等待，数组用完就绕回开头</strong>——一条首尾相接的传送带：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>        ┌──▶ [0][1][2][3][4][5][6][7] ──┐
        │      ↑tail          head↑     │
        └────────── 绕回开头 ─────────────┘
</code></pre>
</shiki-code>
<ul>
 <li>
  <p><code>rxHead</code>（写下标）：数据往哪存，只有生产者（中断）动它</p>
 </li>
 <li>
  <p><code>rxTail</code>（读下标）：数据从哪取，只有消费者（主循环）动它</p>
 </li>
 <li>
  <p>存一个字节 head 前进一步，取一个字节 tail 前进一步，追上对方就停</p>
 </li>
</ul>
<h2 id="2.-%E4%B8%89%E4%BB%B6%E5%A5%97%E4%B8%8E%E4%B8%89%E4%B8%AA%E7%B2%BE%E5%A6%99%E8%AE%BE%E8%AE%A1">2. 三件套与三个精妙设计</h2>
<h3 id="2.0-%E6%95%B0%E6%8D%AE%E4%B8%8E%E4%B8%8B%E6%A0%87%E5%AE%9A%E4%B9%89">2.0 数据与下标定义</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">#define RXBUF_SIZE 128                     // 2 的幂：可用 &amp; (RXBUF_SIZE-1) 代替 % 取模（单周期）
uint8_t  rxbuf[RXBUF_SIZE];                // 存储区
volatile uint16_t rxHead = 0;              // 写下标：只有中断动它
volatile uint16_t rxTail = 0;              // 读下标：只有主循环动它
volatile uint32_t rxErr = 0;               // 丢包账本：满了没挤进来的字节数
</code></pre>
</shiki-code>
<h3 id="2.1-%E8%AE%BE%E8%AE%A1%E4%B8%80%EF%BC%9A%E7%89%BA%E7%89%B2%E4%B8%80%E6%A0%BC%E2%80%94%E2%80%94%E5%8C%BA%E5%88%86%22%E6%BB%A1%22%E5%92%8C%22%E7%A9%BA%22">2.1 设计一：牺牲一格——区分"满"和"空"</h3>
<p>最坑的问题：<code>head == tail</code> 到底是"空"还是"满"？两种状态长得一模一样！</p>
<p>解法：<strong>宁可少用一个格子</strong>——留一格空作为"满"的标志：</p>
<ul>
 <li>
  <p>空：<code>rxHead == rxTail</code>（两者重合）</p>
 </li>
 <li>
  <p>满：<code>(rxHead + 1) % RXBUF_SIZE == rxTail</code>（还差一格重合）</p>
 </li>
</ul>
<p>128 格的缓冲区有效容量是 127。用一格容量换一个无歧义的状态判断，值。</p>
<h3 id="2.2-%E8%AE%BE%E8%AE%A1%E4%BA%8C%EF%BC%9Acount-%E7%94%A8%E5%85%AC%E5%BC%8F%E7%AE%97%E2%80%94%E2%80%94%E6%B4%BE%E7%94%9F%E9%87%8F%E4%B8%8D%E5%8F%A6%E5%AD%98">2.2 设计二：count 用公式算——派生量不另存</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">// 当前存量字节数
uint16_t rb_count(void)
{
    return (rxHead - rxTail + RXBUF_SIZE) % RXBUF_SIZE;
}
</code></pre>
</shiki-code>
<p>两个细节：</p>
<p><strong>为什么要 </strong><code>+RXBUF_SIZE</code><strong> 再取模？</strong> 环形回绕后 head 可能数值上小于 tail（比如 head=5、tail=120），直接相减得 -115；加上 128 再取模得 13，才是真实存量。C 语言负数取模还是负数，这一步是环形计算的标配。</p>
<p><strong>为什么不用 count 变量存着、每次加减？</strong> 因为 count 会被两边（生产/消费）同时维护——两个写者 = 竞态 = 会漂移的死账本。<strong>能从现有状态推导出来的量，永远不要另存一份</strong>（单一事实来源原则）。</p>
<h3 id="2.3-%E8%AE%BE%E8%AE%A1%E4%B8%89%EF%BC%9Avolatile-%2B-%E5%8D%95%E4%B8%80%E5%86%99%E8%80%85%E2%80%94%E2%80%94%E4%B8%8D%E5%8A%A0%E9%94%81%E7%9A%84%E7%A7%98%E5%AF%86">2.3 设计三：volatile + 单一写者——不加锁的秘密</h3>
<div class="halo-table-wrapper" data-table-layout="auto" style="box-sizing: border-box; overflow-x: auto; overflow-y: hidden; width: 100%; max-width: 100%; min-width: 0;">
 <table data-table-layout="auto" style="display: table; width: 100%; min-width: 100%; table-layout: auto;">
  <tbody>
   <tr>
    <th colspan="1" rowspan="1">
     <p>变量</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>谁写</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>谁读</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>为什么安全</p>
    </th>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>rxHead</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>只有中断写</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>主循环读</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>一个写者，永远不打架</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>rxTail</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>只有主循环写</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>中断读（判满时比较）</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>同上</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>rxErr</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>只有中断写</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>主循环读</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>同上</p>
    </td>
   </tr>
  </tbody>
 </table>
</div>
<p>两层保护各管一件事：</p>
<ul>
 <li>
  <p><strong>volatile 管"看得见"</strong>：禁止编译器把读优化成寄存器缓存。没有它，主循环可能永远读不到中断更新后的值——嵌入式三大玄学 bug 之一（症状：加 printf 就好了、开 -O0 就好了）</p>
 </li>
 <li>
  <p><strong>归属权管"不打架"</strong>：每个下标只有一个写者，不存在写冲突</p>
 </li>
</ul>
<p>这就是这套代码<strong>全程不用关中断</strong>的原因。</p>
<h2 id="3.-%E5%AE%8C%E6%95%B4%E6%9E%B6%E6%9E%84%E4%B8%8E%E4%BB%A3%E7%A0%81">3. 完整架构与代码</h2>
<h3 id="3.1-%E6%95%B0%E6%8D%AE%E6%B5%81%E5%85%A8%E6%99%AF">3.1 数据流全景</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>【生产者：中断世界】               【缓冲区】              【消费者：主循环世界】
电脑串口发字节                     ┌────────────┐        while(1)
  → RX 引脚 → 移位寄存器 → DR      │  rxbuf[128] │          ├─ rb_read(tmp,128) 批量取
  → RXNE=1 → 中断                  │  rxHead 写下标│◄─中断写  ├─ 逐字节处理（回显/解析）
  → HAL 读 DR → 回调               │  rxTail 读下标│◄─主循环写 ├─ 报账（buf/err）
    ├─ 有空位：存 + head++        │  rxErr 总账本 │          └─ HAL_Delay(10)
    ├─ 满了：rxErr++（丢新）      └────────────┘
    └─ 挂下一单 Receive_IT
  → 中断退出（全程几微秒）
</code></pre>
</shiki-code>
<h3 id="3.2-%E4%B8%AD%E6%96%AD%E4%BE%A7%EF%BC%9A%E5%9B%9E%E8%B0%83%E4%B8%89%E4%BB%B6%E4%BA%8B%EF%BC%88%E5%AD%98%E3%80%81%E8%AE%B0%E3%80%81%E6%8C%82%EF%BC%89">3.2 中断侧：回调三件事（存、记、挂）</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">uint8_t rxbyte;   // HAL 的落地字节

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart-&gt;Instance == USART1) {
        uint16_t next = (rxHead + 1) % RXBUF_SIZE;
        if (next != rxTail) {                      // 没满：写入
            rxbuf[rxHead] = rxbyte;
            rxHead = next;
        } else {                                   // 满了：进不来（丢新策略），记一笔账
            rxErr++;
        }
        HAL_UART_Receive_IT(huart, &amp;rxbyte, 1);    // 挂下一单（不断链）
    }
}
</code></pre>
</shiki-code>
<p>三个细节：</p>
<ul>
 <li>
  <p><strong>挂单不能忘</strong>：<code>HAL_UART_Receive_IT</code> 是"一单制"——收完这单就结束，回调里不续挂，链路就断（症状：只响应第一个字节，之后任你怎么发都装死）</p>
 </li>
 <li>
  <p><strong>中断快进快出</strong>：三件事加起来几微秒，这是中断的铁律</p>
 </li>
 <li>
  <p><strong>rxErr 记账</strong>：丢可以（策略决定），但必须数出来——无声丢数据是最恶劣的 bug</p>
 </li>
</ul>
<h3 id="3.3-%E6%B6%88%E8%B4%B9%E8%80%85%E4%BE%A7%EF%BC%9A%E6%89%B9%E9%87%8F%E8%AF%BB-%2B-%E6%8E%92%E7%A9%BA%E5%BC%8F%E6%B6%88%E8%B4%B9">3.3 消费者侧：批量读 + 排空式消费</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">// 批量读：最多取 n 个字节放进 buf，返回实际取到的个数（0 = 空）
uint16_t rb_read(uint8_t *buf, uint16_t n)
{
    uint16_t avail = (rxHead - rxTail + RXBUF_SIZE) % RXBUF_SIZE;
    if (n &gt; avail) n = avail;              // 要 100 只有 30 就只给 30，不越界
    for (uint16_t i = 0; i &lt; n; i++) {
        buf[i] = rxbuf[rxTail];
        rxTail = (rxTail + 1) % RXBUF_SIZE;
    }
    return n;
}

// 主循环
uint8_t tmp[RXBUF_SIZE];
while (1)
{
    uint16_t n = rb_read(tmp, sizeof(tmp));    // 排空式：有多少取多少
    for (uint16_t i = 0; i &lt; n; i++) {
        // 逐字节处理（回显 / 喂协议解析器 / 执行命令）
    }
    HAL_Delay(10);                            // 轮询周期 = 响应延迟上限
}
</code></pre>
</shiki-code>
<p><strong>为什么必须批量读</strong>：如果主循环每次只取 1 个字节就去睡觉，积压 8 个字节要 8 个周期才消费完（睡 2 秒的话要 16 秒）。批量取让"消费速率"和"到达速率"彻底解耦——睡觉时中断照样收货，醒来一口气处理完。</p>
<h2 id="4.-%E4%B8%A2%E5%8C%85%E7%AD%96%E7%95%A5%EF%BC%9A%E4%B8%A2%E6%96%B0-vs-%E4%B8%A2%E6%97%A7">4. 丢包策略：丢新 vs 丢旧</h2>
<p>缓冲区满了怎么办？两种相反的策略：</p>
<div class="halo-table-wrapper" data-table-layout="auto" style="box-sizing: border-box; overflow-x: auto; overflow-y: hidden; width: 100%; max-width: 100%; min-width: 0;">
 <table data-table-layout="auto" style="display: table; width: 100%; min-width: 100%; table-layout: auto;">
  <tbody>
   <tr>
    <th colspan="1" rowspan="1">
     <p></p>
    </th>
    <th colspan="1" rowspan="1">
     <p>丢新（本文主线）</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>丢旧（进阶变体）</p>
    </th>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>行为</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>新字节不写入</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>挤掉最老字节，新字节一定写入</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>实现代价</p>
    </td>
    <td colspan="1" rowspan="1">
     <p><strong>零</strong>（写方只动 head）</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>rxTail 变成两边都写 → 读方更新下标必须关中断</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>适用场景</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>协议帧（帧断了两种都救不了，正解是容量 ≥ 最大帧长）</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>命令流/遥控器（连按 50 下，要的是第 50 下的值）</p>
    </td>
   </tr>
  </tbody>
 </table>
</div>
<p>丢旧的实现与代价：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">// 进阶变体：丢旧不丢新
void rb_put_old(uint8_t c)                    // 中断里调用
{
    uint16_t next = (rxHead + 1) % RXBUF_SIZE;
    if (next == rxTail) {                     // 满了
        rxTail = (rxTail + 1) % RXBUF_SIZE;   // 中断里挤掉最老的（中断改 rxTail 不会被主循环插队，安全）
        rxErr++;
    }
    rxbuf[rxHead] = c;                        // 无论如何都写入
    rxHead = next;
}

int rb_get_old(void)                          // 主循环里调用
{
    if (rxTail == rxHead) return -1;
    uint8_t c = rxbuf[rxTail];
    __disable_irq();                          // ★ 丢旧策略的代价：读方更新 rxTail 要关中断
    rxTail = (rxTail + 1) % RXBUF_SIZE;       //   否则会被中断的"挤掉"动作覆盖掉
    __enable_irq();
    return c;
}
</code></pre>
</shiki-code>
<p><strong>选型口诀：命令流要最新 → 丢旧；协议帧 → 加大缓冲区，靠 err 发现不够用。</strong></p>
<h2 id="5.-%E9%94%99%E8%AF%AF%E8%B4%A6%E6%9C%AC%EF%BC%9A%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7">5. 错误账本：可观测性</h2>
<p>丢包有两种来源，都要记账：</p>
<div class="halo-table-wrapper" data-table-layout="auto" style="box-sizing: border-box; overflow-x: auto; overflow-y: hidden; width: 100%; max-width: 100%; min-width: 0;">
 <table data-table-layout="auto" style="display: table; width: 100%; min-width: 100%; table-layout: auto;">
  <tbody>
   <tr>
    <th colspan="1" rowspan="1">
     <p>记账位置</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>丢的原因</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>性质</p>
    </th>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>RxCpltCallback 里 rxErr++</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>缓冲区满，装不下</p>
    </td>
    <td colspan="1" rowspan="1">
     <p><strong>软件丢</strong>（自己的策略）</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>ErrorCallback 里 rxErr++</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>ORE 溢出 / 帧错误 / 噪声</p>
    </td>
    <td colspan="1" rowspan="1">
     <p><strong>硬件丢</strong>（字节没进 DR 或已损坏）</p>
    </td>
   </tr>
  </tbody>
 </table>
</div>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">// 硬件错误回调：溢出等错误发生时 HAL 会终止接收，这里记账 + 重新挂单自愈
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
    if (huart-&gt;Instance == USART1) {
        rxErr++;
        HAL_UART_Receive_IT(huart, &amp;rxbyte, 1);
    }
}
</code></pre>
</shiki-code>
<p>没有 ErrorCallback 的后果：一次溢出后 HAL 静默终止接收，串口从此"装死"，直到重新上电。</p>
<p><strong>一个数字看链路健康度</strong>：err=0 一切正常；err 涨了，要么缓冲区不够大，要么中断里干活太慢。</p>
<h2 id="6.-%E5%90%8C%E4%B8%80%E4%B8%AA%E5%8E%9F%E5%AD%90%E6%93%8D%E4%BD%9C%E7%9A%84%E4%B8%89%E5%BC%A0%E9%9D%A2%E5%AD%94">6. 同一个原子操作的三张面孔</h2>
<p>"取一个字节"的两行核心（取数 + 下标前移），包装成三种接口：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">// 面孔一：取一个，空返回 -1（轮询式消费）
int rb_get(void) { /* 判空 → 取 → tail++ → 返回 */ }

// 面孔二：取一批，返回实际个数（排空式消费）
uint16_t rb_read(uint8_t *buf, uint16_t n) { /* ... */ }

// 面孔三：取一个，空就等（阻塞式，scanf/getchar 的入口）
int __io_getchar(void)
{
    while (rxTail == rxHead) { }         // 空：原地等（中断会把字节放进来）
    uint8_t c = rxbuf[rxTail];
    rxTail = (rxTail + 1) % RXBUF_SIZE;
    return c;
}
</code></pre>
</shiki-code>
<p>基础原子学扎实，三种包装随手就来。</p>
<h2 id="7.-%E5%AE%9E%E9%AA%8C%E9%AA%8C%E8%AF%81%EF%BC%88%E7%8E%B0%E8%B1%A1%E5%8D%B3%E8%AF%81%E6%8D%AE%EF%BC%89">7. 实验验证（现象即证据）</h2>
<div class="halo-table-wrapper" data-table-layout="auto" style="box-sizing: border-box; overflow-x: auto; overflow-y: hidden; width: 100%; max-width: 100%; min-width: 0;">
 <table data-table-layout="auto" style="display: table; width: 100%; min-width: 100%; table-layout: auto;">
  <tbody>
   <tr>
    <th colspan="1" rowspan="1">
     <p>实验</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>操作</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>现象</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>证明了什么</p>
    </th>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>连珠炮回显</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>主循环睡觉窗口里发 8 字符</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>醒来 8 字符一坨回显（0.7ms 内到齐）</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>睡觉时中断照样收货（生产者不停工）</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>过载</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>一次粘贴 200 字符</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>回显前 127 个，err=73</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>丢新策略 + 牺牲一格（128-1=127）+ 记账可见</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>LED 奇偶</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>发 N 字符看 LED</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>奇数净翻转、偶数不动</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>回调按字节触发（不是按"消息"）</p>
    </td>
   </tr>
  </tbody>
 </table>
</div>
<h2 id="8.-%E4%B8%89%E4%B8%AA%E5%8F%8D%E9%9D%A2%E6%A1%88%E4%BE%8B%EF%BC%88%E8%AF%BB%E5%88%AB%E4%BA%BA%E4%BB%A3%E7%A0%81%E6%97%B6%E7%9A%84%E6%89%BE%E8%8C%AC%E9%A2%98%EF%BC%89">8. 三个反面案例（读别人代码时的找茬题）</h2>
<p>真实项目的环形缓冲区常见三个坑，读代码时值得带着怀疑去找：</p>
<p><strong>① 命名撒谎</strong>：函数叫 <code>isEmpty</code>，返回 1 却表示"有数据"——所有调用处都得写 <code>== 0</code> 才敢用。命名的诚实度就是代码的可维护性。</p>
<p><strong>② count 死账本</strong>：写方和读方都维护 count 变量（两个写者 = 竞态 = 漂移），结果查询函数干脆不用它、用公式重算。<strong>派生量别另存</strong>——本文 2.2 节从第一天就做对了。</p>
<p><strong>③ 腾位不用</strong>：丢旧实现里满了挤掉一个老字节后直接 return——当前字节也没写进去。持续过载时效果是"丢一个旧、丢一个新、再收一个"约丢一半，新旧交错。读代码时自己推演一遍边界行为，是最好的练习。</p>
<h2 id="9.-%E6%89%A9%E5%B1%95%E8%A7%86%E9%87%8E">9. 扩展视野</h2>
<ul>
 <li>
  <p><strong>2 的幂优化</strong>：<code>% RXBUF_SIZE</code> 换成 <code>&amp; (RXBUF_SIZE-1)</code>——与指令单周期，除法指令要多周期。取模对任何大小都能用，2 的幂的真正好处是这个位运算技巧</p>
 </li>
 <li>
  <p><strong>多实例化</strong>：把下标打包成结构体、缓冲区由 init 传入（数据与控制器分离），一套代码实例化 N 个队列（多串口各收发一个）</p>
 </li>
 <li>
  <p><strong>发送也进队列</strong>：send 只塞 TX 缓冲区，TXE 中断慢慢搬——非阻塞发送</p>
 </li>
 <li>
  <p><strong>DMA + 环形缓冲</strong>：DMA 往缓冲区搬字节，CPU 只管处理——性能天花板</p>
 </li>
 <li>
  <p><strong>现实世界</strong>：操作系统内核、网络协议栈、键盘缓冲、音频缓冲——全是这一套</p>
 </li>
</ul>
<h2 id="10.-%E6%80%BB%E7%BB%93">10. 总结</h2>
<div class="halo-table-wrapper" data-table-layout="auto" style="box-sizing: border-box; overflow-x: auto; overflow-y: hidden; width: 100%; max-width: 100%; min-width: 0;">
 <table data-table-layout="auto" style="display: table; width: 100%; min-width: 100%; table-layout: auto;">
  <tbody>
   <tr>
    <th colspan="1" rowspan="1">
     <p>要点</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>内容</p>
    </th>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>解决的问题</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>中断世界与主循环世界的节奏差</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>核心结构</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>数组 + 两个下标（head 写 / tail 读）+ 取模回绕</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>满空区分</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>牺牲一格：(head+1) % SIZE == tail 为满</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>并发安全</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>volatile（看得见）+ 单一写者（不打架）= 不用关中断</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>count</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>公式推导，不另存（单一事实来源）</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>丢包</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>策略明确（丢新/丢旧）+ err 记账（可观测）</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>消费姿势</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>批量读 + 排空式</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>架构位置</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>中断生产 → 缓冲存储 → 主循环消费</p>
    </td>
   </tr>
  </tbody>
 </table>
</div>
<p><strong>一句话收尾：环形缓冲区是嵌入式世界的万能骨架——中断是快递员，缓冲区是快递柜，主循环是收件人，err 是登记簿。</strong></p>]]></description><guid isPermaLink="false">/archives/embedded-ring-buffer</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Sat, 19 Sep 2026 05:59:49 GMT</pubDate></item><item><title><![CDATA[2.4 编译产物与F5调试全流程]]></title><link>https://www.flor.cn/archives/build-output-size-elf-hex-bin-2-4</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=2.4%20%E7%BC%96%E8%AF%91%E4%BA%A7%E7%89%A9%E4%B8%8EF5%E8%B0%83%E8%AF%95%E5%85%A8%E6%B5%81%E7%A8%8B&amp;url=/archives/build-output-size-elf-hex-bin-2-4" width="1" height="1" alt="" style="opacity:0;">
<h2 id="%E4%B8%80%E3%80%81%E7%BC%96%E8%AF%91%E6%88%90%E5%8A%9F%E7%9A%84%E6%A0%87%E5%BF%97%EF%BC%9Asize-%E6%B1%87%E6%80%BB">一、编译成功的标志：size 汇总</h2>
<p><code>make</code> 输出的最后一行是 size 汇总，看到它就代表编译、链接全部通过：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>text    data     bss     dec     hex filename
   4736      12    1644    6392    18f8 build/uart.elf
</code></pre>
</shiki-code>
<p>三个关键数的读法：</p>
<div class="halo-table-wrapper" data-table-layout="auto" style="box-sizing: border-box; overflow-x: auto; overflow-y: hidden; width: 100%; max-width: 100%; min-width: 0;">
 <table data-table-layout="auto" style="display: table; width: 100%; min-width: 100%; table-layout: auto;">
  <tbody>
   <tr>
    <th colspan="1" rowspan="1">
     <p>字段</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>含义</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>占用</p>
    </th>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>text</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>代码 + 常量</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>占 Flash</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>data</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>已初始化变量</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>占 Flash，启动时拷进 SRAM</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>bss</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>清零变量</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>只占 SRAM</p>
    </td>
   </tr>
  </tbody>
 </table>
</div>
<p>这三个数就是程序对 512KB Flash / 64KB SRAM 的占用量</p>
<h2 id="%E4%BA%8C%E3%80%81elf-%2F-hex-%2F-bin-%2F-map%EF%BC%9A%E4%B8%80%E6%AC%A1%E7%BC%96%E8%AF%91%E5%85%A8%E5%87%BA">二、elf / hex / bin / map：一次编译全出</h2>
<p>CubeMX Makefile 的 <code>all</code> 目标是 elf + hex + bin 三件套一起出，编译完去 <code>uart/build/</code> 目录就能看到（连同 map 共四样）：</p>
<div class="halo-table-wrapper" data-table-layout="auto" style="box-sizing: border-box; overflow-x: auto; overflow-y: hidden; width: 100%; max-width: 100%; min-width: 0;">
 <table data-table-layout="auto" style="display: table; width: 100%; min-width: 100%; table-layout: auto;">
  <tbody>
   <tr>
    <th colspan="1" rowspan="1">
     <p>文件</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>是什么</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>谁用它</p>
    </th>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>uart.elf</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>带调试符号的完整档案</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>gdb（F5 调试、flash-remote.sh 烧录都吃这个）</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>uart.hex</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>Intel HEX 文本格式，带地址信息</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>FlyMcu 等串口烧录工具</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>uart.bin</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>裸二进制镜像</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>部分烧录器 / OTA</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>uart.map</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>每个符号放在哪个地址的地图</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>查内存布局</p>
    </td>
   </tr>
  </tbody>
 </table>
</div>
<h2 id="%E4%B8%89%E3%80%81%E8%B0%83%E8%AF%95%EF%BC%9Af5-%E5%85%A8%E6%B5%81%E7%A8%8B">三、调试：F5 全流程</h2>
<h3 id="%E5%85%88%E7%BC%96%E8%AF%91%EF%BC%8C%E5%86%8D%E8%B0%83%E8%AF%95">先编译，再调试</h3>
<p><code>Ctrl+Shift+B</code> 编译。云端 <code>launch.json</code> 没配 <code>preLaunchTask</code>，F5 不会自动编译——改了代码直接 F5、现象没变，99% 是忘了编译（烧的还是旧 elf）。</p>
<h3 id="%E5%90%AF%E5%8A%A8%E4%B8%8E%E8%87%AA%E5%8A%A8%E6%B5%81%E7%A8%8B">启动与自动流程</h3>
<ul>
 <li>
  <p>F5 → 第一次会让你选调试配置，选 <strong>Remote openocd (frp)</strong>（之后记住选择不再问）</p>
 </li>
 <li>
  <p>自动流程：连隧道 → 复位 → 烧录 → 停在 main 入口</p>
 </li>
</ul>
<h3 id="%E5%B8%B8%E7%94%A8%E6%93%8D%E4%BD%9C">常用操作</h3>
<div class="halo-table-wrapper" data-table-layout="auto" style="box-sizing: border-box; overflow-x: auto; overflow-y: hidden; width: 100%; max-width: 100%; min-width: 0;">
 <table data-table-layout="auto" style="display: table; width: 100%; min-width: 100%; table-layout: auto;">
  <tbody>
   <tr>
    <th colspan="1" rowspan="1">
     <p>操作</p>
    </th>
    <th colspan="1" rowspan="1">
     <p>方式</p>
    </th>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>断点</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>点行号左侧出现红点；F9 切换</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>继续</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>F5</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>单步跳过</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>F10</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>单步进入</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>F11</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>跳出</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>Shift+F11</p>
    </td>
   </tr>
   <tr>
    <td colspan="1" rowspan="1">
     <p>结束调试</p>
    </td>
    <td colspan="1" rowspan="1">
     <p>Shift+F5（干净退出，释放 openocd 连接）</p>
    </td>
   </tr>
  </tbody>
 </table>
</div>
<p>左侧三个面板：</p>
<ul>
 <li>
  <p><strong>VARIABLES</strong> — 局部/全局变量实时值</p>
 </li>
 <li>
  <p><strong>WATCH</strong> — 右键变量 → Add to Watch</p>
 </li>
 <li>
  <p><strong>CALL STACK</strong> — 调用堆栈</p>
 </li>
</ul>
<p>DEBUG CONSOLE 里可以直接输表达式读寄存器，比如看 GPIOA 输出寄存器：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>p/x *(volatile unsigned int*)0x4001080C
</code></pre>
</shiki-code>
<p>外设寄存器图形面板（CORTEX PERIPHERALS）目前 <code>launch.json</code> 还没配 <code>svdFile</code>，可能为空——用控制台表达式读完全够用，想图形化加一行配置即可。</p>
<h3 id="%E4%B8%A4%E4%B8%AA%E6%AD%A3%E5%B8%B8%E7%8E%B0%E8%B1%A1">两个正常现象</h3>
<ol>
 <li>
  <p><strong>断点暂停 = 芯片被 halt</strong>，串口停发 abc；F5 继续后恢复</p>
 </li>
 <li>
  <p><strong>调试会话独占 openocd</strong>——F5 期间跑 <code>flash-remote.sh</code> 会连不上，先 Shift+F5 退出</p>
 </li>
</ol>
<p></p>]]></description><guid isPermaLink="false">/archives/build-output-size-elf-hex-bin-2-4</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Mon, 14 Sep 2026 09:24:27 GMT</pubDate></item><item><title><![CDATA[2.3 USART初始化配置与HAL发送函数]]></title><link>https://www.flor.cn/archives/usart-init-and-transmit-2-3</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=2.3%20USART%E5%88%9D%E5%A7%8B%E5%8C%96%E9%85%8D%E7%BD%AE%E4%B8%8EHAL%E5%8F%91%E9%80%81%E5%87%BD%E6%95%B0&amp;url=/archives/usart-init-and-transmit-2-3" width="1" height="1" alt="" style="opacity:0;">
<h2 id="%E4%B8%80%E3%80%81cubemx-%E5%8F%82%E6%95%B0%E9%85%8D%E7%BD%AE%E8%AF%A6%E8%A7%A3">一、CubeMX 参数配置详解</h2>
<p>在 CubeMX 的 USART1 配置页中，核心参数如下：</p>
<table>
 <thead>
  <tr>
   <th>参数</th>
   <th>值</th>
   <th>含义</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>Baud Rate</td>
   <td>115200</td>
   <td>波特率</td>
  </tr>
  <tr>
   <td>Word Length</td>
   <td>8 Bits (including Parity)</td>
   <td>8 数据位</td>
  </tr>
  <tr>
   <td>Parity</td>
   <td>None</td>
   <td>无校验</td>
  </tr>
  <tr>
   <td>Stop Bits</td>
   <td>1</td>
   <td>1 停止位</td>
  </tr>
  <tr>
   <td>Data Direction</td>
   <td>Receive and Transmit</td>
   <td>收发双向</td>
  </tr>
  <tr>
   <td>Over Sampling</td>
   <td>16 samples</td>
   <td>16 倍过采样</td>
  </tr>
 </tbody>
</table>
<p><strong>逐项原理说明：</strong></p>
<ul>
 <li><strong>Baud Rate（波特率）</strong>：每秒传输的比特数。115200 是嵌入式最常用速率，由 BRR 寄存器分频得到：<code>BRR = f_PCLK / 波特率</code>（USART1 挂 APB2 = 72MHz，72000000/115200 = 625，无误差）。</li>
 <li><strong>Word Length（字长）</strong>：一个数据帧的数据位长度。注意 HAL 的 8B 是"含校验位"的——若开启奇偶校验，实际有效数据只有 7 位；无校验时才是完整 8 位。</li>
 <li><strong>Parity（校验）</strong>：None / Even（偶校验）/ Odd（奇校验）。无校验最常用；开启后校验位占用数据帧一位。</li>
 <li><strong>Stop Bits（停止位）</strong>：帧尾电平保持时间，1 位最常用（0.5/1/1.5/2 可选）。</li>
 <li><strong>Data Direction（模式）</strong>：TX_RX 同时使能发送与接收（对应 CR1 寄存器的 TE、RE 位）。</li>
 <li><strong>Over Sampling（过采样）</strong>：16 倍过采样对每位采样 16 次取多数，抗干扰强，是默认推荐；8 倍过采样可支撑更高波特率但容错差。</li>
</ul>
<p>一帧完整格式：<strong>起始位(1) + 数据位(8) + 校验位(0) + 停止位(1)</strong>，即 115200-8-N-1。</p>
<h2 id="%E4%BA%8C%E3%80%81cubemx-%E7%94%9F%E6%88%90%E7%9A%84%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BB%A3%E7%A0%81">二、CubeMX 生成的初始化代码</h2>
<p><code>usart.c</code> 中的核心结构体与初始化函数：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">UART_HandleTypeDef huart1;

void MX_USART1_UART_Init(void)
{
  huart1.Instance           = USART1;
  huart1.Init.BaudRate      = 115200;
  huart1.Init.WordLength    = UART_WORDLENGTH_8B;
  huart1.Init.StopBits      = UART_STOPBITS_1;
  huart1.Init.Parity        = UART_PARITY_NONE;
  huart1.Init.Mode          = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl     = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling  = UART_OVERSAMPLING_16;
  HAL_UART_Init(&amp;huart1);
}
</code></pre>
</shiki-code>
<p><strong>结构体字段与硬件的对应关系：</strong></p>
<table>
 <thead>
  <tr>
   <th>字段</th>
   <th>写入的寄存器/位</th>
   <th>作用</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>Instance</td>
   <td>—</td>
   <td>指向 USART1 外设基地址</td>
  </tr>
  <tr>
   <td>BaudRate</td>
   <td>BRR</td>
   <td>波特率分频值</td>
  </tr>
  <tr>
   <td>WordLength</td>
   <td>CR1.M</td>
   <td>数据位长度</td>
  </tr>
  <tr>
   <td>StopBits</td>
   <td>CR2.STOP</td>
   <td>停止位</td>
  </tr>
  <tr>
   <td>Parity</td>
   <td>CR1.PCE / PS</td>
   <td>校验使能与类型</td>
  </tr>
  <tr>
   <td>Mode</td>
   <td>CR1.TE / RE</td>
   <td>发送/接收使能</td>
  </tr>
  <tr>
   <td>HwFlowCtl</td>
   <td>CR3.RTSE / CTSE</td>
   <td>硬件流控（RTS/CTS）</td>
  </tr>
  <tr>
   <td>OverSampling</td>
   <td>CR1.OVER8</td>
   <td>8/16 倍过采样</td>
  </tr>
 </tbody>
</table>
<p><code>HAL_UART_Init()</code> 内部依次完成：参数校验 → 调用 <code>HAL_UART_MspInit()</code>（CubeMX 生成在 <code>usart.c</code> 底部：使能 USART1 与 GPIOA 时钟、配置 PA9/PA10 复用推挽）→ 按 Init 结构体写入 CR1/CR2/CR3/BRR → 使能外设。</p>
<p><strong>为什么代码里看不到 RCC 和 GPIO 配置？</strong> 因为 CubeMX 把"芯片级初始化"（时钟使能、引脚复用）拆进了 <code>HAL_UART_MspInit()</code>，与"协议参数配置"（<code>MX_USART1_UART_Init()</code>）分离——前者管硬件资源，后者管通信格式，各司其职。</p>
<h2 id="%E4%B8%89%E3%80%81hal_uart_transmit-%E5%8F%91%E9%80%81%E5%87%BD%E6%95%B0">三、HAL_UART_Transmit 发送函数</h2>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">HAL_StatusTypeDef HAL_UART_Transmit(
    UART_HandleTypeDef *huart,  // 串口句柄
    uint8_t *pData,             // 数据指针
    uint16_t Size,              // 数据长度
    uint32_t Timeout            // 超时(ms)
);
</code></pre>
</shiki-code>
<p><strong>工作流程（阻塞式发送）：</strong></p>
<ol>
 <li>逐字节把数据写入 DR（数据寄存器）</li>
 <li>等待 TXE（发送数据寄存器空）标志，再写下一字节</li>
 <li>全部写完后等待 TC（发送完成）标志</li>
 <li>期间用 Timeout 计数，超时则放弃并返回 <code>HAL_TIMEOUT</code></li>
</ol>
<p><strong>返回值：</strong></p>
<table>
 <thead>
  <tr>
   <th>返回值</th>
   <th>含义</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>HAL_OK</td>
   <td>发送成功</td>
  </tr>
  <tr>
   <td>HAL_ERROR</td>
   <td>参数错误</td>
  </tr>
  <tr>
   <td>HAL_BUSY</td>
   <td>串口被占用（如上次发送未完成）</td>
  </tr>
  <tr>
   <td>HAL_TIMEOUT</td>
   <td>超时放弃</td>
  </tr>
 </tbody>
</table>
<p><strong>使用示例：</strong></p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code class="language-c">uint8_t msg[] = "Hello USART1\r\n";
HAL_UART_Transmit(&amp;huart1, msg, sizeof(msg) - 1, 100);  // 100ms 超时
</code></pre>
</shiki-code>
<p><strong>要点：</strong></p>
<ul>
 <li>阻塞式发送——CPU 停在函数里等硬件搬完，期间不能干别的；大数据量场景应改用中断（<code>HAL_UART_Transmit_IT</code>）或 DMA 版本。</li>
 <li><code>sizeof(msg) - 1</code> 减掉字符串末尾的 <code>\0</code>，串口不需要发它。</li>
 <li>发送速率由波特率决定：115200bps 下每字节约 87μs，发 100 字节约 8.7ms。</li>
</ul>]]></description><guid isPermaLink="false">/archives/usart-init-and-transmit-2-3</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Fri, 11 Sep 2026 05:18:36 GMT</pubDate></item><item><title><![CDATA[2.2 USART硬件架构与挂载使能总链路]]></title><link>https://www.flor.cn/archives/usart-hardware-architecture-2-2</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=2.2%20USART%E7%A1%AC%E4%BB%B6%E6%9E%B6%E6%9E%84%E4%B8%8E%E6%8C%82%E8%BD%BD%E4%BD%BF%E8%83%BD%E6%80%BB%E9%93%BE%E8%B7%AF&amp;url=/archives/usart-hardware-architecture-2-2" width="1" height="1" alt="" style="opacity:0;">
<h2 id="%E4%B8%80%E3%80%81%E5%85%AD%E4%B8%AA%E6%A0%87%E5%87%86%E9%97%AE%E9%A2%98">一、六个标准问题</h2>
<h3 id="%E2%91%A0-%E6%8C%82%E5%93%AA%E6%9D%A1%E6%80%BB%E7%BA%BF">① 挂哪条总线</h3>
<p>USART1 挂 <strong>APB2</strong>（72MHz）。</p>
<p>回顾：F103 有三个 USART（大容量 ZET6 有 USART1/2/3 + UART4/5），总线分配不同：</p>
<table>
 <thead>
  <tr>
   <th>外设</th>
   <th>总线</th>
   <th>时钟频率</th>
   <th>本板用途</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td><strong>USART1</strong></td>
   <td><strong>APB2</strong></td>
   <td><strong>72MHz</strong></td>
   <td><strong>CH340K 串口（PA9/PA10）</strong></td>
  </tr>
  <tr>
   <td>USART2</td>
   <td>APB1</td>
   <td>36MHz</td>
   <td>扩展（PA2/PA3）</td>
  </tr>
  <tr>
   <td>USART3</td>
   <td>APB1</td>
   <td>36MHz</td>
   <td>扩展（PB10/PB11）</td>
  </tr>
  <tr>
   <td>UART4/5</td>
   <td>APB1</td>
   <td>36MHz</td>
   <td>扩展</td>
  </tr>
 </tbody>
</table>
<p>USART1 是唯一挂在 APB2 的串口，时钟直接 72MHz，不需要倍频补偿，波特率计算最精确。这也是为什么调试串口<strong>首选 USART1</strong>。</p>
<h3 id="%E2%91%A1-%E6%97%B6%E9%92%9F%E6%9D%A5%E6%BA%90%E4%B8%8E%E7%89%B9%E6%AE%8A%E5%80%8D%E9%A2%91">② 时钟来源与特殊倍频</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>HSE 8MHz → PLL×9 → SYSCLK 72MHz
                              → HCLK 72MHz
                                  → PCLK2 72MHz（APB2 不分频）
                                      → USART1 72MHz
</code></pre>
</shiki-code>
<p>USART1 时钟 = PCLK2 = <strong>72MHz</strong>，无倍频、无分频，直接用。</p>
<blockquote>
 <p>对比：USART2/3/4/5 挂 APB1，PCLK1=36MHz，波特率计算时基准时钟是 36MHz。</p>
</blockquote>
<h3 id="%E2%91%A2-%E9%97%A8%E6%8E%A7%E5%BC%80%E5%85%B3">③ 门控开关</h3>
<p>USART1 的时钟使能位在 <strong>RCC_APB2ENR</strong> 的 <strong>bit14（USART1EN）</strong>：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>RCC_APB2ENR |= RCC_APB2ENR_USART1EN;  // 开 USART1 时钟
</code></pre>
</shiki-code>
<p>同时，USART1 用 PA9/PA10，GPIOA 也要开：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>RCC_APB2ENR |= RCC_APB2ENR_IOPAEN;    // 开 GPIOA 时钟
</code></pre>
</shiki-code>
<p><strong>配电箱比喻</strong>：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>总闸 SYSCLK 72MHz
  └─ 分闸 APB2ENR（72MHz 母线）
       ├─ IOPAEN  → GPIOA 房间（PA9/PA10 引脚）
       └─ USART1EN → USART1 房间（波特率发生器/收发器）
</code></pre>
</shiki-code>
<p>两个开关都要开，缺一不可：GPIO 负责引脚电平物理输出，USART 负责协议帧时序。</p>
<h3 id="%E2%91%A3-cubemx-%E4%B8%AD%E5%AF%B9%E5%BA%94%E9%85%8D%E7%BD%AE">④ CubeMX 中对应配置</h3>
<p>CubeMX 中配置 USART1 非常简单，大部分参数 CubeMX 自动处理：</p>
<table>
 <thead>
  <tr>
   <th>配置项</th>
   <th>CubeMX 位置</th>
   <th>是否可见</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>USART1 时钟使能</td>
   <td>勾选 USART1 → 自动开</td>
   <td>隐藏，自动生成</td>
  </tr>
  <tr>
   <td>GPIOA 时钟使能</td>
   <td>选 PA9/PA10 → 自动开</td>
   <td>隐藏，自动生成</td>
  </tr>
  <tr>
   <td>PA9 = USART1_TX</td>
   <td>Connectivity → USART1</td>
   <td>可见</td>
  </tr>
  <tr>
   <td>PA10 = USART1_RX</td>
   <td>Connectivity → USART1</td>
   <td>可见</td>
  </tr>
  <tr>
   <td>波特率/字长/停止位/校验</td>
   <td>Parameter Settings</td>
   <td>可见</td>
  </tr>
 </tbody>
</table>
<blockquote>
 <p>注意：GPIOA 的时钟使能（IOPAEN）在 CubeMX 界面无可见开关，选了 PA9/PA10 后自动推断生成。这和你学 GPIO 时 IOPBEN 不可见是同样的道理。</p>
</blockquote>
<h3 id="%E2%91%A4-%E5%AF%84%E5%AD%98%E5%99%A8%E5%8F%82%E6%95%B0%EF%BC%88usart-%E6%A0%B8%E5%BF%83%E5%AF%84%E5%AD%98%E5%99%A8%EF%BC%89">⑤ 寄存器参数（USART 核心寄存器）</h3>
<p>USART 外设核心寄存器就几个，理解这 5 个就够：</p>
<table>
 <thead>
  <tr>
   <th>寄存器</th>
   <th>作用</th>
   <th>关键位</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td><strong>USART_SR</strong>（状态寄存器）</td>
   <td>查状态</td>
   <td>TXE（发送缓冲空）、TC（发送完成）、RXNE（接收缓冲非空）</td>
  </tr>
  <tr>
   <td><strong>USART_DR</strong>（数据寄存器）</td>
   <td>读写数据</td>
   <td>写→发送、读→接收，只有低8位有效</td>
  </tr>
  <tr>
   <td><strong>USART_BRR</strong>（波特率寄存器）</td>
   <td>设波特率</td>
   <td>DIV_Mantissa + DIV_Fraction = 分频值</td>
  </tr>
  <tr>
   <td><strong>USART_CR1</strong>（控制寄存器1）</td>
   <td>总开关</td>
   <td>UE（USART使能）、TE（发送使能）、RE（接收使能）、RXNEIE（接收中断使能）</td>
  </tr>
  <tr>
   <td><strong>USART_CR2</strong>（控制寄存器2）</td>
   <td>帧参数</td>
   <td>STOP[1:0]（停止位）、CLKEN（同步时钟）</td>
  </tr>
 </tbody>
</table>
<p><strong>波特率计算公式</strong>（异步模式）：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>波特率 = fPCLK / (16 × DIV)
</code></pre>
</shiki-code>
<p>其中 <code>fPCLK</code> = 72MHz（USART1），<code>DIV</code> = BRR 里的分频值（整数+小数，共12位整数+4位小数）。</p>
<p>以 <strong>115200</strong> 为例：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>DIV = 72000000 / (16 × 115200) = 39.0625
整数部分 = 39 = 0x27
小数部分 = 0.0625 × 16 = 1 = 0x1
BRR = 0x271
</code></pre>
</shiki-code>
<p>CubeMX 填 115200 后自动算好写进 BRR，不用手算。但原理要知道。</p>
<h3 id="%E2%91%A5-%E7%A1%AC%E4%BB%B6%E8%A1%8C%E4%B8%BA%EF%BC%88%E6%95%B0%E6%8D%AE%E4%BB%8E%E5%8F%91%E9%80%81%E5%88%B0%E6%8E%A5%E6%94%B6%E7%9A%84%E5%AE%8C%E6%95%B4%E9%93%BE%E8%B7%AF%EF%BC%89">⑥ 硬件行为（数据从发送到接收的完整链路）</h3>
<p><strong>发送链路</strong>（STM32 → 电脑）：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>写 DR 寄存器
  → 移位寄存器逐位输出
    → TX 引脚(PA9) 按协议帧输出 TTL 电平
      → CH340K 收到 TTL 信号，转成 USB 协议
        → USB 线传到电脑
          → 电脑端 CH340 驱动转为虚拟 COM 口
            → 串口助手软件显示数据
</code></pre>
</shiki-code>
<p><strong>接收链路</strong>（电脑 → STM32）：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>串口助手发送数据
  → 电脑 USB → CH340K
    → CH340K 转 TTL → PA10(RX) 引脚
      → USART1 接收移位寄存器逐位采样
        → 收满一帧 → 数据写入 DR
          → 置 RXNE 标志 → 可读 DR 获取数据
</code></pre>
</shiki-code>
<h2 id="%E4%BA%8C%E3%80%81ch340k-%E8%BF%9E%E6%8E%A5%E7%94%B5%E8%B7%AF%EF%BC%88tx%2Frx-%E4%BA%A4%E5%8F%89%EF%BC%89">二、CH340K 连接电路（TX/RX 交叉）</h2>
<p>这是<strong>最容易接错</strong>的地方，务必记住：<strong>TX→RX，RX→TX，交叉连接</strong>。</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>STM32                CH340K
PA9  (USART1_TX) ──→ RXD
PA10 (USART1_RX) ←── TXD
GND ─────────────── GND    （共地！必须接）
</code></pre>
</shiki-code>
<blockquote>
 <p>为什么必须交叉？TX 是输出，RX 是输入。如果 TX→TX，两个输出对冲，谁也收不到数据。就像打电话，你的嘴（TX）要对对方的耳朵（RX），不可能嘴对嘴。</p>
</blockquote>
<p>你开发板上 CH340K 和 STM32 已经在 PCB 上固定连好了（PA9→CH340 RX、PA10→CH340 TX、共地），用一根 USB 线接电脑即可，不用飞线。</p>
<h2 id="%E4%B8%89%E3%80%81%E5%AE%8C%E6%95%B4%E6%8C%82%E8%BD%BD%E4%BD%BF%E8%83%BD%E6%80%BB%E9%93%BE%E8%B7%AF%E5%9B%BE">三、完整挂载使能总链路图</h2>
<p>把六层串起来，这是整个 USART1 的全景：</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>Cortex-M3 内核
  │
  ▼ SYSCLK = 72MHz（HSE 8MHz × PLL9）
  │
  ▼ HCLK = 72MHz
  │
  ▼ PCLK2 = 72MHz（APB2，不分频）
  │
  ├── RCC_APB2ENR.IOPAEN ───→ GPIOA 使能
  │                                ├── PA9  → 复用推挽输出（USART1_TX）
  │                                └── PA10 → 浮空/上拉输入（USART1_RX）
  │
  └── RCC_APB2ENR.USART1EN ─→ USART1 使能
                                   ├── BRR  = 0x271（115200bps）
                                   ├── CR1  = UE|TE|RE（使能+收发）
                                   ├── SR.TXE → 发送缓冲空，可写 DR
                                   ├── SR.RXNE → 接收缓冲有数据，可读 DR
                                   └── DR  → 读写数据
                                        │
                                        ▼
                                   PA9/PA10 引脚电平变化
                                        │
                                        ▼
                                   CH340K（TTL↔USB）
                                        │
                                        ▼
                                   电脑 USB → 虚拟 COM → 串口助手
</code></pre>
</shiki-code>
<h2 id="%E5%9B%9B%E3%80%81pa9%2Fpa10-%E7%9A%84-gpio-%E6%A8%A1%E5%BC%8F">四、PA9/PA10 的 GPIO 模式</h2>
<p>USART 用的是<strong>复用功能（AF）</strong>，GPIO 模式和普通 IO 不同：</p>
<table>
 <thead>
  <tr>
   <th>引脚</th>
   <th>功能</th>
   <th>GPIO 模式</th>
   <th>说明</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>PA9</td>
   <td>USART1_TX</td>
   <td><strong>复用推挽输出</strong></td>
   <td>数据由 USART 外设驱动输出，不由 GPIO_ODR 驱动</td>
  </tr>
  <tr>
   <td>PA10</td>
   <td>USART1_RX</td>
   <td><strong>浮空输入/上拉输入</strong></td>
   <td>接收由 USART 外设采样，GPIO 只需配置为输入</td>
  </tr>
 </tbody>
</table>
<blockquote>
 <p>关键理解：一旦开启 USART 的 TE（发送使能），PA9 的输出控制权就<strong>从 GPIO 模块移交给了 USART 外设</strong>，此时写 GPIO_ODR 无效，数据要写 USART_DR 才会从 PA9 发出。这就是"复用"的含义——引脚被外设"借用"了。</p>
</blockquote>
<p>CubeMX 中你只需选 PA9 = USART1_TX、PA10 = USART1_RX，GPIO 模式自动配好，不用手动设。</p>]]></description><guid isPermaLink="false">/archives/usart-hardware-architecture-2-2</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Tue, 8 Sep 2026 08:04:52 GMT</pubDate></item><item><title><![CDATA[RCC外设依赖]]></title><link>https://www.flor.cn/archives/1787237355790</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=RCC%E5%A4%96%E8%AE%BE%E4%BE%9D%E8%B5%96&amp;url=/archives/1787237355790" width="1" height="1" alt="" style="opacity:0;">
<p>好问题，这正是理解 STM32F1 架构的关键点。RCC 的位置其实有"两个身份"，很多人混淆就在这里。</p>
<hr>
<h2 id="%E4%B8%80%E3%80%81rcc-%E5%88%B0%E5%BA%95%E5%9C%A8%E5%93%AA%E4%B8%80%E5%B1%82%EF%BC%9F%E2%80%94%E2%80%94%E5%8F%8C%E9%87%8D%E8%BA%AB%E4%BB%BD">一、RCC 到底在哪一层？——双重身份</h2>
<p><strong>物理上：RCC 是挂在 AHB 总线上的一颗"外设"</strong></p>
<ul>
 <li>
  <p>地址 <code>0x40021000</code>，有自己的寄存器组（CR、CFGR、AHBENR、APB1ENR、APB2ENR...）</p>
 </li>
 <li>
  <p>和 DMA、SDIO、CRC 一样是 AHB 总线上的成员</p>
 </li>
</ul>
<p><strong>逻辑上：RCC 是整个芯片的"时钟分发中心"（中枢）</strong></p>
<ul>
 <li>
  <p>它决定 SYSCLK 来源（HSI/HSE/PLL）、各总线分频（HCLK/PCLK1/PCLK2）</p>
 </li>
 <li>
  <p>它掌管<strong>每个外设的时钟使能</strong>（门控开关）——不开就点不亮</p>
 </li>
</ul>
<p><strong>特殊性</strong>：RCC 是<strong>唯一不需要别人给它开时钟的外设</strong>。上电后芯片默认用 HSI 8MHz 直接运行，RCC 自己就能工作，然后它再去"点亮"其他所有人。所以它既是"外设"，又是所有外设依赖链的<strong>第一环</strong>。</p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>             Cortex-M3
                │
        SYSCLK（系统时钟）
                │
     ┌──────────┴──────────┐
     │   AHB 总线 (72MHz)   │
     └──┬──────┬─────┬─────┘
        │      │     │
     ┌──▼──┐ ┌▼───┐ ┌▼────────────┐
     │ DMA │ │SDIO│ │ RCC ◄──── 时钟中枢 │
     │ CRC │ │Flash│ │(自己先跑起来，再发   │
     └─────┘ └────┘ │ 时钟给所有外设)    │
                    └──────┬───────────┘
              ┌────────────┼────────────┐
        ┌─────▼─────┐ ┌────▼─────┐
        │ APB2 总线  │ │ APB1 总线 │
        │ (72MHz)   │ │ (36MHz)  │
        └───────────┘ └──────────┘
</code></pre>
</shiki-code>
<hr>
<h2 id="%E4%BA%8C%E3%80%81f1-%E6%97%B6%E9%92%9F%E6%A0%91%EF%BC%9Arcc-%E8%BE%93%E5%87%BA%E5%93%AA%E4%BA%9B%E6%97%B6%E9%92%9F">二、F1 时钟树：RCC 输出哪些时钟</h2>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>HSE 8MHz ──────┐
               ├─→ PLL(×9) ──→ SYSCLK 72MHz ──→ HCLK 72MHz
HSI 8MHz ──────┘                 │                 │
                          ┌──────┴──────┐   ┌──────┴──────┐
                          │ APB1 分频 /2 │   │ APB2 分频 /1 │
                          ▼             ▼   ▼             ▼
                     PCLK1 = 36MHz  PCLK2 = 72MHz
LSE 32.768KHz ──→ RTC（独立于总线）
LSI 40KHz ──────→ IWDG（独立于总线）
PLL ─────────────→ USB 48MHz（专门输出）
</code></pre>
</shiki-code>
<hr>
<h2 id="%E4%B8%89%E3%80%81%E5%90%84%E5%A4%96%E8%AE%BE%E6%8C%82%E5%9C%A8%E5%93%AA%E4%B8%AA%E6%80%BB%E7%BA%BF%EF%BC%88f1-%E7%89%B9%E4%BE%8B%E6%B3%A8%E6%84%8F%EF%BC%81%EF%BC%89">三、各外设挂在哪个总线（F1 特例注意！）</h2>
<p><strong>APB2（高速 72MHz）</strong>——注意 <strong>F1 的 GPIO 在 APB2</strong>（F4 在 AHB1，别混）：</p>
<div style="overflow-x: auto; overflow-y: hidden;">
 <table style="width: 200px">
  <colgroup>
   <col style="width: 100px">
   <col style="width: 100px">
  </colgroup>
  <tbody>
   <tr style="height: 60px;">
    <th colspan="1" colwidth="100" rowspan="1">
     <p>外设</p>
    </th>
    <th colspan="1" colwidth="100" rowspan="1">
     <p>时钟使能函数</p>
    </th>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>GPIOA~GPIOG</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_GPIOx_CLK_ENABLE()</code></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>AFIO</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_AFIO_CLK_ENABLE()</code></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>USART1</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_USART1_CLK_ENABLE()</code></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>SPI1</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_SPI1_CLK_ENABLE()</code></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>TIM1 / TIM8</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_TIM1/8_CLK_ENABLE()</code></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>ADC1/2/3</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_ADCx_CLK_ENABLE()</code></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>EXTI</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>（由 AFIO 管理，使能 AFIO 即可）</p>
    </td>
   </tr>
  </tbody>
 </table>
</div>
<p><strong>APB1（低速 36MHz）</strong>：</p>
<div style="overflow-x: auto; overflow-y: hidden;">
 <table style="width: 200px">
  <colgroup>
   <col style="width: 100px">
   <col style="width: 100px">
  </colgroup>
  <tbody>
   <tr style="height: 60px;">
    <th colspan="1" colwidth="100" rowspan="1">
     <p>外设</p>
    </th>
    <th colspan="1" colwidth="100" rowspan="1">
     <p>时钟使能函数</p>
    </th>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>USART2/3</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_USART2/3_CLK_ENABLE()</code></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>SPI2/3</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_SPI2/3_CLK_ENABLE()</code></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>I2C1/2</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_I2Cx_CLK_ENABLE()</code></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>TIM2~TIM7</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_TIMx_CLK_ENABLE()</code></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>CAN1</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_CAN1_CLK_ENABLE()</code></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>USB</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_USB_CLK_ENABLE()</code>（还要 PLL 出 48MHz）</p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>DAC</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_DAC_CLK_ENABLE()</code></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>PWR / BKP</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_PWR_CLK_ENABLE()</code>（RTC 解锁用）</p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>WWDG</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>__HAL_RCC_WWDG_CLK_ENABLE()</code></p>
    </td>
   </tr>
  </tbody>
 </table>
</div>
<p><strong>AHB（72MHz）</strong>：DMA1/2、SDIO、CRC、Flash 接口</p>
<p><strong>独立时钟源（不经过总线）</strong>：IWDG←LSI、RTC←LSE/LSI</p>
<hr>
<h2 id="%E5%9B%9B%E3%80%81%E5%90%84%E5%A4%96%E8%AE%BE%E5%AE%8C%E6%95%B4%E4%BE%9D%E8%B5%96%E9%93%BE%EF%BC%88%E9%87%8D%E7%82%B9%EF%BC%89">四、各外设完整依赖链（重点）</h2>
<p>每个外设不是"开自己的时钟就行"，它的<strong>完整依赖链</strong>如下（★=必须，☆=可选）：</p>
<h3 id="1.-gpio%EF%BC%88%E6%9C%80%E5%9F%BA%E7%A1%80%EF%BC%89">1. GPIO（最基础）</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>★ RCC 使能 GPIOx 时钟 → ★ 配置 MODER/OTYPER/PUPDR → (无 AFIO/NVIC)
</code></pre>
</shiki-code>
<p><strong>只依赖 RCC</strong>，这就是为什么它排在阶段 1 最前。</p>
<h3 id="2.-afio%EF%BC%88%E5%A4%8D%E7%94%A8%E6%8E%A5%E7%BA%BF%E5%91%98%EF%BC%89">2. AFIO（复用接线员）</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>★ RCC 使能 AFIO 时钟 → ★ 配置复用重映射
</code></pre>
</shiki-code>
<p>依赖 RCC；它本身是 GPIO"切换成外设功能"的桥梁。</p>
<h3 id="3.-exti-%E5%A4%96%E9%83%A8%E4%B8%AD%E6%96%AD">3. EXTI 外部中断</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>★ RCC 使能 AFIO（EXTI 源选择在 AFIO 里）
★ GPIO 配输入模式
★ 配置 EXTI 线路 + 边沿
★ NVIC 使能 + 优先级
</code></pre>
</shiki-code>
<p><strong>依赖：RCC + GPIO + AFIO + NVIC</strong>（四层全占）。</p>
<h3 id="4.-usart1%EF%BC%88%E9%98%B6%E6%AE%B5-2%EF%BC%89">4. USART1（阶段 2）</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>★ RCC 使能 USART1（APB2）
★ RCC 使能 GPIOA
★ PA9/PA10 配置复用（AFIO）
★ 配置波特率/数据位
☆ NVIC（中断接收）
☆ DMA（DMA1_CH4=TX、CH5=RX）
</code></pre>
</shiki-code>
<p><strong>依赖：RCC + GPIO + AFIO +（NVIC/DMA）</strong>。</p>
<h3 id="5.-tim%EF%BC%88%E9%98%B6%E6%AE%B5-3%EF%BC%89">5. TIM（阶段 3）</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>★ RCC 使能 TIMx
★ 配置 PSC/ARR/计数模式
☆ GPIO 复用（PWM 输出、输入捕获、编码器才需要）
☆ NVIC（定时中断）
</code></pre>
</shiki-code>
<p>基础定时<strong>只依赖 RCC</strong>；输出功能再加 GPIO。</p>
<h3 id="6.-adc%EF%BC%88%E9%98%B6%E6%AE%B5-7%EF%BC%89">6. ADC（阶段 7）</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>★ RCC 使能 ADCx（APB2）
★ RCC 使能 GPIO（模拟输入模式！）
★ ADC 时钟分频（APB2 72MHz → ADC ≤14MHz，如 /6=12MHz）
★ 校准 → 配置通道/采样时间
☆ DMA（ADC1→DMA1_CH1，多通道必备）
</code></pre>
</shiki-code>
<p><strong>注意</strong>：模拟输入引脚不能配推挽/复用，要配 <strong>Analog 模式</strong>。</p>
<h3 id="7.-spi%EF%BC%88%E9%98%B6%E6%AE%B5-5%EF%BC%89">7. SPI（阶段 5）</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>★ RCC 使能 SPIx
★ RCC 使能 GPIO（PA5~7 或 PB13~15）
★ 复用功能配置
★ 配置速率/CPOL/CPHA
★ CS 用普通 GPIO 软件控制（F1 硬件 NSS 常不用）
</code></pre>
</shiki-code>
<h3 id="8.-i2c%EF%BC%88%E9%98%B6%E6%AE%B5-6%EF%BC%89">8. I2C（阶段 6）</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>★ RCC 使能 I2Cx（APB1）
★ RCC 使能 GPIOB（PB6/7）
★ 复用**开漏**输出（I2C 特殊性！）
★ 上拉（内部或外部）
</code></pre>
</shiki-code>
<h3 id="9.-dma%EF%BC%88%E9%98%B6%E6%AE%B5-4%EF%BC%89">9. DMA（阶段 4）</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>★ RCC 使能 DMA1/2（AHB）
★ 配置通道/方向/源/目的/模式
☆ NVIC（传输完成中断）
</code></pre>
</shiki-code>
<p><strong>DMA 自身只依赖 RCC</strong>，但它的意义是服务 USART/ADC/SPI，所以放在它们前面学。</p>
<h3 id="10.-can-%2F-usb%EF%BC%88%E9%98%B6%E6%AE%B5-11%2F12%EF%BC%8C%E5%8D%8F%E8%AE%AE%E7%BA%A7%EF%BC%89">10. CAN / USB（阶段 11/12，协议级）</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>CAN：★ RCC(CAN1) ★ GPIO(PA11/12 复用) ★ 波特率/滤波器 ★ 外部收发器 TJA1050
USB：★ RCC(USB) ★ PLL 输出 48MHz ★ GPIO(PA11/12 复用) ★ NVIC ★ USB 协议库
</code></pre>
</shiki-code>
<p><strong>USB 是唯一额外要求"PLL 同时输出 48MHz"的外设</strong>，时钟配置最特殊。</p>
<h3 id="11.-rtc-%2F-iwdg%EF%BC%88%E9%98%B6%E6%AE%B5-9%2F10%EF%BC%8C%E5%A4%87%E4%BB%BD%E5%9F%9F%E4%B8%8E%E7%8B%AC%E7%AB%8B%E6%BA%90%EF%BC%89">11. RTC / IWDG（阶段 9/10，备份域与独立源）</h3>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>RTC：★ RCC(PWR+BKP) → ★ 解锁备份域写保护(DBP) → ★ LSE 32.768KHz → ★ 配置分频
IWDG：★ 直接用 LSI，不开任何总线时钟（写保护解除即可）
</code></pre>
</shiki-code>
<hr>
<h2 id="%E4%BA%94%E3%80%81%E4%B8%80%E5%BC%A0%E5%9B%BE%E7%9C%8B%E6%87%82%22%E8%B0%81%E4%BE%9D%E8%B5%96%E8%B0%81%22">五、一张图看懂"谁依赖谁"</h2>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>              RCC（时钟中枢，第一环）
              │
     ┌────────┼──────────────────┐
     ▼        ▼                  ▼
   GPIO    AFIO ←──GPIO的复用功能──┐
     │        │                  │
     ▼        ▼                  │
   EXTI ←──NVIC←──所有中断的入口──┤
     │                           │
     ▼                           ▼
  USART/TIM/ADC/SPI/I2C/CAN/USB   DMA（服务它们的数据搬运）
     │                           │
     └──────────┬────────────────┘
                ▼
        综合项目（全部串联）
</code></pre>
</shiki-code>
<p><strong>关键结论</strong>：</p>
<ol>
 <li>
  <p><strong>RCC 在 AHB 上，但它是时钟中枢</strong>——所有外设的第一依赖</p>
 </li>
 <li>
  <p><strong>GPIO + AFIO + NVIC 是三大公共前置</strong>——几乎所有通信/中断外设都要先过这三关</p>
 </li>
 <li>
  <p><strong>DMA 只依赖 RCC，但服务于外设</strong>——所以它早学（阶段 4），因为 USART/ADC/SPI 后面都要用</p>
 </li>
 <li>
  <p><strong>IWDG/RTC 是"独立王国"</strong>——不依赖总线时钟，学的时候不用管 RCC</p>
 </li>
</ol>
<p>这也正好解释了目录顺序为什么是 <strong>RCC→GPIO→AFIO→SysTick→EXTI→NVIC</strong>，先把公共前置全部打通，后面的外设实验就只剩"该外设自己的参数配置"了。</p>]]></description><guid isPermaLink="false">/archives/1787237355790</guid><dc:creator>flor</dc:creator><pubDate>Thu, 20 Aug 2026 14:49:50 GMT</pubDate></item><item><title><![CDATA[pwm]]></title><link>https://www.flor.cn/archives/1786503239668</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=pwm&amp;url=/archives/1786503239668" width="1" height="1" alt="" style="opacity:0;">
<h2 id="%E6%9C%AC%E8%AF%BE%E7%9B%AE%E6%A0%87%EF%BC%882.2-pwm-%E5%91%BC%E5%90%B8%E7%81%AF%EF%BC%89">本课目标（2.2 PWM 呼吸灯）</h2>
<ul>
 <li>
  <p>理解 PWM：占空比 = CCR ÷ ARR，频率 = 时钟 ÷ (PSC+1) ÷ (ARR+1)</p>
 </li>
 <li>
  <p>用定时器通道输出 PWM，用改变 CCR 的办法让 LED 平滑地“亮→暗→亮”</p>
 </li>
 <li>
  <p>验收标准：LED 亮度连续变化、不闪烁（仿真里用逻辑分析仪看 PA1 波形更直观）</p>
 </li>
</ul>
<h2 id="%E6%A6%82%E5%BF%B5%EF%BC%9A%E5%91%BC%E5%90%B8%E7%81%AF%E6%98%AF%E6%80%8E%E4%B9%88%E2%80%9C%E9%AA%97%E2%80%9D%E8%BF%87%E7%9C%BC%E7%9D%9B%E7%9A%84">概念：呼吸灯是怎么“骗”过眼睛的</h2>
<p>LED 其实是在<strong>快速开关</strong>，只是开关太快人眼看不出来。亮的时间占一个周期的比例叫<strong>占空比</strong>：占空比 0% = 全灭，100% = 全亮，50% = 半亮。呼吸灯就是让占空比从 0 慢慢加到 100%，再慢慢减回 0%。</p>
<ul>
 <li>
  <p><code>ARR</code>（Period）决定周期：一个周期数多少个计数</p>
 </li>
 <li>
  <p><code>CCR</code>（Pulse）决定占空比：一个周期里高电平多少个计数</p>
 </li>
 <li>
  <p>在 8MHz、PSC=79、ARR=999 下，PWM 频率 = 8M÷80÷1000 = <strong>100Hz</strong>，人眼完全看不出闪</p>
 </li>
</ul>
<div style="overflow-x: auto; overflow-y: hidden;">
 <table style="width: 200px">
  <colgroup>
   <col style="width: 100px">
   <col style="width: 100px">
  </colgroup>
  <tbody>
   <tr style="height: 60px;">
    <th colspan="1" colwidth="100" rowspan="1">
     <p>频率</p>
    </th>
    <th colspan="1" colwidth="100" rowspan="1">
     <p>周期</p>
    </th>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>1kHz</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>1ms</p>
    </td>
   </tr>
   <tr style="height: 59px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p><strong>10kHz</strong></p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p><strong>100µs</strong></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>100kHz</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>10µs</p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>1MHz</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>1µs</p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>1000Hz = 1kHz</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>1ms</p>
    </td>
   </tr>
  </tbody>
 </table>
</div>
<p></p>]]></description><guid isPermaLink="false">/archives/1786503239668</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Wed, 12 Aug 2026 02:54:06 GMT</pubDate></item><item><title><![CDATA[定时器中断]]></title><link>https://www.flor.cn/archives/1786500625933</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=%E5%AE%9A%E6%97%B6%E5%99%A8%E4%B8%AD%E6%96%AD&amp;url=/archives/1786500625933" width="1" height="1" alt="" style="opacity:0;">
<p></p>
<div style="overflow-x: auto; overflow-y: hidden;">
 <table style="width: 889px">
  <colgroup>
   <col style="width: 100px">
   <col style="width: 437px">
   <col style="width: 352px">
  </colgroup>
  <tbody>
   <tr style="height: 60px;">
    <th colspan="1" colwidth="100" rowspan="1">
     <p>函数</p>
    </th>
    <th colspan="1" colwidth="437" rowspan="1">
     <p>拆解</p>
    </th>
    <th colspan="1" colwidth="352" rowspan="1">
     <p>意思</p>
    </th>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>HAL_TIM_Base_Start_IT</code></p>
    </td>
    <td colspan="1" colwidth="437" rowspan="1">
     <p>Base=基础、Start=启动、IT=Interrupt(中断)</p>
    </td>
    <td colspan="1" colwidth="352" rowspan="1">
     <p><strong>启动定时器基础计时功能（中断方式）</strong></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>TIM2_IRQHandler</code></p>
    </td>
    <td colspan="1" colwidth="437" rowspan="1">
     <p>TIM2 + IRQ(中断请求) + Handler(处理函数)</p>
    </td>
    <td colspan="1" colwidth="352" rowspan="1">
     <p><strong>定时器2的中断服务函数（硬件入口）</strong></p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p><code>HAL_TIM_PeriodElapsedCallback</code></p>
    </td>
    <td colspan="1" colwidth="437" rowspan="1">
     <p>Period=周期、Elapsed=到点/流逝、Callback=回调</p>
    </td>
    <td colspan="1" colwidth="352" rowspan="1">
     <p><strong>一个周期到了的回调（你的代码）</strong></p>
    </td>
   </tr>
  </tbody>
 </table>
</div>
<p><strong>② 三段式记忆（和 EXTI 完全同构）：</strong></p>
<shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
 <pre><code>定时器版：  TIM2_IRQHandler → HAL_TIM_IRQHandler → HAL_TIM_PeriodElapsedCallback
按键版：    EXTI15_10_IRQHandler → HAL_GPIO_EXTI_IRQHandler → HAL_GPIO_EXTI_Callback
结构：      硬件入口 → HAL 中转 → 你的回调</code></pre>
</shiki-code>
<p>记住一个模板，两个外设都通：</p>
<blockquote>
 <p><code>xxx_IRQHandler</code><strong> 是入口（Cube 生成），</strong><code>HAL_xxx_IRQHandler</code><strong> 是公共处理（清标志），</strong><code>HAL_xxx_Callback</code><strong> 是你干活的地方。</strong></p>
</blockquote>
<p><strong>③ 几个容易混的点：</strong></p>
<ul>
 <li>
  <p><strong>Base</strong>：表示"基础计时"，只数数不用 PWM 通道；以后学 PWM 是 <code>HAL_TIM_PWM_Start</code>；</p>
 </li>
 <li>
  <p><strong>IT</strong>：Interrupt 的缩写，代表"中断方式"。还有不带 IT 的版本：</p>
  <ul>
   <li>
    <p><code>HAL_TIM_Base_Start()</code> = 只启动计数，不产生中断（轮询用）；</p>
   </li>
   <li>
    <p><code>HAL_TIM_Base_Start_IT()</code> = 启动计数 + 开中断；</p>
   </li>
  </ul>
 </li>
 <li>
  <p><strong>PeriodElapsed</strong> = "周期流逝/到点"——定时器数满一圈那一刻。以后串口还有 <code>HAL_UART_RxCpltCallback</code>（接收完成），都是"事件 + Callback"。</p>
 </li>
</ul>
<p><strong>④ 一句话口诀：</strong></p>
<blockquote>
 <p>启动用 <strong>Start_IT</strong>，入口找 <strong>IRQHandler</strong>，干活进 <strong>PeriodElapsedCallback</strong>；
  <br>
  定时器 = TIM + 事件名 + Callback，和按键中断同一套骨架。</p>
</blockquote>
<p></p>]]></description><guid isPermaLink="false">/archives/1786500625933</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Wed, 12 Aug 2026 02:10:38 GMT</pubDate></item><item><title><![CDATA[外设中断]]></title><link>https://www.flor.cn/archives/1786182141281</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=%E5%A4%96%E8%AE%BE%E4%B8%AD%E6%96%AD&amp;url=/archives/1786182141281" width="1" height="1" alt="" style="opacity:0;">
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fdata%2Fphoto%2Fimage-ohDm.png&amp;size=m" srcset="https://www.flor.cn/upload/data/photo/image-ohDm.png?width=1600 1600w, https://www.flor.cn/upload/data/photo/image-ohDm.png?width=800 800w, https://www.flor.cn/upload/data/photo/image-ohDm.png?width=400 400w, https://www.flor.cn/upload/data/photo/image-ohDm.png?width=1200 1200w" width="2255px">
</figure>
<p></p>]]></description><guid isPermaLink="false">/archives/1786182141281</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Sat, 8 Aug 2026 09:42:28 GMT</pubDate></item><item><title><![CDATA[时钟树]]></title><link>https://www.flor.cn/archives/1786181649223</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=%E6%97%B6%E9%92%9F%E6%A0%91&amp;url=/archives/1786181649223" width="1" height="1" alt="" style="opacity:0;">
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fdata%2Fphoto%2Fimage-OCAF.png&amp;size=m" srcset="https://www.flor.cn/upload/data/photo/image-OCAF.png?width=1600 1600w, https://www.flor.cn/upload/data/photo/image-OCAF.png?width=800 800w, https://www.flor.cn/upload/data/photo/image-OCAF.png?width=400 400w, https://www.flor.cn/upload/data/photo/image-OCAF.png?width=1200 1200w" width="1053px">
</figure>
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fdata%2Fphoto%2Fimage-IIVm.png&amp;size=m" srcset="https://www.flor.cn/upload/data/photo/image-IIVm.png?width=1600 1600w, https://www.flor.cn/upload/data/photo/image-IIVm.png?width=800 800w, https://www.flor.cn/upload/data/photo/image-IIVm.png?width=400 400w, https://www.flor.cn/upload/data/photo/image-IIVm.png?width=1200 1200w" width="1625px">
</figure>
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fdata%2Fphoto%2Fimage-VZAe.png&amp;size=m" srcset="https://www.flor.cn/upload/data/photo/image-VZAe.png?width=1600 1600w, https://www.flor.cn/upload/data/photo/image-VZAe.png?width=800 800w, https://www.flor.cn/upload/data/photo/image-VZAe.png?width=400 400w, https://www.flor.cn/upload/data/photo/image-VZAe.png?width=1200 1200w" width="2255px">
</figure>
<p></p>]]></description><guid isPermaLink="false">/archives/1786181649223</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Sat, 8 Aug 2026 09:38:37 GMT</pubDate></item><item><title><![CDATA[io模式]]></title><link>https://www.flor.cn/archives/1786180656285</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=io%E6%A8%A1%E5%BC%8F&amp;url=/archives/1786180656285" width="1" height="1" alt="" style="opacity:0;">
<div style="overflow-x: auto; overflow-y: hidden;">
 <table style="width: 769px">
  <colgroup>
   <col style="width: 100px">
   <col style="width: 100px">
   <col style="width: 100px">
   <col style="width: 212px">
   <col style="width: 257px">
  </colgroup>
  <tbody>
   <tr style="height: 60px;">
    <th colspan="1" colwidth="100" rowspan="1">
     <p>#</p>
    </th>
    <th colspan="1" colwidth="100" rowspan="1">
     <p>MODE</p>
    </th>
    <th colspan="1" colwidth="100" rowspan="1">
     <p>CNF</p>
    </th>
    <th colspan="1" colwidth="212" rowspan="1">
     <p>模式名称</p>
    </th>
    <th colspan="1" colwidth="257" rowspan="1">
     <p>典型用途</p>
    </th>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>1</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>00</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>00</p>
    </td>
    <td colspan="1" colwidth="212" rowspan="1">
     <p>模拟输入 Analog</p>
    </td>
    <td colspan="1" colwidth="257" rowspan="1">
     <p>ADC 采集、RTC</p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>2</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>00</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>01</p>
    </td>
    <td colspan="1" colwidth="212" rowspan="1">
     <p>浮空输入 Floating</p>
    </td>
    <td colspan="1" colwidth="257" rowspan="1">
     <p>临时悬空（少用，易受干扰）</p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>3</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>00</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>10</p>
    </td>
    <td colspan="1" colwidth="212" rowspan="1">
     <p>上拉输入 Pull-Up</p>
    </td>
    <td colspan="1" colwidth="257" rowspan="1">
     <p>按键（默认高电平）</p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>4</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>00</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>11</p>
    </td>
    <td colspan="1" colwidth="212" rowspan="1">
     <p>下拉输入 Pull-Down</p>
    </td>
    <td colspan="1" colwidth="257" rowspan="1">
     <p>按键（默认低电平）</p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>5</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>10/11</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>00</p>
    </td>
    <td colspan="1" colwidth="212" rowspan="1">
     <p>推挽输出 Push-Pull</p>
    </td>
    <td colspan="1" colwidth="257" rowspan="1">
     <p>LED、普通驱动（本板 LED）</p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>6</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>10/11</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>01</p>
    </td>
    <td colspan="1" colwidth="212" rowspan="1">
     <p>开漏输出 Open-Drain</p>
    </td>
    <td colspan="1" colwidth="257" rowspan="1">
     <p>I2C、线与</p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>7</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>10/11</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>10</p>
    </td>
    <td colspan="1" colwidth="212" rowspan="1">
     <p>复用推挽 AF Push-Pull</p>
    </td>
    <td colspan="1" colwidth="257" rowspan="1">
     <p>USART_TX、SPI_MOSI</p>
    </td>
   </tr>
   <tr style="height: 60px;">
    <td colspan="1" colwidth="100" rowspan="1">
     <p>8</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>10/11</p>
    </td>
    <td colspan="1" colwidth="100" rowspan="1">
     <p>11</p>
    </td>
    <td colspan="1" colwidth="212" rowspan="1">
     <p>复用开漏 AF Open-Drain</p>
    </td>
    <td colspan="1" colwidth="257" rowspan="1">
     <p>I2C_SCL/SDA</p>
    </td>
   </tr>
  </tbody>
 </table>
</div>
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fimage-xwRL.png&amp;size=m" srcset="https://www.flor.cn/upload/image-xwRL.png?width=1600 1600w, https://www.flor.cn/upload/image-xwRL.png?width=800 800w, https://www.flor.cn/upload/image-xwRL.png?width=400 400w, https://www.flor.cn/upload/image-xwRL.png?width=1200 1200w" width="2255px">
</figure>
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fimage-DBId.png&amp;size=m" srcset="https://www.flor.cn/upload/image-DBId.png?width=1600 1600w, https://www.flor.cn/upload/image-DBId.png?width=800 800w, https://www.flor.cn/upload/image-DBId.png?width=400 400w, https://www.flor.cn/upload/image-DBId.png?width=1200 1200w" width="2255px">
</figure>
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fimage-LYFU.png&amp;size=m" srcset="https://www.flor.cn/upload/image-LYFU.png?width=1600 1600w, https://www.flor.cn/upload/image-LYFU.png?width=800 800w, https://www.flor.cn/upload/image-LYFU.png?width=400 400w, https://www.flor.cn/upload/image-LYFU.png?width=1200 1200w" width="2255px">
</figure>
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fimage-fbeD.png&amp;size=m" srcset="https://www.flor.cn/upload/image-fbeD.png?width=1600 1600w, https://www.flor.cn/upload/image-fbeD.png?width=800 800w, https://www.flor.cn/upload/image-fbeD.png?width=400 400w, https://www.flor.cn/upload/image-fbeD.png?width=1200 1200w">
</figure>
<p></p>
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fdata%2Fphoto%2Fimage.png&amp;size=m" srcset="https://www.flor.cn/upload/data/photo/image.png?width=1600 1600w, https://www.flor.cn/upload/data/photo/image.png?width=800 800w, https://www.flor.cn/upload/data/photo/image.png?width=400 400w, https://www.flor.cn/upload/data/photo/image.png?width=1200 1200w" width="2255px">
</figure>
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fdata%2Fphoto%2Fimage-RErv.png&amp;size=m" srcset="https://www.flor.cn/upload/data/photo/image-RErv.png?width=1600 1600w, https://www.flor.cn/upload/data/photo/image-RErv.png?width=800 800w, https://www.flor.cn/upload/data/photo/image-RErv.png?width=400 400w, https://www.flor.cn/upload/data/photo/image-RErv.png?width=1200 1200w" width="2255px">
</figure>
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fdata%2Fphoto%2Fimage-GHHI.png&amp;size=m" srcset="https://www.flor.cn/upload/data/photo/image-GHHI.png?width=1600 1600w, https://www.flor.cn/upload/data/photo/image-GHHI.png?width=800 800w, https://www.flor.cn/upload/data/photo/image-GHHI.png?width=400 400w, https://www.flor.cn/upload/data/photo/image-GHHI.png?width=1200 1200w" width="2255px">
</figure>
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fdata%2Fphoto%2Fimage-fhQU.png&amp;size=m" srcset="https://www.flor.cn/upload/data/photo/image-fhQU.png?width=1600 1600w, https://www.flor.cn/upload/data/photo/image-fhQU.png?width=800 800w, https://www.flor.cn/upload/data/photo/image-fhQU.png?width=400 400w, https://www.flor.cn/upload/data/photo/image-fhQU.png?width=1200 1200w" width="2255px">
</figure>
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fdata%2Fphoto%2Fimage-yzdZ.png&amp;size=m" srcset="https://www.flor.cn/upload/data/photo/image-yzdZ.png?width=1600 1600w, https://www.flor.cn/upload/data/photo/image-yzdZ.png?width=800 800w, https://www.flor.cn/upload/data/photo/image-yzdZ.png?width=400 400w, https://www.flor.cn/upload/data/photo/image-yzdZ.png?width=1200 1200w" width="2255px">
</figure>
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fdata%2Fphoto%2Fimage-BYUC.png&amp;size=m" srcset="https://www.flor.cn/upload/data/photo/image-BYUC.png?width=1600 1600w, https://www.flor.cn/upload/data/photo/image-BYUC.png?width=800 800w, https://www.flor.cn/upload/data/photo/image-BYUC.png?width=400 400w, https://www.flor.cn/upload/data/photo/image-BYUC.png?width=1200 1200w" width="2255px">
</figure>
<p></p>]]></description><guid isPermaLink="false">/archives/1786180656285</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Sat, 8 Aug 2026 09:17:45 GMT</pubDate></item><item><title><![CDATA[外设io组]]></title><link>https://www.flor.cn/archives/1786180562626</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=%E5%A4%96%E8%AE%BEio%E7%BB%84&amp;url=/archives/1786180562626" width="1" height="1" alt="" style="opacity:0;">
<figure data-content-type="image" style="align-items: start; display: flex; flex-direction: column">
 <img sizes="(max-width: 640px) 94vw, (max-width: 768px) 92vw, (max-width: 1024px) 88vw, min(800px, 85vw)" src="https://www.flor.cn/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=https%3A%2F%2Fwww.flor.cn%2Fupload%2Fimage.png&amp;size=m" srcset="https://www.flor.cn/upload/image.png?width=1600 1600w, https://www.flor.cn/upload/image.png?width=800 800w, https://www.flor.cn/upload/image.png?width=400 400w, https://www.flor.cn/upload/image.png?width=1200 1200w" width="2194px">
</figure>
<p></p>]]></description><guid isPermaLink="false">/archives/1786180562626</guid><dc:creator>flor</dc:creator><category>stm32</category><pubDate>Sat, 8 Aug 2026 09:16:48 GMT</pubDate></item><item><title><![CDATA[矩阵按钮控制数码显示]]></title><link>https://www.flor.cn/archives/1780497801022</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=%E7%9F%A9%E9%98%B5%E6%8C%89%E9%92%AE%E6%8E%A7%E5%88%B6%E6%95%B0%E7%A0%81%E6%98%BE%E7%A4%BA&amp;url=/archives/1780497801022" width="1" height="1" alt="" style="opacity:0;">
<div class="markdown-edited">
 <h1 id="矩阵按键--数码管倒计时系统--使用手册">矩阵按键 + 数码管倒计时系统 — 使用手册</h1>
 <blockquote>
  <p>芯片: STC8A8K64D4 @ 24MHz | 函数库: ESD_STC8 V3.0</p>
 </blockquote>
 <hr>
 <h2 id="一全局变量">一、全局变量</h2>
 <table>
  <thead>
   <tr>
    <th>变量名</th>
    <th>类型</th>
    <th>说明</th>
   </tr>
  </thead>
  <tbody>
   <tr>
    <td><code>gear</code></td>
    <td>uchar</td>
    <td>挡位，范围 1~9，默认 1</td>
   </tr>
   <tr>
    <td><code>time_set</code></td>
    <td>uchar</td>
    <td>倒计时预设秒数，范围 1~99，默认 30</td>
   </tr>
   <tr>
    <td><code>time_disp</code></td>
    <td>uchar</td>
    <td>数码管当前显示的值（设置态 = time_set，运行态 = time_left）</td>
   </tr>
   <tr>
    <td><code>running</code></td>
    <td>uchar</td>
    <td>系统状态：0=设置模式，1=倒计时运行中</td>
   </tr>
   <tr>
    <td><code>time_left</code></td>
    <td>uchar</td>
    <td>倒计时剩余秒数，运行态时逐秒减 1</td>
   </tr>
   <tr>
    <td><code>ms_cnt</code></td>
    <td>uint</td>
    <td>毫秒累加器，每 1ms 加 1，满 1000 进 1 秒</td>
   </tr>
  </tbody>
 </table>
 <p><strong>变量关系图：</strong></p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>设置态 (running=0)              运行态 (running=1)
┌─────────────────┐            ┌─────────────────┐
│ time_set = 30   │──K5启动→  │ time_left = 30   │
│                 │            │   ↓ 每秒减1     │
│ time_disp = 30  │            │ time_left = 29   │
│                 │            │   ↓              │
│ 按键可调        │            │ time_disp = 29   │
│ gear 1~9       │            │   ↓              │
│ time_set 1~99  │            │ ...到0           │
│                 │←─到0自动回─│ running变回0     │
└─────────────────┘            └─────────────────┘
</code></pre>
 </shiki-code>
 <hr>
 <h2 id="二自定义函数">二、自定义函数</h2>
 <h3 id="函数1scan_keys">函数1：<code>Scan_Keys()</code></h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>原型:    uchar Scan_Keys(void)
参数:    无
返回:    0 = 无按键
        1 = K1 按下（挡位+）
        2 = K2 按下（挡位-）
        3 = K3 按下（时间+）
        4 = K4 按下（时间-）
        5 = K5 按下（启动）
</code></pre>
 </shiki-code>
 <p><strong>原理：矩阵扫描法，2 行 × 3 列，分两轮检测</strong></p>
 <ul>
  <li><strong>第①轮</strong>：P16=0（选中行1）→ 读 P14 查 K1，读 P15 查 K2</li>
  <li><strong>第②轮</strong>：P17=0（选中行2）→ 读 P14 查 K3，读 P15 查 K4，读 P35 查 K5</li>
 </ul>
 <p>每次只激活一行，被激活行的"强 0"才能把列线拉低；另一行输出"强 1"，列线不受影响。</p>
 <p><strong>坐标系理解：</strong></p>
 <ul>
  <li>Y 轴（行扫描）：P16 / P17 轮流输出强 0</li>
  <li>X 轴（列检测）：P14 / P15 / P35 默认弱 1，被拉低 = 按键按下</li>
 </ul>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>第1步：Y=P16=0 → 查 X 轴 → 找到第1行的按键（K1 或 K2）
第2步：Y=P17=0 → 查 X 轴 → 找到第2行的按键（K3 或 K4 或 K5）
</code></pre>
 </shiki-code>
 <blockquote>
  <p><strong>注意</strong>：如果同时按下多个键，函数只返回后检测到的那一个。</p>
 </blockquote>
 <hr>
 <h3 id="函数2tm0_isr">函数2：<code>TM0_isr()</code></h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>原型:    void TM0_isr(void)
调用者:  isr.c 中的 TM0_I() interrupt 1（每 1ms 自动调用）
参数:    无
返回:    无
</code></pre>
 </shiki-code>
 <h4 id="功能-a数码管动态扫描333hz-无闪烁刷新">功能 A：数码管动态扫描（333Hz 无闪烁刷新）</h4>
 <p><strong>流程：</strong></p>
 <ol>
  <li><strong>消隐</strong>：关闭所有位选（P36=P37=P33=0），关闭所有段选（P2.0~P2.7 全写 1 → 全灭）</li>
  <li><strong>轮流亮一位</strong>：
   <ul>
    <li><code>pos=0</code>：送 gear 的段码到 P2 → 开 P36（第 1 位亮）</li>
    <li><code>pos=1</code>：送 time_disp 十位的段码 → 开 P37（第 2 位亮）</li>
    <li><code>pos=2</code>：送 time_disp 个位的段码 → 开 P33（第 3 位亮）</li>
   </ul></li>
  <li><strong>pos 循环</strong>：0 → 1 → 2 → 0 ...</li>
 </ol>
 <p><strong>时间轴：</strong></p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>t=0ms   消隐 → 亮第1位（挡位）      pos=1
t=1ms   消隐 → 亮第2位（秒十位）    pos=2
t=2ms   消隐 → 亮第3位（秒个位）    pos=0
t=3ms   循环 ...

每位亮 1ms，3 位一轮 = 3ms，刷新率 333Hz
</code></pre>
 </shiki-code>
 <h4 id="功能-b倒计时仅-running1-时执行">功能 B：倒计时（仅 running==1 时执行）</h4>
 <ol>
  <li><code>ms_cnt</code> 每 1ms 加 1</li>
  <li><code>ms_cnt</code> 满 1000 → 经过了 1 秒</li>
  <li><code>time_left</code> 减 1</li>
  <li><code>time_left</code> 到 0 → running 置 0，LED 灭（<code>Out_IO(10,1)</code>）</li>
 </ol>
 <hr>
 <h2 id="三用到的库函数esd_stc8-v30">三、用到的库函数（ESD_STC8 V3.0）</h2>
 <table>
  <thead>
   <tr>
    <th>库函数</th>
    <th>说明</th>
   </tr>
  </thead>
  <tbody>
   <tr>
    <td><code>GPIO_init_allpin(mode)</code></td>
    <td>初始化所有 IO，mode: 0=准双向 1=推挽 2=高阻 3=开漏</td>
   </tr>
   <tr>
    <td><code>GPIO_init_8pin(port, mode)</code></td>
    <td>初始化整组 IO，port: 0=P0 2=P2...</td>
   </tr>
   <tr>
    <td><code>GPIO_init_pin(pin, mode)</code></td>
    <td>初始化单个 IO，pin: 36=P3.6 14=P1.4...</td>
   </tr>
   <tr>
    <td><code>Out_IO(pin, val)</code></td>
    <td>设置 IO 输出，val: 0=低电平 1=高电平</td>
   </tr>
   <tr>
    <td><code>Get_IO(pin)</code></td>
    <td>读取 IO 输入，返回 bit（0 或 1）</td>
   </tr>
   <tr>
    <td><code>PIT_init_ms(n, t)</code></td>
    <td>启动定时器中断，n: 0<del>4，t: 1</del>32ms（24MHz）</td>
   </tr>
   <tr>
    <td><code>Delay_X_mS(x)</code></td>
    <td>毫秒延时，x: 1~1000</td>
   </tr>
   <tr>
    <td><code>Delay_X_uS(x)</code></td>
    <td>微秒延时，x: 1~1000</td>
   </tr>
  </tbody>
 </table>
 <p><strong>引脚编号规则</strong>：两位数字，十位 = 端口号，个位 = 引脚号</p>
 <blockquote>
  <p><code>14</code> = P1^4 / <code>16</code> = P1^6 / <code>20</code> = P2^0 / <code>27</code> = P2^7 <code>33</code> = P3^3 / <code>35</code> = P3^5 / <code>36</code> = P3^6 / <code>37</code> = P3^7</p>
 </blockquote>
 <hr>
 <h2 id="四段码表">四、段码表</h2>
 <p>P2 口位：<code>.7=DP .6=G .5=F .4=E .3=D .2=C .1=B .0=A</code></p>
 <p>共阳规则：<strong>0 = 亮，1 = 灭</strong></p>
 <table>
  <thead>
   <tr>
    <th align="center">数字</th>
    <th align="center">十六进制</th>
    <th>亮的段</th>
   </tr>
  </thead>
  <tbody>
   <tr>
    <td align="center">0</td>
    <td align="center"><code>0xC0</code></td>
    <td>A B C D E F</td>
   </tr>
   <tr>
    <td align="center">1</td>
    <td align="center"><code>0xF9</code></td>
    <td>B C</td>
   </tr>
   <tr>
    <td align="center">2</td>
    <td align="center"><code>0xA4</code></td>
    <td>A B D E G</td>
   </tr>
   <tr>
    <td align="center">3</td>
    <td align="center"><code>0xB0</code></td>
    <td>A B C D G</td>
   </tr>
   <tr>
    <td align="center">4</td>
    <td align="center"><code>0x99</code></td>
    <td>B C F G</td>
   </tr>
   <tr>
    <td align="center">5</td>
    <td align="center"><code>0x92</code></td>
    <td>A C D F G</td>
   </tr>
   <tr>
    <td align="center">6</td>
    <td align="center"><code>0x82</code></td>
    <td>A C D E F G</td>
   </tr>
   <tr>
    <td align="center">7</td>
    <td align="center"><code>0xF8</code></td>
    <td>A B C</td>
   </tr>
   <tr>
    <td align="center">8</td>
    <td align="center"><code>0x80</code></td>
    <td>A B C D E F G（全部）</td>
   </tr>
   <tr>
    <td align="center">9</td>
    <td align="center"><code>0x90</code></td>
    <td>A B C D F G</td>
   </tr>
  </tbody>
 </table>
 <hr>
 <h2 id="五硬件接线速查表">五、硬件接线速查表</h2>
 <h3 id="按键矩阵2-行--3-列">按键矩阵（2 行 × 3 列）</h3>
 <table>
  <thead>
   <tr>
    <th></th>
    <th align="center">P14（列1）</th>
    <th align="center">P15（列2）</th>
    <th align="center">P35（列3）</th>
   </tr>
  </thead>
  <tbody>
   <tr>
    <td><strong>P16（行1）</strong></td>
    <td align="center">K1 挡位+</td>
    <td align="center">K2 挡位-</td>
    <td align="center">（空）</td>
   </tr>
   <tr>
    <td><strong>P17（行2）</strong></td>
    <td align="center">K3 时间+</td>
    <td align="center">K4 时间-</td>
    <td align="center">K5 启动</td>
   </tr>
  </tbody>
 </table>
 <p><strong>省线技巧</strong>：K1、K2 朝向 P16 那端焊在一起，共用一根飞线到 P16；K3、K4、K5 朝向 P17 那端焊在一起，共用一根飞线到 P17。</p>
 <p><strong>电阻</strong>：不需要外部电阻（代码用内部上拉）。</p>
 <hr>
 <h3 id="数码管-5641bs共阳极12-脚">数码管 5641BS（共阳极，12 脚）</h3>
 <p><strong>段码</strong>（各串 100Ω 电阻）：</p>
 <table>
  <thead>
   <tr>
    <th align="center">P2 脚</th>
    <th align="center">数码管脚</th>
    <th align="center">段名</th>
   </tr>
  </thead>
  <tbody>
   <tr>
    <td align="center">P2.0</td>
    <td align="center">脚11</td>
    <td align="center">A</td>
   </tr>
   <tr>
    <td align="center">P2.1</td>
    <td align="center">脚7</td>
    <td align="center">B</td>
   </tr>
   <tr>
    <td align="center">P2.2</td>
    <td align="center">脚4</td>
    <td align="center">C</td>
   </tr>
   <tr>
    <td align="center">P2.3</td>
    <td align="center">脚2</td>
    <td align="center">D</td>
   </tr>
   <tr>
    <td align="center">P2.4</td>
    <td align="center">脚1</td>
    <td align="center">E</td>
   </tr>
   <tr>
    <td align="center">P2.5</td>
    <td align="center">脚10</td>
    <td align="center">F</td>
   </tr>
   <tr>
    <td align="center">P2.6</td>
    <td align="center">脚5</td>
    <td align="center">G</td>
   </tr>
   <tr>
    <td align="center">P2.7</td>
    <td align="center">脚3</td>
    <td align="center">DP</td>
   </tr>
  </tbody>
 </table>
 <p><strong>位选</strong>：</p>
 <table>
  <thead>
   <tr>
    <th align="center">单片机脚</th>
    <th align="center">数码管脚</th>
    <th>说明</th>
   </tr>
  </thead>
  <tbody>
   <tr>
    <td align="center">P3.6</td>
    <td align="center">脚12</td>
    <td>第1位 = 挡位显示</td>
   </tr>
   <tr>
    <td align="center">P3.7</td>
    <td align="center">脚9</td>
    <td>第2位 = 秒十位</td>
   </tr>
   <tr>
    <td align="center">P3.3</td>
    <td align="center">脚8</td>
    <td>第3位 = 秒个位</td>
   </tr>
   <tr>
    <td align="center">—</td>
    <td align="center">脚6</td>
    <td>悬空（第4位不用）</td>
   </tr>
  </tbody>
 </table>
 <hr>
 <h3 id="led-指示灯">LED 指示灯</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>VCC(5V) → [1KΩ电阻] → LED正极（长脚） → LED负极（短脚） → P1.0
</code></pre>
 </shiki-code>
 <ul>
  <li>P1.0 = 高电平（5V）：LED 两端无压差 → <strong>灭</strong></li>
  <li>P1.0 = 低电平（0V）：电流从 VCC 流向 P1.0 → <strong>亮</strong></li>
 </ul>
 <hr>
 <h2 id="六程序执行流程">六、程序执行流程</h2>
 <h3 id="主循环后台">主循环（后台）</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>main()
  │
  ├→ GPIO 配置：全部准双向 → P2/位选/行扫/LED 改推挽
  ├→ LED 初始灭：Out_IO(10,1)
  ├→ 定时器启动：PIT_init_ms(0,1)  每 1ms 进中断
  │
  └→ while(1):
       │
       ├→ ① time_disp = running ? time_left : time_set
       │     更新数码管数据源
       │
       ├→ ② key = Scan_Keys()  扫描按键
       │     if (有键) {
       │       Delay_15ms 防抖
       │       再次确认
       │       if (!locked) {
       │         locked = 1
       │         if (!running) {   // 设置态才处理
       │           key=1 → gear++
       │           key=2 → gear--
       │           key=3 → time_set++
       │           key=4 → time_set--
       │           key=5 → 启动倒计时
       │         }
       │       }
       │     } else {
       │       locked = 0   // 松手解锁
       │     }
       │
       └──→ 循环
</code></pre>
 </shiki-code>
 <h3 id="定时器中断前台每-1ms">定时器中断（前台，每 1ms）</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>TM0_isr()
  │
  ├→ 消隐：关位选 + 关段选
  │
  ├→ 轮流亮位：
  │   pos=0: Seg[gear]       → P2口 → P36=1
  │   pos=1: Seg[time_disp十位] → P2口 → P37=1
  │   pos=2: Seg[time_disp个位] → P2口 → P33=1
  │
  └→ 倒计时（running==1 时）：
       ms_cnt++
       ms_cnt 到 1000 → time_left--
       time_left 到 0 → running=0, LED灭
</code></pre>
 </shiki-code>
 <blockquote>
  <p><strong>主循环和中断通过全局变量 <code>gear</code> / <code>time_disp</code> / <code>running</code> / <code>time_left</code> 通信，彼此独立运行。</strong></p>
 </blockquote>
 <hr>
 <h2 id="七按键防抖机制">七、按键防抖机制</h2>
 <p><strong>问题</strong>：机械按键按下/松开的瞬间，金属弹片反复弹跳，产生一串极短的通→断→通→断脉冲（持续约 5~15ms）。</p>
 <p><strong>时间轴：</strong></p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>按下瞬间  ├── 抖动区（~15ms）──┤  稳定按住
电平: 1   │ 1 0 1 0 1 0 1 0 1  │  0 0 0 0 0
          ↑                    ↑
     第1次读数              第2次读数
     (可能是1)              (稳定是0)
     
     两次一致 → 确认有效 ✓
</code></pre>
 </shiki-code>
 <h3 id="防长按locked-标志位">防长按（locked 标志位）</h3>
 <table>
  <thead>
   <tr>
    <th align="center">循环</th>
    <th align="center">locked</th>
    <th>动作</th>
   </tr>
  </thead>
  <tbody>
   <tr>
    <td align="center">第1次</td>
    <td align="center">0</td>
    <td>执行功能 → locked=1</td>
   </tr>
   <tr>
    <td align="center">第2次</td>
    <td align="center">1</td>
    <td>跳过（按住不放）</td>
   </tr>
   <tr>
    <td align="center">第N次</td>
    <td align="center">1</td>
    <td>跳过</td>
   </tr>
   <tr>
    <td align="center">松手</td>
    <td align="center">—</td>
    <td>key=0 → locked=0</td>
   </tr>
   <tr>
    <td align="center">再按</td>
    <td align="center">0</td>
    <td>可以再次执行</td>
   </tr>
  </tbody>
 </table>
 <blockquote>
  <p><strong>效果</strong>：按一次只触发一次，按住不放不会连续触发。</p>
 </blockquote>
 <hr>
 <h2 id="八常见问题排查">八、常见问题排查</h2>
 <p><strong>Q：数码管不亮？</strong></p>
 <ol>
  <li>检查共阳脚是否接对（脚 12、9、8）</li>
  <li>段码电阻是否每根都接了</li>
  <li>万用表测 P36/P37/P33 有无轮流出现高电平</li>
  <li>确认 5641BS 是共阳型（不是共阴）</li>
 </ol>
 <p><strong>Q：数码管显示乱码？</strong></p>
 <p>段码脚位和代码不匹配。用 5V → 100Ω 逐个碰段脚，确认 A/B/C/D/E/F/G/DP 对应哪个脚，调整 P2 接线。</p>
 <p><strong>Q：按一下数字跳了好几次？</strong></p>
 <p>按键接触不良或抖动没消干净。</p>
 <ol>
  <li>杜邦线插紧</li>
  <li><code>Delay_X_mS(15)</code> 可以改大到 20~30</li>
 </ol>
 <p><strong>Q：按了按键没反应？</strong></p>
 <ol>
  <li>检查按键矩阵接线是否正确（特别注意共用的行线）</li>
  <li>万用表测按下去是否导通</li>
  <li>检查 <code>running</code> 是否为 0（运行态按键被屏蔽）</li>
 </ol>
 <p><strong>Q：倒计时不准？</strong></p>
 <p>确认 <code>config.h</code> 中的 <code>MAIN_Fosc</code> 和 ISP 下载软件中设置的主频一致。</p>
 <p><strong>Q：编译有 MULTIPLE CALL 警告？</strong></p>
 <p>正常，不影响运行。<code>Out_IO</code> 在主循环和 ISR 中都被调用，Keil 提醒可能重入，但 <code>Out_IO</code> 内部无静态变量，实际不会出错。</p>
 <p><strong>Q：LED 不亮？</strong></p>
 <ol>
  <li>检查正负极是否接反（长脚 → 电阻 → VCC，短脚 → P1.0）</li>
  <li>检查 running=1 时 P1.0 是否为低电平</li>
 </ol>
</div>
<p></p>]]></description><guid isPermaLink="false">/archives/1780497801022</guid><dc:creator>flor</dc:creator><category>8051</category><pubDate>Wed, 3 Jun 2026 14:43:25 GMT</pubDate></item><item><title><![CDATA[串口屏多档位舵机控制]]></title><link>https://www.flor.cn/archives/1779945886904</link><description><![CDATA[<img src="https://www.flor.cn/plugins/feed/assets/telemetry.gif?title=%E4%B8%B2%E5%8F%A3%E5%B1%8F%E5%A4%9A%E6%A1%A3%E4%BD%8D%E8%88%B5%E6%9C%BA%E6%8E%A7%E5%88%B6&amp;url=/archives/1779945886904" width="1" height="1" alt="" style="opacity:0;">
<div class="markdown-edited">
 <h1 id="串口屏多档位舵机控制--从-main-函数开始的完整代码解读">串口屏多档位舵机控制 — 从 main 函数开始的完整代码解读</h1>
 <blockquote>
  <p>日期：2026-05-28
   <br>
   项目：串口屏 + 舵机，触摸按钮控制四档往复扫动
   <br>
   MCU：STC8A8K64D4 @ 24MHz | 屏幕：迪文 DGUS T5L | 舵机：SG90（外部 5V 供电）
   <br>
   工程路径：<code>功能实现/</code></p>
 </blockquote>
 <hr>
 <p>当 CPU 上电复位后，STARTUP.A51 最先执行——它清零内部 RAM、初始化堆栈指针，最后 <code>LJMP ?C_START</code> 跳转到 C 运行环境，进入 <code>main()</code>。</p>
 <p>这就是我们故事的起点。</p>
 <hr>
 <h2 id="目录">目录</h2>
 <ol>
  <li><a href="https://www.flor.cn#1-%E5%B7%A5%E7%A8%8B%E6%A6%82%E8%A7%88">工程概览</a></li>
  <li><a href="https://www.flor.cn#2-configh--main-%E7%9C%8B%E5%88%B0%E7%9A%84%E7%AC%AC%E4%B8%80%E4%BB%BD%E8%93%9D%E5%9B%BE">config.h — main() 看到的第一份蓝图</a></li>
  <li><a href="https://www.flor.cn#3-main-%E5%88%9D%E5%A7%8B%E5%8C%96--%E5%85%AD%E6%AD%A5%E8%AE%A9%E7%A1%AC%E4%BB%B6%E5%B0%B1%E7%BB%AA">main() 初始化 — 六步让硬件就绪</a></li>
  <li><a href="https://www.flor.cn#4-uart2-%E6%95%B0%E6%8D%AE%E6%8E%A5%E6%94%B6--%E4%B8%AD%E6%96%AD%E4%B8%8E%E4%B8%BB%E5%BE%AA%E7%8E%AF%E7%9A%84%E9%85%8D%E5%90%88">UART2 数据接收 — 中断与主循环的配合</a></li>
  <li><a href="https://www.flor.cn#5-%E5%B8%A7%E8%A7%A3%E6%9E%90%E7%8A%B6%E6%80%81%E6%9C%BA--%E4%BB%8E%E5%AD%97%E8%8A%82%E6%B5%81%E4%B8%AD%E6%8F%90%E5%8F%96%E5%AE%8C%E6%95%B4%E5%B8%A7">帧解析状态机 — 从字节流中提取完整帧</a></li>
  <li><a href="https://www.flor.cn#6-%E8%88%B5%E6%9C%BA%E6%89%AB%E5%8A%A8%E5%BE%AA%E7%8E%AF--%E5%BE%80%E5%A4%8D%E8%BF%90%E5%8A%A8%E7%9A%84%E8%BD%AF%E4%BB%B6%E5%AE%9E%E7%8E%B0">舵机扫动循环 — 往复运动的软件实现</a></li>
  <li><a href="https://www.flor.cn#7-dgusc--%E5%B1%8F%E5%B9%95%E9%80%9A%E4%BF%A1%E5%9B%9B%E5%87%BD%E6%95%B0">dgus.c — 屏幕通信四函数</a></li>
  <li><a href="https://www.flor.cn#8-%E5%AE%8C%E6%95%B4%E6%89%A7%E8%A1%8C%E6%B5%81--%E4%BB%8E%E8%A7%A6%E6%91%B8%E5%88%B0%E8%88%B5%E6%9C%BA%E5%93%8D%E5%BA%94">完整执行流 — 从触摸到舵机响应</a></li>
  <li><a href="https://www.flor.cn#9-%E9%99%84%E5%BD%95">附录</a></li>
 </ol>
 <hr>
 <h2 id="1-工程概览">1. 工程概览</h2>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>功能实现/
├── main.c          ★ 业务主程序：初始化 + 帧解析 + 舵机扫动         (3435 B)
├── dgus.c          ★ DGUS 通信模块：档位切换 + 写变量 + LED 控制   (1347 B)
├── dgus.h          模块接口声明                                    (265 B)
├── config.h        全局配置：主频、外设头文件、类型宏               (2550 B)
├── isr.c           中断向量表（21 个中断入口，UART2 中断转发）      (9793 B)
├── isr.h           中断处理函数 extern 声明                         (1208 B)
├── inisr.asm       扩展中断向量跳转表                              (2217 B)
├── STARTUP.A51     Keil 启动文件                                   (6376 B)
├── STC8.uvproj     Keil 工程描述 (XML, 含编译选项和文件分组)        (17793 B)
│
└── ESD-库/         底层驱动库（.obj 预编译）
    ├── GPIO.h/obj  GPIO 初始化和引脚操作
    ├── UART.h/obj  串口初始化和字节发送
    ├── PWM.h/obj   PWM 初始化和占空比更新
    └── ...         其他模块（delay, ADC, EEPROM 等）
</code></pre>
 </shiki-code>
 <p><strong>编译配置</strong>（来自 STC8.uvproj）：</p>
 <table>
  <thead>
   <tr>
    <th>项</th>
    <th>值</th>
   </tr>
  </thead>
  <tbody>
   <tr>
    <td>器件</td>
    <td>STC8A8K64D4 Series</td>
   </tr>
   <tr>
    <td>主频</td>
    <td>35MHz（编译设置）/ 24MHz（代码中实际使用）</td>
   </tr>
   <tr>
    <td>存储模型</td>
    <td>Large（变量默认在 XDATA）</td>
   </tr>
   <tr>
    <td>优化级别</td>
    <td>8（最高，侧重速度）</td>
   </tr>
   <tr>
    <td>链接选项</td>
    <td>REMOVEUNUSED（移除未用代码段）</td>
   </tr>
  </tbody>
 </table>
 <p><strong>Keil 工程文件分组</strong>：</p>
 <ul>
  <li><strong>User 组</strong>：STARTUP.A51, main.c, isr.c, inisr.asm — 用户代码</li>
  <li><strong>ESD 组</strong>：GPIO, delay, PWM, UART 等 12 个库模块 — 外设驱动</li>
  <li><strong>dgus 组</strong>：dgus.c, dgus.h — 屏幕通信</li>
 </ul>
 <hr>
 <h2 id="2-configh--main-看到的第一份蓝图">2. config.h — main() 看到的第一份蓝图</h2>
 <p><code>main.c</code> 的第一行：</p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">#include "config.h"
</code></pre>
 </shiki-code>
 <p>编译时 config.h 先展开，铺好整个工程的基础设施。</p>
 <h3 id="21-主频选择">2.1 主频选择</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">#define MAIN_Fosc  24000000L    // 主频 24MHz
</code></pre>
 </shiki-code>
 <p>STC8A8K64D4 最高可跑 35MHz，这里稳定选用 24MHz。这个值被 PWM、UART、delay 等所有外设库引用——改这一个数字，所有需要时钟计算的地方自动适配。</p>
 <p>附带几个便捷宏：</p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">#define Main_Fosc2   (MAIN_Fosc / 2)      // 12MHz
#define Main_Fosc_KHZ (MAIN_Fosc / 1000)   // 24000 KHz
#define Main_Fosc_MHZ (MAIN_Fosc / 1000000) // 24 MHz
</code></pre>
 </shiki-code>
 <h3 id="22-类型别名">2.2 类型别名</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">#define uchar  unsigned char       // 8 位,  范围 0~255
#define uint   unsigned int        // 16 位, 范围 0~65535
#define u16    unsigned int
#define u32    unsigned long       // 32 位, 范围 0~4294967295
</code></pre>
 </shiki-code>
 <p>8051 中 <code>int</code> 是 16 位的。这些别名让变量声明更紧凑。</p>
 <h3 id="23-外设模块引用">2.3 外设模块引用</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">#include "GPIO.h"    // GPIO_init_allpin, GPIO_init_pin, Out_IO, Get_IO
#include "PWM.h"     // PWM_init, PWM_change
#include "UART.h"    // UART_init, UART_Send_byte, UART_Send_string
#include "delay.h"   // delay_ms, delay_us
#include "ADC.h"
#include "EEPROM.h"
// ... 其他模块
</code></pre>
 </shiki-code>
 <p>每个 <code>.h</code> 文件声明了对应 <code>.obj</code> 预编译库中的函数接口。链接时 Keil 找到这些符号，绑在一起生成最终的 <code>.hex</code> 固件。</p>
 <hr>
 <h2 id="3-main-初始化--六步让硬件就绪">3. main() 初始化 — 六步让硬件就绪</h2>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">void main(void) {
    // ========== 第 1 步：GPIO ==========
    GPIO_init_allpin(0);       // 所有 IO 设为准双向模式
    GPIO_init_pin(20, 1);      // P20 单独设为推挽输出

    // ========== 第 2 步：串口 ==========
    UART_init(1, 115200, 1, 0);  // UART1: 调试口, 不开启接收中断
    UART_init(2, 115200, 1, 1);  // UART2: DGUS 屏通信口, 开启接收中断

    // ========== 第 3 步：PWM ==========
    PWM_init(20, 50, 250);     // P20, 50Hz, 初始占空比 250

    // ========== 第 4 步：中断总开关 ==========
    EA = 1;

    // ========== 第 5、6 步：主循环 ==========
    while(1) {
        // 帧解析 + 舵机扫动
    }
}
</code></pre>
 </shiki-code>
 <p>下面逐行解释每一步"做了什么"和"为什么这么做"。</p>
 <h3 id="第-1-步gpio_init_allpin0--gpio_init_pin20-1">第 1 步：GPIO_init_allpin(0) + GPIO_init_pin(20, 1)</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>GPIO_init_allpin(0)  →  所有引脚设为准双向模式
</code></pre>
 </shiki-code>
 <p>准双向模式是 8051 的经典 IO 模式——输出高电平时靠内部弱上拉电阻（约 50KΩ），驱动电流不到 1mA；输出低电平时主动拉低，驱动能力较强。适合大多数数字 IO 场景。</p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>GPIO_init_pin(20, 1)  →  P20 单独设为推挽输出 (模式=1)
</code></pre>
 </shiki-code>
 <p><strong>为什么要单独改 P20？</strong> 因为 P20 要输出 PWM 信号驱动舵机。准双向模式的弱上拉会让 PWM 波形的上升沿变得缓慢（RC 充电效应），导致舵机抖动或无法准确响应。推挽模式用一对 MOS 管主动推拉电平，波形干净利落。</p>
 <p><code>GPIO_init_pin()</code> 的参数：20 表示 P2.0 引脚，1 表示推挽输出模式。</p>
 <h3 id="第-2-步uart_init-两个串口">第 2 步：UART_init() 两个串口</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>UART_init(串口号, 波特率, 定时器, 是否开启中断)
         (1,     115200,   1,      0)     UART1: P30/P31, 调试用, 无中断
         (2,     115200,   1,      1)     UART2: P10/P11, DG通信用, 开中断
</code></pre>
 </shiki-code>
 <p><strong>UART1（调试口）</strong>：连接 PC 的 USB 转串口模块。在主循环中收到 DGUS 帧后原样转发到 UART1，PC 端串口助手可以看到数据内容，用于调试。不需要接收数据，所以不开启接收中断。</p>
 <p><strong>UART2（DGUS 屏通信口）</strong>：连接串口屏。需要<strong>实时接收</strong>屏幕发来的触摸返回帧，所以第四个参数为 1——开启接收中断，每收到一个字节就触发 <code>UART2_I() interrupt 8</code>。</p>
 <h3 id="第-3-步pwm_init20-50-250">第 3 步：PWM_init(20, 50, 250)</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>PWM_init(引脚, 频率Hz, 初始占空比)
        (P20,    50,      250)
</code></pre>
 </shiki-code>
 <p>SG90 舵机需要 50Hz PWM（周期 20ms）。STC8 的 PWM 模块在 50Hz 下，占空比取值范围 0-10000 对应 0%-100%：</p>
 <table>
  <thead>
   <tr>
    <th>占空比 duty</th>
    <th>脉宽</th>
    <th>占空比%</th>
    <th>舵机角度</th>
   </tr>
  </thead>
  <tbody>
   <tr>
    <td>250</td>
    <td>0.5ms</td>
    <td>2.5%</td>
    <td>0°</td>
   </tr>
   <tr>
    <td>500</td>
    <td>1.0ms</td>
    <td>5.0%</td>
    <td>45°</td>
   </tr>
   <tr>
    <td>750</td>
    <td>1.5ms</td>
    <td>7.5%</td>
    <td>90°</td>
   </tr>
   <tr>
    <td>1000</td>
    <td>2.0ms</td>
    <td>10.0%</td>
    <td>135°</td>
   </tr>
   <tr>
    <td>1250</td>
    <td>2.5ms</td>
    <td>12.5%</td>
    <td>180°</td>
   </tr>
  </tbody>
 </table>
 <p>这是线性关系：每 1° 对应 duty 变化 <code>(1250-250)/180 = 1000/180 ≈ 5.556</code>。</p>
 <p>初始占空比设为 250，舵机上电即停在 0°，等待指令。</p>
 <h3 id="第-4-步ea--1">第 4 步：EA = 1</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>EA = 1;   // 总中断使能
</code></pre>
 </shiki-code>
 <p><strong>必须放在初始化的最后</strong>。如果反了——先开中断再初始化 UART——可能在 UART 初始化中途就触发中断，此时 UART 寄存器还是乱的，后果不可控。</p>
 <hr>
 <h2 id="4-uart2-数据接收--中断与主循环的配合">4. UART2 数据接收 — 中断与主循环的配合</h2>
 <h3 id="41-中断层isrc--mainc">4.1 中断层：isr.c → main.c</h3>
 <p>串口屏发送的数据以字节为单位到达 STC 的 P10 (RXD2) 引脚。硬件自动将字节存入 S2BUF 寄存器，并置起 RI 中断标志。</p>
 <p>在 isr.c 中，UART2 中断入口：</p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">void UART2_I() interrupt 8 {          // 中断号 8 = UART2
    if (S2CON &amp; 0x01) {               // 检查 RI（接收完成标志）
        UART2_isr();                  // 调用 main.c 中的处理函数
        S2CON &amp;= ~0x01;               // 清除 RI 标志
    }
}
</code></pre>
 </shiki-code>
 <p><strong>为什么分两层？</strong> Keil C51 要求 <code>interrupt N</code> 函数有特殊的序言/尾声代码（保存恢复寄存器、使用指定寄存器组）。如果在 main.c 里写 <code>interrupt 8</code>，会影响整个文件的编译方式。分层设计让 isr.c 专管硬件中断入口，main.c 写普通 C 函数，互不干扰。</p>
 <h3 id="42-消费层uart2_isr">4.2 消费层：UART2_isr()</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">void UART2_isr(void) {
    UART_DATA[next] = S2BUF;   // 从硬件寄存器读字节，写入环形缓冲
    next++;                     // 生产指针后移
}
</code></pre>
 </shiki-code>
 <p>就两行，极致精简。<code>next</code> 是 <code>uchar</code> 类型（0~255），加到 256 时自动溢出回绕为 0，天然实现了 256 字节的环形缓冲——不需要 <code>next % 256</code>。</p>
 <h3 id="43-环形缓冲的生产-消费模型">4.3 环形缓冲的生产-消费模型</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>               ┌──────────────────────────┐
               │  UART_DATA[256]          │
  UART2 ISR ──→│  next 写入 (生产者)       │──→ 主循环
               │  current 读取 (消费者)    │    读取
               └──────────────────────────┘
</code></pre>
 </shiki-code>
 <p>这是一个<strong>单生产者单消费者</strong>的环形缓冲：</p>
 <ul>
  <li><strong>只有</strong> ISR 写 <code>next</code>，<strong>只有</strong> 主循环读 <code>current</code>——没有竞争</li>
  <li>不需要关中断、不需要互斥锁</li>
  <li><code>next != current</code> 意味着有未处理数据</li>
 </ul>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">if(next != current) {    // 有数据？
    // 消费一个字节……
    current++;            // 消费后指针后移
}
</code></pre>
 </shiki-code>
 <hr>
 <h2 id="5-帧解析状态机--从字节流中提取完整帧">5. 帧解析状态机 — 从字节流中提取完整帧</h2>
 <p>串口传输的是连续的字节流，没有天然的"帧边界"。状态机要做的是：在这片字节流中找到帧头、确定帧长度、收齐完整帧。</p>
 <h3 id="51-dgus-帧格式">5.1 DGUS 帧格式</h3>
 <p>屏幕收发的数据帧统一遵循这个结构：</p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>5A A5 [长度N] [数据1] [数据2] ... [数据N]
│  │     │       └──────── N 字节数据 ────────┘
│  │     └── 数据部分长度（不含帧头和长度自身）
│  └── 帧头第二字节（固定 0xA5）
└── 帧头第一字节（固定 0x5A）
</code></pre>
 </shiki-code>
 <p>完整帧总字节数 = <strong>N + 3</strong>（帧头 2 字节 + 长度 1 字节 + 数据 N 字节）。</p>
 <h3 id="52-四状态转换">5.2 四状态转换</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>           找到5A?            确认A5?            取长度N            收N字节?
              │                  │                  │                  │
   ┌──────────▼──────┐  ┌────────▼────────┐  ┌─────▼──────┐  ┌───────▼────────┐
   │  case 0         │  │  case 1         │  │  case 2    │  │  case 3        │
   │  寻找帧头 0x5A  │──│  确认帧头 0xA5   │──│  读长度字节  │──│  逐字节收数据   │
   │                 │  │                 │  │  ja = N+1  │  │  ja-- 直到 ja=1 │
   └─────────────────┘  └─────────────────┘  └────────────┘  └───────┬────────┘
                                                                      │ ja==1
                                                          ┌───────────▼──────────┐
                                                          │ 帧收齐！              │
                                                          │ UART_1(1, frame)  回显│
                                                          │ gears(frame)      切档│
                                                          │ led01(frame)      亮灯│
                                                          │ flag=0, ia=0     复位 │
                                                          └──────────────────────┘
</code></pre>
 </shiki-code>
 <h4 id="状态-0寻找-0x5a">状态 0：寻找 0x5A</h4>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">case 0: {
    if(UART_DATA[current] == 0x5a) {
        uart2_data[ia] = UART_DATA[current];  // ia=0，写入帧头第一个字节
        ia++;                                  // ia=1
        UART_FLAG = 1;                         // → 状态 1
    }
    // 不是 0x5A → 丢弃该字节，保持状态 0 继续找
} break;
</code></pre>
 </shiki-code>
 <h4 id="状态-1确认-0xa5">状态 1：确认 0xA5</h4>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">case 1: {
    if(UART_DATA[current] == 0xa5) {
        uart2_data[ia] = UART_DATA[current];  // ia=1，写入帧头第二个字节
        ia++;                                  // ia=2
        UART_FLAG = 2;                         // → 状态 2
    }
    // 不是 0xA5 → 保持状态 1，等下一个字节（不会误判，因为 DGUS 帧 5A 后必跟 A5）
} break;
</code></pre>
 </shiki-code>
 <h4 id="状态-2读取长度">状态 2：读取长度</h4>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">case 2: {
    if(ia == 2) {                              // 加守卫：只在第一次进入状态 2 时执行
        uart2_data[ia] = UART_DATA[current];   // ia=2，写入长度字节
        ia++;                                   // ia=3
        ja = uart2_data[2] + 1;                // 倒数计数器 = N+1
        UART_FLAG = 3;                          // → 状态 3
    }
} break;
</code></pre>
 </shiki-code>
 <p><code>ia == 2</code> 是一个守卫条件——状态 2 可能被执行多次（如果状态 1 之后立刻又收到字节）。<code>ia</code> 只在首次置为 2 然后递增为 3，后续进入状态 2 时 <code>ia</code> 已经是 3，条件不成立，不会重复读取。</p>
 <p><strong><code>ja = uart2_data[2] + 1</code></strong>：<code>uart2_data[2]</code> 是帧长度 N。N 字节数据加 1 作为倒数计数器初始值——在状态 3 中每收一字节 <code>ja--</code>，当 <code>ja == 1</code> 时意味着收到了 N 字节数据，帧完整。</p>
 <h4 id="状态-3逐字节收数据">状态 3：逐字节收数据</h4>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">case 3: {
    ja--;                                      // 倒数计数器递减
    uart2_data[ia] = UART_DATA[current];       // 写入当前字节
    ia++;                                      // 缓冲指针后移

    if(ja == 1) {                              // 倒数到 1 → 完整帧收齐
        ia = 0;                                // 帧缓冲指针复位
        UART_1(1, uart2_data);                 // ┐
        gears(uart2_data);                     // ├ 三合一处理
        led01(uart2_data);                     // ┘
        UART_FLAG = 0;                         // 回到状态 0
    }
} break;
</code></pre>
 </shiki-code>
 <p>帧收齐后调用三个函数，然后 <code>UART_FLAG = 0</code> 回到原点，准备寻找下一个帧。</p>
 <p><code>ia = 0</code> 复位帧缓冲写入指针，下一个帧的数据从头开始覆盖 <code>uart2_data[]</code>。</p>
 <hr>
 <h2 id="6-舵机扫动循环--往复运动的软件实现">6. 舵机扫动循环 — 往复运动的软件实现</h2>
 <p>UART 帧处理只在 <code>if(next != current)</code> 里执行。而下面的舵机控制<strong>每个循环周期都执行</strong>：</p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">while(1) {
    // ... 帧解析 ...

    sweep_timer++;
    if(sweep_timer &gt;= 9000) {
        sweep_timer = 0;
        servo_sweep();
    }
}
</code></pre>
 </shiki-code>
 <h3 id="61-软件定时器的原理">6.1 软件定时器的原理</h3>
 <p>24MHz 下，主循环空转一圈大约几十微秒。<code>sweep_timer</code> 每圈加 1，累积到 9000 大约耗时 300~500ms，触发一次舵机步进。</p>
 <p>这是一个<strong>软件定时器</strong>——不占用硬件定时器资源的节拍发生器。改变阈值即可调节扫动速度：阈值越小越快，越大越慢。</p>
 <h3 id="62-四档参数">6.2 四档参数</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">uchar gear_mode = 0;                    // 当前档位 (0=停机, 1/2/3=扫动)
int  max_angle[4] = {0, 60, 120, 180}; // 每档最大扫动角度
</code></pre>
 </shiki-code>
 <table>
  <thead>
   <tr>
    <th>档位</th>
    <th>最大角度</th>
    <th>扫动范围</th>
    <th>效果</th>
   </tr>
  </thead>
  <tbody>
   <tr>
    <td>0</td>
    <td>0°</td>
    <td>不动</td>
    <td>停机</td>
   </tr>
   <tr>
    <td>1</td>
    <td>60°</td>
    <td>0° ↔ 60°</td>
    <td>小幅往复</td>
   </tr>
   <tr>
    <td>2</td>
    <td>120°</td>
    <td>0° ↔ 120°</td>
    <td>中幅往复</td>
   </tr>
   <tr>
    <td>3</td>
    <td>180°</td>
    <td>0° ↔ 180°</td>
    <td>大幅往复</td>
   </tr>
  </tbody>
 </table>
 <h3 id="63-servo_sweep--扫动算法">6.3 servo_sweep() — 扫动算法</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">void servo_sweep(void) {
    uint step = 1;                       // 每次步进 1°
    uint target = max_angle[gear_mode];  // 查表获取当前档目标

    if(gear_mode == 0) {                 // 停机档
        cur_angle = 0;
        PWM_change(20, 250);             // 舵机归 0°
        return;                          // 直接返回，不扫动
    }

    if(sweep_up) {                       // 正向：角度增大
        cur_angle += step;
        if(cur_angle &gt;= target) {        // 到达上限
            cur_angle = target;
            sweep_up = 0;                // 翻转方向
        }
    } else {                             // 反向：角度减小
        if(cur_angle &lt;= step) {          // 到达下限
            cur_angle = 0;
            sweep_up = 1;                // 翻转方向
        } else {
            cur_angle -= step;
        }
    }

    PWM_change(20, angle_to_duty(cur_angle));  // 更新舵机位置
}
</code></pre>
 </shiki-code>
 <p><strong>逻辑流程</strong>：</p>
 <ol>
  <li>查 <code>max_angle[gear_mode]</code> 获取当前档位目标角度</li>
  <li>0 档直接停在 0°，返回</li>
  <li>非 0 档时，根据 <code>sweep_up</code> 标志决定方向：正向则 <code>cur_angle += 1</code>，反向则 <code>cur_angle -= 1</code></li>
  <li>到达边界（≥target 或 ≤0）时翻转 <code>sweep_up</code> 标志</li>
  <li>调用 <code>PWM_change()</code> 更新舵机位置</li>
 </ol>
 <p><strong><code>step = 1</code></strong> 意味着每次调用只改变 1°。60° 的目标需要 60 次调用（约 18~30 秒）才能扫完。如果改成 <code>step = 3</code>，速度就是原来的 3 倍。<code>step</code> 和 <code>sweep_timer</code> 阈值配合，可以独立调节速度和平滑度。</p>
 <h3 id="64-angle_to_duty--角度到占空比">6.4 angle_to_duty() — 角度到占空比</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">uint angle_to_duty(uint angle) {
    return (uint)(250 + ((unsigned long)angle * 1000 / 180));
}
</code></pre>
 </shiki-code>
 <p>推导过程：SG90 在 50Hz PWM 下，0° 对应 duty = 250，180° 对应 duty = 1250。线性关系：</p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>duty = 250 + angle × (1250 - 250) / 180
     = 250 + angle × 1000 / 180
</code></pre>
 </shiki-code>
 <p><strong><code>(unsigned long)</code> 强制转换是关键</strong>：<code>angle * 1000</code> 在 <code>angle = 180</code> 时等于 180000，超过 <code>uint</code> 的表示范围 65535。先提升到 32 位（<code>unsigned long</code>）计算，再截断回 16 位，避免溢出。</p>
 <hr>
 <h2 id="7-dgusc--屏幕通信四函数">7. dgus.c — 屏幕通信四函数</h2>
 <p>dgus.c 是独立模块，封装了所有与串口屏交互的功能。main.c 通过 <code>#include "dgus.h"</code> 引用。</p>
 <h3 id="71-gears--档位切换">7.1 gears() — 档位切换</h3>
 <p>整个系统最核心的业务函数：</p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">uchar gears(uchar* pp) {
    uchar i;
    i = pp[2];                          // 帧长度 N

    if(pp[i+2] == 0x05) {              // 帧尾字节 = 0x05 → 升档
        if(gear_mode &lt; 3) {
            gear_mode++;                // 1→2, 2→3
        }
        dgus_write_vp(0x2000, gear_mode);  // 同步屏幕显示
    }
    else if(pp[i+2] == 0x04) {         // 帧尾字节 = 0x04 → 降档
        if(gear_mode &gt; 0) {
            gear_mode--;                // 3→2, 2→1, 1→0
        }
        if(gear_mode &lt;= 0) gear_mode = 0;   // 防下溢
        dgus_write_vp(0x2000, gear_mode);
    }
    return 0;
}
</code></pre>
 </shiki-code>
 <p><strong>帧尾取字节技巧</strong>：</p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>pp[2] = N (帧长度)
pp[N+2] = 帧头(2字节) + 长度(1字节) 之后的第 N 字节 = 帧的最后一字节

例如 N=6:
  索引: 0   1   2   3   4   5   6   7   8
  内容: 5A  A5  06  83  20  01  01  00  05
  pp[2]=6, pp[6+2]=pp[8]=0x05 = 键值低字节（升档）
</code></pre>
 </shiki-code>
 <p>这种解析方式不关心 DGUS 帧内部字段（命令、变量地址、键值位置）的具体偏移量，直接读最后一字节做判断。如果 DGUS Tool 中改了变量地址或键值参数，C 代码不用改。</p>
 <p><strong>边界保护</strong>：</p>
 <ul>
  <li>升档 <code>if(gear_mode &lt; 3)</code> → 最多 3 档</li>
  <li>降档 <code>if(gear_mode &gt; 0)</code> + 兜底 <code>if(gear_mode &lt;= 0) gear_mode = 0</code></li>
 </ul>
 <p><strong>同步屏幕</strong>：<code>dgus_write_vp(0x2000, gear_mode)</code> 向屏幕的变量地址 <code>0x2000</code> 写入当前档位值。屏幕的"变量图标"控件根据这个值自动切换显示的图标。</p>
 <h3 id="72-dgus_write_vp--向屏幕写变量">7.2 dgus_write_vp() — 向屏幕写变量</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">void dgus_write_vp(uint addr, uint val) {
    uchar cmd[8];
    cmd[0] = 0x5A;                     // 帧头 1
    cmd[1] = 0xA5;                     // 帧头 2
    cmd[2] = 0x05;                     // 数据长度：5 字节
    cmd[3] = 0x82;                     // 命令：写变量寄存器
    cmd[4] = (addr &gt;&gt; 8) &amp; 0xFF;       // 变量地址高字节
    cmd[5] = addr &amp; 0xFF;              // 变量地址低字节
    cmd[6] = (val &gt;&gt; 8) &amp; 0xFF;        // 数据高字节
    cmd[7] = val &amp; 0xFF;               // 数据低字节
    UART_1(2, cmd);                    // UART2 发送
}
</code></pre>
 </shiki-code>
 <p>按照 DGUS "写变量寄存器" 帧格式拼装 8 字节命令：</p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>5A A5 05 82 [addr_H] [addr_L] [val_H] [val_L]
</code></pre>
 </shiki-code>
 <p><code>(addr &gt;&gt; 8) &amp; 0xFF</code> 显式加了 <code>&amp; 0xFF</code> 掩码——某些 8051 编译器对 <code>uint</code> 移位的高位处理在不同优化级别下不一致，显式掩码确保行为确定。</p>
 <h3 id="73-led01--辅助-led">7.3 led01() — 辅助 LED</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">uchar led01(uchar* pp) {
    uchar i;
    i = pp[2];                         // 帧长度
    if(pp[i+2] == 0x00) {
        Out_IO(01, 0);                 // P01 低电平 → LED 灭
    } else if(pp[i+2] == 0x01) {
        Out_IO(01, 1);                 // P01 高电平 → LED 亮
    }
    return 0;
}
</code></pre>
 </shiki-code>
 <p>同样用帧尾字节技巧，根据最后一字节的值控制 P01 引脚。<code>Out_IO(01, 0)</code> 中第一个参数 <code>01</code> 是 IO 组号——<code>01</code> = P0 口第 1 位（即 P01），仅验证状态机功能。</p>
 <h3 id="74-uart_1--通用帧发送">7.4 UART_1() — 通用帧发送</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code class="language-c">void UART_1(unsigned char pin, unsigned char *pt) {
    uint i;
    uint len = pt[2] + 3;              // N + 3 = 完整帧总字节数
    for(i = 0; i &lt; len; i++) {
        UART_Send_byte(pin, *pt++);
    }
}
</code></pre>
 </shiki-code>
 <p><code>len</code> 用 <code>uint</code> 而非 <code>uchar</code>：如果 N = 254，则 <code>254 + 3 = 257</code>。<code>uchar</code> 只能表示 0~255，257 会溢出截断为 1。<code>uint</code> 能正确处理这情况。</p>
 <p><code>pin</code> 参数指定串口号（1 = UART1 调试，2 = UART2 屏幕）。</p>
 <hr>
 <h2 id="8-完整执行流--从触摸到舵机响应">8. 完整执行流 — 从触摸到舵机响应</h2>
 <h3 id="81-上电启动">8.1 上电启动</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code> 上电复位
   │
   ▼
 STARTUP.A51  →  清零内部 RAM (IDATA区域)  →  初始化堆栈 SP  →  LJMP main()
   │
   ▼
 main() 初始化:
   GPIO:   全 IO 准双向  →  P20 推挽
   UART:   UART1(调试) + UART2(DGUS,中断)
   PWM:    P20, 50Hz, duty=250 (0°)
   EA=1    ← 中断总开 (放在最后!)
   │
   ▼
 while(1) 主循环开始
</code></pre>
 </shiki-code>
 <h3 id="82-触摸--按钮从-0-档升到-1-档">8.2 触摸 [+] 按钮，从 0 档升到 1 档</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code> 手指触摸屏幕 [+] 按钮
   │
   ▼
 DGUS 通过 UART2 TX 逐字节发送触摸返回帧:
   5A A5 06 83 20 01 01 00 05   (9 字节, 最后一字节=0x05)
            └────┬────┘ └──┬──┘
            变量地址0x2001  键值0x0005
   │
   ▼ (逐字节到达 STC P10)
 UART2 硬件 → S2BUF → RI=1 → 触发中断
   │
   ▼
 isr.c: UART2_I() interrupt 8
   └── UART2_isr()  →  UART_DATA[next++] = S2BUF
   │
   ▼ (中断返回)
 main() while(1) 帧解析:
   next != current → 有数据!
   case 0: 找到 0x5A ✓ → flag=1
   case 1: 确认 0xA5 ✓ → flag=2
   case 2: ia==2, 长度=06 ✓ → ja=7, flag=3
   case 3: 逐字节接收 83→20→01→01→00→05
           ja: 7→6→5→4→3→2→1 → ja==1! 帧收齐
           ├─ UART_1(1, frame)   → PC 串口助手看到完整帧
           ├─ gears(frame)       → 帧尾=0x05
           │   gear_mode: 0→1
           │   └─ dgus_write_vp(0x2000, 1) → 屏幕显示 1 档图标
           └─ led01(frame)       → P01=1, LED 亮
   │
   ▼
 舵机扫动开始 (gear_mode=1):
   sweep_timer: 1→2→...→8999→0 → servo_sweep():
     target = max_angle[1] = 60°
     cur_angle: 0→1→2→...→60→59→...→0→1→...
     PWM_change(20, angle_to_duty(cur_angle))
   │
   ▼
 SG90 舵机在 0° ↔ 60° 之间来回摆动
</code></pre>
 </shiki-code>
 <h3 id="83-连续触摸切到-3-档再降到-0-档">8.3 连续触摸，切到 3 档再降到 0 档</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code> 触摸 [+]       → gears(): 帧尾=0x05 → gear_mode: 1→2
    target = max_angle[2] = 120°  → 扫动范围扩大到 0°↔120°

 触摸 [+]       → gears(): 帧尾=0x05 → gear_mode: 2→3
    target = max_angle[3] = 180°  → 扫动范围扩大到 0°↔180°

 触摸 [-]       → gears(): 帧尾=0x04 → gear_mode: 3→2
    target = 120°  → 范围缩小

 触摸 [-]       → gear_mode: 2→1 → target=60°

 触摸 [-]       → gear_mode: 1→0
    servo_sweep(): gear_mode==0 → cur_angle=0, PWM_change(20,250)
    舵机停转
</code></pre>
 </shiki-code>
 <h3 id="84-主循环的并发时序">8.4 主循环的并发时序</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>while(1) 每圈做的事情:

  ┌─ 检查 UART → 如果有数据，执行状态机一步(一字节)
  │     ↓ (无数据时跳过)
  ├─ sweep_timer++
  └─ if(sweep_timer &gt;= 9000) → servo_sweep()

  两个任务在同一循环中交替，互不阻塞:
  即使舵机在扫动中，收到触摸帧也能立即响应
  即使正在处理帧，舵机扫动计时也不会停止
</code></pre>
 </shiki-code>
 <hr>
 <h2 id="9-附录">9. 附录</h2>
 <h3 id="a-硬件接线">A. 硬件接线</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>DGUS T5L 串口屏            STC8A8K64D4             SG90 舵机
┌──────────┐             ┌──────────────┐        ┌──────────┐
│ TXD2     │────→────────│ P10 (RXD2)   │        │          │
│ RXD2     │←────────────│ P11 (TXD2)   │        │          │
│ GND      │──────┬──────│ GND          │──┬─────│ 棕 GND   │
└──────────┘      │      └──────────────┘  │     └──────────┘
                  │                        │
                  │             P20 ───────┼──→ 橙 SIG
                  │                        │
                  │             外部 5V ───┼──→ 红 VCC
                  │                        │
                  └────────────────────────┘ (共地)
</code></pre>
 </shiki-code>
 <ul>
  <li>串口参数：UART2, 115200 bps, 8N1</li>
  <li>舵机供电：必须外部独立 5V（堵转电流可达 700mA+，STC 板载 LDO 带不动）</li>
 </ul>
 <h3 id="b-dgus-屏幕控件配置">B. DGUS 屏幕控件配置</h3>
 <table>
  <thead>
   <tr>
    <th>控件</th>
    <th>变量地址 (VP)</th>
    <th>参数</th>
    <th>功能</th>
   </tr>
  </thead>
  <tbody>
   <tr>
    <td>变量图标</td>
    <td><code>0x2000</code></td>
    <td>4 张图标 (值 0/1/2/3)</td>
    <td>显示当前档位</td>
   </tr>
   <tr>
    <td>按键返回</td>
    <td><code>0x2001</code></td>
    <td>[-] 键值 <code>0x0004</code></td>
    <td>发送降档帧</td>
   </tr>
   <tr>
    <td>按键返回</td>
    <td><code>0x2001</code></td>
    <td>[+] 键值 <code>0x0005</code></td>
    <td>发送升档帧</td>
   </tr>
  </tbody>
 </table>
 <h3 id="c-dgus-协议帧格式">C. DGUS 协议帧格式</h3>
 <p><strong>触摸返回帧（屏 → MCU）</strong>：</p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>5A A5 06 83 VP_H VP_L 01 KEY_H KEY_L
</code></pre>
 </shiki-code>
 <p><strong>写变量帧（MCU → 屏）</strong>：</p>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>5A A5 05 82 VP_H VP_L VAL_H VAL_L
</code></pre>
 </shiki-code>
 <p>两种帧的帧头（5A A5）、长度字段、命令字段位置完全一致，状态机能统一解析。</p>
 <h3 id="d-关键全局变量">D. 关键全局变量</h3>
 <table>
  <thead>
   <tr>
    <th>变量</th>
    <th>类型</th>
    <th>位置</th>
    <th>用途</th>
   </tr>
  </thead>
  <tbody>
   <tr>
    <td><code>gear_mode</code></td>
    <td>uchar</td>
    <td>main.c 定义, dgus.h extern</td>
    <td>当前档位，gears() 写, servo_sweep() 读</td>
   </tr>
   <tr>
    <td><code>max_angle[4]</code></td>
    <td>int[]</td>
    <td>main.c</td>
    <td>各档目标角度</td>
   </tr>
   <tr>
    <td><code>cur_angle</code></td>
    <td>uint</td>
    <td>main.c</td>
    <td>舵机当前角度 (0~180)</td>
   </tr>
   <tr>
    <td><code>sweep_up</code></td>
    <td>bit</td>
    <td>main.c</td>
    <td>扫动方向标志</td>
   </tr>
   <tr>
    <td><code>sweep_timer</code></td>
    <td>uint</td>
    <td>main.c</td>
    <td>软件节拍计数器</td>
   </tr>
   <tr>
    <td><code>UART_DATA[256]</code></td>
    <td>uchar[]</td>
    <td>main.c</td>
    <td>环形接收缓冲</td>
   </tr>
   <tr>
    <td><code>UART_FLAG</code></td>
    <td>uchar</td>
    <td>main.c</td>
    <td>帧解析状态 (0~3)</td>
   </tr>
  </tbody>
 </table>
 <h3 id="e-中断系统架构">E. 中断系统架构</h3>
 <shiki-code dark-theme="slack-dark" font-size="0.875em" light-theme="slack-ochin" variant="simple">
  <pre><code>inisr.asm ──→ P0~P9 INT 扩展中断 → 跳转到 isr.c 的 GPIO_I() (interrupt 25)

isr.c ──→ 21 个中断入口函数
  ├─ UART2_I() interrupt 8   →  UART2_isr() (main.c)   ★ 核心
  ├─ UART1_I() interrupt 4   →  只清标志 (调试口不发不收)
  ├─ PCA_I()  interrupt 7    →  模式分发 (预留, 业务函数未启用)
  └─ 其他 18 个               →  清标志 / 空壳 (保持中断向量表完整)
</code></pre>
 </shiki-code>
 <h3 id="f-编译和烧录">F. 编译和烧录</h3>
 <ul>
  <li>编译输出：<code>HEX/STC8_HEX.hex</code> (18554 bytes)</li>
  <li>烧录工具：STC-ISP (通过 USB 转串口连接 STC 的 UART1)</li>
  <li>包含模块：GPIO, UART, PWM, delay 等 12 个底层库 + User 代码</li>
 </ul>
 <hr>
 <blockquote>
  <p>工程路径：
   <br>
   Windows Keil：<code>Desktop/stc8a8k64d4/Project/功能实现/</code>
   <br>
   Linux 备份：<code>/home/ctyun/.openclaw/workspace/</code></p>
 </blockquote>
</div>
<p></p>]]></description><guid isPermaLink="false">/archives/1779945886904</guid><dc:creator>flor</dc:creator><category>8051</category><pubDate>Thu, 28 May 2026 05:24:55 GMT</pubDate></item></channel></rss>