3. 数据帧结构:帧头、帧尾、数据长度、校验和、命令字、数据域的定义与设计
好,咱们进入正题。数据帧结构这东西,说白了就是通信双方约定好的「说话格式」。你想想看,如果上位机和按摩仪各说各的,那不乱套了?我刚开始做嵌入式通信那会儿,就吃过这个亏——自己定义的帧结构太随意,结果设备死活不认,查了两天才发现是帧头跟数据内容撞车了。
一个完整的数据帧,通常包含这么几个部分:帧头、数据长度、命令字、数据域、校验和、帧尾。咱们一个一个拆开讲。
3.1 帧头(Frame Header)
帧头的作用就一个:告诉接收方「注意,我要开始说话了」。它通常是一个固定的字节序列,比如 0xAA 0x55 或者 0x5A 0xA5。
我个人习惯用两个字节做帧头。为什么是两个?一个字节太容易跟数据内容混淆,三个以上又浪费带宽。两个刚刚好。
我在项目中遇到过一个问题:有个同事用 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 个字节。接收方收到后,先找帧头,再读数据长度,然后接收指定字节数的数据,计算校验和,最后检查帧尾。全部通过,才算一帧有效数据。
嗯,说到这儿,我想起一个项目:有个同事设计的帧结构没有数据长度字段,全靠帧尾来判断结束。结果有一次数据里出现了帧尾的值,接收方提前截断,导致后续数据全部错位。从那以后,我设计的每个协议都老老实实加上数据长度字段。
你想想看,如果连数据有多长都不知道,接收方怎么知道什么时候读完?所以,数据长度字段,千万别省。