3、MAVLink消息定义与XML规范
聊到MAVLink的消息定义,我脑子里第一个蹦出来的就是那堆XML文件。说实话,刚接触的时候我也觉得有点懵——怎么一个通信协议还要用XML来定义?后来做项目多了才明白,这恰恰是MAVLink最聪明的地方。
3.1 消息定义文件的结构
MAVLink的消息定义,核心就靠几个XML文件。最常用的是common.xml和ardupilotmega.xml。前者是所有飞控厂商都遵守的公共定义,后者是ArduPilot生态的扩展。
我习惯把common.xml看作「通用字典」,里面定义了所有飞控都应该理解的消息。比如心跳包、姿态数据、GPS信息这些。而ardupilotmega.xml则是「方言」,里面是ArduPilot特有的消息,比如一些高级调试参数。
来看一个典型的消息定义长什么样:
<mavlink>
<version>2</version>
<messages>
<message id="0" name="HEARTBEAT">
<description>心跳包,用于检测连接状态</description>
<field type="uint8_t" name="type">飞行器类型</field>
<field type="uint8_t" name="autopilot">飞控类型</field>
<field type="uint8_t" name="base_mode">基础模式标志位</field>
<field type="uint32_t" name="custom_mode">自定义模式</field>
<field type="uint8_t" name="system_status">系统状态</field>
</message>
</messages>
</mavlink>
嗯,这里要注意:每个消息都必须有唯一的ID。ID的范围是0到255,其中0到199是公共消息,200到255留给厂商自定义。
3.2 消息ID分配规则
消息ID的分配,说白了就是「谁先占坑谁先用」。我刚开始做无人机项目时,就踩过这个坑——自己定义了一个消息ID=200,结果跟别人的冲突了,调试了整整两天才发现。
官方推荐的分配规则是这样的:
| ID范围 | 用途 | 示例 |
|---|---|---|
| 0 - 49 | 核心协议消息 | HEARTBEAT(0), SYS_STATUS(1) |
| 50 - 99 | 传感器数据 | GPS_RAW(24), ATTITUDE(30) |
| 100 - 149 | 执行器控制 | SET_ATTITUDE_TARGET(82) |
| 150 - 199 | 任务与规划 | MISSION_ITEM(39) |
| 200 - 255 | 厂商自定义 | ArduPilot专用消息 |
我个人建议:如果你在开发自己的飞控,尽量先用公共ID范围内的消息。实在不够用了,再去申请厂商自定义ID。别问我为什么知道——我曾经图省事直接用了ID=200,结果后来要跟PX4通信,两边消息定义对不上,改代码改到怀疑人生。
3.3 消息负载字段的数据类型
MAVLink支持的数据类型不多,但够用。常用的就这几种:
- uint8_t / int8_t:1字节,适合标志位、小范围数值
- uint16_t / int16_t:2字节,适合角度、小范围传感器值
- uint32_t / int32_t:4字节,适合时间戳、计数器
- float:4字节,适合GPS坐标、姿态角
- char数组:变长,适合文本信息
这里有个坑:MAVLink的float是IEEE 754标准的单精度浮点数。我遇到过有人用double,结果编译出来消息长度对不上,通信直接崩了。记住,MAVLink不支持double,只能用float。
看一个实际的消息定义:
<message id="24" name="GPS_RAW_INT">
<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">海拔(mm)</field>
<field type="uint16_t" name="eph">水平精度(cm)</field>
<field type="uint16_t" name="epv">垂直精度(cm)</field>
<field type="uint16_t" name="vel">速度(cm/s)</field>
<field type="uint16_t" name="cog">航向角(度×100)</field>
<field type="uint8_t" name="satellites_visible">可见卫星数</field>
</message>
你想想看,为什么GPS坐标要用int32_t而不是float?因为int32_t的精度更高,而且传输过程中不会丢失精度。float只有23位尾数,而int32_t有31位有效位。这就是MAVLink设计者的用心之处。
3.4 枚举与位掩码定义
枚举和位掩码,是MAVLink里用来表示状态和选项的利器。枚举适合互斥的状态,位掩码适合可以同时存在的标志。
先看枚举的例子:
<enum name="MAV_TYPE">
<entry value="0" name="MAV_TYPE_GENERIC">通用飞行器</entry>
<entry value="1" name="MAV_TYPE_FIXED_WING">固定翼</entry>
<entry value="2" name="MAV_TYPE_QUADROTOR">四旋翼</entry>
<entry value="3" name="MAV_TYPE_HELICOPTER">直升机</entry>
<entry value="4" name="MAV_TYPE_GROUND_ROVER">地面车辆</entry>
</enum>
再看位掩码:
<enum name="MAV_MODE_FLAG" bitmask="true">
<entry value="1" name="MAV_MODE_FLAG_CUSTOM_MODE_ENABLED">自定义模式</entry>
<entry value="2" name="MAV_MODE_FLAG_TEST_ENABLED">测试模式</entry>
<entry value="4" name="MAV_MODE_FLAG_AUTO_ENABLED">自动模式</entry>
<entry value="8" name="MAV_MODE_FLAG_GUIDED_ENABLED">引导模式</entry>
<entry value="16" name="MAV_MODE_FLAG_STABILIZE_ENABLED">稳定模式</entry>
<entry value="32" name="MAV_MODE_FLAG_HIL_ENABLED">硬件在环</entry>
<entry value="64" name="MAV_MODE_FLAG_MANUAL_INPUT_ENABLED">手动输入</entry>
<entry value="128" name="MAV_MODE_FLAG_SAFETY_ARMED">已解锁</entry>
</enum>
注意看,位掩码的value都是2的幂次方。这样设计的好处是,你可以用按位或运算组合多个状态。比如MAV_MODE_FLAG_AUTO_ENABLED | MAV_MODE_FLAG_SAFETY_ARMED就表示「自动模式已解锁」。
核心要点:枚举用value表示互斥状态,位掩码用2的幂次方表示可组合标志。两者不要混用,否则解析的时候会出大问题。
我的小技巧:定义枚举时,尽量给每个entry都加上description。虽然不强制,但后来维护代码的人会感谢你。我曾经接手过一个项目,枚举值没有注释,我花了整整一天才搞清楚每个值的含义。
避坑指南:我曾经在定义位掩码时,不小心把两个entry的value写成了相同的值。结果在解析消息时,这两个标志位永远同时为真。调试了三天才发现是XML定义写错了。所以,写完XML后一定要用官方工具验证一下。
3.5 知识体系总览
下面这张图,是我自己总结的MAVLink消息定义的知识体系。你可以把它当作一个快速索引:
这张图把消息定义的几个核心要素串起来了。你从中心节点出发,沿着分支往下看,就能理清整个知识脉络。我个人做项目时,经常把这张图打印出来贴在工位上,写XML定义的时候对照着看,基本不会出错。
好了,关于消息定义与XML规范,核心内容就这些。记住:XML文件是MAVLink的「宪法」,所有通信行为都得按它来。定义消息时,ID别冲突,类型别用错,枚举和位掩码别混用。做到这几点,你的MAVLink通信基本就稳了。