3. 数据帧结构:帧头、帧尾、数据长度、校验和、命令字、数据域的定义与设计

好,咱们进入正题。数据帧结构这东西,说白了就是通信双方约定好的「说话格式」。你想想看,如果上位机和按摩仪各说各的,那不乱套了?我刚开始做嵌入式通信那会儿,就吃过这个亏——自己定义的帧结构太随意,结果设备死活不认,查了两天才发现是帧头跟数据内容撞车了。

一个完整的数据帧,通常包含这么几个部分:帧头、数据长度、命令字、数据域、校验和、帧尾。咱们一个一个拆开讲。

3.1 帧头(Frame Header)

帧头的作用就一个:告诉接收方「注意,我要开始说话了」。它通常是一个固定的字节序列,比如 0xAA 0x55 或者 0x5A 0xA5

我个人习惯用两个字节做帧头。为什么是两个?一个字节太容易跟数据内容混淆,三个以上又浪费带宽。两个刚刚好。

关键点:帧头必须选择数据域中几乎不会出现的值。比如你的数据域里经常出现 0xAA,那就别用它做帧头。

我在项目中遇到过一个问题:有个同事用 0x7E 做帧头,结果数据域里恰好有 0x7E,每次通信都丢包。后来改成 0xAA 0x55 组合,问题就解决了。

3.2 帧尾(Frame Tail)

帧尾是帧的结束标志。常见的有 0x0D 0x0A(回车换行)或者 0x7E。它的作用是告诉接收方「我说完了,你可以处理了」。

嗯,这里要注意:帧尾不要跟帧头重复。我见过一个产品,帧头是 0xAA,帧尾也是 0xAA,结果接收方经常把帧尾当成帧头,解析全乱套了。

3.3 数据长度(Data Length)

数据长度字段,用来告诉接收方「我这帧里有多少有效数据」。它通常占 1 个或 2 个字节。

举个例子:如果数据域有 10 个字节,那数据长度字段就填 10(或者 0x0A)。

数据长度字节数 最大表示范围 适用场景
1 字节 0 ~ 255 小数据量,如按摩仪控制指令
2 字节 0 ~ 65535 大数据量,如固件升级

我个人建议:按摩仪这种小数据量的场景,用 1 字节就够了。你想想看,一次通信能发 255 个字节,完全够用。

3.4 命令字(Command Word)

命令字,就是告诉设备「你要干什么」。比如 0x01 表示启动按摩,0x02 表示停止,0x03 表示设置力度。

命令字的设计有个原则:清晰、无歧义。我习惯把命令字分成几类:

  • 查询类:0x10 ~ 0x1F,比如查询设备状态
  • 控制类:0x20 ~ 0x2F,比如启动、停止、设置参数
  • 配置类:0x30 ~ 0x3F,比如修改设备名称、设置默认模式
  • 升级类:0x40 ~ 0x4F,比如固件升级相关
小技巧:命令字最好留一些保留位,方便以后扩展。我曾经因为没留保留位,后来加新功能时不得不重新定义协议,那叫一个痛苦。

3.5 数据域(Data Field)

数据域,就是实际要传输的数据。它可以是任何内容:力度值、温度值、模式编号、时间参数等等。

数据域的格式,我建议用 TLV 格式(Type-Length-Value)。什么意思呢?就是每个数据项都包含:

  • 类型(Type):1 字节,表示这是什么数据
  • 长度(Length):1 字节,表示这个数据的字节数
  • 值(Value):N 字节,实际数据

举个例子,设置按摩力度为 5:

类型:0x01(力度参数)
长度:0x01(1 个字节)
值:  0x05(力度值 5)

TLV 格式的好处是灵活。你加新参数时,不需要改协议结构,直接加一个新的 TLV 项就行。我在做按摩仪项目时,一开始没考虑扩展性,后来客户要加「加热温度」参数,我改协议改到凌晨三点。后来学乖了,全部改成 TLV。

3.6 校验和(Checksum)

校验和,用来检查数据在传输过程中有没有出错。常见的校验方式有:

  • 累加和:把所有字节加起来,取低 8 位或低 16 位
  • 异或校验:把所有字节异或一遍
  • CRC 校验:循环冗余校验,更可靠但计算量大

我个人习惯用累加和。简单、高效,对于按摩仪这种低速通信场景完全够用。

注意:校验和的计算范围要明确。是只算数据域,还是算整个帧(包括帧头、命令字等)?我建议算「从数据长度到数据域结束」这部分,帧头和帧尾不参与校验。

我曾经犯过一个错:把帧头也算进校验范围,结果帧头变了,校验和也跟着变,调试时根本分不清是帧头错了还是数据错了。后来改成只校验有效数据,问题就清晰多了。

3.7 完整帧结构示例

好了,咱们把上面这些拼起来,看看一个完整的帧长什么样:

帧头:    0xAA 0x55
数据长度:0x05
命令字:  0x21(设置力度)
数据域:  0x01 0x01 0x05(TLV:力度=5)
校验和:  0x2C(0x05 + 0x21 + 0x01 + 0x01 + 0x05 = 0x2C)
帧尾:    0x0D 0x0A

发送时,上位机依次发送这 9 个字节。接收方收到后,先找帧头,再读数据长度,然后接收指定字节数的数据,计算校验和,最后检查帧尾。全部通过,才算一帧有效数据。

总结一下:帧结构设计得好,通信就成功了一半。帧头要独特,帧尾要明确,数据长度要准确,命令字要清晰,数据域要灵活,校验和要可靠。这六个要素缺一不可。

嗯,说到这儿,我想起一个项目:有个同事设计的帧结构没有数据长度字段,全靠帧尾来判断结束。结果有一次数据里出现了帧尾的值,接收方提前截断,导致后续数据全部错位。从那以后,我设计的每个协议都老老实实加上数据长度字段。

你想想看,如果连数据有多长都不知道,接收方怎么知道什么时候读完?所以,数据长度字段,千万别省。