3. 消息帧结构

好,咱们进入第三讲。前两章我们把 MAVLink 的协议栈和心跳包聊透了。这一章,我带你看看 MAVLink 的“源代码”——也就是那些 XML 定义文件。

说白了,MAVLink 里所有的消息长什么样、每个字段叫什么、取值范围是多少,全写在这些 XML 里。你想想看,没有这些定义,你连 HEARTBEAT 里哪个字节是系统状态都搞不清。

核心观点:XML 定义文件是 MAVLink 的“宪法”。解析器、代码生成器、甚至地面站里的消息列表,都是从这些 XML 里自动生成的。你改一个 XML,整个生态都跟着变。

3.1 mavlink/mavlink.git 仓库长什么样?

我第一次打开这个仓库时,说实话有点懵。目录挺多,但核心就几个。我挑重要的说。

目录/文件 作用 我个人的关注点
message_definitions/ 存放所有 XML 定义文件,按厂商或协议版本分 这是咱们的主战场,自定义消息都放这里
message_definitions/v1.0/ MAVLink 1.0 和 2.0 的官方标准定义 别被“v1.0”骗了,MAVLink 2.0 的 XML 也在这
message_definitions/ardupilotmega.xml ArduPilot 的扩展定义,量最大 我经常拿它当自定义模板的参考
message_definitions/development.xml 还在草案中的新消息 想提前尝鲜?看这里
generator/ Python 代码生成器,把 XML 转成 C/Python 源码 你写 XML,它帮你生成解析代码
include/ C 语言头文件,生成器的输出目标 实际编译时引用的就是这些 .h 文件

嗯,这里要注意:千万别直接改 include/ 下的 .h 文件。我见过有人这么干,结果下次重新生成时,改的全没了。正确的做法是改 XML,然后跑生成器。

3.2 XML 消息定义语法:从根到叶子

一个 MAVLink XML 文件,结构其实很清晰。我把它拆成三层:根节点 → 枚举 → 消息。

<?xml version="1.0"?>
<mavlink>
    <!-- 版本和包含关系 -->
    <version>3</version>
    <include>common.xml</include>

    <!-- 枚举定义 -->
    <enum name="MY_CUSTOM_STATE">
        <description>自定义状态枚举</description>
        <entry value="0" name="MY_STATE_IDLE">
            <description>空闲</description>
        </entry>
        <entry value="1" name="MY_STATE_BUSY">
            <description>忙碌</description>
        </entry>
    </enum>

    <!-- 消息定义 -->
    <message id="150" name="MY_CUSTOM_MESSAGE">
        <description>我的第一条自定义消息</description>
        <field type="uint8_t" name="state" enum="MY_CUSTOM_STATE">当前状态</field>
        <field type="float" name="temperature">温度值,单位摄氏度</field>
        <field type="char[16]" name="label">标签字符串</field>
    </message>
</mavlink>

这段代码我手写过无数次。咱们拆开看。

3.2.1 枚举(enum)

枚举用来定义一组有名字的常量。比如飞行模式、状态码、错误类型。好处是代码里不用记数字,直接写名字。

  • name:枚举的名字,在 C 代码里会生成对应的 enum 类型。
  • entry:每个枚举值,value 是数字,name 是字符串。
  • description:可选的说明文字,生成文档时很有用。

我的习惯:枚举值从 0 开始,留一些保留值(比如 200-255)给未来扩展。我曾经因为枚举值冲突,导致地面站把“降落中”显示成“紧急迫降”,场面一度很尴尬。

3.2.2 消息(message)

消息是 MAVLink 的核心。每个消息都有一个唯一的 ID(0-255 是 MAVLink 1.0 的范围,2.0 扩展到 0-16777215)。

属性 说明 避坑点
id 消息的唯一标识,接收方靠它区分消息类型 不要和已有消息 ID 冲突,查一下 common.xml
name 消息的名字,建议全大写加下划线 比如 MY_CUSTOM_MESSAGE,别用驼峰
field 消息里的字段,每个字段有类型、名字和描述 字段顺序就是字节序,别乱排

3.2.3 字段(field)

