数据帧基础概念:帧头、帧尾、长度、校验
各位,咱们开始聊数据帧。说实话,我入行头两年,一直觉得帧结构就是一堆二进制位的排列组合,背下来就行。直到有一次在现场调试XY2-100设备,发现通讯死活不通,抓包一看——帧尾少了一个字节。从那以后,我才真正理解:帧里的每一个字段,都不是摆设。
今天这节,我就把自己这些年踩过的坑和经验摊开来讲。咱们不绕弯子,直接拆解帧头、帧尾、长度字段和校验算法这四个核心要素。你想想看,一个数据帧就像一封挂号信:帧头是信封上的邮戳,帧尾是封口蜡印,长度是信件页数,校验就是防伪水印。任何一个出错,信都到不了收件人手里。
1. 帧头(Frame Header)—— 同步的起点
帧头的核心作用就两个:同步和标识。没有它,接收端根本不知道什么时候该开始读数据。
- 同步码:通常是一个固定模式,比如 0xAA 0x55 或者 0x7E。接收端在比特流里不断寻找这个序列,一旦匹配,就知道帧开始了。
- 地址字段(可选):多设备总线里,帧头会携带目标地址。比如XY2-100的地址是由DIP开关设定的,帧头里必须匹配才能响应。
🧠 经验点: 我建议帧头至少用2字节的同步码,并且这两字节要不同(比如0xAA和0x55),避免数据里偶然出现的相同模式导致误同步。我在一个项目中用过0xFF 0xFF做同步,结果数据里连续出现0xFF时就翻车了……后来改成0xAA 0x55,再没出过问题。
2. 帧尾(Frame Trailer)—— 收尾的标记
如果说帧头是“开始读”,帧尾就是“读到这为止”。帧尾通常也是一个固定模式,或者干脆用结束符。
- 结束符:类似0x0D 0x0A(回车换行)在ASCII协议里很常见。
- 长度隐含结束:如果帧头里有精确的长度字段,帧尾可以省略,但为了健壮性,很多协议还是会加一个固定结尾码。
⚠️ 注意: 我曾经遇到一个设备,帧尾使用0x7E,但数据字段里也可能出现0x7E,于是它需要用字节填充(Byte Stuffing)转义,否则接收端会把数据里的0x7E误当成帧尾。这个坑,你设计帧结构时一定要提前想好。
3. 长度字段(Length)—— 知道读多少
长度字段声明了紧随其后的数据字段(Payload)的字节数。它通常放在帧头之后、数据之前。
| 字段位置 | 常见长度 | 作用 |
|---|---|---|
| 帧头内 | 1~2字节 | 支持可变长度帧 |
| 帧校验前 | 1~2字节 | 便于解析器预分配缓冲区 |
注意两点:
- 长度字段是否包含自己?我习惯让它只表示数据字段长度,不包含帧头、校验或帧尾。这样解析逻辑简单,也符合大多数协议规范。
- 长度字段丢失或错误怎么办?那就是校验字段上场的时候了。
4. 校验(Check / CRC)—— 容错的最后防线
校验的作用:确保帧数据在传输中没有被篡改或损坏。最常见的两种:
- 累加和校验(Checksum):把所有字节加起来取低字节或取反。简单,但碰撞率高。
- CRC校验:比如 CRC-16-IBM(多项式 0x8005),碰撞率极低,工业协议几乎都用它。
💡 小巧门: 我习惯把校验范围定在「从帧头第一个字节到数据字段最后一个字节」、不包括帧尾。如果你把帧尾也放进校验,那帧尾本身变化会干扰校验码计算,多一事不如少一事。
下面这张图,把上面说的内容串起来,就是一根完整的数据帧骨架:
5. 归纳总结:一表看清
| 字段 | 功能 | 常见实现 | 我踩过的坑 |
|---|---|---|---|
| 帧头 | 同步、标识帧起始 | 0xAA 0x55 + 地址 | 用连续相同字节做同步码,误触发 |
| 长度 | 声明数据字段大小 | 1字节 ≤255、2字节 ≤65535 | 忘记排除帧头自身,解析多算了几个字节 |
| 数据 | 承载有效信息 | 字节序列 | —— |
| 校验 | 检错、防篡改 | CRC-16/CCITT、Checksum | Checksum碰撞导致错误数据被当成正确 |
| 帧尾 | 标记帧结束 | 0x0D 0x0A 或 0x7E | 数据中含帧尾模式时未做字节填充 |
好了,如果你现在手边有XY2-100的协议文档,可以拿出来对照着看——绝大多数设备的数据帧结构,跑不出这个模型。我自己的习惯是,每拿到一个新协议,先用这个框架套一下,三分钟就能理清它到底传输了什么。
嗯,下节我们会实际抓一包XY2-100的报文,用Python把它拆开来看看。到时候你会发现,刚才讲的这些字段,每个都能在二进制里找到对应位置。