4、MAVLink协议进阶:自定义MAVLink消息、消息序列化与反序列化、CRC校验

好,咱们接着聊。上一章我们把MAVLink的基础通信流程走了一遍,你大概已经知道怎么收发心跳包、怎么解析遥控指令了。但说实话,真正做无人机系统集成的时候,光靠标准消息是不够的。你想想看,每个飞控的硬件配置不一样,传感器组合千奇百怪,总有一些私有数据需要传递。这时候,你就得学会自定义MAVLink消息。

我个人习惯把自定义消息叫做“私房菜”。标准协议是公共食堂,管饱但口味固定。自定义消息就是你自己开的小灶,想加什么料都行。不过,加料之前得先搞清楚MAVLink的序列化规则和CRC校验机制,否则飞控那边解析出来一堆乱码,那就尴尬了。

4.1 自定义MAVLink消息的完整流程

先说说怎么定义一条新消息。MAVLink的消息定义都在XML文件里,比如common.xml或者你自己建的my_custom.xml。我建议你单独建一个文件,别去改官方的,否则版本升级时你会想哭。

举个例子,我想定义一条消息,用来传输飞控的CPU负载和剩余内存。这在调试时特别有用。定义如下:

<message id="150" name="CPU_MEM_STATUS">
    <description>CPU负载和内存状态</description>
    <field type="uint8_t" name="cpu_load">CPU负载百分比</field>
    <field type="uint16_t" name="free_mem">剩余内存,单位KB</field>
    <field type="uint8_t" name="temp">核心温度,摄氏度</field>
</message>

这里我选了ID 150,因为MAVLink 2.0里0-255是用户自定义区间,不会和官方消息冲突。字段类型我用了定长整数,为什么?因为序列化时定长类型好处理,解析效率高。我在项目中遇到过有人用float传温度,结果精度没控制好,反而多出两个字节的浪费。

定义好XML后,用MAVLink的代码生成器跑一下,就会自动生成对应的C语言结构体和序列化函数。嗯,这里要注意:生成代码时记得指定MAVLink 2.0协议,因为1.0不支持超过255的消息ID。

4.2 消息序列化与反序列化:底层到底在干什么?

说白了,序列化就是把结构体里的数据,按顺序打包成一个字节流。反序列化就是反过来,把字节流还原成结构体。你可能会觉得这很简单,不就是memcpy吗?其实没那么简单。

MAVLink的序列化规则有几个关键点:

  • 小端字节序:所有多字节字段都是小端存储。x86平台天然支持,但如果你用ARM Cortex-M,默认也是小端,所以一般不用操心。但如果你接了一个大端的外设,就得手动转换。
  • 字段对齐:MAVLink不做内存对齐,字段是紧密排列的。比如uint8_t后面跟uint16_t,uint16_t不会自动对齐到2字节边界。这跟C语言结构体的默认对齐方式不一样,所以你不能直接把结构体指针当字节流发出去。
  • 无填充字节:序列化后的数据长度,就是所有字段长度之和。没有额外的填充或对齐字节。这一点我特别喜欢,因为节省带宽。

来看一个实际的序列化代码片段,这是我之前项目里用过的:

// 假设我们定义的结构体
typedef struct {
    uint8_t cpu_load;
    uint16_t free_mem;
    uint8_t temp;
} cpu_mem_status_t;

// 手动序列化函数
void serialize_cpu_mem_status(const cpu_mem_status_t *status, uint8_t *buffer) {
    buffer[0] = status->cpu_load;
    buffer[1] = status->free_mem & 0xFF;        // 低字节
    buffer[2] = (status->free_mem >> 8) & 0xFF; // 高字节
    buffer[3] = status->temp;
}

你看,uint16_t被拆成了两个字节,先放低字节再放高字节。这就是小端序。反序列化时,按同样的顺序拼回去就行。我刚开始做的时候,直接用了memcpy,结果在STM32上跑得好好的,换到某个国产MCU上就出问题了——因为那个MCU默认是大端。从那以后,我再也不敢偷懒了。

4.3 CRC校验:别让一个比特毁了你的无人机

CRC校验是MAVLink协议里最容易被忽视,但也是最关键的部分。你想想看,无人机在天上飞,无线链路随时可能受到干扰。一个比特翻转,可能把“降落”指令变成“自毁”指令。这不是开玩笑,我在测试中就遇到过遥控器丢包导致误触发的情况。