字段的类型决定了它在二进制流里占几个字节。MAVLink 支持的类型我列一下:

  • 整数型:uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, uint64_t, int64_t
  • 浮点型:float(4 字节), double(8 字节)
  • 数组:char[10](字符串), uint8_t[4](字节数组)
  • 特殊:uint8_t_mavlink_version(自动填充协议版本)

注意:MAVLink 2.0 默认使用小端字节序(Little-Endian)。如果你在 ARM 或 x86 上开发,这没问题。但如果你用某些大端 MCU,记得在生成代码时指定字节序。我曾经在 PowerPC 平台上踩过这个坑,解析出来的数据全是反的。

3.3 自定义消息模板:从零开始写一个

好,理论说完了。咱们动手写一个自定义消息。假设我要给无人机加一个“电池健康状态”的消息,包含电压、电流、剩余百分比和电池编号。

第一步:确定消息 ID。我查了一下 common.xml,150-249 是用户自定义范围。我选 150。

第二步:写 XML。

<?xml version="1.0"?>
<mavlink>
    <version>3</version>
    <include>common.xml</include>

    <enum name="BATTERY_HEALTH_FLAG">
        <description>电池健康标志位</description>
        <entry value="0" name="BATTERY_HEALTH_OK">
            <description>正常</description>
        </entry>
        <entry value="1" name="BATTERY_HEALTH_LOW">
            <description>低电量</description>
        </entry>
        <entry value="2" name="BATTERY_HEALTH_CRITICAL">
            <description>严重低电量</description>
        </entry>
    </enum>

    <message id="150" name="BATTERY_HEALTH_EXTENDED">
        <description>扩展电池健康信息,包含电压、电流、百分比和健康标志</description>
        <field type="uint8_t" name="battery_id">电池编号,从0开始</field>
        <field type="float" name="voltage">总电压,单位伏特</field>
        <field type="float" name="current">总电流,单位安培</field>
        <field type="int8_t" name="remaining_pct">剩余百分比,-1表示未知</field>
        <field type="uint8_t" name="health_flags" enum="BATTERY_HEALTH_FLAG">健康标志位</field>
    </message>
</mavlink>

第三步:保存文件。我习惯把它放在 message_definitions/custom/ 下,取名 my_battery.xml。这样和官方文件分开,方便管理。

第四步:生成代码。在仓库根目录跑:

python -m mavgenerate

或者用命令行:

python generator/mavgen.py --lang=C --output=include/custom message_definitions/custom/my_battery.xml

生成后,你会得到一个 include/custom/my_battery.h。里面会有 MAVLINK_MSG_ID_BATTERY_HEALTH_EXTENDED 这个宏,以及对应的结构体和编解码函数。

避坑指南:我曾经把 remaining_pct 定义成 uint8_t,结果发现 -1 表示未知时没法赋值。后来改成 int8_t,问题解决。记住:需要表示“无效值”的字段,用有符号类型。

3.4 本章知识结构图

下面这张图帮你理清 XML 定义文件的整体脉络。从仓库结构到消息定义,再到自定义扩展,一目了然。

MAVLink XML 定义文件知识结构 mavlink/mavlink.git 仓库 message_definitions/ generator/ include/ XML 文件结构 <mavlink> ├── <version> └── <include> 枚举 (enum) <enum name="..."> ├── <entry value="0"/> └── <entry value="1"/> 消息 (message) <message id="150"> ├── <field type="..."/> └── <field type="..."/> 字段类型 uint8_t / int16_t / float char[16] / uint8_t[4] 枚举字段 field type + enum 属性 自动映射到枚举类型 数组字段 固定长度数组 不支持变长数组 自定义消息扩展:写 XML → 生成代码 → 集成使用 消息 ID 150-249 为自定义范围

3.5 小结

这一章我们干了三件事:

  • 搞清楚了 mavlink/mavlink.git 的仓库结构,知道 XML 放哪、生成器在哪、头文件在哪。
  • 学会了 XML 消息定义语法:<mavlink> 根节点、<enum> 枚举、<message> 消息、<field> 字段。
  • 手写了一个自定义电池健康消息的 XML 模板,并生成了 C 代码。

嗯,说实话,自定义消息是 MAVLink 扩展能力里最实用的技能。你想想看,当你需要给无人机加一个官方协议里没有的传感器数据时,自己写个 XML 消息,比硬塞进现有消息里要优雅得多。