4. 串口通信协议帧结构解析
水下调制解调器的串口(通常是 RS-232/RS-485 或 TTL UART)并不是“把数据直接倒进水里”,而是主机与调制解调器之间的本地命令通道。真正决定通信是否可靠、解析是否正确的,是这一串字节如何被组织成一帧。帧结构一旦理解错位,哪怕波特率、校验位都设置正确,也会出现“收到数据但全是乱码”或“命令发出却无响应”的典型故障。本节围绕水下调制解调器常见的帧格式展开,重点讲清起始位、地址、命令、数据、校验、结束位六个字段的作用与排布,并对比二进制帧与 ASCII 帧的差异。
一、帧的六个基本字段及其职责
水下调制解调器厂商的私有协议各不相同,但绝大多数帧都可以拆成六个逻辑字段。理解每个字段“为什么存在”,比死记某个型号的字节偏移更重要。
- 起始位(帧头):用于让接收方从连续字节流中定位一帧的开头。常见做法是固定单字节(如 0x7E、0xAA)或多字节同步头(如 0x55 0xAA)。帧头必须具有低误判概率,否则数据区出现同样字节时会被误认为新帧开始。
- 地址:标识目标设备。水下网络常为一点对多点(一个主机挂多台调制解调器或换能器),地址字段让命令只被目标设备执行。地址还常区分“广播地址”(如 0xFF)与“本机地址”。
- 命令:操作码,决定这一帧是“发送数据”“读取状态”“设置参数”还是“复位”。命令字段通常 1 字节,取值由厂商定义。
- 数据:命令的载荷。长度可变,因此帧中一般需要长度字段,或由命令隐含固定长度。数据区是最容易出现转义、粘包问题的地方。
- 校验:用于检测传输错误。串口层面已有奇偶校验位,但那只是单字节的弱校验;帧级校验常用累加和(Checksum)、异或校验(XOR)或 CRC-16,覆盖范围通常是“地址到数据末尾”。
- 结束位(帧尾):标志一帧结束,常为固定字节(如 0x0D、0x0A 或 0x7F)。它与帧头配合,帮助接收状态机判断边界。
需要特别强调:串口物理层的“起始位/停止位”(UART 每字节前后的电平位)与协议层的“帧头/帧尾”是两个不同层次的概念。前者由 UART 硬件自动处理,后者由软件解析。初学者常把两者混为一谈,导致在解析代码里错误地寻找“起始位”。
二、二进制帧与 ASCII 帧的差异
水下调制解调器常见两种帧风格。二进制帧把每个字段编码为原始字节,紧凑高效;ASCII 帧把字段转成可打印字符,用逗号、星号等分隔符切分。二者不是“新旧”关系,而是适用场景不同。
| 对比维度 | 二进制帧 | ASCII 帧 |
|---|---|---|
| 字段定位方式 | 固定偏移 + 长度字段 | 分隔符切分(如逗号、星号) |
| 传输效率 | 高,1 字节承载 8 位信息 | 低,一个字节值需 2 个十六进制字符 |
| 人工可读性 | 差,需查表或工具解析 | 好,串口助手直接可见 |
| 转义问题 | 数据区出现帧头/帧尾字节需转义 | 数据区出现分隔符需转义或禁用 |
| 典型校验 | CRC-16、累加和 | 十六进制文本形式的 XOR 或 CRC |
| 适用场景 | 大数据量、低带宽水声链路 | 参数配置、现场调试、低速命令 |
以“向地址 0x01 的设备发送文本 HELLO”为例。二进制帧可能是:7E 01 10 05 48 45 4C 4C 4F CRC_H CRC_L 0D,其中 0x10 是“发送数据”命令,0x05 是长度,后面 5 字节是 HELLO 的 ASCII 码。ASCII 帧则可能是:$01,SEND,HELLO,A5*0D0A,其中 A5 是校验的十六进制文本。两者表达同一语义,但接收端解析逻辑完全不同:前者按偏移和长度读,后者按分隔符切分并做字符到字节的转换。
三、解析实现的关键点
实际开发中,接收端不能假设“一次 read 就是一帧”。串口是字节流,一帧可能被拆成多次到达,也可能多帧粘在一起。稳妥的做法是写一个接收状态机:先找帧头,再按长度字段或分隔符收集剩余字节,最后校验并交付。下面是一段二进制帧解析的伪代码示例,重点在状态流转与校验范围。
状态机字段:WAIT_HEAD → WAIT_ADDR → WAIT_CMD → WAIT_LEN → WAIT_DATA → WAIT_CRC → WAIT_TAIL
收到字节 b:
WAIT_HEAD: 若 b == 0x7E,转 WAIT_ADDR
WAIT_ADDR: addr = b,转 WAIT_CMD
WAIT_CMD : cmd = b,转 WAIT_LEN
WAIT_LEN : len = b,data = [],转 WAIT_DATA
WAIT_DATA: 收集 b 直到 len 个,转 WAIT_CRC
WAIT_CRC : 收满 2 字节 CRC,计算覆盖 addr..data 的 CRC-16 并比对
WAIT_TAIL: 若 b == 0x0D,帧完整,交付上层;否则丢弃并回到 WAIT_HEAD
几个容易踩的坑:其一,校验范围必须与发送端严格一致,常见错误是把帧头或帧尾也算进 CRC,导致永远校验失败;其二,长度字段本身是否计入校验要确认清楚;其三,数据区若允许出现 0x7E 或 0x0D,必须约定转义规则(如 0x7D 转义),否则状态机会提前截断。ASCII 帧的坑则在分隔符:如果数据本身含逗号,必须约定转义或改用长度前缀。
四、一个可执行的核对清单
在把帧解析接入正式程序前,建议用下面这份清单逐项确认,每项都对应一个真实故障来源:
- 帧头字节是否唯一?数据区是否可能出现同值?若可能,转义规则是什么?
- 地址字段是否区分广播地址与本机地址?多设备时如何过滤?
- 命令字段的取值表是否完整?未知命令应丢弃还是回错误帧?
- 长度字段是“数据字节数”还是“整帧字节数”?是否包含校验?
- 校验算法、初值、覆盖范围是否与发送端逐字节一致?
- 帧尾是否固定?接收超时时间设为多少毫秒合适?
- ASCII 帧的分隔符在数据中出现时如何处理?
把这七项确认清楚,帧解析的绝大部分问题在编码前就能被排除。水下链路的带宽和误码率都远不如有线环境,帧结构设计得越明确、解析实现得越严格,后续排查“为什么命令没生效”时就越有据可依。