MAVLink使用CRC-16-CCITT算法,多项式是0x1021。但它的特殊之处在于:每条消息的CRC计算,除了数据本身,还要混入一个消息特定的CRC种子。这个种子是根据消息ID和消息名称生成的。为什么要这么做?为了防止消息ID被误判。比如你自定义的消息ID 150,如果链路噪声把ID变成了151,而151恰好是另一条消息,那接收方就会用错误的CRC种子去校验,大概率校验失败,从而丢弃这条消息。

CRC校验的流程是这样的:

  1. 发送方计算整个消息载荷的CRC,同时混入该消息的CRC种子。
  2. 将计算出的CRC附加到消息末尾。
  3. 接收方收到消息后,用同样的方式重新计算CRC,并与收到的CRC比较。
  4. 如果一致,说明数据完整;否则丢弃。

这里有个坑:CRC种子不是随便写的。MAVLink官方提供了一个Python脚本mavgen.py,它会根据XML定义自动生成每个消息的CRC种子。你如果手写自定义消息,一定要用这个脚本重新生成一遍。我曾经手算过一个CRC种子,结果算错了,导致飞控和地面站互相不认,排查了整整两天。

核心要点:自定义消息时,一定要重新生成CRC种子表。不要复用其他消息的种子,否则会出现CRC冲突,导致消息被错误地接受或拒绝。

4.4 实战中的避坑指南

讲几个我踩过的坑,你遇到了可以少走弯路。

坑一:消息ID冲突

我曾经在一个项目里,团队A和团队B各自定义了自己的自定义消息,结果ID都是200。合并代码时,飞控直接崩溃了。后来我们定了个规矩:自定义消息ID按模块分配区间,比如100-150给传感器,151-200给执行器,201-255给调试用。

技巧:使用MAVLink 2.0的签名功能

如果你的无人机在开放环境中飞行,比如城市上空,建议开启MAVLink 2.0的消息签名。它会在CRC校验的基础上,再加一层基于密钥的HMAC验证。这样即使有人伪造了CRC,也无法通过签名验证。我有个做安防无人机的朋友,就因为没开签名,被人在地面站附近放了干扰器,差点炸机。

坑二:序列化时字段顺序搞错

MAVLink的序列化顺序严格按XML中字段定义的顺序来。如果你在XML里先写了free_mem再写cpu_load,那序列化时也是这个顺序。但如果你在C结构体里定义成了先cpu_load再free_mem,那就全乱了。我的建议是:让XML定义和C结构体定义保持完全一致,包括字段顺序和类型。

4.5 性能考量:嵌入式环境下的优化

在资源受限的飞控上,比如STM32F4系列,CPU主频只有168MHz,RAM可能只有192KB。这时候序列化和CRC计算的开销就不能忽视。

我一般会做两件事:

  • 预计算CRC表:CRC-16的查表法比逐位计算快10倍以上。在初始化时把256个字节的CRC表算好存起来,后面直接查表。
  • 批量序列化:如果多条消息要连续发送,比如同时发CPU状态、GPS状态、姿态数据,我会先把它们序列化到一个大缓冲区里,然后一次性通过DMA发送。这样能减少中断次数,提高CPU利用率。

举个例子,预计算CRC表的代码:

static uint16_t crc_table[256];

void init_crc_table(void) {
    for (uint16_t i = 0; i < 256; i++) {
        uint16_t crc = i << 8;
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x8000)
                crc = (crc << 1) ^ 0x1021;
            else
                crc = crc << 1;
        }
        crc_table[i] = crc;
    }
}

uint16_t calc_crc(const uint8_t *data, uint16_t len, uint16_t seed) {
    uint16_t crc = seed;
    for (uint16_t i = 0; i < len; i++) {
        uint8_t index = (crc >> 8) ^ data[i];
        crc = (crc << 8) ^ crc_table[index];
    }
    return crc;
}

这段代码我用了好几年,在多个飞控平台上验证过。你直接拿去用就行,注意把seed换成你消息对应的CRC种子。

4.6 小结

自定义MAVLink消息,说白了就是三件事:定义XML、实现序列化、加上CRC。但每一件事都有细节。XML定义要注意ID区间和字段类型;序列化要记住小端序和无对齐;CRC校验要混入消息特定的种子。把这些搞定了,你的无人机通信协议就算是真正掌握了。

下一章我们会讲MAVLink的扩展机制和高级调试技巧,包括怎么用Wireshark抓包分析MAVLink流量。到时候我会分享一个我抓到的真实故障案例,保证让你大开眼界。