4. 软件协议优化:分包策略、校验机制、流控制、超时重传机制

硬件调完了,波特率也拉上去了。结果呢?烧到一半卡死,或者校验失败,反复重试。这时候你可能会怀疑人生——是不是硬件不行?

其实不一定。很多时候,问题出在软件协议上。我见过不少团队,硬件方案一模一样,就因为协议写得糙,速率死活上不去。反过来,协议优化好了,同样的硬件,速率能翻倍。

这一章,咱们就聊聊软件协议优化的四个核心点:分包策略、校验机制、流控制、超时重传。说白了,就是怎么把数据包组织好、怎么保证它没丢没坏、怎么让收发双方步调一致、出错了怎么补救。

核心观点:协议优化的目标不是“不出错”,而是“出错后能快速恢复”。稳定性和速率,从来不是二选一。

4.1 分包策略:别让缓冲区撑死,也别让链路饿死

先说说分包。你想想看,一次发 1 个字节,效率肯定低;一次发 10KB,接收端的缓冲区可能直接炸了。所以,分包的核心就是找到那个“刚刚好”的尺寸。

我个人的习惯是:先看接收端的缓冲区大小。比如 MCU 的 UART FIFO 只有 64 字节,那你一包发 128 字节,它根本接不住。这时候就得拆。

分包策略一般分两种:

  • 固定长度分包:每包固定 N 字节。简单,好解析。缺点是如果数据本身不是 N 的整数倍,最后一包要填充,浪费带宽。
  • 变长分包:包头带长度字段,接收端根据长度收完一包。灵活,但解析逻辑复杂一点。

我在项目中遇到过一种情况:用固定长度分包,每包 256 字节,结果接收端 FIFO 只有 128 字节。每次收完半包就溢出,数据全乱。后来改成 64 字节一包,问题立刻解决。嗯,这里要注意:分包尺寸一定要小于接收端缓冲区的一半,留出余量给协议处理。

分包方式 优点 缺点 适用场景
固定长度 解析简单,无长度字段 最后一包需填充,浪费带宽 数据块大小固定,如固件镜像
变长 带宽利用率高,无填充 需解析长度字段,逻辑复杂 数据长度不固定,如日志流

小技巧:如果数据量很大(比如几百 KB 的固件),建议用固定长度分包。接收端可以预分配缓冲区,解析效率高。变长分包适合数据长度变化大的场景,比如 AT 指令响应。

4.2 校验机制:别信运气,信算法

串口通信,干扰是常态。尤其是工业现场,电机一启动,数据就可能错位。所以,校验机制是必须的。

常见的校验方式有几种:

  • 奇偶校验:硬件自带,开销小。但只能检测奇数位错误,而且不能纠错。说实话,我基本不用它做数据完整性校验,太弱了。
  • 累加和校验:把数据字节加起来,取低 8 位或 16 位。简单,但碰撞概率高。两个字节互换位置,累加和不变,检测不出来。
  • CRC 校验:循环冗余校验。这是工业标准,碰撞概率极低。CRC-16 或 CRC-32 都行,看你对可靠性的要求。

我曾经在一个项目中,用累加和校验,结果烧录 100 次有 3 次校验失败。换成 CRC-16 之后,跑了 10000 次,一次都没错。你想想看,这差距有多大。

我建议:只要不是极端资源受限(比如 8 位单片机,ROM 只剩几百字节),一律用 CRC。如果实在想省资源,至少用累加和 + 异或的双重校验,能降低碰撞概率。

避坑指南:我曾经见过有人把 CRC 算完直接放在包尾,结果接收端没算对。后来发现是字节序问题——发送端用大端,接收端用小端。CRC 算法本身不关心字节序,但你的实现必须统一。否则,校验永远过不了。

4.3 流控制:别让发送端跑得太快

流控制,说白了就是“你慢点,我跟不上”。串口通信中,接收端处理数据需要时间。如果发送端不管不顾地狂发,接收端的缓冲区迟早会满,然后丢数据。

流控制分两种:

  • 硬件流控制(RTS/CTS):用两根额外的信号线。接收端缓冲区快满时,拉低 CTS,发送端暂停。这是最可靠的方式,但需要硬件支持。
  • 软件流控制(XON/XOFF):用特殊字符。接收端发 XOFF(0x13)让发送端暂停,发 XON(0x11)让发送端继续。不需要额外引脚,但问题是,如果数据里恰好有 0x13 或 0x11,就冲突了。

我个人更倾向于硬件流控制。原因很简单:可靠。软件流控制虽然省引脚,但处理起来麻烦。你想想看,如果固件里正好有 0x13 这个字节,发送端收到后以为是 XOFF,直接停了,那不就乱套了?

当然,有些场景下硬件流控制不可用(比如只有 TX/RX 两根线)。这时候,我建议用数据包确认机制代替流控制:发送端发一包,等接收端确认后再发下一包。虽然效率低一点,但至少不会丢数据。

流控制方式 可靠性 硬件需求 适用场景
硬件流控制 高 需 RTS/CTS 引脚 高速、大数据量传输
软件流控制 中 仅需 TX/RX 低速、数据中无特殊字符
包确认机制 高 仅需 TX/RX 任何场景,但效率略低

