消息结构与编码:MAVLink v1 vs v2、消息帧格式、CRC校验机制、消息ID与载荷

好,咱们进入第二章。这一章是 MAVLink 的硬核基础——消息到底长什么样?怎么编解码?为什么要有两个版本?

我记得刚接触 MAVLink 时,第一反应是:这不就是个串口协议吗?后来踩了坑才发现,它远比想象中精巧。尤其是 v2 引入的签名机制,直接决定了你的无人机能不能在公共频段安全飞行。

MAVLink v1 与 v2:为什么要有两个版本?

说白了,v1 是 2009 年的设计,那时候无人机还比较单纯。v2 是 2017 年推出的,主要解决三个痛点:

  • 消息 ID 不够用:v1 只有 8 位 ID(0-255),大团队自定义消息经常冲突
  • 缺乏安全机制:v1 没有消息签名,容易被伪造
  • 载荷太小:v1 最大 255 字节,对于高分辨率遥测数据捉襟见肘

我在一个农业植保项目中,需要传输多光谱相机的 16 通道数据,v1 的载荷根本塞不下,只能拆成多条消息发送——不仅效率低,还容易丢包。后来切换到 v2,一条消息搞定。

核心差异速览

特性 MAVLink v1 MAVLink v2
消息 ID 长度 8 位(0-255) 24 位(0-16777215)
载荷最大长度 255 字节 255 字节(不变)
签名支持 无 可选(13 字节签名)
兼容性 基础 向后兼容 v1
帧头长度 6 字节 10-12 字节(含签名)

你可能会问:既然 v2 这么好,为什么还有人用 v1?嗯,有些老飞控硬件资源有限,v2 的帧头解析需要更多 RAM,而且签名计算对 8 位 MCU 是个负担。

消息帧格式:逐字节拆解

先看 v1 的帧格式。我习惯把它分成三段:帧头、载荷、校验。

MAVLink v1 帧格式(共 8 + payload 字节):
+------+------+------+------+------+------+------+------+------+------+
| 0xFE | LEN  | SEQ  | SYS  | COMP | MSGID| PAYLOAD...       | CKA  | CKB  |
+------+------+------+------+------+------+------+------+------+------+
  1B      1B     1B     1B     1B     1B     LEN 字节          1B     1B
  • 0xFE:v1 起始标志,固定值
  • LEN:载荷长度(0-255)
  • SEQ:序列号,用于丢包检测
  • SYS:系统 ID(飞控、地面站各自不同)
  • COMP:组件 ID(同一系统内的 GPS、IMU 等)
  • MSGID:消息 ID(0-255)
  • PAYLOAD:实际数据
  • CKA/CKB:16 位 CRC 校验

v2 的帧头稍微复杂一点:

MAVLink v2 帧格式:
+------+------+------+------+------+------+------+------+------+------+------+------+
| 0xFD | LEN  | INCFL| CMPFL| SEQ  | SYS  | COMP | MSGID(3B)    | PAYLOAD... | CKA  | CKB  | [SIG...]
+------+------+------+------+------+------+------+------+------+------+------+------+------+
  1B      1B     1B     1B     1B     1B     1B      3B            LEN 字节     1B     1B     13B(可选)
  • 0xFD:v2 起始标志
  • INCFL:不兼容标志位(比如签名启用时置位)
  • CMPFL:兼容标志位
  • MSGID:扩展为 3 字节,小端序
  • SIG:13 字节签名(可选,由 INCFL 控制)

实战技巧:解析 v2 帧时,先检查 INCFL 的第 0 位。如果为 1,说明启用了签名,必须验证签名后才能处理载荷。我曾经在调试时忘了这个检查,结果地面站收到了伪造的“解锁”指令——还好是在仿真环境。

CRC 校验机制:不是简单的 CRC16

MAVLink 的 CRC 校验有个特点:它把消息 ID 也混进了校验码。这意味着即使载荷相同,不同消息 ID 的 CRC 结果也不同。

具体算法是这样的:

  1. 先计算载荷的 CRC16-CCITT(多项式 0x1021)
  2. 再把消息 ID(v1 是 1 字节,v2 是 3 字节)追加到 CRC 计算中
  3. 最终得到 2 字节的 CKA/CKB

