2. 消息格式深度解析:消息帧结构、起始标志、负载长度、序列号、系统ID、组件ID、消息ID、校验和
各位同学,欢迎来到第二章。上一章我们聊了 MAVLink 的由来和整体设计哲学。这一章,咱们要真正动手拆解一个 MAVLink 消息了。
说白了,MAVLink 消息就是一堆字节。但这一堆字节怎么排布、每个字节代表什么含义,这就是协议的核心。我当年第一次看协议文档时,对着那几张表格看了整整一下午,才把每个字段的来龙去脉搞清楚。今天我把这些经验直接给你,让你少走弯路。
2.1 消息帧结构总览
一个标准的 MAVLink 1.0 消息帧,就像一列火车。每节车厢都有固定的位置和用途。我们先看整体结构,再逐个拆解。
核心要点:MAVLink 1.0 消息帧最小 8 字节(无负载),最大 263 字节(负载 255 字节 + 头部 8 字节)。
2.2 起始标志(STX)—— 帧同步的哨兵
起始标志是消息帧的第一个字节。MAVLink 1.0 固定为 0xFE。它的作用很简单:告诉接收端「注意,一条新消息要来了」。
你可能会问:为什么是 0xFE 而不是别的值?其实没有特别深奥的原因,就是协议设计者选了一个不太常见的值,降低误判概率。我见过有人把 STX 改成 0x55 或 0xAA,结果在串口上频繁误同步——因为这些值在空闲状态太常见了。
个人经验:我在调试一个四轴飞行器时,遇到过地面站频繁丢帧。排查了两天,最后发现是串口线太长导致 0xFE 被干扰成了 0xFC。从那以后,我习惯在接收端加一个「连续 3 个无效 STX 就复位」的逻辑。
2.3 负载长度(LEN)—— 告诉你要读多少
第二个字节是负载长度。它告诉接收端:后面跟着的 Payload 有多少个字节。
取值范围是 0 到 255。0 表示这条消息没有负载,只有头部和校验和。比如 HEARTBEAT 消息的负载长度就是 0?不对,HEARTBEAT 的负载是 9 字节。嗯,这里要注意,很多初学者以为心跳包没有数据,其实它携带了系统状态。
| 负载长度 | 含义 | 常见消息 |
|---|---|---|
| 0 | 无负载 | 某些命令确认 |
| 1-50 | 小负载 | HEARTBEAT (9B), ATTITUDE (28B) |
| 51-255 | 大负载 | GPS_RAW_INT (30B), 自定义扩展消息 |
2.4 序列号(SEQ)—— 丢包检测的利器
第四个字节是序列号。每个发送端维护一个独立的计数器,每发一条消息就加 1,从 0 到 255 循环。
接收端通过检查序列号是否连续,就能判断有没有丢包。比如收到 SEQ=5,下一个却是 SEQ=7,那 SEQ=6 就丢了。
我曾经踩过的坑:序列号是每个系统独立维护的。如果你有两个飞控同时往一个地面站发消息,它们的 SEQ 是各自计数的。地面站必须按「系统 ID + 组件 ID」来分别跟踪序列号,否则会误判丢包。我早期的一个项目就因为这个 bug,导致地面站疯狂重发请求,把串口带宽占满了。
2.5 系统 ID(SYS)与组件 ID(COMP)—— 谁在说话
系统 ID 标识一个完整的无人机系统(飞控 + 所有外设)。组件 ID 标识系统内的具体模块。
举个例子:
- 系统 ID=1,组件 ID=1:飞控主控
- 系统 ID=1,组件 ID=100:GPS 模块
- 系统 ID=1,组件 ID=200:光流传感器
- 系统 ID=2,组件 ID=1:地面站
这样设计的好处是:一条总线上可以挂多个系统,每个系统内又有多个组件,彼此互不干扰。说白了,这就是一个两级寻址方案。
| 字段 | 字节数 | 取值范围 | 典型值 |
|---|---|---|---|
| 系统 ID | 1 | 1-255 | 1(飞控),255(地面站) |
| 组件 ID | 1 | 1-255 | 1(主控),100(GPS) |
2.6 消息 ID(MSG ID)—— 这条消息是什么
消息 ID 是 MAVLink 协议的灵魂。它告诉接收端:这条消息是心跳包、姿态数据、GPS 坐标,还是别的什么。
MAVLink 1.0 的消息 ID 只有 1 个字节,所以最多支持 256 种消息类型。常用的有:
0:HEARTBEAT(心跳)30:ATTITUDE(姿态)24:GPS_RAW_INT(GPS 原始数据)74:VFR_HUD(飞行仪表数据)
我个人的习惯是:在调试时先打印消息 ID,看看飞控到底在发什么。有一次客户说飞机不响应指令,我一查日志,发现飞控一直在发 MSG_ID=0(心跳),根本没发 MSG_ID=76(命令确认)。原来是飞控的指令处理线程卡死了。
2.7 校验和(CKA)—— 数据完整性的最后防线
校验和是消息帧的最后两个字节,使用 CRC16-CCITT 算法。它覆盖了从负载长度开始到负载结束的所有字节。
为什么不算起始标志?因为起始标志是帧同步用的,如果把它也算进校验和,万一 STX 被干扰,整个帧就废了。设计者故意把它排除在外,这样即使 STX 出错,只要后面的数据完整,还能通过校验和发现错误。
校验和计算范围:LEN + SEQ + SYS + COMP + MSG_ID + Payload(不包括 STX)
接收端收到消息后,会用同样的算法重新计算校验和,然后跟收到的 CKA 对比。不一致就丢弃整条消息。
避坑指南:我曾经在移植 MAVLink 库到 STM32 时,发现校验和总是算不对。查了两天,最后发现是 CRC 表的字节序搞反了。MAVLink 用的是大端 CRC,而 STM32 的硬件 CRC 默认是小端。记住:软件实现一定要跟协议文档的参考代码对齐。
2.8 完整的消息解析示例
我们来看一个真实的抓包数据。假设串口上收到了这样一串字节:
FE 09 1A 01 01 00 00 00 00 00 00 00 00 00 00 03 51
| | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | +-- CKA (0x0351)
| | | | | | | | | | | | | | | +-- CKA (0x0351)
| | | | | | | | | | | | | | +-- Payload[8] = 0x00
| | | | | | | | | | | | | +-- Payload[7] = 0x00
| | | | | | | | | | | | +-- Payload[6] = 0x00
| | | | | | | | | | | +-- Payload[5] = 0x00
| | | | | | | | | | +-- Payload[4] = 0x00
| | | | | | | | | +-- Payload[3] = 0x00
| | | | | | | | +-- Payload[2] = 0x00
| | | | | | | +-- Payload[1] = 0x00
| | | | | | +-- Payload[0] = 0x00
| | | | | +-- MSG_ID = 0x00 (HEARTBEAT)
| | | | +-- COMP = 0x01 (主控)
| | | +-- SYS = 0x01 (飞控)
| | +-- SEQ = 0x1A (26)
| +-- LEN = 0x09 (9字节负载)
+-- STX = 0xFE
解析结果:
- 这是一条 HEARTBEAT 消息(MSG_ID=0)
- 来自系统 ID=1,组件 ID=1
- 序列号是 26
- 负载 9 个字节全是 0(表示系统状态为未连接)
- 校验和 0x0351 正确
你看,只要掌握了帧结构,解析一条消息就是按字节拆解的事。我当年在嵌入式设备上写解析器时,就是对着这个结构写了一个 switch-case 状态机,从 STX 开始逐个状态流转,直到校验和验证通过。