4、MAVLink消息结构深度解析:起始标志、负载长度、序列号、系统ID、组件ID、消息ID、负载数据、校验和

各位同学,今天我们来聊聊MAVLink协议最核心的部分——消息结构。说白了,就是无人机和地面站之间互相传递的“电报”到底长什么样。

我刚开始接触MAVLink时,看着那一串十六进制数,说实话有点懵。后来在SITL仿真里调试通信,才真正搞明白每个字节的用途。嗯,今天我就把这块掰开揉碎了讲给你听。

核心要点:MAVLink 2.0的消息帧结构由10个字段组成,总长度最小12字节(无负载时),最大可达283字节(含255字节负载)。每个字段都有其不可替代的作用。

4.1 消息帧全景图

我们先看一张完整的消息结构图。这张图我画了很多遍,每次带新人时都会先让他们看这个。

MAVLink 2.0 消息帧结构 起始标志 0xFD 负载长度 LEN 不兼容 INC 兼容 COMP 序列号 SEQ 系统ID SYS 组件ID COMP 消息ID MSG_ID 负载数据 PAYLOAD 校验和 CRC 签名 (可选) 1字节 1字节 1字节 1字节 1字节 1字节 1字节 3字节 0-255字节 2字节 13字节 关键说明: • MAVLink 1.0 起始标志为 0xFE,MAVLink 2.0 为 0xFD • 消息ID在MAVLink 2.0中扩展为3字节,支持更多消息类型 • 签名段仅在启用安全认证时存在,非必须 • 校验和覆盖整个消息(含负载),但不含起始标志和签名

4.2 逐个字段深度解析

4.2.1 起始标志(Start-of-Frame)

这个字段就一个字节,值固定为 0xFD(MAVLink 2.0)或 0xFE(MAVLink 1.0)。它的作用很简单——告诉接收端:“嘿,一条新消息要来了!”

我在SITL仿真中调试时,经常用Wireshark抓包。如果看到一堆乱码里突然出现 0xFD,就知道这里是一条消息的起点。你想想看,没有这个标志,接收端怎么知道从哪里开始解析?

小技巧:在SITL仿真中,如果你发现地面站收不到数据,先检查起始标志对不对。我曾经花了一下午排查,最后发现是串口配置把 0xFD 当成了控制字符给过滤掉了。

4.2.2 负载长度(Payload Length)

这个字段表示后面跟着的负载数据有多少个字节。取值范围是0到255。为什么是255?因为一个字节最大就是255嘛。

举个例子,HEARTBEAT 消息的负载长度是9字节,而 GPS_RAW_INT 是30字节。接收端读到这个值后,就知道要再收多少个字节才算完。

4.2.3 不兼容标志与兼容标志

这两个字段是MAVLink 2.0新增的。说白了,就是为了向后兼容。

  • 不兼容标志(Incompatibility Flag):如果接收端不理解这个标志位,就必须丢弃整条消息。比如签名位(bit 0)就是典型的不可兼容标志。
  • 兼容标志(Compatibility Flag):接收端不理解可以忽略,继续处理剩余部分。

我个人习惯在自定义消息里,把兼容标志置0,除非真的需要新特性。这样能最大程度保证兼容性。

4.2.4 序列号(Sequence Number)

序列号是一个循环计数器,从0到255,然后回到0。它的作用是什么?

你想想看,在无线通信中,丢包是家常便饭。接收端通过检查序列号,就能知道中间有没有丢消息。比如上一帧是5,这一帧是7,那6号消息肯定丢了。

注意:序列号是每个系统独立维护的,不是全局的。也就是说,无人机A和地面站B各自维护自己的序列号计数器。我曾经在SITL仿真中遇到过一个问题:两个飞控同时发消息,序列号互相干扰,结果地面站以为丢包了。后来才发现是端口配置错了。

4.2.5 系统ID(System ID)

系统ID用来标识网络中不同的设备。比如在SITL仿真中,我通常这样分配:

系统ID 设备 说明
1 飞控(主控) 运行ArduPilot/PX4的SITL实例
2 地面站 Mission Planner / QGroundControl
3 伴计算机 运行视觉处理等任务的辅助计算机
255 广播 发送给所有设备

系统ID的范围是1到255。0是保留值,一般不用。

4.2.6 组件ID(Component ID)

组件ID用来区分同一个系统内的不同组件。比如一个飞控系统里,可能有:

  • 1:自动驾驶仪(主控)
  • 5:GPS模块
  • 100:摄像头
  • 154:云台

为什么要区分组件?因为一条消息可能只针对某个特定组件。比如云台控制指令,你发给飞控主控就没用。

4.2.7 消息ID(Message ID)

消息ID是MAVLink协议里最重要的字段之一。它告诉接收端:“这条消息是什么类型?”

