4、MAVLink消息结构深度解析:起始标志、负载长度、序列号、系统ID、组件ID、消息ID、负载数据、校验和
各位同学,今天我们来聊聊MAVLink协议最核心的部分——消息结构。说白了,就是无人机和地面站之间互相传递的“电报”到底长什么样。
我刚开始接触MAVLink时,看着那一串十六进制数,说实话有点懵。后来在SITL仿真里调试通信,才真正搞明白每个字节的用途。嗯,今天我就把这块掰开揉碎了讲给你听。
核心要点:MAVLink 2.0的消息帧结构由10个字段组成,总长度最小12字节(无负载时),最大可达283字节(含255字节负载)。每个字段都有其不可替代的作用。
4.1 消息帧全景图
我们先看一张完整的消息结构图。这张图我画了很多遍,每次带新人时都会先让他们看这个。
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,哪里是负载数据了吧?