4. 消息定义与XML规范:理解XML消息定义文件、消息ID分配规则、字段类型
好,咱们进入第四章。这一章要聊的是MAVLink的“语法书”——XML消息定义文件。
你想想看,无人机和地面站之间要传那么多数据:GPS坐标、姿态角、电池电压、心跳包……如果没有一套统一的“说话方式”,那不乱套了?XML规范就是干这个的。它把每一条消息长什么样、里面装什么数据、数据占几个字节,全都写得清清楚楚。
我个人习惯,拿到一个新协议栈,第一件事就是翻它的XML定义。这比看代码快多了,一眼就能知道整个通信体系里有哪些消息。
4.1 XML消息定义文件的结构
MAVLink的XML文件,说白了就是一个消息的“户口本”。每个文件对应一个“方言”(dialect),比如common.xml是通用消息集,ardupilotmega.xml是ArduPilot的扩展。
一个典型的消息定义长这样:
<mavlink>
<version>2</version>
<dialect>1</dialect>
<enums>
<enum name="MAV_TYPE">
<description>无人机/车辆类型枚举</description>
<entry value="0" name="MAV_TYPE_GENERIC">
<description>通用类型</description>
</entry>
<entry value="1" name="MAV_TYPE_FIXED_WING">
<description>固定翼</description>
</entry>
<entry value="2" name="MAV_TYPE_QUADROTOR">
<description>四旋翼</description>
</entry>
</enum>
</enums>
<messages>
<message id="0" name="HEARTBEAT">
<description>心跳包,用于检测系统是否在线</description>
<field type="uint8_t" name="type" enum="MAV_TYPE">车辆类型</field>
<field type="uint8_t" name="autopilot" enum="MAV_AUTOPILOT">飞控类型</field>
<field type="uint8_t" name="base_mode" enum="MAV_MODE_FLAG">系统模式</field>
<field type="uint32_t" name="custom_mode">自定义模式</field>
<field type="uint8_t" name="system_status" enum="MAV_STATE">系统状态</field>
</message>
</messages>
</mavlink>
结构其实很清晰:
- <mavlink>:根节点,整个文件的“大管家”
- <enums>:枚举定义区,放各种常量、状态码
- <messages>:消息定义区,每条消息一个<message>节点
每个<message>节点里,id属性就是消息ID,name是消息名称。里面的<field>子节点定义每个字段的类型、名称和含义。
4.2 消息ID分配规则
消息ID是MAVLink通信的“身份证”。每条消息都有一个唯一的ID,接收方靠这个ID知道该解析哪个消息。
ID分配有明确的规则:
| ID范围 | 用途 | 示例 |
|---|---|---|
| 0 - 255 | 核心消息(common.xml) | HEARTBEAT (0), SYS_STATUS (1) |
| 256 - 511 | MAVLink 1 扩展 | ATTITUDE (30), GLOBAL_POSITION_INT (33) |
| 512 - 1023 | MAVLink 2 扩展 | 新协议版本引入的消息 |
| 1024 - 2047 | 厂商自定义消息 | 各飞控厂商的私有消息 |
| 2048 - 65535 | 保留/未来使用 | 暂未分配 |
这里有个坑,我踩过。有一次我给一个自定义传感器写驱动,随手选了个ID 300。结果发现这个ID已经被某个飞控的扩展消息占用了。两台设备一通信,地面站直接崩溃——它把传感器数据当成姿态角来解析了。
4.3 字段类型详解
MAVLink的字段类型,说白了就是C语言基本类型的一个子集。但有几个地方特别容易搞错。
4.3.1 基础类型
| XML类型 | C语言对应 | 字节数 | 取值范围 |
|---|---|---|---|
| uint8_t | unsigned char | 1 | 0 ~ 255 |
| int8_t | signed char | 1 | -128 ~ 127 |
| uint16_t | unsigned short | 2 | 0 ~ 65535 |
| int16_t | signed short | 2 | -32768 ~ 32767 |
| uint32_t | unsigned int | 4 | 0 ~ 4.29e9 |
| int32_t | signed int | 4 | -2.14e9 ~ 2.14e9 |
| uint64_t | unsigned long long | 8 | 0 ~ 1.84e19 |
| int64_t | signed long long | 8 | -9.22e18 ~ 9.22e18 |
| float | float | 4 | ±1.18e-38 ~ ±3.4e38 |
| double | double | 8 | ±2.23e-308 ~ ±1.79e308 |
| char | char | 1 | ASCII字符 |
4.3.2 数组和字符串
MAVLink支持定长数组,写法是type[长度]。比如:
<field type="char[20]" name="text">文本消息内容</field>
<field type="uint8_t[4]" name="quaternion">四元数</field>
字符串本质上就是char[]数组。注意:MAVLink的字符串不自动加'\0'结尾!这一点和C语言的习惯不一样。我当年第一次写MAVLink的字符串处理,直接用strcpy去拷贝,结果缓冲区溢出了,飞控直接重启。
4.3.3 枚举类型
XML里定义的枚举,在生成代码时会变成C语言的enum。比如MAV_TYPE枚举:
typedef enum MAV_TYPE {
MAV_TYPE_GENERIC = 0,
MAV_TYPE_FIXED_WING = 1,
MAV_TYPE_QUADROTOR = 2,
// ...
} MAV_TYPE;
字段里引用枚举时,用enum="枚举名"属性。这样生成的代码会自动做类型检查,比直接用裸数字安全多了。
4.4 消息定义的最佳实践
做了这么多年嵌入式,我总结了几条写XML消息定义的经验:
- 字段顺序就是内存布局:把经常一起访问的字段放一起,能提高缓存命中率
- 对齐很重要:MAVLink默认不做内存对齐,但如果你把uint8_t和uint32_t混着放,解析时会有性能损失。我习惯把同类型的字段放一起
- 枚举值别乱改:一旦发布,枚举值就不能改了。改了就是破坏兼容性,所有旧设备都得升级
- 预留扩展位:设计消息时,留几个保留字段。我见过太多因为没留扩展位,后面加功能只能重新定义新消息的情况
下面这张图,是我自己画的消息定义流程,帮你理清思路:
4.5 实战:从零定义一个自定义消息
光说不练假把式。咱们来定义一个实际的消息——给一个气压传感器用的。
假设传感器输出:温度(摄氏度,float)、气压(百帕,float)、海拔(米,float)。
<?xml version="1.0"?>
<mavlink>
<version>2</version>
<dialect>1</dialect>
<messages>
<message id="1024" name="BARO_SENSOR_DATA">
<description>气压传感器数据,包含温度、气压和海拔</description>
<field type="uint64_t" name="time_usec">时间戳,微秒</field>
<field type="float" name="temperature">温度,摄氏度</field>
<field type="float" name="pressure">气压,百帕</field>
<field type="float" name="altitude">海拔,米</field>
<field type="uint8_t" name="sensor_id">传感器ID,支持多传感器</field>
</message>
</messages>
</mavlink>
注意几个细节:
- ID用了1024,在自定义区,不会和标准消息冲突
- 加了时间戳字段,方便地面站做时间同步
- 留了sensor_id,万一以后挂多个气压计,不用重新定义消息
嗯,这就是我平时写自定义消息的套路。先想清楚要传什么数据,再分配ID,最后定义字段。顺序别搞反了——我见过有人先写XML再想用途,结果消息定义得乱七八糟,后面改都改不动。
好了,这一章的内容就到这儿。XML规范看着枯燥,但它是MAVLink通信的基石。你把这个搞明白了,后面写代码、调bug都会顺手很多。