为什么要这么做?防止消息 ID 被篡改。你想想看,如果攻击者把“心跳包”的消息 ID 改成“紧急降落”,虽然载荷校验通过,但行为完全变了。混合 ID 后,这种攻击就失效了。

避坑指南:我曾经在移植 MAVLink 到 FreeRTOS 时,直接用了标准 CRC16 库,结果地面站一直报 CRC 错误。排查了两天才发现——MAVLink 的 CRC 初始值是 0xFFFF,而且计算完还要取反。细节决定成败啊。

消息 ID 与载荷:如何设计高效的消息体系

消息 ID 是 MAVLink 的灵魂。v2 的 24 位 ID 空间被划分为几个区域:

ID 范围 用途 示例
0 - 255 v1 兼容消息 HEARTBEAT (0), SYS_STATUS (1)
256 - 9999 标准 v2 消息 ATTITUDE_QUATERNION (31)
10000 - 19999 厂商自定义 PX4 的 DEBUG_VECT (250)
20000 - 29999 用户自定义 你的私有消息

载荷设计有个黄金法则:能省则省。MAVLink 的载荷最大 255 字节,但实际链路(尤其是数传电台)MTU 往往只有 64 字节。我一般把单条消息控制在 32 字节以内,这样即使丢包重传,代价也小。

举个例子,传输 GPS 数据时:

// 不好的设计:用 float 存经纬度(各 4 字节)
typedef struct {
    float lat;      // 4B
    float lon;      // 4B
    float alt;      // 4B
    float speed;    // 4B
} __attribute__((packed)) gps_bad_t;  // 共 16 字节

// 好的设计:用 int32 存经纬度(精度 1e7,够用了)
typedef struct {
    int32_t lat;    // 4B (精度 1e-7 度 ≈ 1.1cm)
    int32_t lon;    // 4B
    int16_t alt;    // 2B (分米级精度,范围 ±3276.7m)
    uint16_t speed; // 2B (厘米/秒,范围 0-655.35 m/s)
} __attribute__((packed)) gps_good_t;  // 共 12 字节,省了 25%

嗯,这里要注意:__attribute__((packed)) 是 GCC 的语法,告诉编译器不要对齐填充。否则结构体里会多出空洞字节,导致编解码不一致。

SVG 框架图:MAVLink 消息生命周期

下面这张图展示了从飞控生成消息到地面站解析的完整流程。我把它画成流程图,方便你理解每个环节的职责。

MAVLink 消息生命周期 MAVLink 消息生命周期 飞控生成消息 填充帧头 计算 CRC 串口/网络发送 接收端解析 验证 CRC 提取载荷 回调处理 💡 关键点: • CRC 计算包含消息 ID,防止 ID 篡改 • v2 签名在 CRC 之后验证,先验 CRC 再验签名

实战中的编解码陷阱

最后分享几个我踩过的坑:

  • 字节序问题:MAVLink 默认小端序(Little-Endian)。但有些 ARM 飞控默认是大端序,需要手动转换。我见过一个团队在移植时忘了这个,结果高度数据变成了 65535 米——差点以为是 GPS 故障。
  • CRC 初始值:MAVLink 的 CRC 初始值是 0xFFFF,不是常见的 0x0000。如果你用现成的 CRC 库,记得改初始值。
  • v2 签名密钥管理:签名密钥是 32 字节的随机数。我建议把密钥写在飞控的配置文件中,不要硬编码在代码里。否则换密钥时得重新编译固件。
  • 载荷对齐:虽然用了 packed 结构体,但有些 MCU 访问未对齐地址会触发硬件异常。如果遇到奇怪的重启问题,检查一下载荷里有没有 32 位变量放在奇数偏移上。

我的习惯:在开发阶段,我会在飞控端打印每条发送消息的原始字节流,地面站也打印接收到的字节流。两边一对比,CRC 错误、字节序问题、ID 不匹配,一眼就能看出来。这个习惯帮我省了至少 50% 的调试时间。