MAVLink协议基础:帧结构、消息ID与CRC校验

做飞控这些年,我接触过不少通信协议。但要说最顺手的,还得是MAVLink。它轻量、可靠,而且生态成熟——你想想看,几乎所有的开源飞控都在用,这本身就说明问题。

这一章,咱们就把它最核心的几个东西讲透:协议帧长什么样、消息ID怎么分配、CRC校验怎么算,以及最常见的几种消息类型。嗯,这些东西搞明白了,后面讲飞行模式切换,你才能看得懂指令是怎么发出去的。

MAVLink协议帧结构

MAVLink目前有两个主流版本:v1.0和v2.0。我个人更推荐直接用v2.0,尤其是做复杂系统的时候。为什么呢?因为v2.0的帧结构更灵活,支持更大的消息ID空间,而且兼容v1.0。

先看v2.0的帧结构,我直接上表格,这样更清楚:

字节偏移 字段名 长度(字节) 说明
0 STX(起始标志) 1 0xFD,表示v2.0帧
1 LEN(负载长度) 1 有效负载的字节数,0-255
2 INC_FLAGS(不兼容标志) 1 接收方必须理解的标志位
3 CMP_FLAGS(兼容标志) 1 接收方可忽略的标志位
4 SEQ(序列号) 1 每发一帧递增,用于丢包检测
5 SYS_ID(系统ID) 1 标识发送设备,如飞控=1
6 COMP_ID(组件ID) 1 标识组件,如自动驾驶仪=1
7-9 MSG_ID(消息ID) 3 v2.0扩展到24位,范围更大
10..(10+LEN-1) PAYLOAD(有效负载) LEN 实际消息数据
10+LEN CRC(校验码) 2 包含负载和部分头部的CRC16
10+LEN+2 SIGNATURE(签名) 13 可选,用于安全认证

关键点:v2.0的消息ID从v1.0的8位扩展到了24位。这意味着什么?最多支持16,777,216种消息类型。我在做多机协同项目时,自定义消息ID就靠这个特性,完全不用担心冲突。

v1.0的帧结构更简单,起始标志是0xFE,消息ID只有1个字节,没有INC_FLAGS和CMP_FLAGS,也没有签名段。如果你还在维护老设备,可能会遇到。但我建议新项目直接上v2.0。

消息ID:消息的身份证

消息ID,说白了就是告诉接收方「这条消息是干什么的」。每个消息类型都有一个唯一的ID。比如HEARTBEAT的ID是0,COMMAND_LONG的ID是76。

消息ID的分配是有讲究的。MAVLink官方维护了一个XML定义文件,所有标准消息的ID都在里面。我刚开始做的时候,觉得查XML麻烦,直接硬编码数字——结果有一次把ID写错了,地面站死活收不到数据。后来我学乖了,直接用生成好的头文件,省心。

常见的消息ID范围:

  • 0-255:v1.0兼容区,最常用的消息都在这里
  • 256-65535:v2.0扩展区,用于新定义的标准消息
  • 65536以上:用户自定义区,我一般在这里放自己的调试消息

我的习惯:自定义消息ID从0xFFFF+1开始,也就是65536。这样既不会和标准消息冲突,也方便后期维护。你可以在代码里加个注释,标注哪些ID是自己定义的。

CRC校验规则:别让数据飞了

CRC校验,嗯,这个坑我踩过。有一次在野外测试,无人机飞出去几百米,突然地面站显示数据乱跳。排查了半天,发现是数传模块的干扰导致数据包损坏,而我的CRC校验实现有bug,把坏包当成了好包。

MAVLink的CRC校验用的是CRC-16-CCITT,多项式是0x1021。但有个细节:它不只是对负载做校验,还把消息ID和部分头部字段也纳入了计算。具体来说:

  1. 先计算负载数据的CRC16
  2. 再把这个CRC值和消息ID对应的「额外CRC种子」做异或
  3. 最后把结果附加到帧尾

这个「额外CRC种子」是MAVLink协议里一个很巧妙的设计。每个消息类型都有一个固定的种子值,定义在XML里。接收方收到消息后,用同样的方式计算CRC,如果匹配,说明数据完整。

