2. 数据表示与编码:大端与小端、ASCII与二进制编码、BCD码与自定义编码规则
协议解析说白了就是「读懂对方发来的字节」。但字节本身不会说话,你得先搞清楚它怎么排列、用什么编码。我见过太多工程师在字节序上翻车,一查就是半天。这一章咱们就把这些基础打牢。
核心要点:数据表示决定了你怎么解释收到的 0 和 1。选错编码,数据就是一堆乱码。
2.1 大端 vs 小端:字节序的江湖
先问个问题:一个 16 位的整数 0x1234,在内存里怎么放?
大端(Big-Endian)把高位字节放低地址,小端(Little-Endian)把低位字节放低地址。举个例子:
- 大端存储:地址 0x00 存
0x12,地址 0x01 存0x34 - 小端存储:地址 0x00 存
0x34,地址 0x01 存0x12
我个人习惯把大端想象成「从左往右读」,小端是「从右往左读」。你想想看,网络协议几乎清一色用大端,而 x86 处理器天生就是小端。我在项目中遇到过两次因为字节序没对齐,导致设备间通信数据全反了——排查了整整一个下午。
| 特性 | 大端(Big-Endian) | 小端(Little-Endian) |
|---|---|---|
| 高位字节位置 | 低地址 | 高地址 |
| 常见场景 | 网络协议、TCP/IP、Modbus | x86/ARM 处理器、USB、PCIe |
| 人类阅读友好度 | 直观,和书写顺序一致 | 反直觉,但硬件实现简单 |
实用技巧:写嵌入式代码时,用 htons()、htonl() 做主机到网络字节序转换。别自己手写移位,容易出错。
2.2 ASCII 与二进制编码
ASCII 是最基础的字符编码,一个字节表示一个字符。比如 'A' 是 0x41,'0' 是 0x30。但注意,ASCII 只覆盖了 128 个字符,遇到中文就抓瞎了。
二进制编码更直接——你想传什么值,就直接把它的二进制发出去。比如要传整数 255,ASCII 需要三个字节 '2' '5' '5'(即 0x32 0x35 0x35),而二进制编码只需要一个字节 0xFF。
我记得有一次做传感器数据采集,对方协议文档写的是「ASCII 编码传输」。结果每帧数据 200 多个字节,解析起来慢得要命。后来改成二进制编码,帧长直接砍到 50 字节以内。嗯,这里要注意:二进制编码虽然省空间,但调试时肉眼看不出来,得用工具辅助。
避坑指南:我曾经在调试串口时,发现收到的数据总是多一个 0x0D(回车符)。查了半天,原来是终端软件把 0x0A(换行)自动转换成了 0x0D 0x0A。ASCII 里的控制字符在传输中经常被「好心」修改,务必小心。
2.3 BCD 码:用半个字节表示一个数字
BCD(Binary-Coded Decimal)码用 4 位二进制表示一个十进制数字。比如十进制 95,BCD 码就是 1001 0101(即 0x95)。
为什么不用纯二进制?因为 BCD 码没有进位误差,适合金融、计时等场景。比如 RTC 芯片里的时间寄存器,几乎清一色用 BCD 码。
| 十进制 | 二进制 | BCD 码 |
|---|---|---|
| 12 | 0000 1100 | 0001 0010 |
| 99 | 0110 0011 | 1001 1001 |
| 255 | 1111 1111 | 0010 0101 0101 |
BCD 码的转换也很简单:高位 BCD 乘以 10 再加低位 BCD。比如 0x95 转十进制:9 * 10 + 5 = 95。反过来,十进制转 BCD 就是不断除以 10 取余数。
实用技巧:解析 RTC 时间时,记得把 BCD 转成整数再比较。我见过有人直接拿 BCD 值做大小判断,结果 0x59(59秒)比 0x01(1秒)还小——因为 0x59 的数值是 89,确实比 1 大,但 BCD 的 59 就是 59 秒。别搞混了。
2.4 自定义编码规则:灵活但要有章法
有时候标准编码不够用,就得自己定义。比如工业协议里常见的「压缩 BCD」:用 3 个字节存 5 个十进制数字,省一个 nibble。或者用 7 位编码代替 8 位 ASCII,省带宽。
自定义编码的核心原则:
- 明确边界:每个字段占多少位,从哪开始,到哪结束
- 保留扩展:留几个保留位,方便以后加功能
- 文档化:编码规则必须写清楚,不然别人看不懂
我在项目中做过一个自定义编码:用 4 个字节表示一个带符号的定点数,前 2 字节是整数部分,后 2 字节是小数部分。这样传输效率高,而且精度可控。但后来维护的人换了三茬,每次都要翻文档才能看懂。所以自定义编码一定要配套文档,别偷懒。
经验之谈:能用标准编码就别自己造轮子。实在要自定义,务必在协议头里加一个「编码类型标识符」,方便后续兼容。
2.5 本章知识体系
下面这张图把本章的核心知识点串起来了。你可以把它当作一个快速索引:
这张图把字节序、字符编码、数值编码和自定义编码的关系理清楚了。实际工作中,你往往需要组合使用它们。比如一个 Modbus 帧,地址用二进制编码,寄存器值用大端,时间戳用 BCD——你得全部吃透才能正确解析。
最后提醒:协议解析的第一步永远是「确认字节序和编码方式」。别上来就写解析代码,先看文档,或者用抓包工具确认一下。我曾经因为没确认编码方式,浪费了三天时间——结果只是对方用了小端,而我默认是大端。