找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1543

积分

0

好友

201

主题
发表于 昨天 09:25 | 查看: 5| 回复: 0

本文面向有一定嵌入式开发经验的工程师,聊聊自定义通信协议中最容易被忽视的基础问题——帧边界与转义机制。

一、从一个真实的现场问题说起

前段时间有人在群里问:MCU 和上位机通过串口通信,数据发过去大概率正常,但隔一段时间就会出现一帧“错位”,后续所有解析全部乱掉,重启才能恢复。

这个问题很典型。根本原因不在波特率,不在中断,而在于协议里根本没有明确的帧边界——程序不知道一包数据从哪里开始、到哪里结束,一旦中间丢了一个字节,整个接收状态机就垮了。

帧头、帧尾、转义,是自定义串口协议里最基础的三件事,但也是最多人“差不多得了”的三件事。

二、字节流没有天然边界

UART 本质上是一条连续的字节流通道,它只管把字节一个一个搬过去,不区分包与包之间的边界。

发送方视角:
[0xAA][0x01][0x10][0x03][0xBB][0xAA][0x02][0x20][0x01][0xBB]
 ↑_______第一包_______↑    ↑________第二包________↑

接收方视角(没有帧结构时):
0xAA 0x01 0x10 0x03 0xBB 0xAA 0x02 0x20 0x01 0xBB ...
                   ???哪里是分界线???

如果通信双方没有约定好“一包数据长什么样”,接收方面对这串字节就是瞎子摸象。帧结构要解决的核心问题只有一个:让接收方能可靠地从字节流中找到每一包数据的起点和终点。

三、帧头:数据包的“门牌号”

帧头(Frame Header / SOF, Start of Frame)是接收方扫描字节流时最先寻找的标志。

常见的设计方式有两种:

方案 A:固定魔数
数据帧结构示意图:帧头+帧尾方案

0xAA 0x55 这样的固定组合做帧头,接收状态机不断扫描,匹配到这两个字节就认为一帧开始了。

选两个字节而不是一个,是为了降低误同步概率——有效载荷中随机出现 0xAA 0x55 连续序列的概率远小于单独出现 0xAA

方案 B:帧头 + 长度字段(推荐)
数据帧结构示意图:带长度字段的方案

在帧头之后紧跟长度字段,是工程中最稳定的做法之一。接收方逻辑变得非常清晰:找到帧头 → 读长度 → 按长度收数据 → 校验

四、帧尾:可选,但有它更安全

帧尾(EOF, End of Frame)不是必须的,尤其当协议有长度字段时,理论上帧尾可以省略。但在实际工程里,帧尾依然有价值:

  • 双重校验:帧尾作为第二道防线,配合 CRC 一起判断这帧是否完整
  • 硬件调试方便:用逻辑分析仪抓波形时,能直观看到每帧的终止位置
  • 异常恢复:当状态机检测到帧尾位置不对时,可以主动丢弃并重同步

一个典型的完整帧结构长这样:
带长度与帧尾的完整帧结构示意图

帧尾选 0xFF 或其他特殊值,同样建议避开在 DATA 中高频出现的数值——这引出了最后一个问题:如果 DATA 里真的出现了和帧头/帧尾相同的字节,怎么办?

五、转义:协议的“元字符”机制

这是整个帧设计里最容易被新人跳过的一步,也是最容易埋坑的地方。

问题场景:
假设帧头是 0xAA 0x55,有效载荷里偶然出现了这两个连续字节,接收状态机就会把它当成一个新帧的开始,导致解析完全错乱。
DATA区域包含控制字节导致的误判示意图

解决方案:字节填充(Byte Stuffing)
引入一个转义字节(Escape Byte),例如 0x7D,规则如下:

  • 发送前:DATA 中凡是出现 0xAA0x550xFF0x7D 的地方,在其前面插入 0x7D,并将原字节异或一个固定值(常用 0x20
  • 接收后:遇到 0x7D,跳过它,将下一个字节再异或 0x20 还原

HDLC风格的字节转义规则示意

完整的收发流程变成:
串口数据收发全流程:转义与去转义

注意: 转义处理只针对 DATA 区域,帧头和帧尾本身不做转义,因为它们是协议控制字段,接收方需要直接识别它们。

六、一个完整的协议规范示例

把以上内容整合成一个可直接落地的协议设计定义:

协议名称:CustomSerial v1.0

帧格式(发送时 DATA 已经过转义处理):
+------+------+-------+-------+------------------+-------+-------+------+
| 0xAA | 0x55 | LEN_H | LEN_L |   DATA(已转义)  | CRC_H | CRC_L | 0xFF |
+------+------+-------+-------+------------------+-------+-------+------+
  1字节  1字节   1字节    1字节     0~512字节          1字节    1字节  1字节

字段说明:
  帧头  : 0xAA 0x55(固定,不参与转义)
  LEN   : DATA 转义后的字节数(大端,16位)
  DATA  : 有效载荷,发送前需做转义处理
  CRC   : 对【原始DATA(未转义)】计算 CRC16-MODBUS
  帧尾  : 0xFF(固定,不参与转义)

转义字节:0x7D
需转义的字节集合:{0xAA, 0x55, 0xFF, 0x7D}
转义方式:在原字节前插入 0x7D,原字节 XOR 0x20 后发送

接收状态机:
  IDLE → 等待 0xAA
  SOF1 → 等待 0x55(否则回到 IDLE)
  SOF2 → 读取 LEN_H、LEN_L(共2字节)
  LEN  → 按 LEN 长度读取 DATA(实时去转义)
  DATA → 读取 CRC_H、CRC_L(2字节)
  CRC  → 等待 0xFF 帧尾
  DONE → 验证 CRC,通过则投递数据,失败则丢弃

这个设计的关键点在于:LEN 记录的是转义后的长度,这样接收方在 DATA 阶段只需按固定字节数读取,不需要实时计算去转义后的长度,逻辑更简单,出错也少。

七、写在最后

帧头、帧尾、转义,三件事组合解决的是同一个问题:在一条没有语义的字节管道上,让通信双方都能可靠地找到数据的边界。

没有长度字段时,依赖帧尾定界;有长度字段时,帧尾做双重保险;有特殊字节冲突风险时,转义处理。这几个机制不是非此即彼,而是根据实际场景灵活组合。

最后一点经验:协议文档要在写代码之前定好,而不是写完代码后补。 两端各自理解的“帧格式”差一个字节,联调的时候会让你怀疑人生。希望这篇关于MCU通信基础的文章能帮你避开一些坑,更多深入的技术讨论,欢迎来云栈社区交流。




上一篇:唤醒抽屉里的小米1S与红米Note,重温Android刷机与MIUI的黄金时代
下一篇:分布式SQL:构建企业级Agentic AI记忆层的生产实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-25 09:06 , Processed in 0.479049 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表