2. MAVLink消息结构:消息帧格式详解

好,咱们今天来聊聊MAVLink的消息帧结构。说白了,就是数据在空中飞的时候,到底长什么样。

我记得刚接触MAVLink那会儿,看着一堆十六进制数,头都大了。后来拆开一个个字节看,才发现其实特别规整。你只要掌握了这个帧格式,解析任何MAVLink数据包都不在话下。

2.1 消息帧的整体结构

MAVLink 1.0和2.0的帧结构略有不同,但核心思想一致。我们先看最常用的MAVLink 1.0版本。

一个完整的MAVLink消息帧,由以下几个字段组成:

字段 长度(字节) 说明
起始标志 1 固定值 0xFE(MAVLink 1.0)
负载长度 1 Payload部分的字节数
序列号 1 0~255,循环递增
系统ID 1 标识发送方系统
组件ID 1 标识系统内的具体组件
消息ID 1 标识消息类型
负载 0~255 实际数据内容
校验和 2 CRC-16校验

嗯,这里要注意:MAVLink 2.0的起始标志是0xFD,消息ID扩展到了3个字节。但基础框架是一样的。

2.2 各字段详解

起始标志

这是帧的“身份证”。接收端一看到0xFE(V1)或0xFD(V2),就知道一个新包来了。我在项目中遇到过一个问题:串口缓冲区里偶尔会出现0xFE,但那不是帧头,而是干扰数据。所以光靠起始标志不够,还得配合校验。

负载长度

告诉你后面跟着多少字节的有效数据。范围0~255。为什么是255?因为1个字节最大就是255。你想想看,如果负载长度和实际对不上,那这个包基本就废了。

序列号

这个字段容易被忽略,但其实特别重要。每个包都有一个递增的序列号,从0到255循环。我曾经调试丢包问题时,就是靠序列号发现飞控每隔一段时间就会跳过一个号——嗯,串口缓冲区溢出了。

小技巧: 序列号可以用来检测丢包和乱序。如果连续收到两个包,序列号差大于1,那中间肯定有包丢了。

系统ID与组件ID

这两个字段一起用,能唯一标识一个发送者。

  • 系统ID: 比如飞控是1,地面站是255。
  • 组件ID: 比如飞控里的GPS模块是220,IMU是81。

我个人的习惯是:系统ID尽量用1~250,255留给地面站。这样不容易冲突。

消息ID

这是灵魂字段。它告诉接收方:这个包是心跳包(HEARTBEAT,ID=0),还是姿态数据(ATTITUDE,ID=30),还是GPS信息(GLOBAL_POSITION_INT,ID=33)。

消息ID决定了你怎么解析后面的负载数据。比如消息ID=0,你就按心跳包的格式去解析;消息ID=30,你就按姿态数据的格式去解析。

负载

这才是真正的数据。长度由前面的“负载长度”字段决定。具体怎么解析,得看消息ID对应的定义。

校验和

2个字节的CRC-16校验。它覆盖了从“负载长度”到“负载”的所有字节。我曾经犯过一个低级错误:自己组包时忘了把校验和算进去,结果飞控死活不认。排查了半天才发现是校验和算错了。

注意: MAVLink的CRC校验还额外包含一个“密钥字节”(extra byte),这个字节由消息ID决定。所以不同消息类型的CRC算法其实不一样。别拿通用CRC-16去算,会出问题。

2.3 字节序与填充规则

这个坑我踩过,所以得重点说说。

MAVLink使用小端字节序(Little-Endian)。什么意思?就是低字节在前,高字节在后。

举个例子:一个16位的数值0x1234,在小端模式下存储为:

字节0: 0x34
字节1: 0x12

如果你用大端去解析,读出来就是0x3412,完全错了。我在做地面站时,就因为这个原因,读出来的高度数据一直是错的,后来才发现是字节序没转换。

填充规则

MAVLink的负载字段没有填充(padding)要求。数据是紧凑排列的,一个字节挨着一个字节。但要注意:结构体对齐可能会引入填充。比如你在C语言里定义了一个结构体:

typedef struct {
    uint8_t  a;  // 1字节
    uint16_t b;  // 2字节
} my_msg_t;

编译器可能会在a后面填充1个字节,让b对齐到2字节边界。但MAVLink不认这个填充。所以发送时,你得手动把结构体“打包”成紧凑格式,或者用__attribute__((packed))。

核心要点: MAVLink的负载是紧凑排列的,没有填充。任何结构体对齐产生的填充字节,都会导致解析错误。

2.4 消息帧结构图

下面这张图,把整个帧结构串起来了。你可以对照着看,每个字节在帧里的位置。

MAVLink 1.0 消息帧结构 MAVLink 1.0 消息帧结构 起始标志 0xFE 负载长度 1字节 序列号 0~255 系统ID 1字节 组件ID 1字节 消息ID 1字节 负载 0~255字节 校验和 2字节 说明: • 起始标志:固定值0xFE(V1)或0xFD(V2),用于帧同步 • 负载长度:Payload的实际字节数,范围0~255 • 序列号:0~255循环递增,用于检测丢包和乱序 • 系统ID + 组件ID:唯一标识发送者 • 消息ID:决定负载的解析方式 • 校验和:CRC-16,覆盖负载长度到负载的所有字节

2.5 实际组包示例

光说不练假把式。咱们手动组一个心跳包看看。

心跳包(HEARTBEAT)的消息ID是0,负载长度是9个字节(V1版本)。假设系统ID=1,组件ID=1,序列号=0。

那么完整的帧就是:

起始标志: 0xFE
负载长度: 0x09
序列号:   0x00
系统ID:   0x01
组件ID:   0x01
消息ID:   0x00
负载:     9个字节(具体内容取决于心跳包定义)
校验和:   2个字节(CRC-16计算结果)

你把这个序列发出去,接收端就能正确解析出心跳包。我刚开始学的时候,就是拿这种最简单的包练手,一步步拆解,很快就上手了。

2.6 避坑指南

最后,分享几个我踩过的坑:

  • 校验和算错: 记得包含那个“密钥字节”。不同消息ID的密钥字节不一样,查表吧。
  • 字节序搞反: 多字节数值一定要用小端解析。尤其是做地面站时,C#和Java默认是大端,得手动转。
  • 结构体对齐: 发送前确保结构体是紧凑的。用#pragma pack(1)或__attribute__((packed))。
  • 序列号溢出: 255之后是0,不是256。判断丢包时,要考虑循环。