我曾经踩过的坑:在移植MAVLink到新平台时,忘了更新CRC种子表。结果所有消息的CRC校验都失败,折腾了两天才找到原因。记住:CRC种子表必须和消息定义同步更新,否则校验永远过不了。

下面是一个简化的CRC计算示例,我用C语言写的,实际项目中可以直接用MAVLink官方库:

// CRC-16-CCITT 初始化
uint16_t crc = 0xFFFF;

// 对每个字节计算CRC
for (int i = 0; i < len; i++) {
    crc = crc_accumulate(buf[i], crc);
}

// 异或额外种子
crc ^= extra_crc_seed;

// 最终校验值
uint16_t final_crc = crc;

你可能会问:为什么非要加这个额外种子?我个人的理解是,它相当于给每个消息类型加了一把「小锁」。即使负载数据一样,不同消息类型的CRC值也不同,能防止消息类型被误判。

常见MAVLink消息类型

实际开发中,你打交道最多的就是那几种消息。我列个清单,都是我在项目里反复用到的:

消息名称 消息ID 用途 频率
HEARTBEAT 0 心跳包,报告系统状态和飞行模式 1Hz(默认)
COMMAND_LONG 76 发送长命令,如起飞、降落、模式切换 按需
COMMAND_INT 75 带整数坐标的命令,用于航点任务 按需
GLOBAL_POSITION_INT 33 全球定位信息(经纬度、高度) 5-10Hz
ATTITUDE 30 姿态信息(滚转、俯仰、偏航) 10-50Hz
RC_CHANNELS_OVERRIDE 70 遥控器通道覆盖,用于手动控制 按需

这里重点说一下HEARTBEAT和COMMAND_LONG,因为后面讲飞行模式切换,全靠它们俩。

HEARTBEAT:系统的生命线

HEARTBEAT是所有MAVLink设备必须发送的消息。它告诉地面站:我还活着,这是我的状态。它的负载结构很简单:

  • type:设备类型,比如MAV_TYPE_QUADROTOR(2)
  • autopilot:飞控类型,比如MAV_AUTOPILOT_ARDUPILOTMEGA(3)
  • base_mode:基础模式标志位,比如是否已武装、是否在引导
  • custom_mode:自定义模式,这个就是飞行模式的核心字段
  • system_status:系统状态,比如MAV_STATE_ACTIVE(4)

我记得有一次调试,地面站一直显示「未连接」。我抓包一看,HEARTBEAT的system_status字段填错了,填了个MAV_STATE_UNINIT。地面站认为系统还没初始化,直接忽略了。改过来就好了。

COMMAND_LONG:发号施令

COMMAND_LONG是执行动作的主要方式。它的负载包含一个命令ID和最多7个参数(浮点数)。比如切换飞行模式,用的就是MAV_CMD_DO_SET_MODE(命令ID=176)。

命令执行后,飞控会回复一个COMMAND_ACK消息,告诉你成功还是失败。这个机制很重要——你不能发了命令就不管了,一定要等ACK确认。

实战经验:我在写地面站软件时,每次发送COMMAND_LONG都会启动一个超时定时器。如果3秒内没收到ACK,就重发一次。最多重试3次。这样即使数传链路有丢包,命令也能可靠送达。

知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个快速参考:

MAVLink协议基础核心结构 MAVLink协议 帧结构 消息ID CRC校验 v1.0 (0xFE) v2.0 (0xFD) 标准ID (0-255) 扩展ID (256+) CRC-16-CCITT + 额外种子 常见消息类型 HEARTBEAT (ID=0) COMMAND_LONG (ID=76) GLOBAL_POSITION_INT ATTITUDE (ID=30) RC_OVERRIDE 核心要点 帧结构决定数据怎么装 → 消息ID决定数据是什么 → CRC校验保证数据没错

嗯,这张图把帧结构、消息ID、CRC校验和常见消息类型串在了一起。你写代码的时候,可以把它当作一个快速索引。