2. MAVLink协议基础:消息格式、消息ID、CRC校验、心跳包机制

好,咱们进入正题。MAVLink 这东西,说白了就是无人机世界里的一种“通用语言”。你想想看,飞控、地面站、摄像头、机械臂……这么多设备要互相说话,总得有个统一的规矩。这个规矩,就是 MAVLink 协议。

我最早接触 MAVLink 是在一个农业植保项目里。当时飞机在天上撒农药,地面站突然收不到高度数据了。排查了半天,发现是消息格式里一个字节对齐的问题。从那以后,我对协议细节就再也不敢马虎了。

2.1 消息格式:每一帧都长什么样?

MAVLink 的消息帧,就像一封信。信封上写着地址、邮戳,里面才是正文。咱们先看 MAVLink 1.0 的经典格式,这是最常用的版本。

字节偏移 字段名 长度(字节) 说明
0 STX(起始标志) 1 固定为 0xFE,表示 MAVLink 1.0
1 LEN(负载长度) 1 Payload 部分的字节数,0-255
2 SEQ(序列号) 1 每个组件独立计数,用于丢包检测
3 SYS ID(系统ID) 1 标识整个无人机系统,比如 1 号飞机
4 COMP ID(组件ID) 1 标识系统内的具体模块,比如飞控、GPS
5 MSG ID(消息ID) 1 标识消息类型,0-255
6 到 6+LEN-1 Payload(负载) LEN 真正的数据内容
6+LEN CKA / CKB(校验和) 2 CRC-16-CCITT 校验结果

嗯,这里要注意。MAVLink 2.0 的起始标志是 0xFD,而且消息 ID 扩展到了 3 个字节(0-16777215)。不过咱们做 SITL 仿真,大部分场景还是用 1.0 版本,兼容性更好。

我的小习惯: 在解析 MAVLink 数据流时,我习惯先校验 STX 和 LEN。如果 LEN 明显超出预期(比如大于 255),那这帧数据大概率是乱码,直接丢弃就好。别等到算完 CRC 才发现,浪费 CPU。

2.2 消息ID:给每条消息一个身份证

消息 ID 是 MAVLink 的灵魂。它告诉接收方:“我这条消息是干嘛的”。比如 ID=0 是心跳包,ID=30 是姿态信息,ID=74 是 GPS 原始数据。

我举个例子。你在 SITL 里跑仿真,地面站想看看飞机当前的速度。地面站不会傻等着,而是监听所有消息。当它收到一条 MSG ID=32 的消息(LOCAL_POSITION_NED),就知道里面装的是位置和速度数据,直接解析对应字段就行。

常用的消息 ID 有哪些?我列几个你一定会遇到的:

  • 0 (HEARTBEAT):心跳包,每秒 1-10 Hz,告诉你设备还活着
  • 1 (SYS_STATUS):系统状态,电池电压、通信质量等
  • 24 (GPS_RAW_INT):GPS 原始数据,经纬度、海拔、卫星数
  • 30 (ATTITUDE):姿态四元数、角速度
  • 32 (LOCAL_POSITION_NED):局部坐标系下的位置和速度
  • 74 (VFR_HUD):空速、地速、航向,地面站最常用的显示数据
我曾经踩过的坑: 在自定义消息时,千万别用 MAVLink 官方已经定义的 ID。我曾经为了省事,把自定义消息塞进了 ID=0 的 payload 里。结果地面站一收到,以为是心跳包,直接解析错误,导致整个通信链路崩溃。后来我老老实实申请了用户自定义 ID(256-511),再也没出过问题。

2.3 CRC校验:别让噪声毁了你的数据

无人机在天上飞,电磁环境复杂得很。数据在无线链路里传着传着,可能一个比特就翻了个跟头。CRC 校验就是用来揪出这种错误的。

MAVLink 用的是 CRC-16-CCITT,多项式是 0x1021。计算范围包括:负载(Payload)加上一个“额外秘钥”。这个秘钥是什么?是消息定义里每个字段的 CRC 值,说白了就是消息的“指纹”。

我直接给你看代码,这是 MAVLink 官方库里的核心 CRC 函数:

// 经典的 CRC-16-CCITT 实现
uint16_t crc_calculate(const uint8_t* data, uint32_t len) {
    uint16_t crc = 0xFFFF;
    for (uint32_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0x8408;  // 0x8408 是 0x1021 的反转
            } else {
                crc = crc >> 1;
            }
        }
    }
    return crc;
}

为什么校验和要算两遍?第一遍算 payload,第二遍把消息定义的 CRC 额外值加进去再算。这样做的好处是:接收方不仅能知道数据有没有传错,还能确认消息格式是不是自己认识的那个版本。

关键点: 在 SITL 仿真里,如果你自己写 MAVLink 解析器,一定要把 CRC 校验做完整。很多初学者只校验 payload,忽略了那个“额外秘钥”。结果就是:明明数据是对的,但地面站就是不认。我当年调试 Pixhawk 时,就因为这个 bug 浪费了整整一个下午。

2.4 心跳包机制:设备还活着吗?

心跳包,英文叫 HEARTBEAT,消息 ID=0。它是 MAVLink 通信里最基础、最重要的消息。没有心跳,其他消息都是白搭。

心跳包的结构很简单,只有 9 个字节的 payload:

字段 类型 说明
type uint8_t 系统类型,比如 2 是四旋翼,12 是地面站
autopilot uint8_t 飞控类型,比如 3 是 PX4,4 是 ArduPilot
base_mode uint8_t 位掩码,表示是否已上锁、是否在自动模式等
custom_mode uint32_t 自定义模式,比如 PX4 的 manual、mission、offboard
system_status uint8_t 系统状态,4 表示已解锁,3 表示已就绪
mavlink_version uint8_t MAVLink 协议版本,固定为 3

心跳包的作用,说白了就是三个字:报平安。地面站每隔一段时间(通常是 1 秒)收不到心跳,就会认为飞机失联了。在 SITL 仿真里,如果你看到地面站提示“通信超时”,十有八九是心跳包没发出来。

我个人的习惯是:在 SITL 启动后,先用 Wireshark 抓一下 UDP 包,确认心跳包是否以 1 Hz 的频率正常发出。如果心跳正常,其他消息基本不会出大问题。

MAVLink 消息帧结构 & 心跳包流程 消息帧结构(MAVLink 1.0) STX LEN SEQ SYS COMP MSG ID Payload (LEN) CKA CKB 0xFE 消息类型 CRC 高字节 心跳包通信流程 飞控 (SITL) 每秒发送 HEARTBEAT 地面站 (QGC) 监听 & 解析 UDP 广播 (14550 端口) 心跳包内容:type=2, autopilot=3 base_mode=81, system_status=4

你看这张图,左边是飞控(SITL 仿真),右边是地面站。飞控每秒发一条心跳包,地面站收到后解析出系统状态。如果超过 3 秒没收到,地面站就会弹出“通信丢失”的警告。

避坑指南: 在 SITL 仿真里,如果你同时开了多个仿真实例(比如 3 架飞机),每架飞机的 SYS ID 必须不同。我曾经犯过这个错:3 架飞机都用 SYS ID=1,结果地面站只认最后一架,前两架直接“失联”。后来我改成 1、2、3,一切正常。