4.4 超时重传机制:别死等,该放弃就放弃

通信不可能永远不出错。一旦出错,怎么办?死等?那系统就卡死了。所以,超时重传机制是必须的。

超时重传的核心是两个参数:超时时间和重传次数。

  • 超时时间:发送端发完一包后,等待确认的最长时间。这个时间怎么定?我一般用“最大数据包传输时间 × 3”。比如,一包 64 字节,波特率 115200,传输时间大约是 5.5ms。那超时时间就设 15-20ms。太短了容易误判,太长了浪费等待时间。
  • 重传次数:超时后重发几次?我建议 3 次。超过 3 次还没收到确认,基本可以判定链路断了,直接报错。别无限重传,否则系统会陷入死循环。

我曾经遇到过一个坑:超时时间设得太短,只有 5ms。结果接收端处理确认包稍微慢了一点,发送端就重传了。重传的包和原来的包混在一起,接收端彻底乱套。后来把超时时间调到 20ms,问题解决。嗯,这里要注意:超时时间一定要留够余量,考虑接收端的处理延迟和系统调度抖动。

经验公式:超时时间 = (数据包字节数 × 10 / 波特率) × 3 + 接收端处理时间。其中“×10”是因为串口每字节有 1 个起始位 + 8 个数据位 + 1 个停止位(无校验时)。

4.5 整体协议流程:把这些串起来

分包、校验、流控制、超时重传,这四个东西不是孤立的。它们需要组合成一个完整的协议流程。下面这张图,就是我常用的烧录协议框架:

烧录协议框架流程图 烧录协议框架流程图 发送端(主机) 接收端(设备) 1. 分包(固定64字节) 2. 添加CRC-16校验 3. 发送数据包 数据包(64字节 + CRC) 4. 接收并校验 校验 5. 校验通过,发ACK 6. 校验失败,发NAK ACK(确认) NAK(重传请求) 7. 收到ACK,发下一包 8. 超时/收到NAK,重传 重传数据包 说明: • 发送端每发一包,启动超时定时器。收到ACK后发下一包,收到NAK或超时则重传。 • 重传超过3次,判定链路故障,终止烧录并报错。

这个流程,说白了就是“发一包,等一包,确认了再发下一包”。虽然看起来简单,但实际项目中,很多人会忽略超时和重传的细节。比如,超时时间设得太短,导致频繁重传,速率反而下降。或者重传次数设得太多,链路断了还在傻等。

我建议:在开发初期,就把超时时间和重传次数做成可配置的参数。这样在调试时,可以根据实际链路质量动态调整。等稳定了,再固化下来。

实战经验:我在一个项目中,用这套协议把烧录速率从 20KB/s 提升到了 80KB/s。关键就是两点:一是分包尺寸从 256 字节降到 64 字节,减少了单包出错概率;二是超时时间从 50ms 降到 15ms,减少了等待时间。你想想看,同样的硬件,只是协议优化了一下,速率翻了 4 倍。

4.6 代码示例:一个简单的超时重传实现

光说不练假把式。下面是一个简化版的超时重传代码,用伪代码写的,方便你移植到自己的平台。

// 发送一包,带超时重传
int send_packet_with_retry(uint8_t *data, uint16_t len, uint8_t max_retry) {
    uint8_t retry_count = 0;
    uint8_t ack_received = 0;
    
    while (retry_count < max_retry) {
        // 发送数据包
        uart_send(data, len);
        
        // 启动超时定时器
        start_timer(TIMEOUT_MS);
        
        // 等待ACK或超时
        while (!is_timer_expired()) {
            if (uart_has_data()) {
                uint8_t response = uart_read();
                if (response == ACK) {
                    ack_received = 1;
                    break;
                } else if (response == NAK) {
                    // 收到NAK,立即重传
                    break;
                }
            }
        }
        
        if (ack_received) {
            return 0;  // 成功
        }
        
        retry_count++;
        // 重传前可以加一点随机延迟,避免冲突
        delay_ms(1 + (rand() % 5));
    }
    
    return -1;  // 失败,链路故障
}

这段代码里,有几个细节要注意:

  • 超时定时器:一定要用硬件定时器,别用软件延时。否则,等待期间 CPU 被占死,其他任务没法跑。
  • 随机延迟:重传前加一点随机延迟,可以避免多个设备同时重传导致冲突。这在多设备烧录时特别有用。
  • ACK/NAK 判断:收到 NAK 后,不要等超时,立即重传。这样可以节省时间。

避坑指南:我曾经在重传逻辑里忘了清空接收缓冲区。结果重传时,上次的 ACK 还在缓冲区里,程序误判为收到确认,直接发了下一包。数据全乱。所以,重传前一定要清空接收缓冲区。

4.7 小结

协议优化,说白了就是四个字:稳中求快。分包策略决定了链路利用率,校验机制决定了数据可靠性,流控制决定了收发节奏,超时重传决定了容错能力。这四个点,缺一不可。

你想想看,如果分包太大,缓冲区溢出;校验太弱,数据损坏;没有流控制,接收端被撑死;没有超时重传,一次出错就卡死。任何一个环节出问题,速率和稳定性都无从谈起。

所以,别急着调硬件。先把协议优化好,你会发现,很多问题其实根本不在硬件上。