4. 数据包结构:帧头帧尾设计、校验方式与数据长度对效率的影响

聊到串口烧录,很多人第一反应就是调波特率。波特率上去了,速度就快了,对吧?
其实不然。我见过不少项目,波特率设到 921600,结果烧录速度还是上不去。为什么?
问题往往出在数据包结构上。帧头、帧尾、校验方式、数据长度——这几个参数没搭好,再高的波特率也是白搭。

今天我们就把这几个东西掰开揉碎了讲。你理解透了,烧录效率至少能翻一倍。

4.1 帧头帧尾:通信的“门禁系统”

帧头和帧尾的作用很简单——告诉接收方:“我要发数据了”和“我发完了”。
但设计得好不好,直接影响稳定性和效率。

4.1.1 帧头设计原则

  • 唯一性:帧头字节不能是数据中频繁出现的值。比如 0xAA、0x55 这种,数据里到处都是,容易误判。
  • 长度:我个人习惯用 2 字节帧头。1 字节太容易冲突,3 字节以上又浪费带宽。
  • 固定值:比如 0xEB 0x90,这种不常见的组合,误触发概率极低。
我的经验:曾经有个项目,帧头用了 0xAA 0x55,结果数据里频繁出现 0x55,导致接收端反复误同步。后来改成 0xEB 0x90,问题立刻解决。选帧头时,先看看你的固件镜像里哪些字节出现频率最低。

4.1.2 帧尾设计原则

帧尾通常用来标记一帧结束。但它的作用不止于此——很多校验信息也放在帧尾。
常见的做法有两种:

  • 纯帧尾:比如 0x0D 0x0A,只表示结束。
  • 帧尾+校验:帧尾前紧跟着 CRC 或 Checksum 字节,接收方收到后先校验,再确认帧尾。

我个人更推荐第二种。为什么呢?因为校验放在帧尾,接收方可以边收边算,收完最后一个字节直接比对,效率最高。

4.2 校验方式:CRC vs Checksum

校验是保证数据完整性的最后一道防线。选错了,要么浪费带宽,要么漏掉错误。

4.2.1 Checksum(累加和校验)

Checksum 是最简单的校验方式。把所有数据字节加起来,取低 8 位或低 16 位作为校验值。

// 8位累加和示例
uint8_t calc_checksum(uint8_t *data, uint16_t len) {
    uint8_t sum = 0;
    for (uint16_t i = 0; i < len; i++) {
        sum += data[i];
    }
    return sum;
}

优点:计算快,代码简单,占用带宽小(1~2 字节)。
缺点:检测能力弱。比如两个字节同时出错,累加和可能不变。

注意:Checksum 只适合对可靠性要求不高的场景。我在一个消费类产品里用过,结果偶尔出现烧录校验通过但固件跑起来死机的情况——后来发现是 Checksum 没检测到双比特翻转。

4.2.2 CRC(循环冗余校验)

CRC 是工业界的主流选择。它用多项式除法生成校验值,检测能力远强于 Checksum。

// CRC-16 计算示例(多项式 0x8005)
uint16_t calc_crc16(uint8_t *data, uint16_t len) {
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}

优点:检测能力强,能检测出几乎所有常见错误。
缺点:计算量稍大,占用带宽多(2~4 字节)。

4.2.3 怎么选?

场景 推荐校验方式 原因
高速烧录(1Mbps 以上) CRC-16 或 CRC-32 检测能力强,误码率低
低端 MCU(资源受限) Checksum 或 CRC-8 计算量小,节省 Flash
噪声环境(电机、变频器附近) CRC-32 抗干扰能力最强
OTA 升级(无线传输) CRC-32 + 重传机制 无线丢包率高,需要强校验
核心建议:如果你拿不准,直接用 CRC-16。它比 Checksum 可靠,比 CRC-32 省带宽,是性价比最高的选择。我大部分项目都用它,没出过问题。

4.3 数据长度:效率的“双刃剑”

数据长度,就是一帧里实际携带的有效数据字节数。它直接影响烧录效率。

4.3.1 长度太小

如果每帧只带 16 字节数据,帧头帧尾校验加起来可能也有 6~8 字节。有效载荷占比不到 70%,剩下的全是开销。
说白了,大部分时间都在传“管理信息”,真正有用的数据没传多少。

4.3.2 长度太大

反过来,如果一帧带 1024 字节,出错的概率也大了。一旦校验失败,整个帧都要重传。重传 1024 字节的成本,可比重传 64 字节高多了。

4.3.3 怎么找到最佳长度?

我总结了一个经验公式:

  • 低波特率(9600~115200):每帧 64~128 字节。因为传输慢,重传成本相对低,小帧更安全。
  • 中波特率(230400~460800):每帧 128~256 字节。平衡了效率和可靠性。
  • 高波特率(921600 以上):每帧 256~512 字节。传输快,可以适当放大帧,减少帧头帧尾的开销占比。
避坑指南:我曾经在一个项目里把数据长度设到 1024 字节,波特率 2Mbps。结果在强干扰环境下,重传率高达 30%,烧录速度反而比 256 字节还慢。后来改成 512 字节,重传率降到 5%,速度翻倍。记住:不是越大越快。

4.4 知识体系总览

下面这张图把数据包结构的核心要素串起来了。你看一眼,就能明白它们之间的关系。

数据包结构核心要素 数据包结构 帧头帧尾设计 校验方式 数据长度 唯一性 长度选择 Checksum CRC 小帧 大帧 设计要点总结 • 帧头帧尾:2字节帧头 + 1~2字节帧尾,避免与数据冲突 • 校验方式:CRC-16 是性价比最优选择 • 数据长度:根据波特率动态调整,64~512字节之间

4.5 实战建议:一个完整的数据包结构

说了这么多,给一个我常用的模板吧。你直接拿去用,基本不会踩坑。

// 数据包结构定义
typedef struct {
    uint8_t  head1;        // 帧头1: 0xEB
    uint8_t  head2;        // 帧头2: 0x90
    uint16_t length;       // 数据长度(2字节,小端)
    uint8_t  data[256];    // 有效数据(最大256字节)
    uint16_t crc16;        // CRC-16校验值(2字节)
    uint8_t  tail;         // 帧尾: 0x0A
} __attribute__((packed)) FramePacket;

这个结构的特点:

  • 帧头 2 字节,冲突概率极低
  • 数据长度最大 256 字节,适合大多数波特率
  • CRC-16 校验,可靠性够用
  • 帧尾 1 字节,节省带宽
注意:上面代码里的 __attribute__((packed)) 不能少。否则编译器会在结构体里插入填充字节,导致接收方解析错位。我见过有人因为这个坑排查了三天。

好了,数据包结构这块就讲到这里。你理解了帧头帧尾、校验方式和数据长度的关系,烧录协议的骨架就算搭好了。下一层,我们聊聊怎么让这个骨架跑得更快、更稳。