4. MAVLink 数据封装与解析:从 XML 到 C 代码的完整链路

好,咱们进入第四章。这一章我打算聊聊 MAVLink 的“骨架”——也就是它的消息定义文件、代码生成工具,还有那两个常用的 C 语言库。说白了,就是搞清楚 MAVLink 的消息到底是怎么定义、怎么生成、怎么在代码里用的。

我记得刚接触 MAVLink 那会儿,第一反应是:这玩意儿怎么这么多文件?后来才明白,它的核心其实就一个 XML 文件。你只要把这个文件搞懂了,整个协议就通了一半。

核心逻辑:MAVLink 的消息结构不是手写的,而是从 XML 定义自动生成的。你改 XML,代码就跟着变。这就是它的设计哲学——定义与实现分离。

4.1 XML 消息定义文件:common.xml 到底长什么样?

common.xml 是所有 MAVLink 消息的“总纲”。它定义了消息 ID、字段、枚举、命令……几乎所有你能想到的东西。我建议你直接去 GitHub 上把最新版的 common.xml 拉下来看看,大概 8000 多行。

它的结构其实很清晰。我拆成三块来讲:

  • 枚举(Enums):定义状态、模式、错误码等常量。比如 MAV_STATE、MAV_MODE。
  • 消息(Messages):定义具体的数据包结构。比如 HEARTBEAT、GPS_RAW_INT。
  • 命令(Commands):定义 MAV_CMD 系列,用于任务和动作。

咱们看一个最简单的例子——心跳包(HEARTBEAT)的定义:

<message id="0" name="HEARTBEAT">
  <description>The heartbeat message shows that a system is present and responding.</description>
  <field type="uint8_t" name="type">Type of the MAV (quadrotor, helicopter, etc.)</field>
  <field type="uint8_t" name="autopilot">Autopilot type / class.</field>
  <field type="uint8_t" name="base_mode">System mode bitmap.</field>
  <field type="uint32_t" name="custom_mode">A bitfield for use for autopilot-specific flags.</field>
  <field type="uint8_t" name="system_status">System status flag.</field>
  <field type="uint8_t" name="mavlink_version">MAVLink version (always 3).</field>
</message>

你看,每个字段都有类型、名字和描述。这就是 MAVLink 的“元数据”。代码生成工具就是靠这些信息来生成结构体和序列化函数的。

我的习惯:每次修改 common.xml 之前,我都会先跑一遍 mavgen 看看生成的代码有没有编译警告。这能省掉很多调试时间。

4.2 代码生成工具:mavgen 是怎么工作的?

mavgen 是 MAVLink 官方提供的代码生成器。它读取 XML 文件,然后输出 C、Python、C++ 等语言的源码。说白了,它就是个“翻译器”——把人类可读的 XML 翻译成机器可编译的代码。

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

  1. 解析 XML,构建内部数据结构(枚举、消息、命令)。
  2. 根据目标语言模板,生成对应的头文件和源文件。
  3. 输出到指定目录。

我常用的命令是这样的:

python mavgen.py --lang=C --output=./generated common.xml

这条命令会生成一堆 .h 和 .c 文件。你想想看,如果你手动去写这些结构体和序列化函数,得花多少时间?而且容易出错。mavgen 一次搞定,还保证一致性。

我曾经踩过的坑:有一次我改了 XML 里的字段顺序,但忘了重新生成代码。结果地面站和飞控对不上数据,折腾了两天才发现是代码没更新。所以,改完 XML 一定要重新跑 mavgen,千万别偷懒。

4.3 C 语言库:mavlink 和 mavsdk 怎么选?

现在有两个主流的 C 语言库:一个是官方的 mavlink 库,另一个是 mavsdk。它们俩的定位不太一样,我列个表对比一下:

