消息帧结构深度解析:起始标志、负载长度、序列号、系统ID、组件ID、消息ID、校验机制
MAVLink 的帧结构,说白了就是无人机和地面站之间通信的「信封」。你想想看,如果两个人写信,总得有个格式——谁写的、写给谁、信多长、内容是什么、怎么保证信没被篡改。MAVLink 的帧结构干的就是这些事。
我刚开始接触 MAVLink 时,觉得这东西不就是个协议头嘛,有啥好研究的?直到有一次在野外调试,飞机突然丢包,我盯着串口日志看了三个小时,才发现是帧同步出了问题。嗯,从那以后我再也不敢小看这十几个字节了。
帧结构总览
MAVLink 有两种版本:v1 和 v2。v2 是主流,咱们重点讲它。一个完整的 MAVLink v2 帧长这样:
+------+------+------+------+------+------+------+------+------+------+------+------+------+------+------+------+
| STX | LEN | INC | CMP | SEQ | SYS | COMP | MSG ID (3字节) | PAYLOAD (0-255字节) | CRC1 | CRC2 |
| 0xFD | | FLG | FLG | | ID | ID | | | | |
+------+------+------+------+------+------+------+------+------+------+------+------+------+------+------+------+
| 1B | 1B | 1B | 1B | 1B | 1B | 1B | 3B | 0-255B | 2B | |
+------+------+------+------+------+------+------+------+------+------+------+------+------+------+------+------+
总共最少 12 字节(无负载时),最多 279 字节。每个字段都有它的使命,咱们一个一个拆开看。
起始标志(STX)—— 帧同步的哨兵
起始标志就一个字节,v1 是 0xFE,v2 是 0xFD。它的作用很简单:告诉接收端「注意,一帧数据要开始了」。
但这里有个坑。我曾经在一个项目中,接收端用的是 DMA + 环形缓冲区,结果起始标志在数据流里偶然出现了两次,导致帧同步错位。后来怎么解决的?加了一个超时判断——如果收到 STX 后 500 微秒内没收到完整帧,就丢弃重新同步。
负载长度(LEN)—— 告诉你要读多少
LEN 字段占 1 字节,表示 PAYLOAD 部分的字节数。范围是 0 到 255。注意,它不包含 CRC 和头部字段。
这个字段我吃过亏。有一次我自定义了一个消息,负载长度写死了 20 字节,但实际发送时只填充了 10 字节有效数据。接收端按 20 字节去读,结果把后面帧的头部数据当成了负载内容,整个数据流全乱了。
兼容标志与不兼容标志(INC FLG / CMP FLG)
这两个字段是 v2 新增的,各占 1 字节。它们的作用是告诉接收端:「这帧数据用了哪些扩展功能」。
- 兼容标志(INC FLG): 如果接收端不支持某个标志位,可以安全地忽略该帧。
- 不兼容标志(CMP FLG): 如果接收端不支持某个标志位,必须丢弃该帧。
举个例子,CMP FLG 的第 0 位表示「本帧使用了签名机制」。如果接收端不支持签名验证,看到这个标志位为 1,就应该直接丢帧,而不是尝试解析。
我个人习惯在开发新功能时,先把标志位放在 INC FLG 里测试,等稳定了再移到 CMP FLG。这样即使旧版本设备不识别,也不会导致通信中断。
序列号(SEQ)—— 丢包检测的利器
SEQ 占 1 字节,范围 0-255,循环使用。每发送一帧,序列号加 1。
序列号的作用有三个:
- 检测丢包: 如果接收端发现序列号跳变,就知道中间有帧丢了。
- 去重: 如果收到相同序列号的帧,说明是重传,直接丢弃。
- 排序: 虽然 MAVLink 通常基于可靠传输(如 UART),但在无线链路上,乱序到达也是可能的。
我记得有一次在飞控测试中,地面站显示的数据突然卡住了。我一看日志,序列号从 45 直接跳到了 78,中间丢了 32 帧。排查后发现是串口缓冲区溢出导致的。加了一个流控就解决了。
系统 ID(SYS ID)与组件 ID(COMP ID)—— 谁在说话
这两个字段各占 1 字节,共同标识消息的来源。
- 系统 ID: 标识一个独立的系统,比如一架无人机、一个地面站。范围 1-255,0 保留。
- 组件 ID: 标识系统内的一个组件,比如飞控、GPS、摄像头。范围 1-255,0 保留。
举个例子,一架无人机可能有:
| 组件 | 系统 ID | 组件 ID |
|---|---|---|
| 飞控 | 1 | 1 |
| GPS | 1 | 6 |
| 摄像头 | 1 | 100 |
地面站的系统 ID 通常是 255。这样,任何设备收到消息,都能通过 SYS ID + COMP ID 知道消息来自哪里。
我在一个多无人机协同项目中,用了 5 架飞机,每架的系统 ID 分别是 1-5。结果调试时发现,地面站收到了 ID 为 0 的消息。查了半天,原来是某个组件的初始化代码里忘了设置系统 ID,默认就是 0。嗯,从那以后我每次初始化都会显式设置这两个字段。
消息 ID(MSG ID)—— 这帧数据是什么
消息 ID 在 v2 中占 3 字节,支持最多 16,777,216 种消息类型。v1 只有 1 字节,最多 256 种。
消息 ID 决定了 PAYLOAD 的解析方式。比如:
MSG ID = 0表示HEARTBEAT,负载是 9 字节的固定结构。MSG ID = 30表示ATTITUDE,负载包含滚转、俯仰、偏航角。
接收端收到消息后,先读 MSG ID,然后根据 ID 查找对应的消息定义,再按定义解析 PAYLOAD。如果遇到未知的 MSG ID,直接丢帧。
校验机制(CRC)—— 数据完整性的最后防线
MAVLink 使用 16 位 CRC(循环冗余校验),算法基于 CRC-16-IBM(多项式 0x8005)。CRC 覆盖的范围包括:
- v1:LEN + SEQ + SYS ID + COMP ID + MSG ID + PAYLOAD
- v2:LEN + INC FLG + CMP FLG + SEQ + SYS ID + COMP ID + MSG ID + PAYLOAD + 签名(如果有)
注意,v2 的 CRC 还包含一个「消息特定种子」。每个消息类型都有一个固定的 CRC 种子,定义在 XML 消息定义文件中。这个设计是为了防止消息类型误匹配——即使两个消息的 ID 相同,如果 CRC 种子不同,校验也会失败。
我曾经遇到过一个诡异的问题:地面站和飞控之间的通信偶尔会解析出乱码。排查了三天,最后发现是飞控的固件版本和地面站的协议定义不一致,同一个 MSG ID 对应的负载结构不同。CRC 种子不匹配,导致校验失败。从那以后,我每次升级固件都会同步更新协议定义文件。
mavgen 工具重新生成 CRC 种子,不要手动指定。我曾经手动指定了一个种子,结果和自动生成的冲突,排查了整整一个下午。
帧结构流程图
下面这张图展示了接收端处理一帧数据的完整流程:
实际调试中的经验
说了这么多理论,最后分享几个我在实际项目中踩过的坑:
- STX 误触发: 在无线链路上,噪声数据里可能出现
0xFD。我习惯在接收端维护一个「帧同步状态机」,只有连续收到 STX 并且后续字段合理时,才认为同步成功。 - CRC 计算位置: v2 的 CRC 包含了 INC FLG 和 CMP FLG,但很多人写代码时容易漏掉这两个字段。我见过一个开源项目因此出了 bug,修了两个月。
- 序列号回绕: 8 位序列号 256 帧就回绕一次。如果链路延迟大,接收端可能收到乱序的帧。我一般用「序列号差值取模 256」来判断新旧,而不是直接比较大小。