4. 数据包结构:帧头帧尾设计、校验方式与数据长度对效率的影响
聊到串口烧录,很多人第一反应就是调波特率。波特率上去了,速度就快了,对吧?
其实不然。我见过不少项目,波特率设到 921600,结果烧录速度还是上不去。为什么?
问题往往出在数据包结构上。帧头、帧尾、校验方式、数据长度——这几个参数没搭好,再高的波特率也是白搭。
今天我们就把这几个东西掰开揉碎了讲。你理解透了,烧录效率至少能翻一倍。
4.1 帧头帧尾:通信的“门禁系统”
帧头和帧尾的作用很简单——告诉接收方:“我要发数据了”和“我发完了”。
但设计得好不好,直接影响稳定性和效率。
4.1.1 帧头设计原则
- 唯一性:帧头字节不能是数据中频繁出现的值。比如 0xAA、0x55 这种,数据里到处都是,容易误判。
- 长度:我个人习惯用 2 字节帧头。1 字节太容易冲突,3 字节以上又浪费带宽。
- 固定值:比如 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 字节)。
缺点:检测能力弱。比如两个字节同时出错,累加和可能不变。
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 + 重传机制 | 无线丢包率高,需要强校验 |
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 字节。传输快,可以适当放大帧,减少帧头帧尾的开销占比。
4.4 知识体系总览
下面这张图把数据包结构的核心要素串起来了。你看一眼,就能明白它们之间的关系。
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)) 不能少。否则编译器会在结构体里插入填充字节,导致接收方解析错位。我见过有人因为这个坑排查了三天。
好了,数据包结构这块就讲到这里。你理解了帧头帧尾、校验方式和数据长度的关系,烧录协议的骨架就算搭好了。下一层,我们聊聊怎么让这个骨架跑得更快、更稳。