在MAVLink 2.0中,消息ID扩展到了3个字节(24位),支持最多16,777,216种消息类型。常用的消息ID有:

消息ID 消息名称 用途
0 HEARTBEAT 心跳包,表示设备在线
24 GPS_RAW_INT GPS原始数据(经纬度、高度等)
30 ATTITUDE 飞行姿态(横滚、俯仰、偏航)
74 VFR_HUD 飞行仪表数据(空速、地速、高度)

在SITL仿真中,我经常用 mavlink_msg_heartbeat_pack() 来生成心跳包。你只要填好系统ID、组件ID和消息ID,剩下的交给库函数处理。

4.2.8 负载数据(Payload)

负载数据就是消息真正要传递的内容。长度由前面的 LEN 字段决定,最多255字节。

每个消息类型都有自己固定的负载结构。比如 HEARTBEAT 消息的负载包含:

typedef struct __mavlink_heartbeat_t {
    uint8_t  type;           // 飞行器类型(四旋翼、固定翼等)
    uint8_t  autopilot;      // 飞控类型(ArduPilot、PX4等)
    uint8_t  base_mode;      // 基础模式(是否已解锁、是否在自动模式等)
    uint32_t custom_mode;    // 自定义模式(具体飞行模式)
    uint8_t  system_status;  // 系统状态(正常、故障等)
    uint8_t  mavlink_version;// MAVLink协议版本
} mavlink_heartbeat_t;

你看,9个字节就把飞行器的核心状态说清楚了。这就是MAVLink高效的地方。

4.2.9 校验和(Checksum)

校验和是消息完整性的最后一道防线。它使用CRC-16-CCITT算法,覆盖从 LEN 字段开始到负载结束的所有字节。

为什么不算起始标志?因为起始标志是固定的,算进去也没意义。为什么不算签名?因为签名是可选的,加了反而麻烦。

我曾在SITL仿真中故意篡改一个字节,看看地面站会怎么反应。结果地面站直接丢弃了那条消息,并在日志里记录“CRC校验失败”。嗯,这说明校验和机制在正常工作。

避坑指南:如果你自己实现MAVLink解析器,一定要先校验CRC,再解析负载。我曾经见过有人先解析再校验,结果解析到一半发现CRC不对,还得回滚状态。这不是给自己找麻烦吗?

4.3 消息解析实战

光说不练假把式。我们来看一个实际的MAVLink消息帧,把它拆开看看。

假设我在SITL仿真中抓到一个数据包:

FD 09 00 00 01 01 00 00 00 00 01 02 03 04 05 06 07 08 09 12 34

我们来逐字节解析:

字节偏移 值 字段 含义
0 0xFD 起始标志 MAVLink 2.0
1 0x09 负载长度 负载有9个字节
2 0x00 不兼容标志 无不兼容特性
3 0x00 兼容标志 无兼容特性
4 0x01 序列号 第1条消息
5 0x01 系统ID 系统1(飞控)
6 0x01 组件ID 组件1(自动驾驶仪)
7-9 0x00 0x00 0x00 消息ID 消息ID=0,即HEARTBEAT
10-18 0x01 0x02 ... 0x09 负载数据 9字节心跳数据
19-20 0x12 0x34 校验和 CRC-16校验值

你看,一条完整的心跳包就这么解析出来了。在SITL仿真中,你可以用 mavlink_msg_heartbeat_decode() 函数直接解析,不用自己手动算偏移。

4.4 常见问题与避坑

最后,我总结几个我在SITL仿真中踩过的坑:

  • 字节序问题:MAVLink使用小端字节序(Little-Endian)。如果你在x86机器上开发,这没问题。但如果你在ARM或MIPS上做交叉编译,一定要确认字节序配置正确。
  • 校验和计算范围:很多人以为校验和要算整个消息,包括起始标志。其实不是。CRC只从 LEN 字段开始算,到负载结束。起始标志和签名都不参与计算。
  • 序列号溢出:序列号到255后会回到0。如果你的接收端用“当前序列号 > 上次序列号”来判断新消息,那在255->0的瞬间就会出问题。我建议用环形缓冲区的方式处理。
  • 消息ID冲突:如果你自定义消息,一定要使用MAVLink官方预留的“用户自定义”范围(256-511)。不要占用官方已定义的消息ID,否则地面站会解析出乱码。

我的建议:在SITL仿真中调试MAVLink通信时,先用 mavlink_parse_char() 这个函数。它会把消息解析的脏活累活都干了。等你把整个流程跑通了,再考虑自己实现解析器也不迟。

好了,MAVLink消息结构的每个字段,我都给你讲清楚了。从起始标志到校验和,每个字节都有它的使命。下次你在SITL仿真中看到一串十六进制数,应该能一眼认出哪里是消息ID,哪里是负载数据了吧?