
前阵子帮一位做工业采集的朋友调板子场景很朴素一块 STM32 从机负责采 8 路模拟量一根三芯线电源、信号、地拉到三米外的主机板。第一版直接用 UART115200 波特率台面上跑得好好的装到机柜里就间歇性丢字节。用示波器一量两板之间的地电位差随着大功率设备启停来回跳接收端的判决门限跟着漂再加上线材本身是个低通方波的边沿被磨成了圆角采样点正好落在最难受的位置。换过 RS485 收发器、加过隔离成本上去了但项目本身利润薄不想再堆料。最后改了一个思路既然收发两端都是自己写的固件那就把时钟信息直接塞进波形里用曼彻斯特编码做基带传输接收端靠波形自身的跳变恢复节拍不用再跟绝对电平和长期漂移较劲。改完之后连续跑了七十二小时误码率是零。这件事让我意识到曼彻斯特编码这东西虽然课本上讲得很早但真正到了要手写编解码、要抠定时器参数的时候能落地的资料并不多。搜出来的内容要么停在中间跳变表示时钟、跳变方向表示数据这一句要么直接给你一段 MATLAB 仿真跟 STM32 的定时器和 DMA 完全对不上。所以这篇我就按自己实际写过的代码、算过的参数把这条路完整走一遍它到底解决什么问题、两种极性约定怎么区分、在 STM32 上用定时器和 DMA 怎么把波形生出来、接收端怎么从边沿里把比特抠出来、以及那些只有踩过才知道的坑。不管你是刚接触嵌入式的新手还是做了几年想补一下通信基础的工程师看完应该都能直接抄着改。1. 为什么还要在 2025 年折腾曼彻斯特编码1.1 一个典型取舍现场两线制数据回传先把这个场景说透因为脱离了场景去谈编码方式全是空话。上面说的那根三芯线实际约束是这样的线长三米到十米不等现场有大功率变频器在附近工作共模干扰明显两端都是 STM32一端是 F103一端是 F407数据量不大每 10 毫秒上报一帧一帧 16 字节左右成本敏感不想加隔离芯片和专用收发器。这种条件下能选的方案其实就那么几个。SPI 只能板内用走不了线I2C 抗干扰能力弱线长了电容直接把上升沿吃掉UART 是异步的接收端靠自己的内部时钟去采样一旦收发两端的时钟偏差累积到半个位周期采样点就滑出去了而且它同样怕共模干扰。CAN 需要收发器成本上去了还得配对端电阻。以太网就更不用说了。曼彻斯特编码在这类场景里的价值就出来了——它是自同步的。所谓自同步就是接收端不需要事先知道发送端的时钟频率是多少也不需要一根单独的时钟线只要盯着波形上的跳变沿就能推算出位周期进而推算出每一位的边界在哪。这一点对于长线、低成本、双端都是自己写固件的场合几乎是量身定做。代价也很明确它把信号的带宽需求翻了一倍。这个后面会算。工程上所有的选择都是在拿代价换收益关键是要算清楚这笔账划不划算。1.2 曼彻斯特编码到底解决了哪三件事我把它的作用拆成三条这样记忆起来不容易混。第一条是消除直流分量。如果直接传 NRZ不归零码连续发送一长串 0 或者一长串 1线上电平会长时间保持在一个固定值上。这时候变压器耦合就传不过去了变压器不传直流交流耦合的接收端也会因为耦合电容的充放电而让基线发生偏移等这一串走完基线已经漂到不知道哪里去了后面几个比特直接判错。曼彻斯特编码强制每个比特周期内必须有一次跳变高低电平出现的概率基本均衡平均值稳定在中间电平附近直流分量被压得很小交流耦合的电路就能正常工作。第二条是自带时钟信息。每比特中间那个跳变沿就是接收端用来对齐节拍的鼓点。哪怕收发两端的晶振差个百分之几接收端也能每个比特重新对齐一次误差不会累积。这跟异步串口那种一帧只对齐一次、靠内部时钟往后数的方式完全不同鲁棒性差着量级。第三条是实现成本低。它的编解码逻辑非常简单编码就是把一位变成两位解码就是测边沿间隔然后查表不需要任何浮点、不需要大缓冲区一个 F103 的定时器和 DMA 就能完全搞定CPU 几乎不参与。我见过有人把这三条总结成抗直流、抗漂移、易实现。现场那三米线、地电位乱跳的工况恰好把前两条的痛点全占了所以换编码方式之后效果立竿见影。1.3 一个非典型的参照125kHz 门禁卡再举个离大家更近的例子。很多 125kHz 的低频门禁卡、动物耳标、工业标签用的就是曼彻斯特编码。卡片本身没有电池靠读卡器辐射出来的载波取电能量非常有限不可能再维持一个稳定的本地振荡器去做精确采样。它的做法就是从载波上把时钟抠出来——曼彻斯特码的跳变沿正好提供了这个时钟用锁相环或者简单的边沿检测就能恢复。这个场景把自同步的价值体现得淋漓尽致越是没有稳定时钟、越是能量紧张的系统越依赖把时钟藏进数据里。值得一提的是IEEE 802.3 定义的经典 10BASE-T 以太网也用的是曼彻斯特编码10 Mbps 的数据率对应 20 MBd 的符号率信号频谱从 0 一直延伸到 20 MHz 附近。这个例子很好记波特率是比特率的两倍这是我见过最多人算错的一个参数。2. 跳变规则背后的逻辑两种约定与一张编码表2.1 IEEE 802.3 与 G.E. Thomas同一件事的两种极性曼彻斯特编码的核心规则只有一句话每个比特周期被分成前后两个相等的小段叫半位half-bit前段和后段的电平必须相反。这样一来比特周期的正中间一定存在一次跳变。至于这次跳变是从高到低还是从低到高就产生了两种主流约定。约定名称逻辑 0逻辑 1中间跳变方向常见使用场景IEEE 802.3前半位高、后半位低前半位低、后半位高0 为下降沿1 为上升沿经典以太网、部分工业总线G.E. Thomas前半位低、后半位高前半位高、后半位低0 为上升沿1 为下降沿门禁卡、部分低速遥控、教学示例两套约定在数学上完全等价信息量一样抗干扰能力一样区别只是把上升沿定义成 0 还是定义成 1。真正的问题是收发双方必须用同一套约定。我见过不止一次发送端按 Thomas 写接收端按 802.3 解结果收到的数据全部取反而且因为帧头校验也没过排查了半天以为是硬件问题。有个很实用的记忆法把0想象成一个从高往下掉的动作802.3 就是这么定的字母 O 的形状像个圈从上面掉下来而1是往上顶的。你也可以干脆反过来记但一定要在自己项目里写死一条并且在代码注释里标清楚别让下一个人接手的时候猜。2.2 逐位推演从比特到电平序列光看表还是抽象的我们拿一个具体字节走一遍。假设按 G.E. Thomas 约定逻辑 0 编码成低高记作 01逻辑 1 编码成高低记作 10。这里我先约定写01表示前半位低、后半位高。要发送一个字节 0xB5二进制是 1011 0101从最高位开始位序数据位编码结果累计电平序列b711010b60011001b5110100110b411010011010b30011001101001b2110100110100110b100110011010011001b01101001101001100110最终在线上的电平序列是1 0 0 1 1 0 1 0 0 1 1 0 0 1 1 0一共 16 个半位。你可以注意两点第一任意两个相邻位之间边界处的电平可能相同也可能不同这取决于前一位的后半位和后一位的前半位第二每个位周期的中间一定有一次跳变这就是接收端的对齐依据。再用 IEEE 802.3 约定0 记作 101 记作 01编同一个字节结果就变成0 1 1 0 0 1 0 1 1 0 0 1 1 0 0 1刚好整个取反。这一点很关键两种约定的输出是互补的。如果你发现解码出来的数据总是和发送的相反八成就是极性搞错了。2.3 波特率是比特率的两倍这笔账怎么算很多人第一次算这个会绕进去。我们来算一遍。假设数据率是 1 Mbps也就是每秒传 1,000,000 个比特。每个比特被拆成两个半位所以每秒需要在线路上输出 2,000,000 个电平符号。也就是说符号率波特率是 2 MBd。如果用一个定时器以固定频率翻转 GPIO 来生成这个波形那么定时器的更新频率应该是多少答案就是 2 MHz也就是每个半位触发一次而不是每个比特触发一次。我最初写代码的时候就是这里栽的跟头定时器配了 1 MHz结果波形出来速度只有一半接收端解出来一堆错。带宽方面理论上曼彻斯特信号的功率谱主瓣宽度大约是比特率的两倍跟 2 MBd 的符号率对得上。所以 1 Mbps 的数据率你至少得准备 2 MHz 甚至更宽的通道带宽。线材的截止频率、驱动器的压摆率、接收端的采样带宽都得按这个来选。这也是它相比 NRZ 最明显的劣势同样一根线用 NRZ 能跑 2 Mbps用曼彻斯特只能跑 1 Mbps。换算关系就一条公式符号率Bd 比特率bps× 2 半位周期s 1 / 符号率 1 / (比特率 × 2)举例目标 500 kbps半位周期 1 / 1,000,000 1 微秒目标 1 Mbps半位周期 0.5 微秒。这些数字后面算定时器参数的时候会直接用到。3. 和其他线路码对比什么时候该用它3.1 NRZ、曼彻斯特、差分曼彻斯特、4B/5B 横向对照选编码方式之前把几个常见选项放在一起看会更清楚。这张表是我自己在选型时用的整理出来供参考。编码方式每比特符号数是否含时钟信息直流分量带宽效率典型应用NRZ1无差长串 0/1 时严重高UART、SPI、内部总线曼彻斯特2强每比特必跳变好低0.5 bps/Hz10BASE-T、低频卡、工业短距差分曼彻斯特2强好低令牌环网、部分遥控4B/5B1.25中靠编码保证跳变密度好中高0.8 bps/Hz100BASE-TX、FDDI从表里能看出来曼彻斯特的定位非常清晰它在带宽效率这一栏几乎垫底但在时钟信息和直流分量两栏都是满分。所以它适合的场景是——线路带宽够用、但是对同步和抗漂移要求高、同时又不想加额外时钟线或者贵收发器的地方。反过来如果你有的是带宽、缺的是速率比如要跑几十兆那 4B/5B 或者更高级的编码才是正解。特别提醒不要因为曼彻斯特看起来简单就在高速率场合硬上。1 Mbps 数据率要占 2 MHz 带宽10 Mbps 就要占 20 MHz而 STM32 的 GPIO 翻转速率、线上传输的边沿质量、接收端的采样率都会跟着吃紧。我个人的经验界线是在普通 FR4 板加一两米排线的条件下用定时器加 DMA 直接发波形1 Mbps 到 2 Mbps 数据率是比较舒服的区间再往上就该考虑换方案了。3.2 差分曼彻斯特多出来的那一点好处差分曼彻斯特值得单独提一句因为它的设计思路很巧妙。它同样保证每个比特周期中间有一次跳变但比特值的判定不再看跳变方向而是看位起始处有没有跳变位起始处有跳变代表某一种值没有跳变代表另一种值。这样做的好处是极性无关。也就是说如果你把两条信号线接反了或者整个通道的极性被反相了解码结果依然正确——因为判定依据是跳变的有无而不是跳变的方向。这在接插件可能插反、或者经过一级反相放大的场合非常有用。代价是解码逻辑稍微复杂一点需要同时盯着位中间的跳变和位起始的跳变状态机多一个分支。如果你的通道极性是确定的、不会变用普通曼彻斯特就够了如果存在极性不确定的风险差分曼彻斯特值那点额外的代码量。我在另一个项目里因为接插件没有防呆、又懒得改硬件就用差分曼彻斯特兜住了这个坑实测挺省心。4. STM32 上四种实现路线选型4.1 软件 GPIO 翻转最土但最快上手最简单粗暴的做法就是写个循环按半位周期依次设置 GPIO 输出高低电平中间用延时或者定时器等待。代码大概十几行五分钟就能跑起来。它的优点是直观、不用配 DMA、不用查手册对通道映射改一个电平就能在示波器上看到变化非常适合第一版验证协议逻辑。我调编码正确性的时候永远先用这个版本跑一遍确认波形对了再去优化。缺点同样明显。CPU 全程被占死几百 kbps 以上基本就没法兼顾别的事了延时精度受中断影响一旦有别的中断插进来位周期就被拉长波形抖动明显而且它没法产生连续的长数据流发到一半被中断打断接收端就废了。我的建议是把它当调试脚手架而不是最终方案。协议逻辑用软件版跑通、用示波器抓波形确认无误之后再迁移到 DMA 版本这样排查问题的维度少一个效率高很多。4.2 定时器 PWM 加 DMA 写 BSRR推荐方案这是我最终用的方案也是我认为在 STM32 上做曼彻斯特编码最舒服的一条路。核心思路是这样的让一个定时器以半位周期为间隔产生更新事件每次更新事件触发一次 DMA 请求DMA 把一个预先算好的值写进 GPIO 的 BSRR 寄存器。BSRR 的低 16 位写 1 表示把对应引脚置高高 16 位写 1 表示把对应引脚拉低。所以我们只需要准备一个数组数组里每个元素要么是置高的值要么是拉低的值DMA 循环着往外搬就行了。这样做有几个好处CPU 零参与一旦启动只要数据备好波形会一直连续输出抖动极小因为每次电平变化的时刻由定时器硬件决定跟中断响应没关系速度快F103 在 72 MHz 主频下半位周期做到 0.5 微秒完全没有压力也就是 1 Mbps 数据率。唯一需要注意的是 DMA 的搬运节奏必须和定时器严格同步。做法是把定时器的更新事件配成 DMA 的触发源不同系列的映射方式略有差异F1 系列用 TIM_DMACmd 打开对应通道的 DMA 请求F4 系列在新版 HAL 里通过 TIM 的 DMA Burst 或者简单地把 DMA 通道挂到 TIMx_UP 上。具体的映射关系一定要查对应型号的参考手册里DMA 请求映射那张表我见过有人凭印象配错通道结果波形完全不出来查了两小时。4.3 SPI 主机加查表速率高、CPU 占用低还有一条很聪明的路把每个数据位映射成两个比特然后用 SPI 以两倍于目标数据率的时钟把这些比特打出去。比如目标数据率 1 Mbps就把 SPI 时钟配成 2 MHz数据随便发SPI 硬件会自动按位输出每个比特的时长是 0.5 微秒正好是我们要的半位。编码方式就是查表。把每个输入字节拆成两个半字节每个半字节查一次表得到一个字节两个字节拼起来就是最终的输出。这是典型的以空间换时间表占 16 字节的 Flash但编码速度是常数级的一个字节查两次表就完了。/* 按 G.E. Thomas 约定0 - 011 - 10 */ /* 输入 4 位输出 8 位高位先出 */ static const uint8_t man_table[16] { 0x55, 0x56, 0x59, 0x5A, 0x65, 0x66, 0x69, 0x6A, 0x95, 0x96, 0x99, 0x9A, 0xA5, 0xA6, 0xA9, 0xAA }; /* 把一个字节编码成两个字节返回长度固定为 2 */ void man_encode_byte(uint8_t in, uint8_t *out) { out[0] man_table[(in 4) 0x0F]; out[1] man_table[in 0x0F]; }注意这张表是按0 记作 01、1 记作 10生成的如果你要用 IEEE 802.3 约定把每个值按位取反就行了或者直接用互补表思路一样。SPI 方案的限制在于它的位序、空闲极性、时钟相位都必须和接收端对得上而且 SPI 是主机驱动的没法像定时器那样灵活地在帧与帧之间插入自定义的前导码其实可以插入到缓冲区里就行。另外SPI 的片选信号在这里没用得关掉或者忽略。4.4 硬件异或加载波把 CPU 彻底解放出来最后一条路是纯硬件的用一个异或门比如 74HC86一路输入接定时器产生的方波载波另一路接数据。载波频率是两倍的比特率数据在每个比特周期保持稳定异或之后输出的就是标准的曼彻斯特波形。原理很简单载波本身每个半位翻转一次数据位为 0 时异或结果等于载波低高数据位为 1 时异或结果是载波的反相高低。这正好就是我们前面推的编码规则。想要切换约定反过来接一下数据的极性就行。这个方案的好处是主控只需要用 UART 或者 SPI 按普通方式把数据发出来异或门负责加时钟这件事主控侧几乎没有任何额外负担也不需要 DMA 配合定时器。代价是板子上要多一个芯片、多占两个 GPIO而且载波和数据之间的相位关系必须严格对齐——如果数据的跳变时刻恰好落在载波的边沿附近异或输出会出现毛刺。解决办法是让数据在载波的固定相位上更新比如用载波的下降沿去锁存数据用一个 D 触发器就能搞定。什么时候值得上这套硬件我自己的判断是主控资源紧张比如跑着复杂算法DMA 和定时器都得留给别的外设或者速率要求超出软件方案的舒适区又或者要做多路并行发送。否则一颗几毛钱的逻辑芯片加上两个额外的布线对大多数项目来说是得不偿失的。5. 参数计算与核心代码落地5.1 定时器时钟树与实际 ARR 计算参数计算这一步是我见过出错最多的地方根源都在时钟树上。我们拿 STM32F103C8T6 举个完整的例子把每一步都算清楚。F103 的定时器分两组TIM1 挂在 APB2 上TIM2 到 TIM4 挂在 APB1 上。这里有个很容易被忽略的规则当 APB 预分频系数不为 1 时挂在该总线上的定时器时钟会被自动乘以 2。默认配置下APB1 是 36 MHzAPB2 是 72 MHz但 TIM2 到 TIM4 的实际时钟是 36 × 2 72 MHzTIM1 的时钟是 72 MHz。所以两组定时器的时钟其实都是 72 MHz这个乘以 2的规则一定要记住否则算出来的频率会差整整一倍。现在要生成 1 Mbps 的曼彻斯特码半位周期是 0.5 微秒。定时器计数频率 72 MHz那么计算项公式结果目标半位周期1 / (2 × 1 Mbps)0.5 μs定时器计数频率72 MHz72 MHz需要的计数值0.5 μs × 72 MHz36预分频 PSC取 0不分频0自动重装 ARR36 - 135所以 PSC 0ARR 35更新事件频率就是 72 / 36 2 MHz正好对应 1 Mbps 的数据率。这个数字很干净实际用的时候直接把 35 写进去。如果要跑 500 kbps半位周期就是 1 微秒ARR 72 - 1 71。如果要跑 250 kbps半位周期 2 微秒ARR 144 - 1 143。都能直接心算出来。再提醒一个细节如果目标速率不是 72 MHz 的整数分频比如想要 1.2 Mbps半位周期 416.67 纳秒ARR 就得取 30实际半位周期是 31/72 ≈ 430.6 纳秒对应数据率约 1.161 Mbps存在 3% 多的误差。这个误差对曼彻斯特解码是可接受的后面会讲容差但如果你的系统里还有其他时钟依赖就要评估一下。降低误差的办法是提高定时器时钟或者换主频比如把系统跑到 96 MHz 或 168 MHz。5.2 编码缓冲区的组织方式DMA 往外搬的那个数组元素内容要写成 BSRR 的写入值。假设信号脚是 PA5那么置高(1UL 5)也就是 0x00000020拉低(1UL 21)也就是 0x00200000我们可以定义两个宏代码可读性好很多#define PIN_TX 5 #define MAN_HIGH (1UL PIN_TX) #define MAN_LOW (1UL (PIN_TX 16))然后从编码后的两个字节展开成 16 个 BSRR 值。展开函数这样写static uint32_t man_buf[MAN_MAX_BITS * 2]; /* 把编码后的字节流展开成 BSRR 写入序列 */ static uint32_t *man_expand(const uint8_t *enc, uint32_t enc_len) { uint32_t *p man_buf; for (uint32_t i 0; i enc_len; i) { uint8_t b enc[i]; for (int k 7; k 0; k--) { *p (b (1u k)) ? MAN_HIGH : MAN_LOW; } } return p; }这里enc是上一节的表查出来的编码结果每个比特在enc里是一个比特展开之后每个比特变成一个 BSRR 条目DMA 每个更新事件搬一个节奏正好是半位周期。关于缓冲区大小算一下就知道一个 16 字节的帧编码后是 32 字节展开后是 256 个 uint32_t也就是 1 KB 的 RAM。F103C8 有 20 KB RAM完全够用。如果你要发更长的帧记得把 DMA 的传输长度和缓冲区都对应放大并且注意 DMA 循环模式下的缓冲区地址对齐。启动的时候先把 DMA 配成循环模式源地址指向 man_buf目标地址写GPIOA-BSRR数据宽度都是 32 位每一项搬运长度为展开后的总数。然后再打开定时器的 DMA 请求和更新中断如果不需要中断就只开 DMA 请求最后启动定时器。顺序很重要先备好数据再开定时器反过来会导致前几个半位输出错误的电平。5.3 前导码与帧同步的设计纯曼彻斯特码流本身没有帧边界的概念接收端一上电看到的就是一连串电平跳变它不知道哪两个半位属于同一个比特也不知道字节从哪里开始。所以必须自己定义一个同步机制。我的做法是在每帧数据前面加一段固定的前导码比如 8 个字节的 0x55二进制 0101 0101。选 0x55 是有讲究的它在曼彻斯特编码之后电平序列会变成规律性极强的交替跳变接收端很容易识别出这个特征。有些传统协议会用 0xAA 或者 0x7E也是同样的道理。前导码之后加一个同步字比如 0x7E用来做帧定界。接收端先用前导码把节拍锁定再用同步字确认字节边界然后才开始正式接收数据。数据后面跟一个校验字段CRC16 或者简单的累加和都行用来判断这一帧有没有出错。这里有个经验值前导码长度不要少于 4 个字节。太短了接收端的位对齐算法还没来得及收敛太长了又浪费带宽。我一般用 4 到 8 个字节视线路质量而定。线路差的场合把前导码加到 8 甚至 16 个字节能让接收算法有更多机会锁定。6. 接收端解码同步、采样与容错6.1 边沿间隔解码的核心思路接收端的任务是从一串电平跳变中恢复出比特。核心逻辑可以概括成一句话测两个相邻边沿之间的时间间隔。一个比特周期 T 被分成两个半位。如果两个相邻边沿的间隔是 T/2说明这是位中间的跳变而且这个位和它相邻的位在边界处没有跳变如果间隔是 T说明这个边沿是从位中间跳变跳到相邻位的边界跳变或者反过来此时边界处发生了跳变。更直接的做法是找到位中间的跳变之后在距离它 T/2 的位置去采样电平采样到的值就是这一位的数据按约定映射。具体实现上我用的是一套状态机typedef enum { RX_WAIT_EDGE, /* 等待第一个边沿 */ RX_SYNC_HALF, /* 等待半个位周期后的跳变 */ RX_DECODE, /* 正常解码状态 */ } rx_state_t; /* 定时器以 4 倍符号率自由运行cnt 是捕获到的计数值 */ static void on_edge_capture(uint32_t cnt) { static uint32_t last_cnt 0; static rx_state_t st RX_WAIT_EDGE; uint32_t delta (cnt - last_cnt) 0xFFFF; uint32_t half HALF_TICK; /* 一个半位对应的计数数 */ /* 容差判断允许 ±25% 的偏差 */ if (st RX_DECODE) { if (is_near(delta, half, TOL)) { /* 正常间距继续下一个比特 */ push_bit_from_level(current_level); } else if (is_near(delta, half * 2, TOL)) { /* 间隔翻倍说明边界处没有跳变补一个位 */ push_bit_from_level(current_level); push_bit_from_level(current_level); } else { /* 超差判定同步丢失回到等待状态 */ st RX_WAIT_EDGE; } } last_cnt cnt; }这段是伪代码但把关键判断都体现了。真实代码里会更啰嗦一些因为要处理位计数、字节组装、CRC 校验这些东西但骨架就是这样。容差设 ±25% 是我实测下来比较稳的值。太小了晶振偏差和边沿抖动会让它频繁丢同步太大了相邻的不同间隔会分不开。如果你的收发两端用的是不同的晶振把容差往大放一点比如 ±30%但要保证 T/2 和 T 的判定区间不重叠否则就会出现误判。6.2 用输入捕获加 DMA 把边沿时间戳抓下来上面那套逻辑如何高效地获取边沿时间戳最简单的是外部中断加定时器读取计数器。但中断频率等于边沿频率1 Mbps 数据率下边沿频率最高可达 2 MHz中断根本扛不住CPU 会被打爆。正确做法是用定时器的输入捕获配合 DMA。思路是让一个高频定时器比如 8 倍或 16 倍符号率自由运行配置一个通道为输入捕获模式捕获到的计数值通过 DMA 自动搬到环形缓冲区里等缓冲区攒够一批再统一处理。这样中断频率降到了 DMA 传输完成中断的级别CPU 负担很小。这里有个小技巧定时器的计数位数要够。16 位定时器在 8 倍符号率下2 MHz 计数频率只能记 32 毫秒就溢出了。如果两帧之间的边沿间隔可能超过这个时间比如帧与帧之间有空闲溢出判断就很重要。解决办法有三个一是降低过采样率二是用 32 位定时器F4 及以上有 TIM2 和 TIM5 是 32 位的三是在捕获中断里累加溢出次数。我一般直接选 32 位定时器省心。过采样率的选择也值得说一句。理论上 4 倍符号率就够了因为只需要区分 T/2 和 T 两种间隔但实际中为了抗抖动我一般用 8 倍。8 倍符号率意味着 1 Mbps 数据率下定时器要跑 16 MHzF103 完全没问题如果是 10 Mbps 数据率就要 160 MHz 的计数频率那就得用 F4 或者换方案了。6.3 接收算法的鲁棒性设计前面说的都是理想情况。实际线路上边沿会抖动、会有毛刺、会有偶发的干扰脉冲接收算法必须能扛住这些。我总结了三条有用的措施。第一是边沿滤波。输入捕获之前先用定时器或 GPIO 的滤波功能把窄于某个宽度的脉冲滤掉。STM32 的输入捕获通道可以配置数字滤波器用几个采样点一致性判断来去毛刺这个功能非常值得用上配置一两行代码的事。第二是超时重置。如果两个边沿之间的间隔超过了若干个位周期说明这一帧已经结束了或者出现了异常此时应该把状态机复位到等待状态准备接收下一帧。这个阈值的设定要结合你的帧结构一般在帧尾之后留出二三十个位周期的空闲超过就认为帧结束。第三是校验加丢弃。收到一帧之后算 CRC不对就丢掉。不要试图去纠错曼彻斯特的误码通常是成串出现的干扰脉冲会打乱好几个位单比特纠错帮不上忙重传反而是更靠谱的方案。我在那个三米线的项目里一开始没加边沿滤波结果现场只要有设备启停接收端就会多出几个虚假边沿解出来的数据全是垃圾。加上滤波之后误码率直接降到了零。所以这条别省。7. 常见问题与排查技巧实录调试过程中踩的坑我整理成一张速查表遇到问题按这个顺序排查能省不少时间。现象可能原因排查方法解决方式波形频率正好是目标的一半定时器时钟源或分频算错示波器测半位周期与公式对比检查 APB 分频与定时器倍频规则核对 ARR波形频率正好是目标的两倍波特率与比特率混淆确认发送的是每半位一个更新事件修正定时器频率为 2 倍比特率解码数据全部取反IEEE 与 Thomas 约定不一致抓一帧已知数据对比编码表统一约定或在接收端取反前几个比特总是错定时器启动早于数据搬运查看首段波形是否与编址不符先备好 DMA 数据再启动定时器长线传输误码率高边沿被磨圆、阻抗不匹配观察远端波形上升沿时间减小驱动电流、串接匹配电阻、降低速率偶发虚假边沿导致乱码干扰脉冲被捕获示波器看是否有多余窄脉冲打开输入捕获数字滤波帧与帧之间丢同步空闲时间超出定时器计数范围检查空闲间隔与定时器溢出换 32 位定时器或累加溢出计数DMA 只搬了一轮就停循环模式未开或长度配错检查 DMA 模式位与传输长度配置循环模式长度设为展开后总数高数据率下波形抖动GPIO 翻转速率不足查芯片手册的最大 GPIO 翻转频率降低速率或改用带高速输出能力的外设除了表格里这些还有几个表格里不好表达的经验。一个是测波形一定要测最远端。我在板子上的测试点看到波形方方正正觉得没问题结果拉到接收端一量边沿已经变成了弧形幅度也衰减了。原因是线材是低通的而且没有做阻抗匹配。后来在发送端串了一个 33 欧的电阻接收端并联了一个小电容做补偿波形才恢复过来。这个教训是测点选在生产端等于没测。另一个是先跑低速再往上加。我习惯先用 10 kbps 把整条链路跑通确认编解码逻辑、同步机制、CRC 都对然后再一档一档往上加每次加一倍每次都用误码率统计确认。这样出问题的时候可以立刻定位到是哪个速率段开始崩的避免在高速下跟一堆问题纠缠。第三个是不要迷信连续测试。连续跑几小时不报错不代表稳定因为现场干扰往往是间歇性的、跟别的设备联动的。我的做法是故意在旁边制造干扰源开关大功率设备、用电机启停观察误码率变化。这个方法比较土但确实找出了好几个只在特定条件下才出现的问题。8. 我在这个项目里最后用的那点东西代码量其实不大。发送端是一个 DMA 展开函数加一个定时器初始化接收端是一个输入捕获加 DMA 的环形缓冲再加一个状态机。加起来也就三四百行但每行都踩过坑。回头看最有价值的其实不是编码本身而是两个认识。第一个是把时钟信息嵌进数据在很多低成本场景里比堆硬件划算得多。这句话不只适用于曼彻斯特编码也适用于所有自同步的编码方案。第二个是参数计算一定要落到具体的时钟树上从系统时钟到 APB 分频到定时器倍频到 ARR每一步都要自己算一遍别抄别人的数字——因为别人的主频可能和你不一样抄过来就是错的。最后再分享一个我自己在用的调试小工具写一段最简单的接收代码不做任何 CRC 校验只把解出来的原始字节通过串口打印出来。配合发送端发固定的测试图案比如 0x55、0xAA、0x0F、0xF0 这几个一眼就能看出是哪一位、哪一段出了问题。这个土办法在定位极性反了边界错了丢了一位这三类问题上比任何逻辑分析仪都快。等这几个图案都对了再上正式协议和 CRC基本就是一次通过。