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 翻译成机器可编译的代码。
它的工作流程其实很简单:
- 解析 XML,构建内部数据结构(枚举、消息、命令)。
- 根据目标语言模板,生成对应的头文件和源文件。
- 输出到指定目录。
我常用的命令是这样的:
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 定义到最终通信的完整链路:
这张图把整个流程串起来了。你从 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 开发的基石。你只要把这条链路走通了,后面写什么通信代码都不怕。