消息结构深度解析:MAVLink 消息帧格式
各位同学,今天我们来啃一块硬骨头——MAVLink 的消息帧格式。说实话,我刚开始接触 MAVLink 时,看着那一串十六进制数据,脑袋都是懵的。后来在飞控项目里被坑过几次,才真正理解了每个字节的用意。
你想想看,无人机在天上飞,地面站发一条指令,中间要经过数传电台、串口、甚至可能绕道卫星链路。任何一个字节错位,轻则指令无效,重则飞控死机。所以,把帧格式吃透,是后续所有开发的基础。
一、MAVLink 1.0 帧结构总览
MAVLink 1.0 的帧结构,说白了就是一套“包裹协议”。原始数据被层层打包,加上头部、校验,才敢扔到信道上。我习惯把它拆成三部分看:头部(Header)、负载(Payload)、校验尾(Checksum)。
核心要点:MAVLink 1.0 固定帧长度为 8 字节头部 + N 字节负载 + 2 字节校验。负载长度由 LEN 字段决定,最大 255 字节。
下面这张图,是我当年做数传协议分析时亲手画的,现在拿出来给你们看,一目了然:
二、各字段深度解读
1. STX —— 帧同步的“哨兵”
STX 固定为 0xFE。为什么选这个值?我猜是因为它不容易在随机数据中出现。我在调试一个数传模块时,遇到过串口丢字节导致帧头错位,结果地面站疯狂解析出乱码。后来我加了个“连续收到两个 0xFE 才认为同步”的逻辑,问题就解决了。
小技巧:实际工程中,建议在接收端维护一个“滑动窗口”,每次收到字节都尝试匹配 STX。不要等丢帧了再重同步,那样效率太低。
2. LEN —— 负载长度的“尺子”
LEN 告诉接收方:PAYLOAD 有多少字节。范围 0~255。注意,LEN 不包括头部和校验。我见过有人把整个帧长度填进 LEN,结果接收方算错偏移,解析出来的数据全是错的。
举个例子:如果 LEN = 10,那么 PAYLOAD 就是 10 个字节,整个帧长度 = 8(头部)+ 10(负载)+ 2(校验)= 20 字节。
3. SEQ —— 序列号,防丢包的“计数器”
SEQ 从 0 开始,每发一条消息加 1,到 255 后回绕到 0。接收方通过检查 SEQ 的连续性,就能知道有没有丢包。
我记得有一次做无人机编队,两架飞机之间的心跳包 SEQ 突然跳变,排查了半天发现是数传电台的缓冲区溢出,导致中间几个包被丢弃了。如果没有 SEQ,这种问题根本发现不了。
4. SYS 和 COMP —— 系统与组件 ID
SYS 标识不同的无人机或地面站(0~255),COMP 标识同一系统内的不同组件(如飞控、GPS、摄像头)。
举个例子:SYS=1 是无人机 A,COMP=1 是飞控,COMP=2 是 GPS。地面站收到消息后,通过 SYS/COMP 就知道数据来自哪里。
注意:SYS=0 是广播地址,所有设备都会处理。COMP=0 表示“所有组件”。生产环境中慎用广播,否则容易造成总线冲突。
5. MSG —— 消息 ID,协议的“灵魂”
MSG 决定了 PAYLOAD 的解析方式。比如 MSG=0 是 HEARTBEAT,MSG=30 是 ATTITUDE。每个 MSG 对应一个固定的数据结构,长度由 LEN 决定。
我建议你把 MSG 定义放在一个头文件里,用枚举管理。这样代码可读性高,也不容易写错。
6. PAYLOAD —— 真正的“肉”
PAYLOAD 是消息的具体数据,长度由 LEN 决定。注意,MAVLink 1.0 的 PAYLOAD 最大 255 字节,但实际中很少超过 100 字节。因为数传电台的 MTU 通常只有 1500 字节,太大的包会被分片,增加丢包风险。
7. CKA 和 CKB —— 校验和,数据的“守门员”
校验和采用 CRC-16-CCITT 算法,覆盖整个消息(从 LEN 到 PAYLOAD 末尾)。接收方重新计算 CRC,如果与收到的 CKA/CKB 不匹配,说明数据在传输中被篡改或损坏,直接丢弃。
我曾经在项目中遇到过一个问题:飞控偶尔会执行错误的指令。排查了三天,最后发现是数传模块的硬件 CRC 校验没开,导致数据在无线传输中发生了比特翻转。从那以后,我坚持在应用层再做一次 CRC 校验,双重保险。
三、字节对齐规则
嗯,这里要注意。MAVLink 1.0 的帧结构是严格按字节对齐的,没有填充字节。也就是说,每个字段紧挨着上一个字段,中间不插空。
为什么强调这个?因为很多嵌入式开发者习惯了结构体默认的 4 字节对齐,写代码时容易出错。比如:
// 错误示例:默认 4 字节对齐
typedef struct {
uint8_t stx; // 1 字节
uint8_t len; // 1 字节
uint16_t seq; // 2 字节 → 编译器可能插入 2 字节填充!
uint8_t sys; // 1 字节
uint8_t comp; // 1 字节
uint16_t msg; // 2 字节
} mavlink_header_t;
上面这个结构体,如果编译器按 4 字节对齐,seq 前面会被插入 2 个填充字节,导致整个帧结构错位。正确的做法是使用 __attribute__((packed)) 或 #pragma pack(1):
// 正确示例:1 字节对齐
typedef struct __attribute__((packed)) {
uint8_t stx; // 0xFE
uint8_t len; // 负载长度
uint8_t seq; // 序列号
uint8_t sys; // 系统 ID
uint8_t comp; // 组件 ID
uint8_t msg; // 消息 ID
uint8_t payload[0]; // 柔性数组,实际长度由 len 决定
} mavlink_frame_t;
核心原则:MAVLink 帧结构是“紧凑打包”的,任何编译器优化都不能改变字段顺序或插入填充。否则,不同平台之间无法互通。
四、实战:手动解析一个 MAVLink 帧
光说不练假把式。我们拿一个真实的 MAVLink 帧来拆解。假设串口收到以下 18 个字节:
FE 09 01 01 01 00 00 00 00 00 00 00 00 00 00 00 03 51
我们一步步解析:
| 字节偏移 | 值 | 字段 | 含义 |
|---|---|---|---|
| 0 | 0xFE | STX | 帧起始标志 |
| 1 | 0x09 | LEN | 负载长度 = 9 字节 |
| 2 | 0x01 | SEQ | 序列号 = 1 |
| 3 | 0x01 | SYS | 系统 ID = 1 |
| 4 | 0x01 | COMP | 组件 ID = 1 |
| 5 | 0x00 | MSG | 消息 ID = 0(HEARTBEAT) |
| 6~14 | 9 字节 | PAYLOAD | 心跳包数据 |
| 15~16 | 0x03 0x51 | CKA/CKB | CRC 校验值 |
你看,MSG=0 对应 HEARTBEAT 消息,负载 9 字节。接收方根据 MSG 找到对应的解析函数,把 9 字节负载按 HEARTBEAT 的结构拆开,就能得到飞控的状态信息。
个人经验:我写解析代码时,习惯先校验 CRC,再解析头部。如果 CRC 不对,直接丢弃,省得浪费 CPU。另外,SEQ 的连续性检查也很重要,能帮你发现链路质量问题。
五、MAVLink 2.0 的帧格式变化
简单提一下 MAVLink 2.0。它兼容 1.0,但头部多了几个字段:
- STX:改为 0xFD,用于区分版本
- MSG:从 1 字节扩展到 3 字节,支持更多消息类型
- 新增 FLAGS:1 字节标志位,用于签名、加密等
- 新增 SIGNATURE:可选 13 字节签名,用于安全认证
不过,核心的帧结构思想没变:头部 + 负载 + 校验。你只要把 1.0 吃透了,2.0 上手很快。
六、避坑指南
最后,分享几个我踩过的坑:
- 字节序问题:MAVLink 默认使用小端字节序。如果你在 ARM 大端模式下开发,记得做字节序转换。我曾经因为这个,解析出来的姿态角全是反的。
- 柔性数组:PAYLOAD 用柔性数组
payload[0]声明,不要用固定大小数组。否则结构体大小会包含整个数组,导致 memcpy 时越界。 - CRC 计算范围:CRC 从 LEN 开始算,不包括 STX。很多人把 STX 也算进去,结果校验永远通不过。
- SEQ 回绕:SEQ 到 255 后会回绕到 0。判断丢包时,不要用
seq == last_seq + 1,要用(seq - last_seq) & 0xFF == 1。