串口看起来是最简单的外设,接三根线就能通。它的故障也大多不是协议层的,而是电平、地线、时序这些物理层面的问题。排查时先确认物理层,再谈协议解析,顺序反了会浪费大量时间。
UART 帧结构
一帧数据的构成:
[起始位] [数据位 5~9] [校验位 可选] [停止位 1/1.5/2]
0 LSB 先发 奇/偶/无 1
空闲时 TX 保持高电平。起始位是一个位的低电平,接收方靠这个下降沿做位同步。数据位 LSB 先发。停止位回高电平,给接收方留出处理时间,也保证下一帧的起始位有下降沿可用。
两边必须一致的四个参数:波特率、数据位、校验位、停止位。115200 8N1 表示 115200 波特、8 数据位、无校验、1 停止位。
波特率误差容忍度取决于采样方式。接收端通常在每位中点采样,累积的位时序偏差只要不越过停止位的采样点就不会出错。粗略地说,总偏差在百分之几的量级内是安全的(10 位帧大约是几个百分点,具体余量取决于接收端的过采样设计)。
误差来源主要是两端的时钟分频不精确。MCU 的主频如果不能整除波特率,分频器只能取近似值,某些波特率在某些主频下误差会明显偏大。选主频和波特率组合时要注意这一点,必要时换一个能整除的主频或者使用分数分频器。
三种电平与共地
这是最常见的故障源。
| 电平 | 拓扑 | 距离量级 | |
|---|---|---|---|
| TTL | 0 V / 3.3 V 或 5 V,单端 | 点对点 | 板内、几十厘米 |
| RS232 | ±3~±15 V,负逻辑 | 点对点 | 十几米量级 |
| RS485 | 两线差分 ±1.5 V 以上 | 总线,可挂多节点 | 千米量级 |
TTL 是单端的,参考的是双方的地。两个设备必须共地,TX 接 RX、RX 接 TX、GND 接 GND。少了 GND 这根线,信号电平没有参考基准,通信要么完全不通,要么时好时坏随电源波动变化——这是最常见的”偶发错误”原因之一。
RS232 是负逻辑:-3~-15 V 表示逻辑 1,+3~+15 V 表示逻辑 0。直接把 RS232 接到 MCU 的 TTL 引脚上会打坏 IO,必须经过电平转换芯片。反过来也一样。
RS485 是差分,抗共模干扰能力强,适合长距离和工业现场。几个实践要点:
- 总线上要加终端匹配电阻(典型值 120 Ω),长线不加会有反射,表现为高速率下误码
- 收发是半双工的,需要方向控制。方向切换的时机是坑:发完最后一个字节立刻切回接收会丢掉最后一位,要等到发送完成标志(TC 而非 TXE)置位再切
- 总线空闲时要有偏置电阻把差分电平拉到一个确定状态,否则总线悬空时接收端会收到随机数据
- 多点通信要有明确的主从协议,避免两个节点同时抢占总线
电平不匹配是另一类常见问题:3.3 V 器件和 5 V 器件直接相连,5 V 一侧的 TX 输出可能超出 3.3 V 器件的 IO 耐受范围。查清楚两边 IO 是否容忍对方电平,不能确定就加分压或电平转换。
帧同步的三种思路
串口是字节流,没有消息边界。怎么从连续的字节里切出一帧,是所有串口协议的核心问题。
固定帧头 + 长度:0xAA 0x55 之类的同步字,后跟长度字段,按长度收完再校验。优点是可以承载任意二进制数据(包括 0x00 和帧头本身出现的字节),缺点是要处理”数据里恰好出现帧头字节”的假同步——靠长度和 CRC 来排除。
分隔符:以 rn 或特定字符结尾。AT 指令、NMEA、大多数文本协议都这么做。实现最简单,但负载里不能出现分隔符,只适合文本数据。
超时切帧:收到一个字节就重置定时器,超过 N 毫秒没新字节就认为一帧结束。适合不定长协议,也常作为前两种的兜底。超时值要大于字节间隔但小于帧间隔,波特率越低需要的超时越大。一个常用的估算是按传输一个字节所需时间的数倍来定。
实际工程里经常是三种组合:帧头定位 + 长度定界 + 超时兜底。
CRC 的位置与实现要点
CRC 应该覆盖从帧头到负载末尾的所有字节,通常不包含帧头和同步字本身(这取决于协议定义,但必须明确写清),结果放在帧尾。
/* CRC-16/CCITT-FALSE: poly 0x1021, init 0xFFFF */
uint16_t crc16(const uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= (uint16_t)data[i] << 8;
for (uint8_t b = 0; b < 8; b++) {
crc = (crc & 0x8000) ? (uint16_t)((crc << 1) ^ 0x1021) : (uint16_t)(crc << 1);
}
}
return crc;
}
三个容易出错的地方:
- 初值:不同 CRC 标准的初值不同(0xFFFF、0x0000),用错了算出来的校验值必然对不上
- 字节序:CRC 值在帧里是先高字节还是先低字节,协议要写死
- 计算范围:从哪个字节开始算到哪个字节结束,两端必须完全一致
逐位计算在高速率下会吃 CPU,通常预生成 256 项的表做查表法。表生成时用的多项式要和协议一致。
半包与粘包
一次接收中断里不一定能收到完整一帧(半包),也可能一次收到多帧(粘包)。正确处理方式是接收中断只管往环形缓冲里塞字节,解析在状态机里做:
#define RX_CAP 256
static uint8_t rx_buf[RX_CAP];
static volatile uint16_t rx_w, rx_r;
void USART_IRQHandler(void) {
if (USART->SR & RXNE) {
uint8_t b = USART->DR; /* 读 DR 清标志 */
uint16_t next = (rx_w + 1) % RX_CAP;
if (next != rx_r) { rx_buf[rx_w] = b; rx_w = next; }
else overflow_cnt++; /* 满了必须计数,不能静默丢弃 */
}
}
enum { S_SYNC1, S_SYNC2, S_LEN, S_BODY, S_CRC };
void parse_task(void) {
static uint8_t st = S_SYNC1;
static uint8_t frame[128];
static uint8_t idx, need;
while (rx_r != rx_w) {
uint8_t b = rx_buf[rx_r];
switch (st) {
case S_SYNC1: st = (b == 0xAA) ? S_SYNC2 : S_SYNC1; break;
case S_SYNC2: st = (b == 0x55) ? S_LEN : (b == 0xAA ? S_SYNC2 : S_SYNC1);
idx = 0; frame[idx++] = 0xAA; frame[idx++] = 0x55; break;
case S_LEN: need = b; frame[idx++] = b; st = S_BODY; break;
case S_BODY: frame[idx++] = b;
if (--need == 0) st = S_CRC; break;
case S_CRC: frame[idx++] = b;
if (idx >= 4) check_and_dispatch(frame, idx);
st = S_SYNC1; break;
}
rx_r = (rx_r + 1) % RX_CAP;
}
}
注意 S_SYNC2 状态下如果又收到 0xAA,要停在 S_SYNC2 而不是回到 S_SYNC1,否则 AA AA 55 这种序列会漏同步。这类细节正是状态机比”收到完整帧再解析”可靠的原因。
中断里不做解析、不做 printf,只做最小工作。解析放在主循环或任务里,处理慢也不会丢字节。
故障排查表
| 现象 | 优先怀疑 |
|---|---|
| 完全收不到 | TX/RX 是否交叉;波特率;引脚复用配置;供电 |
| 全是乱码 | 波特率不匹配;主频配置错误导致分频偏差;校验位/数据位设置 |
| 偶发错误 | 没共地;波特率误差偏大;长线无终端匹配;中断被关太久 |
| 丢字节 | 接收中断被高优先级中断阻塞;环形缓冲太小;中断里做了耗时操作 |
| 只能单向 | 一方 TX 未使能;方向控制切换时机错(RS485);引脚配置成了输入 |
| 首字节错 | 上电时序,对端先发而本端尚未初始化完成;波特率建立需要时间 |
| 高速率错、低速率正常 | 终端匹配电阻;线缆质量;波特率误差 |
| 加长线缆后失效 | 分布电容导致边沿变缓;共模干扰;改为差分传输 |
排查顺序建议:先用最简单的回环测试(TX 短接 RX)确认本端收发正常,再接对端;用固定字节(比如持续发 0x55)观察波形,比发复杂协议容易定位。
逻辑分析仪的使用时机
串口抓包工具只能在协议层看到字节,看不到物理层。下面这些情况应该上逻辑分析仪或示波器:
- 判断波特率是否准确:量一个位的宽度,反算实际波特率,和理论值对比
- 确认电平幅度是否达标:RS232 是否达到 ±3 V 以上,RS485 差分幅度是否足够
- 观察方向切换时序:RS485 的 DE 信号与数据流的时间关系,看是否在最后一个字节发完前就切了
- 检查帧间隔和字节间隔,用来定超时切帧的参数
- 抓偶发错误:设置触发条件长时间捕获,比靠日志猜有效得多
采样率至少要选波特率的十倍以上,否则量出来的位宽不准。捕获长度要覆盖若干个完整帧周期,太短看不到帧间关系。
协议层能解释的用协议工具,解释不了的物理层问题,波形会直接给出答案。
Comments (2)
这种把踩坑过程写出来的比只给结论强太多。
写得比我预期深,收藏慢慢啃。
Please Sign in to join the discussion.