4、数据帧结构设计:自定义帧头帧尾、长度域、数据域、校验域的设计原则

做UART通信,说白了就是在裸线上传字节。但字节本身没有边界,没有身份,也没有防错能力。你发一串 0x55 0xAA 0x01 0x02 0x03,接收方怎么知道这是数据还是噪声?

所以我们需要一个“信封”——数据帧结构。我做了十几年嵌入式通信,见过太多因为帧结构设计不合理导致的诡异问题。今天咱们就把这个信封怎么折、怎么封、怎么防拆,一次说清楚。

4.1 帧头帧尾:通信的“门神”

帧头的作用就一个:告诉接收方“注意,数据要来了”。帧尾的作用也一个:告诉接收方“数据发完了,开始校验吧”。

我个人习惯用 0x55 0xAA 做帧头。为什么?0x55 是 01010101,0xAA 是 10101010,这两个字节在示波器上能看到最清晰的方波跳变,方便调试。当然,你也可以用 0x7E 0x7E(HDLC风格)或者 0xFE 0xEF(对称模式)。

我的经验:帧头不要用单字节。单字节太容易在数据域中被误匹配。至少两个字节,最好三个。我在一个项目中用过单字节 0x7E,结果数据域里连续出现 0x7E 时,接收方直接崩了。

帧尾我常用 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是字节流,但内部处理时对齐能提升效率。
我曾经踩过的坑:在一个物联网网关项目中,数据域里传了GPS坐标,坐标值恰好是 0x55 0xAA 开头。接收方每次收到这个坐标就误判为帧头,导致后续数据全部错位。后来加了字节填充才解决。

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

校验域的计算范围:从长度域开始,到数据域结束。帧头和帧尾不参与校验。为什么?因为帧头帧尾是固定的,校验它们没有意义。

小技巧:如果你用CRC-16,记得把校验结果也发到总线上。接收方计算收到的数据(长度域+数据域)的CRC,然后与收到的校验值比较。一致则通过,不一致则丢弃整帧。

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 帧结构设计决策树

为了帮你快速选择,我画了一张决策图:

帧结构设计决策树 开始设计帧结构 帧头:双字节 帧头:三字节 长度域:2字节 长度域:2字节 校验:CRC-16 校验:CRC-16 帧尾:0x0D 0x0A 帧尾:0x7E 0x7E 通用帧:8字节开销 适合大多数MCU场景 高可靠帧:9字节开销 适合工业/噪声环境 提示:数据域中若可能出现帧头字节,需增加字节填充(转义)机制

4.7 避坑指南

最后,分享几个我亲身踩过的坑:

  • 帧头误匹配:数据域中出现与帧头相同的字节序列。解决方案:字节填充(发送时在敏感字节前加转义符,接收时去掉)。
  • 长度域错误:如果长度域被干扰,接收方会等N个字节,可能永远等不完。解决方案:加超时机制,超时后重置接收状态机。
  • 校验域范围搞错:有人把帧头也纳入校验,结果帧头变了校验就失败。记住:校验范围从长度域开始,到数据域结束。
  • 帧尾丢失:如果帧尾被噪声吃掉,接收方可能把下一帧的帧头当成数据。解决方案:帧尾也参与状态机判断,连续两次帧尾不匹配就丢弃整帧。
我曾经遇到最头疼的问题:一个设备在强电磁干扰环境下,帧头 0x55 0xAA 被干扰成 0x55 0x55,接收方死活不认。后来我在帧头后面加了一个固定字节 0x00 作为“帧头校验”,才彻底解决。

帧结构设计没有绝对的标准,但遵循这些原则,能让你少走很多弯路。记住:好的帧结构,接收方解析代码不超过50行;差的帧结构,调试能调到你怀疑人生。