3、MAVLink消息定义与生成:XML消息定义文件、代码生成器(codegen)、自定义消息的创建

好,咱们进入正题。这一章聊的是MAVLink的“骨架”——消息定义和代码生成。说白了,就是你怎么告诉系统“我要发什么数据”,然后系统自动帮你把代码写好。

我记得刚接触PX4那会儿,最头疼的就是自定义消息。官方文档翻来覆去,总觉得差那么一口气。后来踩了几个坑,才摸清楚这里面的门道。今天我把这些经验掰开揉碎了讲给你听。

3.1 XML消息定义文件:MAVLink的“语法书”

MAVLink的消息定义,不是写在C代码里的,而是写在XML文件里。为什么用XML?因为可读性好,结构清晰,而且方便工具解析。你想想看,要是让你在几百个C结构体里找消息定义,那得多痛苦。

每个MAVLink的XML文件,都遵循一个固定的schema。核心结构就这几样:

  • mavlink协议版本:v1.0还是v2.0,现在基本都用v2
  • 消息ID:每个消息的唯一标识,0-255是通用消息,256以上留给自定义
  • 字段定义:消息里包含哪些数据,每个字段的类型、名称、单位
  • 枚举定义:一些固定的状态值,比如飞行模式、GPS fix类型

来看一个最简单的例子。这是心跳包的定义,几乎所有MAVLink通信都从它开始:

<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>
    <field type="uint8_t" name="mavlink_version">MAVLink版本号</field>
</message>

看到没?每个字段都有明确的类型和名字。这里有个细节——mavlink_version字段是自动填充的,你不需要手动赋值。我在项目中见过有人硬写这个字段,结果版本号对不上,通信直接挂掉。

关键点:XML文件是“契约”。发送方和接收方都按这个契约来解析数据。只要契约一致,哪怕两端用的编程语言不同,也能正常通信。

3.2 代码生成器(codegen):把XML变成C代码

XML文件写好了,怎么变成能跑的代码?这就轮到codegen上场了。MAVLink官方提供了一个Python脚本——mavgen.py,专门干这个活。

它的工作流程其实很简单:

  1. 读取XML文件,解析消息定义和枚举
  2. 根据目标语言(C、C++、Python等)生成对应的头文件和源文件
  3. 自动计算消息的序列化/反序列化逻辑
  4. 生成CRC校验码,确保消息完整性

在PX4里,你不需要手动跑这个脚本。构建系统(CMake)会自动调用codegen。你只需要把XML文件放到正确的位置就行。

具体路径是这样的:

PX4-Autopilot/src/modules/mavlink/mavlink/message_definitions/v1.0/

嗯,路径有点长,但别嫌麻烦。我刚开始就放错过目录,结果编译死活过不去,折腾了半天才发现是路径不对。

生成出来的代码长什么样?大概是这样:

typedef struct __mavlink_heartbeat_t {
    uint32_t custom_mode;      /*< 自定义模式 */
    uint8_t type;              /*< 系统类型 */
    uint8_t autopilot;         /*< 飞控类型 */
    uint8_t base_mode;         /*< 系统基础模式 */
    uint8_t system_status;     /*< 系统状态 */
    uint8_t mavlink_version;   /*< MAVLink版本号 */
} mavlink_heartbeat_t;

你看,XML里的每个字段,都变成了结构体的成员。类型、顺序、大小,完全一一对应。这就是codegen的魔力——你只管定义数据,它帮你搞定底层细节。

小技巧:如果你修改了XML文件,一定要重新编译整个项目。只编译单个模块有时候不会触发codegen重新生成代码。我习惯在修改后执行make clean再编译,确保万无一失。

3.3 自定义消息的创建:从需求到实现

讲完了基础,咱们聊聊实战。在PX4里,很多时候你需要定义自己的消息。比如,你想把某个传感器的原始数据通过MAVLink发出去,但官方协议里没有对应的消息。

创建自定义消息,分三步走:

第一步:定义XML消息

在mavlink目录下,新建一个XML文件,或者直接在common.xml里追加。我个人建议新建一个文件,比如my_custom_messages.xml,方便管理。

<?xml version="1.0"?>
<mavlink>
    <include>common.xml</include>
    
    <message id="150" name="CUSTOM_SENSOR_DATA">
        <description>自定义传感器数据</description>
        <field type="uint64_t" name="timestamp">时间戳,单位微秒</field>
        <field type="float" name="value_1">传感器值1</field>
        <field type="float" name="value_2">传感器值2</field>
        <field type="uint8_t" name="sensor_id">传感器ID</field>
    </message>
