4、数据帧结构设计:自定义帧头帧尾、长度域、数据域、校验域的设计原则
做UART通信,说白了就是在裸线上传字节。但字节本身没有边界,没有身份,也没有防错能力。你发一串 0x55 0xAA 0x01 0x02 0x03,接收方怎么知道这是数据还是噪声?
所以我们需要一个“信封”——数据帧结构。我做了十几年嵌入式通信,见过太多因为帧结构设计不合理导致的诡异问题。今天咱们就把这个信封怎么折、怎么封、怎么防拆,一次说清楚。
4.1 帧头帧尾:通信的“门神”
帧头的作用就一个:告诉接收方“注意,数据要来了”。帧尾的作用也一个:告诉接收方“数据发完了,开始校验吧”。
我个人习惯用 0x55 0xAA 做帧头。为什么?0x55 是 01010101,0xAA 是 10101010,这两个字节在示波器上能看到最清晰的方波跳变,方便调试。当然,你也可以用 0x7E 0x7E(HDLC风格)或者 0xFE 0xEF(对称模式)。
帧尾我常用 0x0D 0x0A(回车换行),方便在串口助手上直接看。但如果你传输的是二进制数据,建议用 0x7E 0x7E 或者干脆不用帧尾,只靠长度域和校验域来界定。
| 帧头方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 0x55 0xAA | 示波器易识别,调试友好 | 数据域中可能出现 | 通用场景,调试阶段 |
| 0x7E 0x7E | HDLC标准,转义机制成熟 | 需要字节填充处理 | 工业总线,高可靠性 |
| 0xFE 0xEF | 对称性好,误匹配率低 | 占用两个非打印字符 | 二进制协议 |
| 单字节(不推荐) | 节省开销 | 极易误匹配 | 极少使用 |
4.2 长度域:别让接收方猜你要发多少
长度域告诉接收方:数据域有多少个字节。没有长度域,接收方只能靠帧尾或超时来判断结束,这在高速通信中非常不可靠。
我建议长度域用 2字节,小端模式(低字节在前)。为什么?
- 1字节只能表示 0-255,对于大数据包不够用
- 2字节可以表示 0-65535,覆盖绝大多数UART应用
- 小端模式与大多数MCU(STM32、ESP32)的内存布局一致,省去转换
举个例子:
帧结构: [帧头2B] [长度2B] [数据域N B] [校验1B] [帧尾2B]
实际发送: 0x55 0xAA 0x05 0x00 0x01 0x02 0x03 0x04 0x05 0xCC 0x0D 0x0A
|--帧头--| |--长度--| |------数据域(5字节)------| |校验| |--帧尾--|
长度域的值是 0x0005(小端),表示后面有5个字节的数据。接收方读到长度域后,就知道还要再收5个字节才算完。
4.3 数据域:你的业务核心
数据域就是你要传的真正内容。可以是命令、传感器数据、文件片段,随便你。
设计数据域时,我建议遵循几个原则:
- 固定结构优于可变结构:如果可能,让数据域内部也有固定偏移。比如第0字节是命令ID,第1-2字节是参数1,第3-4字节是参数2。这样解析代码简单、稳定。
- 避免敏感字节:如果帧头是 0x55 0xAA,数据域里尽量不要出现连续的 0x55 0xAA。万一出现了,接收方会误认为是帧头。解决方案是“字节填充”或“转义”。
- 对齐考虑:对于32位MCU,数据域长度尽量对齐到4字节。虽然UART是字节流,但内部处理时对齐能提升效率。
4.4 校验域:最后的防线
UART本身有奇偶校验位,但那只能检测单比特错误。对于实际通信,尤其是工业环境,我们需要更强的校验。
常用的校验方式有几种:
| 校验方式 | 字节数 | 检错能力 | 计算开销 | 我的推荐 |
|---|---|---|---|---|
| 累加和(Sum) | 1 | 低(无法检测顺序错误) | 极低 | 不推荐 |
| 异或校验(XOR) | 1 | 中 | 极低 | 简单场景可用 |
| CRC-8 | 1 | 高 | 低 | 推荐 |
| CRC-16 | 2 | 极高 | 中 | 强烈推荐 |
| CRC-32 | 4 | 极高 | 较高 | 大数据包场景 |
我个人习惯用 CRC-16-CCITT(多项式 0x1021)。为什么?
- 2字节校验,开销适中
- 能检测所有单比特、双比特、奇数位错误
- 在STM32等MCU上有硬件CRC模块,几乎不占CPU
校验域的计算范围:从长度域开始,到数据域结束。帧头和帧尾不参与校验。为什么?因为帧头帧尾是固定的,校验它们没有意义。
4.5 完整帧结构示例
把上面所有元素组合起来,一个典型的UART数据帧长这样:
+--------+--------+--------+--------+--------+--------+--------+--------+
| 帧头1 | 帧头2 | 长度L | 长度H | 数据0 | 数据1 | ... | 数据N |
| 0x55 | 0xAA | 0x05 | 0x00 | 0x01 | 0x02 | ... | 0x05 |
+--------+--------+--------+--------+--------+--------+--------+--------+
| 校验L | 校验H | 帧尾1 | 帧尾2 |
| 0xXX | 0xYY | 0x0D | 0x0A |
+--------+--------+--------+--------+
总开销:帧头2B + 长度2B + 校验2B + 帧尾2B = 8字节。对于数据域N字节,有效载荷率 = N / (N+8)。N越大,效率越高。
4.6 帧结构设计决策树
为了帮你快速选择,我画了一张决策图:
4.7 避坑指南
最后,分享几个我亲身踩过的坑:
- 帧头误匹配:数据域中出现与帧头相同的字节序列。解决方案:字节填充(发送时在敏感字节前加转义符,接收时去掉)。
- 长度域错误:如果长度域被干扰,接收方会等N个字节,可能永远等不完。解决方案:加超时机制,超时后重置接收状态机。
- 校验域范围搞错:有人把帧头也纳入校验,结果帧头变了校验就失败。记住:校验范围从长度域开始,到数据域结束。
- 帧尾丢失:如果帧尾被噪声吃掉,接收方可能把下一帧的帧头当成数据。解决方案:帧尾也参与状态机判断,连续两次帧尾不匹配就丢弃整帧。
帧结构设计没有绝对的标准,但遵循这些原则,能让你少走很多弯路。记住:好的帧结构,接收方解析代码不超过50行;差的帧结构,调试能调到你怀疑人生。