2. 消息结构与编码

各位同学,欢迎来到第二章。上一章我们聊了MAVLink的起源和整体架构,今天咱们要动真格的了——直接拆解MAVLink消息的骨头和肉。

说白了,MAVLink就是一个二进制协议。你往地面站发一条数据,比如当前高度、GPS坐标,这些信息不是随便扔过去的,得按照严格的格式打包。这个格式,就是消息帧。

2.1 消息帧格式:从起始位到校验

我刚开始接触MAVLink时,第一反应是:“这玩意儿怎么这么啰嗦?” 后来踩了几个坑才明白,每一个字节都有它的道理。先看最经典的MAVLink 1.0帧结构:

字节偏移 字段名 长度(字节) 说明
0 STX(起始标志) 1 固定值 0xFE(MAVLink 1.0)
1 LEN(负载长度) 1 Payload部分的字节数,0~255
2 SEQ(序列号) 1 每个组件独立计数,用于丢包检测
3 SYS(系统ID) 1 标识飞控、地面站等不同系统
4 COMP(组件ID) 1 标识同一系统内的不同模块
5 MSG ID(消息ID) 1 0~255,标识消息类型
6 ~ 6+LEN-1 Payload(负载) LEN 实际数据,按消息定义排列
6+LEN CKA(校验位A) 1 CRC低字节
6+LEN+1 CKB(校验位B) 1 CRC高字节

关键点:MAVLink 2.0 的起始标志是 0xFD,并且支持24位消息ID(0~16777215),还多了个不签名的标志位。我个人建议新项目直接上2.0,消息空间大得多。

这里有个细节很多人会忽略——校验位。MAVLink的CRC不是只算Payload,而是把整个消息(除了STX和校验位本身)都算进去。我曾经在调试一个地面站时,发现飞控发来的数据偶尔会解析出离谱的值,排查了半天,最后发现是CRC计算时漏掉了SEQ字段。嗯,从那以后我写CRC校验代码都格外小心。

小技巧:MAVLink官方提供了CRC的额外字节(CRC_EXTRA),用于消息签名。如果你自己手写解析器,记得查表,别自己算,容易出错。

2.2 微服务与消息集

MAVLink不是一个大而全的协议,它更像一个工具箱。每个工具(消息)完成一个特定任务。这些任务被组织成微服务。

什么叫微服务?举个例子,HEARTBEAT消息属于“核心协议”微服务,它每隔1秒发一次,告诉地面站“我还活着”。而GPS_RAW_INT属于“定位”微服务,专门传GPS数据。

你想想看,如果没有微服务的概念,所有消息混在一起,你都不知道哪些消息是必须实现的,哪些是可选的。微服务把消息分成了几个等级:

  • 必选微服务:比如HEARTBEAT、SYS_STATUS,任何MAVLink设备都必须支持。
  • 推荐微服务:比如定位、姿态,大部分飞控都会实现。
  • 可选微服务:比如摄像头控制、光流传感器,看具体硬件。

消息集(Message Set)就是这些微服务的集合。每个消息集对应一个XML定义文件。比如common.xml包含了最常用的消息,ardupilotmega.xml是ArduPilot的扩展消息集。

注意:不同飞控厂商可能会在同一个消息ID上定义不同的内容。比如消息ID #150,在PX4和ArduPilot里可能含义不同。所以地面站开发时,一定要根据系统ID或自动识别飞控类型来切换消息集。

2.3 XML消息定义文件解析

好了,现在到了最实用的部分——怎么读懂MAVLink的XML定义文件。说白了,这就是协议的“说明书”。

我习惯把XML文件当成数据库表结构来看。每个消息就是一张表,每个字段就是表的列。来看一个真实的例子:

<mavlink>
    <version>2</version>
    <messages>
        <message id="24" name="GPS_RAW_INT">
            <description>GPS原始数据(整数格式)</description>
            <field type="uint64_t" name="time_usec">时间戳(微秒)</field>
            <field type="int32_t" name="lat">纬度(度 * 1e7)</field>
            <field type="int32_t" name="lon">经度(度 * 1e7)</field>
            <field type="int32_t" name="alt">海拔高度(毫米)</field>
            <field type="uint16_t" name="eph">水平定位精度(厘米)</field>
            <field type="uint16_t" name="epv">垂直定位精度(厘米)</field>
            <field type="uint16_t" name="vel">地面速度(厘米/秒)</field>
            <field type="uint16_t" name="cog">地面航向(度 * 100)</field>
            <field type="uint8_t" name="satellites_visible">可见卫星数</field>
        </message>
    </messages>
</mavlink>

看到没?每个字段都有type、name和description。类型决定了它在内存中占几个字节,以及怎么解析。比如int32_t就是4字节有符号整数,小端字节序。

这里有个坑——缩放因子。你看lat字段的描述是“度 * 1e7”,意思是飞控发来的整数是真实纬度的10000000倍。如果你直接拿来用,那纬度就变成几千万度了,无人机直接飞到外太空。我当年第一次解析GPS数据时就犯过这个错,显示的位置在非洲,实际飞机在北京。

经验之谈:解析MAVLink消息时,一定要看XML里的描述。很多字段都有缩放因子,比如角度可能是度*100,速度可能是厘米/秒。写代码时最好把缩放逻辑封装成一个函数,避免到处写魔法数字。

另外,XML里还有枚举类型和位掩码的定义。比如MAV_MODE枚举定义了飞控的各种模式:

<enum name="MAV_MODE">
    <entry value="0" name="MAV_MODE_PREFLIGHT">预飞模式</entry>
    <entry value="2" name="MAV_MODE_STABILIZE">自稳模式</entry>
    <entry value="3" name="MAV_MODE_GUIDED">引导模式</entry>
    <entry value="4" name="MAV_MODE_AUTO">自动模式</entry>
</enum>

这些枚举值在解析HEARTBEAT消息时特别有用。你收到一个base_mode字段,值是4,就知道飞控当前在自动模式。

最后,我建议你养成一个习惯:不要手动解析XML。MAVLink官方提供了代码生成工具,可以自动生成C、Python、C++等语言的解析代码。你只需要把XML文件扔进去,工具就会帮你生成结构体和序列化/反序列化函数。我自己在项目中就用Python的pymavlink库,它已经内置了所有消息集,开箱即用。

不过,理解XML的结构依然很重要。因为当你遇到自定义消息或者调试奇怪的问题时,能直接看懂XML定义,比依赖工具要快得多。

MAVLink消息帧结构解析流程 原始字节流 帧头解析 负载提取 CRC校验 消息ID匹配 XML定义映射 关键检查点: 1. 起始标志是否为0xFD(V2)或0xFE(V1) 2. 负载长度是否与Payload实际大小一致 3. CRC校验是否通过(防止数据损坏) 消息ID匹配要点: • 根据消息ID查找对应的XML定义 • 注意不同飞控可能使用不同消息集

这张图展示了我自己写解析器时的标准流程。从原始字节流开始,先解析帧头,提取负载,然后做CRC校验。校验通过后,根据消息ID去XML定义里找对应的字段布局,最后把二进制数据映射成可读的结构体。