尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

主从MCU串口通信偶发多余字节?排查思路与协议防护方案

主从MCU串口通信偶发多余字节?排查思路与协议防护方案 做嵌入式这些年最让我挠头的问题不是串口数据完全收不到而是“主从MCU之间的UART通信看起来正常但偶尔多出几个字节”。主控master发的是干净的一帧数据从机slave收到的却莫名其妙多出两三个字节多出来的往往是0x00或者0xFF。这类问题麻烦就麻烦在它不是每次都出现可能跑十分钟才撞上一次排查起来特别费劲。这篇文章就把我在主从MCU串口联调中踩过的坑、排查的思路和最终的防护方案梳理一遍。不管你是正在调两块开发板之间的UART还是遇到从机接收端偶发多余字节、乱码、错位这篇都能给你一条完整的排查路径。问题看着小背后牵扯的因素却不少——硬件电平、波特率误差、初始化时序、协议健壮性每一个都是潜在入口。1. 先搞清楚主从MCU之间多出来的字节是哪来的1.1 典型问题表现先说现象。假设主MCU每隔100ms发送一帧固定长度数据AA 55 01 02 03 04 05 FF按说从机收到的也应该是这8个字节。但实际用调试口把从机的接收数据打出来成了AA 55 01 02 03 04 05 FF 00有时候是帧头前面多了一个0x00有时候是帧中间插入0xFF还有更离谱的——从机收到的数据整体向后错了一位原本的AA 55变成了AA 00 55整条数据全乱。这个现象在不同板子上表现不太一样。我自己遇到过的几类情况主机发送完全正常从机接收端偶尔多字节。频率不固定有时连续几次错误有时半小时一次。调整波特率后问题消失或减轻。从机复位的瞬间收到一两个错误字节之后就正常。只有一台设备异常换一块板子就正常了。这些细节看似零碎但每一条都是定位问题的关键线索。如果你只盯着协议层去查大概率会走很多弯路。1.2 为什么这种问题容易被误判很多初学者遇到多余字节第一反应是“数据格式不匹配”然后去检查帧格式、校验位甚至怀疑从机代码里的缓冲数组是不是越界了。但不是说不该查这些而是有个效率问题UART是物理层的异步串行通信任何多余字节本质上都是接收端采样到了额外的起始位下降沿。UART的一帧数据以起始位低电平开始以停止位高电平结束。接收端只有在空闲状态下检测到高→低的跳变才会认为一帧数据开始了。所以“多出来一个字节”意味着什么意味着接收端的RX引脚上确实出现了一个额外的下降沿这个下降沿把UART外设“骗”了让它以为新的一帧开始了。至于这个下降沿是硬件带来的还是软件带来的要分层排查。这一点是整篇文章的核心理解了它后面所有排查动作都有了解释。1.3 从现象初步锁定排查方向我的习惯是先把问题分为两类从机错收字节但主机发送的波形完全正常问题大概率出在硬件链路或从机的初始化时序上。主机发送的波形本身就不干净那就是主机端的发送引脚配置、复位状态或者电源干扰问题。判断方法很简单——用示波器或者逻辑分析仪抓主机TX引脚的波形。如果波形干净一个个下降沿清晰明确那主机端的发送逻辑就没问题问题在别处。如果波形上有毛刺、抖动、额外的低电平脉冲那就要先从主机端找原因。这个分类能帮你省掉一大半无用功。下面就从硬件层开始说起。2. 硬件层面最先要排除的串口干扰源2.1 共地问题两套独立电源之间的地电位差这是我在实际项目里踩过最多的一类坑尤其当主MCU和从MCU分别用两路电源供电时比如主机用USB供电从机用独立适配器或电池供电。这种情况下如果两块板子之间没有共地UART的RX、TX参考电平就不一致结果就是各种稀奇古怪的乱码和多字节。为什么UART信号是单端信号接收端判断“高电平”和“低电平”的依据是信号相对自己GND的电平差。假设两个地之间存在1V的电位差发送端输出的高电平是3.3V到接收端实际测到的可能是2.3V或者4.3V。一旦跨过或接近接收端的阈值电压接收端就可能出现误判产生额外的下降沿。排查方式很简单用万用表直流电压档测主机GND和从机GND之间的电压正常应该是接近0V。如果测出来有个一两伏甚至更高的电压差问题基本就锁定了。解决办法更简单——把两块板子的GND连到一起。这里有个细节容易被忽略用USB转串口模块插在电脑上做调试时如果USB转串口模块和从机供电不是同一个源也要确保它们共地。我曾经因为一个USB转TTL模块自带隔离导致信号电平来回跳排查了一整天才反应过来。2.2 电平不匹配3.3V与5V的边界情况主从MCU的逻辑电平不一致也会引发多余字节。比如主机是3.3V的STM32从机是5V的STC89C52。有些人觉得“TTL电平都兼容只要高电平超过2V就能识别”这话不能算错但也别太当真。3.3V输出的高电平在干净、短距离的线缆上确实能被5V系统识别但如果线缆长了、线间电容大了、或者中途受到干扰3.3V高电平在传输线上的压降会让接收端的高电平接近甚至低于阈值。一旦低于阈值或者出现振铃就可能误判出额外的下降沿。所以我的建议是如果两边电平不同不要裸连。用MAX232、SP3232这类电平转换芯片或者用MOS管搭一个简单的电平转换电路。同一套系统里最好统一逻辑电平这是避免很多奇怪串口问题的最省事做法。2.3 线缆和PCB走线对信号边沿的影响UART的波特率越高一个位的持续时间越短对信号边沿质量的要求就越高。9600bps时一位大约104us115200bps时约8.68us。如果线缆过长、弯曲、使用杜邦线信号边沿会被拉缓甚至出现反射中间夹着振铃和过冲。这种情况下接收端在采样时可能正好采到振铃位置误判出额外的位。你可以在示波器上看到TX波形带毛刺多出来的下降沿就是从这里来的。实际项目中我碰到过一次主从两块板用20cm杜邦线连接9600波特率完全正常一调到115200就开始偶发多字节。后来把杜邦线换成了双绞线并把信号线靠近地线走问题立刻消失。如果是PCB设计主从MCU之间的UART走线要注意尽量短、尽量直、避免平行长距离走线、必要时在信号线旁边铺地。如果走线和电机驱动、继电器这类大电流线路靠得太近串进来的干扰会更明显。2.4 硬件快速排查清单为了方便对照我整理了一张表适合到现场后按顺序走一遍。现象特征可能原因检查方法处理方式固定间隔多字节主从地电位差万用表测两边GND电压共地波特率调高后更明显线缆过长/反射/振铃示波器看波形边沿和振铃换线、降波特率、加终端电阻复位瞬间多一个字节TX引脚复位状态不确定示波器抓上电瞬间TX波形初始化前先把TX拉高只有特定板卡有问题晶振偏差/钎焊问题测设备晶振频率换晶振或检查焊接USB转串口接入后出错USB转串口模块供电与目标板电位差测模块GND和目标板GND统一共地后再接硬件层检查做完如果问题还在那就要往软件配置层面看。3. 软件配置波特率误差、帧格式与初始化时序3.1 波特率误差的计算与容限UART通信对波特率误差的要求是双方的实际波特率误差累计不能超过接收端允许的最大偏差。UART接收端在每个位的中点采样如果发送方和接收方的波特率累计误差太大采样点就会逐渐偏离位中心最终采到错误的值。经典51单片机用11.0592MHz晶振产生9600波特率误差可以做到0因为9600是11.0592MHz经过特定分频能整除的值。但换成12MHz晶振9600波特率的误差就出来了12MHz下T1定时器模式2自动重装波特率 12000000 / (384 × (256 - TH1))取TH1 0xFD 253时实际波特率 12000000 / (384 × 3) 10416.67误差约8.5%。这种误差下偶尔丢掉起始位、多采样出额外字节都是正常的。STM32这类MCU也有类似问题。以STM32F103为例APB1时钟是36MHzUSART2挂在APB1上。计算USARTDIV 36000000 / (16 × 9600) 234.375写入BRR时要分别取整数部分和分数部分。有些时钟配置下USARTDIV的小数部分无法被精确表示就会产生少量误差。只要不超过2%~3%问题不大但如果你用的是内部RC振荡器而没做校准误差可能直接飙到5%以上。所以排查多余字节时先动笔算一算主从双方的波特率误差到底有多大。算完你就知道问题是不是出在这里了。3.2 帧格式不匹配的隐蔽表现8N1是默认配置但有不少设备默认是8E1或8O1。如果主机配置了偶校验从机配置了无校验那么从机在接收时会把校验位当作数据位的一部分。看起来像是每个字节后面多了一个bit造成整个数据流错位表现和“多字节”很像。帧格式不匹配的典型表现是接收的字节偶尔少一个或者多一个数据内容明显错位。如果主从双方有一端开了9位数据模式另一端是8位数据模式情况会更严重几乎每一帧都会错。查这个问题的办法很简单——确认主从MCU的UART寄存器配置数据位、停止位、校验位必须完全一致。不要依赖“默认配置”直接把双方配置打出来看一眼。3.3 初始化顺序上电瞬间的第一字节往往是最脏的这是个非常隐蔽的问题也是我文章开头那个案例里真正的根源。MCU上电后GPIO一般处于高阻输入状态。如果主机的TX引脚在UART外设初始化之前被外部电路拉低或者处于浮空状态那么从机在主机尚未启动完的这段时间里可能检测到一个下降沿从而错误地接收一个字节。更麻烦的是如果主机代码里先配置GPIO为推挽输出且此时GPIO输出寄存器默认值是0TX引脚就会在初始化UART外设之前的极短时间内被拉低。这个低电平持续时间可能只有几微秒但对于已经完成初始化的从机来说这已经足够被识别为一个起始位了。解决方式有几种初始化时先把TX引脚配置为推挽输出并手动拉高再切换到复用功能。在UART初始化完成的第一个字节发送前加一个小的延时。从机端延迟启动接收比如上电后延时100ms再初始化UART。硬件上在TX引脚加一个10kΩ上拉电阻到VCC保证空闲状态为高。这里的要点是先拉高再复用。很多代码库里的UART初始化函数根本没有处理GPIO复用前的电平状态这是上电瞬间多字节的高发来源。3.4 接收中断与FIFO溢出从机端接收逻辑如果不健壮也会自己“制造”多余字节。最常见的就是接收中断处理时间过长导致UART外设的接收寄存器溢出。以STM32为例UART数据寄存器只有1字节深无FIFO时如果上一字节还没取走下一字节到了会产生溢出错误ORE。ORE置位后后续数据不再写入数据寄存器读到的可能是旧数据或者0x00。如果你在中断里做大量运算导致响应不及时溢出就会频繁发生接收到的数据自然就多了乱七八糟的字节。处理方案很直接中断里只做数据拷贝把数据处理移到主循环。开FIFO如果MCU支持。用DMA接收减少CPU参与。在中断里检查ORE标志并主动清除。很多人会忽略检查ORE导致错误累积。所以排查时也要看一眼接收中断里有没有清除溢出标志。4. 一次真实排查复盘STM32主控与STC89C52从机的多余字节4.1 现场信息和初始判断去年我调过一块板子主控是STM32F103C8T672MHz主频PA9/PA10做UART1从机是STC89C52RC11.0592MHz晶振串口1接收。协议设定为9600bps8N1主机每隔100ms发8字节定长帧。问题表现从机把收到的数据用串口转发到上位机经常在帧头前多出一个0x00偶尔在帧尾后面多出0xFF。用上位机统计错误率大概在1%左右不算高但很影响协议稳定性。当时我们做了几个初步判断主机发送的数据在逻辑分析仪上完全正常TX波形干净没有多余下降沿。换过从机板卡问题依旧。波特率降到4800后短时间内没再复现。从机复位后第一次发的数据必然是错的。看起来像个疑难杂症但其实已经有一条清晰线索了复位后的第一次收发必然出错这非常像初始化时序问题。4.2 用示波器和逻辑分析仪一层层排除第一步我用示波器抓了从机RX引脚在上电期间的波形。结果发现在主机代码执行到UART初始化之前PA9引脚电平不是稳定的高而是低——时长为几十微秒。对从机来说这正是明显的起始位于是多收了一个0x00。第二步检查主机端代码。UART初始化函数里先开启了GPIO外设时钟然后配置PA9为复用推挽输出再配置USART。问题就在配置PA9为复用推挽之前PA9默认是浮空输入电平由外部电路决定——恰好板子上PA9走线附近有一个下拉电阻把电平拉低了。第三步验证。我在UART初始化前手动把PA9配置为推挽输出并输出高电平再初始化USART。同时把外部下拉电阻换成上拉电阻。改完再跑从机复位后的第一个错误字节消失了整晚测试无一例多余字节。4.3 真正的根因主机TX引脚在上电瞬间的不确定电平复盘一下真正的根因是主机TX引脚在上电到UART外设初始化完成这段时间内电平被外部下拉拉低从而在从机端制造了一个“假起始位”。这类问题最容易发生在用杜邦线直连的实验中因为杜邦线容易被干扰也容易被周围元件影响。但为什么降波特率到4800后现象消失了因为9600时一个起始位持续时间约104us从机采样只要在下降沿后约52us读到低电平就能确认起始位。而4800时一个起始位持续约208us从机的采样点更靠后。如果主机的TX引脚被拉低时间比较短几十微秒在4800波特率下从机采样时电平可能已经恢复高所以没触发接收。这解释只能说明“降波特率掩盖了问题”并不能解决问题。这类问题的本质是主机端TX引脚在初始化完成前的电平状态处于不可控区间。这个区间很短但UART的起始位同样很短两者在时间尺度上撞上了。4.4 修复方案和验证结果我的修复分了三步第一步在GPIO初始化阶段先把PA9和PA10都配置成推挽输出PA9输出高电平PA10输出低电平此时还没复用成UART功能。第二步在USART初始化完成后再把PA9、PA10切换到复用功能。第三步在从机端加了一个保护上电后延迟50ms再初始化UART。这样即使主机端还有残余的不确定电平从机也没有进入接收状态。改完后用逻辑分析仪连续抓了两个小时全部帧头正确没有多余字节。这个案例后来我也用在其他项目里只要遇到“复位后第一个字节错误”这类现象我都会先检查TX引脚的上电电平。5. 让UART通信对杂散字节免疫从机接收协议与状态机5.1 为什么光靠“重发”治标不治本排查解决之后我强烈建议还是在协议层加防护。因为UART属于异步通信谁都无法保证线路永远不出现一个毛刺、一个干扰脉冲。就算把所有已知因素都排除电磁环境一变新的干扰可能又冒出来。不少人的做法是“收到错误帧就重传”但重传只能解决“帧内容不对”的问题解决不了“多了一个字节导致帧边界错乱”的问题。一旦多出来的字节出现在帧头位置接收端可能把“假帧头”当成真帧头导致整个帧错位。所以协议层要做的是让接收端能够快速识别并丢弃杂散字节而不是把一个脏字节当成有效数据拼命解析。5.2 一个实用的串口接收状态机下面这段代码是我在多个项目里验证过的一种简单可靠的接收方式适合从机端使用。它的核心思路是用状态机逐字节识别帧头、帧长、数据和校验任何不符合预期的字节都会让状态机回到空闲态从而自动丢弃杂散字节。#define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 #define FRAME_MAXLEN 64 typedef enum { RX_IDLE, RX_HEAD1, RX_HEAD2, RX_LEN, RX_DATA, RX_CHECK } RxState; static RxState rx_state RX_IDLE; static uint8_t rx_buf[FRAME_MAXLEN]; static uint8_t rx_len; static uint8_t rx_index; static uint8_t rx_check; void UART_RxHandler(uint8_t byte) { switch (rx_state) { case RX_IDLE: if (byte FRAME_HEAD1) { rx_state RX_HEAD1; } /* 其他字节直接丢弃状态机保持空闲 */ break; case RX_HEAD1: if (byte FRAME_HEAD2) { rx_state RX_LEN; } else if (byte FRAME_HEAD1) { /* 连续出现帧头1说明是重复帧头保持等待 */ rx_state RX_HEAD1; } else { rx_state RX_IDLE; } break; case RX_LEN: rx_len byte; if (rx_len FRAME_MAXLEN) { /* 长度非法直接丢弃整帧 */ rx_state RX_IDLE; } else { rx_index 0; rx_check byte; rx_state RX_DATA; } break; case RX_DATA: rx_buf[rx_index] byte; rx_check ^ byte; /* 异或校验简单且有基本检错能力 */ if (rx_index rx_len) { rx_state RX_CHECK; } break; case RX_CHECK: if (rx_check byte) { /* 校验通过通知应用层 */ ProcessFrame(rx_buf, rx_len); } /* 无论校验是否通过都要回到空闲态 */ rx_state RX_IDLE; break; default: rx_state RX_IDLE; break; } }这段代码的好处是即使线路中混入一个0xAA 0x55也会被后续的长度或校验环节拦下来不会直接把后面数据当成有效载荷。如果多出来的字节恰好不在帧头位置比如在帧尾部那么校验会失败整帧丢弃也不会误触发下一帧。5.3 校验和与超时判定状态机里我用的是异或校验因为简单、开销小。如果想要更强的保护可以把异或换成累加和或CRC8。对于大多数主从MCU之间的控制指令累加和的长度比异或多1字节但检错能力更强。如果涉及关键控制比如电机启停、参数写入建议用CRC8甚至CRC16。另一个容易漏掉的是超时判定。如果从机收到了帧头但没有等到帧尾状态机会停在RX_DATA或者RX_CHECK状态后面的正常帧头就进不来了。解决办法是加一个帧间超时判断从接收到第一个字节开始计时超过比如10ms还没有收到完整帧就把状态机强制复位到RX_IDLE。用STM32的HAL库时可以在HAL_UART_RxCpltCallback里调用UART_RxHandler同时开一个定时器中断做超时复位。定时器周期建议选在“一个帧最大长度所需时间”的2~5倍防止误杀正常慢速通信。5.4 进阶环形缓冲区 DMA接收如果从机接收的数据量大或者需要把CPU从频繁的中断中解放出来可以考虑用DMA加环形缓冲区的方式。思路是UART的DMA接收模式把数据连续写入内存缓冲区DMA的传输完成中断更新写指针。主循环里用读指针消费数据。这种方式天然适合大数据流而且因为DMA是硬件搬运不容易出现“中断处理不及时导致丢字节”的情况。环形缓冲区配合状态机使用逻辑上会更清晰DMA负责把字节放进缓冲区主循环从缓冲区逐个取出字节喂给状态机。既保证了接收不丢也保证了协议解析的独立性。5.5 最后的经验之谈最后再分享一个调试技巧遇到主从MCU之间的UART杂散字节问题时我习惯先抓波形再算波特率然后查引脚初始化顺序最后才动协议层。这个顺序很重要。很多人一上来就改协议、加校验结果问题依旧因为本质是物理层或初始化时序的问题。协议层防护的价值是“兜底”不是“根治”。硬件和软件配置层面的问题不解决协议层再健壮也只能让杂散字节不至于破坏业务逻辑但通信效率会被反复丢弃、重传拖垮。另外示波器和逻辑分析仪在串口调试中几乎是必备工具。不用太贵能看波形、能解码UART就够用了。现在很多几十块钱的逻辑分析仪都支持常见的UART协议解码对你判断“多出来的下降沿出现在哪个位置”会非常有帮助。如果你也在调主从MCU的UART通信刚好遇到这种“多字节”的怪问题不妨从这篇文章提到的几个层面逐项排查一遍。别急着怀疑代码逻辑有问题很多时候答案就在那一根线上。
返回列表