7AA7 Blog

串口通信调试:协议解析与常见故障

Embedded 徐 UID:132
2026-09-09 21:57
48 2 0

串口看起来是最简单的外设,接三根线就能通。它的故障也大多不是协议层的,而是电平、地线、时序这些物理层面的问题。排查时先确认物理层,再谈协议解析,顺序反了会浪费大量时间。

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 信号与数据流的时间关系,看是否在最后一个字节发完前就切了
  • 检查帧间隔和字节间隔,用来定超时切帧的参数
  • 抓偶发错误:设置触发条件长时间捕获,比靠日志猜有效得多

采样率至少要选波特率的十倍以上,否则量出来的位宽不准。捕获长度要覆盖若干个完整帧周期,太短看不到帧间关系。

协议层能解释的用协议工具,解释不了的物理层问题,波形会直接给出答案。

2kViews
48Likes
2Comments
2Participants
12 minread time

Comments (2)

  • 硅屿 · 2026-09-10 00:09

    这种把踩坑过程写出来的比只给结论强太多。

    Reply
  • Vector 陈 · 2026-09-10 09:11

    写得比我预期深,收藏慢慢啃。

    Reply