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和部分头部字段也纳入了计算。具体来说:
- 先计算负载数据的CRC16
- 再把这个CRC值和消息ID对应的「额外CRC种子」做异或
- 最后把结果附加到帧尾
这个「额外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次。这样即使数传链路有丢包,命令也能可靠送达。
知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个快速参考:
嗯,这张图把帧结构、消息ID、CRC校验和常见消息类型串在了一起。你写代码的时候,可以把它当作一个快速索引。