2. MAVLink消息结构:消息帧格式详解
各位同学,今天我们来聊聊MAVLink最核心的东西——消息帧结构。说白了,就是数据在线上怎么跑、怎么保证不出错。
我记得刚接触MAVLink那会儿,拿着逻辑分析仪抓了一堆十六进制数据,看得一头雾水。后来把帧格式搞明白了,才恍然大悟——原来每个字节都有它的使命。
2.1 消息帧格式:从起始位到校验
MAVLink 2.0的帧结构,我习惯把它拆成三块:头部、负载、尾部。咱们直接看格式:
| 起始位 | 负载长度 | 序列号 | 系统ID | 组件ID | 消息ID | 负载数据 | 校验和 |
| 0xFD | 1字节 | 1字节 | 1字节 | 1字节 | 3字节 | 0-255字节 | 2字节 |
嗯,这里要注意几个关键点:
- 起始位:MAVLink 1.0用
0xFE,2.0用0xFD。我见过有人混用版本,结果地面站死活不认数据。 - 负载长度:告诉你后面跟了多少字节的负载数据。范围0-255,但实际受MTU限制。
- 序列号:每个组件独立计数,用来检测丢包。我在做中继链路时,就是靠这个字段发现丢包率高达30%。
- 系统ID + 组件ID:唯一标识一个设备上的一个功能模块。比如飞控是系统1,它的GPS模块可能是组件220。
- 消息ID:3字节,支持多达16777216种消息类型。够用了吧?
- 校验和:CRC16-CCITT,保证数据完整性。
核心要点:MAVLink 2.0相比1.0,消息ID从1字节扩展到3字节,还增加了签名机制。如果你还在用1.0,我建议尽快升级——2.0的兼容性做得很好,向下兼容1.0的消息集。
2.2 消息ID与消息类型
消息ID是MAVLink的灵魂。每个ID对应一种消息类型,比如:
| 消息ID | 消息名称 | 说明 |
|---|---|---|
| 0 | HEARTBEAT | 心跳包,每秒至少发1次 |
| 33 | GPS_RAW_INT | GPS原始数据(整数格式) |
| 76 | COMMAND_LONG | 长命令,用于航点上传等 |
| 84 | MISSION_ITEM | 航点任务项 |
为什么会这样设计?你想想看,如果每个厂商都自己定义消息格式,那地面站得适配多少种协议?MAVLink用统一的消息ID,让不同厂家的设备能互相理解。
我个人习惯在代码里这样判断消息类型:
// 伪代码示例
if (msg_id == MAVLINK_MSG_ID_HEARTBEAT) {
// 处理心跳
} else if (msg_id == MAVLINK_MSG_ID_MISSION_ITEM) {
// 处理航点
}
小技巧:调试时可以用mavlink_msg_*_to_send_buffer()系列函数自动打包,不用手动拼字节。我早期踩过字节序的坑,后来全用官方库,省心多了。
2.3 常见消息集:common.xml 与 ardupilotmega.xml
MAVLink的消息定义放在XML文件里。最核心的两个:
- common.xml:官方通用消息集,所有厂商都应该支持。包含心跳、GPS、姿态、航点等基础消息。
- ardupilotmega.xml:ArduPilot扩展消息集,增加了大量飞控专用消息。比如地形跟随、光流、EKF状态等。
我做过一个项目,需要同时兼容Pixhawk和某国产飞控。结果发现国产飞控只实现了common.xml里的消息,ardupilotmega.xml里的扩展消息一概不支持。嗯,这就是为什么我建议:优先用common.xml里的消息,兼容性最好。
来看一个实际的消息定义(从common.xml摘录):
<message id="84" name="MISSION_ITEM">
<description>航点任务项</description>
<field type="uint8_t" name="target_system">目标系统ID</field>
<field type="uint8_t" name="target_component">目标组件ID</field>
<field type="uint16_t" name="seq">航点序号</field>
<field type="uint8_t" name="frame">坐标系(0:全球, 1:局部)</field>
<field type="uint16_t" name="command">命令ID(如16:导航到点)</field>
<field type="uint8_t" name="current">是否为当前航点</field>
<field type="uint8_t" name="autocontinue">是否自动继续</field>
<field type="float" name="param1">参数1(如停留时间)</field>
<field type="float" name="param2">参数2</field>
<field type="float" name="param3">参数3</field>
<field type="float" name="param4">参数4</field>
<field type="int32_t" name="x">纬度/本地X</field>
<field type="int32_t" name="y">经度/本地Y</field>
<field type="float" name="z">高度/本地Z</field>
</message>
你看,一个航点消息包含了目标系统、坐标系、命令类型、7个参数和坐标。这就是MAVLink的威力——用标准化的结构承载复杂的任务数据。
避坑指南:我曾经在解析MISSION_ITEM时,没注意坐标系字段。结果上传的航点用的是局部坐标系,飞控却按全球坐标系去飞,飞机直接往反方向窜出去。后来我强制检查frame字段,再没出过这种问题。
2.4 消息帧结构总览
为了让你更直观地理解,我画了一张图:
这张图把帧结构拆得很清楚。头部10字节,负载0-255字节,尾部2字节校验。你写解析代码时,就按这个顺序逐字节处理。
2.5 消息集的组织方式
MAVLink的消息集用XML管理,每个XML文件定义一组消息。我常用的几个:
- common.xml:基础消息,所有设备必须支持
- ardupilotmega.xml:ArduPilot专用扩展
- uAvionix.xml:ADS-B应答机相关
- icarous.xml:空中避撞系统
实际开发中,我一般先看common.xml里有没有现成的消息。没有的话,再去厂商的扩展XML里找。实在不行,才考虑自定义消息——但自定义消息会破坏兼容性,慎用。
实用建议:用mavgen工具把XML生成C/Python代码,比自己手写解析器靠谱得多。命令很简单:python mavgen.py --lang=C common.xml。生成的头文件里,每个消息都有对应的结构体和打包/解包函数。