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循环。我曾经调试丢包问题时,就是靠序列号发现飞控每隔一段时间就会跳过一个号——嗯,串口缓冲区溢出了。
系统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校验。它覆盖了从“负载长度”到“负载”的所有字节。我曾经犯过一个低级错误:自己组包时忘了把校验和算进去,结果飞控死活不认。排查了半天才发现是校验和算错了。
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))。
2.4 消息帧结构图
下面这张图,把整个帧结构串起来了。你可以对照着看,每个字节在帧里的位置。
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。判断丢包时,要考虑循环。