2. 消息格式深度解析:消息帧结构、起始标志、负载长度、序列号、系统ID、组件ID、消息ID、校验和

各位同学,欢迎来到第二章。上一章我们聊了 MAVLink 的由来和整体设计哲学。这一章,咱们要真正动手拆解一个 MAVLink 消息了。

说白了,MAVLink 消息就是一堆字节。但这一堆字节怎么排布、每个字节代表什么含义,这就是协议的核心。我当年第一次看协议文档时,对着那几张表格看了整整一下午,才把每个字段的来龙去脉搞清楚。今天我把这些经验直接给你,让你少走弯路。

2.1 消息帧结构总览

一个标准的 MAVLink 1.0 消息帧,就像一列火车。每节车厢都有固定的位置和用途。我们先看整体结构,再逐个拆解。

核心要点:MAVLink 1.0 消息帧最小 8 字节(无负载),最大 263 字节(负载 255 字节 + 头部 8 字节)。

MAVLink 1.0 消息帧结构 STX 0xFE LEN n 字节 SEQ 0-255 SYS 1-255 COMP 1-255 MSG ID 0-255 Payload 0-255 字节 CKA 2 字节 字段说明: STX (1B):起始标志,固定 0xFE,用于帧同步 LEN (1B):负载长度,取值范围 0-255 SEQ (1B):序列号,用于丢包检测 SYS (1B):系统 ID,标识飞控/地面站 COMP (1B):组件 ID,标识 GPS/IMU 等 MSG ID (1B):消息 ID,标识消息类型 Payload (0-255B):消息负载数据 CKA (2B):CRC16 校验和

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 开始逐个状态流转,直到校验和验证通过。