</mavlink>

注意消息ID的选择。官方协议里,0-255是保留给通用消息的。自定义消息建议从256开始,避免冲突。但如果你只是内部测试,用150-255之间的ID也可以,只要确保不和现有消息重复。

警告:消息ID一旦确定,就不要轻易修改。如果地面站和飞控的ID不一致,数据解析会完全错乱。我曾经在项目中期改过一次ID,结果所有历史数据都得重新解析,教训深刻。

第二步:注册消息处理器

XML定义好了,codegen会自动生成对应的结构体和编解码函数。但要让消息真正跑起来,你还需要在PX4的MAVLink模块里注册它。

打开mavlink_receiver.cpp,找到消息处理函数。添加你的消息处理逻辑:

// 在 mavlink_receiver.cpp 中
void MavlinkReceiver::handle_message_custom_sensor_data(mavlink_message_t *msg)
{
    mavlink_custom_sensor_data_t data;
    mavlink_msg_custom_sensor_data_decode(msg, &data);
    
    // 处理数据,比如发布到uORB
    struct custom_sensor_s sensor_data = {};
    sensor_data.timestamp = data.timestamp;
    sensor_data.value_1 = data.value_1;
    sensor_data.value_2 = data.value_2;
    sensor_data.sensor_id = data.sensor_id;
    
    _custom_sensor_pub.publish(sensor_data);
}

这里有个坑——消息名称的映射。codegen生成的函数名是mavlink_msg_xxx_decode,其中xxx是XML里定义的消息名,全部小写。我见过有人写错大小写,编译报错半天找不到原因。

第三步:发送自定义消息

接收搞定了,发送呢?在需要发送数据的地方,调用编码函数:

// 在某个模块中发送自定义消息
mavlink_message_t msg;
mavlink_custom_sensor_data_t data;

data.timestamp = hrt_absolute_time();
data.value_1 = sensor.value1;
data.value_2 = sensor.value2;
data.sensor_id = 1;

mavlink_msg_custom_sensor_data_encode(
    mavlink_system.sysid,
    mavlink_system.compid,
    &msg,
    &data
);

// 通过MAVLink通道发送
_mavlink_link->send_message(&msg);

注意mavlink_system.sysid和mavlink_system.compid这两个参数。它们分别代表系统ID和组件ID。地面站就是靠这两个ID来区分不同的设备。如果你有多架无人机同时通信,系统ID必须唯一。

3.4 知识体系总览

说了这么多,咱们用一张图把整个流程串起来。从需求到代码,每一步都环环相扣。

MAVLink消息定义与生成流程 通信需求分析 XML消息定义文件 消息ID、字段、枚举 代码生成器(codegen) mavgen.py自动处理 C/C++头文件 + 编解码函数 PX4 MAVLink模块集成 关键:消息ID唯一性、字段对齐、CRC校验、系统ID/组件ID配置

这张图把整个流程分成了五个阶段。从需求分析开始,到XML定义,再到codegen自动生成代码,最后集成到PX4里。每一步都有明确的输入输出,你照着这个流程走,基本不会出大问题。

3.5 避坑指南

最后,分享几个我踩过的坑,希望能帮你省点时间:

  • 字段对齐问题:MAVLink的消息是紧密排列的,没有填充字节。如果你在XML里定义了uint8_t后面紧跟uint32_t,在C语言里结构体可能会有对齐填充,导致编解码错位。解决办法是把大类型字段放在前面,小类型放后面。
  • CRC校验失败:每次修改XML后,codegen会重新计算消息的CRC。如果你在接收端用了旧版本的代码,CRC校验会失败,消息被直接丢弃。记住,发送端和接收端的XML必须完全一致。
  • 消息ID冲突:在common.xml里追加消息时,一定要先检查ID是否被占用。我习惯用grep命令搜一遍:grep 'id="150"' *.xml。
  • 忘记include:如果你新建了XML文件,别忘了在common.xml里用<include>把它引进来。否则codegen根本不会处理你的自定义消息。

嗯,差不多就这些。XML定义、codegen生成、自定义消息创建,这三块是MAVLink开发的基础。你把这些搞明白了,后面不管是调试还是扩展功能,都会顺手很多。