特性 mavlink 库 mavsdk
定位 底层协议实现 高层 SDK
API 风格 面向消息,手动编解码 面向对象,自动处理
适用场景 嵌入式系统、资源受限设备 地面站、仿真、快速开发
依赖 无 需要 C++11 及以上
学习曲线 较陡,需要理解协议细节 较平缓,封装度高

我个人习惯是:做飞控固件用 mavlink 库,做地面站或测试工具用 mavsdk。为什么?因为飞控上资源有限,你不想为了一个心跳包去引入整个 C++ 运行时。而地面站上资源充裕,用 mavsdk 能省掉很多重复劳动。

4.4 实战:用 mavlink 库发送一个心跳包

光说不练假把式。咱们写一段实际代码,看看怎么用 mavlink 库发送一个 HEARTBEAT 消息。

#include <mavlink.h>

// 初始化一个 MAVLink 消息
mavlink_message_t msg;
uint8_t buf[MAVLINK_MAX_PACKET_LEN];

// 打包心跳包
mavlink_msg_heartbeat_pack(
    1,      // system_id
    200,    // component_id
    &msg,   // 输出消息
    MAV_TYPE_QUADROTOR,
    MAV_AUTOPILOT_PX4,
    MAV_MODE_FLAG_CUSTOM_MODE_ENABLED,
    0,      // custom_mode
    MAV_STATE_ACTIVE,
    3       // mavlink_version
);

// 序列化到字节流
uint16_t len = mavlink_msg_to_send_buffer(buf, &msg);

// 现在 buf 里就是可以发送的字节数组了
// 直接通过串口或 UDP 发出去就行

这段代码干了三件事:打包、序列化、准备发送。你想想看,如果没有 mavgen 生成的这些函数,你得手动去拼字节、算校验,多麻烦。

小技巧:调试的时候,可以用 mavlink_msg_heartbeat_print() 把消息内容打印出来,看看字段对不对。这比盯着二进制数据看舒服多了。

4.5 知识体系总览

为了让你对整个流程有个直观的印象,我画了一张图。它展示了从 XML 定义到最终通信的完整链路:

MAVLink 数据封装与解析流程 MAVLink 数据封装与解析流程 common.xml mavgen 代码生成 生成的 C 代码(结构体 + 序列化函数) mavlink 库(底层) mavsdk(高层封装) 最终通信:串口 / UDP / TCP 发送 MAVLink 帧 说明 XML 定义消息结构 mavgen 自动生成 C 代码 库提供编解码和通信接口

这张图把整个流程串起来了。你从 common.xml 开始,经过 mavgen 生成代码,然后选择 mavlink 库或 mavsdk 来使用,最后通过串口或网络发送出去。每一步都有明确的输入和输出。

4.6 避坑指南:我踩过的几个雷

最后,分享几个我在项目中遇到的实际问题,希望能帮你少走弯路:

  • XML 版本不匹配:不同版本的 common.xml 可能消息 ID 不一样。比如 v1.0 和 v2.0 的 HEARTBEAT 字段长度不同。我建议你固定一个版本,不要混用。
  • 字节序问题:MAVLink 默认是小端序。如果你的平台是大端序(比如某些 DSP),记得在 mavgen 时指定 --endian=big。
  • 校验和计算:MAVLink v2.0 的校验和包含了 CRC_EXTRA,这个值是从 XML 里算出来的。如果你手动改了消息定义,CRC_EXTRA 也会变,通信双方必须一致。
  • 内存对齐:生成的 C 结构体可能会有 padding。如果你直接 memcpy 结构体到字节流,可能会出问题。一定要用 mavlink 提供的序列化函数。

我曾经犯过的错:有一次我为了省事,直接 memcpy 了一个结构体到发送缓冲区。结果因为结构体对齐,接收方解析出来的数据全是乱的。后来老老实实用了 mavlink_msg_to_send_buffer(),问题就解决了。

嗯,这一章的内容就到这里。XML 定义、mavgen 生成、库的使用,这三块是 MAVLink 开发的基石。你只要把这条链路走通了,后面写什么通信代码都不怕。