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消息名称说明
0HEARTBEAT心跳包,每秒至少发1次
33GPS_RAW_INTGPS原始数据(整数格式)
76COMMAND_LONG长命令,用于航点上传等
84MISSION_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 消息帧结构总览

为了让你更直观地理解,我画了一张图:

MAVLink 2.0 消息帧结构 起始位 0xFD 负载长度 1字节 序列号 1字节 系统ID 1字节 组件ID 1字节 消息ID 3字节 负载数据(0-255字节) 包含消息的具体字段,如坐标、参数等 校验和(CRC16-CCITT) 2字节 关键说明 • 起始位0xFD标识MAVLink 2.0 • 负载长度决定负载字节数 • 序列号用于检测丢包 • 系统ID+组件ID唯一标识设备 • 消息ID支持16777216种消息 • 校验和保证数据完整性 • 总帧头10字节+负载+校验2字节

这张图把帧结构拆得很清楚。头部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。生成的头文件里,每个消息都有对应的结构体和打包/解包函数。