第2章 MAVLink协议深度解析:消息结构、常用消息集与收发实战
MAVLink,说白了就是无人机和地面站之间的“普通话”。没有它,飞控再牛也只是一块会发热的电路板。我最早接触MAVLink时,觉得它不就是一堆字节嘛,后来被坑过几次才明白——理解协议细节,是系统集成的分水岭。
这一章,我们直接拆解MAVLink的骨架:消息怎么组装的、最常用的几条消息长什么样、以及如何在代码里把消息发出去、收回来。
核心要点:MAVLink v2 是目前的主流版本,ArduPilot 默认使用 v2。v1 向后兼容,但新功能(如消息签名、更长 payload)都在 v2 里。
2.1 MAVLink消息结构——一个字节都不浪费
MAVLink 消息是典型的“帧头+载荷+校验”结构。我习惯把它想象成一个快递包裹:帧头是快递单,载荷是货物,校验码是封条。
先看 v2 的消息帧格式:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| STX(起始标志) | 1 | 0xFD 表示 v2,0xFE 表示 v1 |
| LEN(载荷长度) | 1 | payload 的实际字节数(0-255) |
| INC_FLAGS(不兼容标志) | 1 | 如果接收方不理解此位,必须丢弃整条消息 |
| CMP_FLAGS(兼容标志) | 1 | 接收方不理解时可以忽略的位 |
| SEQ(序列号) | 1 | 每个组件的发送序号,用于丢包检测 |
| SYS_ID(系统ID) | 1 | 标识整个无人机系统(如 1 号飞机) |
| COMP_ID(组件ID) | 1 | 标识系统内的组件(如飞控、摄像头、Gimbal) |
| MSG_ID(消息ID) | 3 | v2 扩展到 24 位,支持更多消息类型 |
| PAYLOAD(载荷) | 0-255 | 实际数据,按消息定义排列 |
| CHECKSUM(校验和) | 2 | CRC16-CCITT,包含整个帧头和载荷 |
| SIGNATURE(签名,可选) | 13 | 用于安全认证,v2 新增 |
我的习惯:调试时先看 STX 和 LEN。如果 STX 不是 0xFD,说明串口波特率或字节对齐有问题。我曾经花了一整天排查,最后发现是 USB 转串口线虚焊——嗯,硬件坑永远比软件坑多。
为什么 v2 要把 MSG_ID 从 1 字节扩到 3 字节?因为消息类型越来越多。v1 最多 256 种,v2 可以到 1600 万种。ArduPilot 目前用了大约 200 多种,但未来肯定不够用。
2.2 常用消息集——飞控的“三大件”
MAVLink 有几百条消息定义,但日常开发中,80% 的时间你只跟 3 条消息打交道:HEARTBEAT、ATTITUDE、GLOBAL_POSITION_INT。我把它们称为“飞控三大件”。
2.2.1 HEARTBEAT(消息ID: 0)
这是最基础的消息。飞控默认以 1Hz 频率发送。它告诉地面站:“我还活着,这是我的状态。”
载荷结构如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| type | uint8_t | MAV_TYPE,如 MAV_TYPE_QUADROTOR (2) |
| autopilot | uint8_t | MAV_AUTOPILOT,如 MAV_AUTOPILOT_ARDUPILOTMEGA (3) |
| base_mode | uint8_t | MAV_MODE_FLAG 位掩码,表示是否 armed、是否在 guided 模式等 |
| custom_mode | uint32_t | ArduPilot 自定义模式,如 STABILIZE=0, AUTO=10, GUIDED=15 |
| system_status | uint8_t | MAV_STATE,如 MAV_STATE_STANDBY (3), MAV_STATE_ACTIVE (4) |
避坑指南:我曾经在集成第三方数传模块时,发现地面站一直显示“未连接”。排查到最后,是 HEARTBEAT 的 system_status 字段我填成了 0(MAV_STATE_UNINIT)。地面站认为飞控还没初始化,直接丢弃了后续所有消息。记住:system_status 至少填 MAV_STATE_STANDBY。
2.2.2 ATTITUDE(消息ID: 30)
姿态数据,飞控通常以 10-50Hz 发送。载荷包含欧拉角(roll, pitch, yaw)和角速度。
| 字段 | 类型 | 单位 | 说明 |
|---|---|---|---|
| time_boot_ms | uint32_t | ms | 飞控启动后的时间戳 |
| roll | float | rad | 横滚角,右滚为正 |
| pitch | float | rad | 俯仰角,抬头为正 |
| yaw | float | rad | 偏航角,顺时针为正(从上方看) |
| rollspeed | float | rad/s | 横滚角速度 |
| pitchspeed | float | rad/s | 俯仰角速度 |
| yawspeed | float | rad/s | 偏航角速度 |
你想想看,为什么用弧度而不是角度?因为飞控内部所有计算都是弧度,省去一次转换。MAVLink 的设计原则就是“贴近硬件,减少计算”。
2.2.3 GLOBAL_POSITION_INT(消息ID: 33)
这是 GPS 位置和高度信息,通常 5-10Hz 发送。注意:高度是厘米级整数,不是浮点。
| 字段 | 类型 | 单位 | 说明 |
|---|---|---|---|
| time_boot_ms | uint32_t | ms | 时间戳 |
| lat | int32_t | degE7 | 纬度,乘以 1e7,东经为正 |
| lon | int32_t | degE7 | 经度,乘以 1e7,北纬为正 |
| alt | int32_t | mm | 海拔高度(毫米) |
| relative_alt | int32_t | mm | 相对起飞点高度(毫米) |
| vx, vy, vz | int16_t | cm/s | 地速分量(NED坐标系) |
| hdg | uint16_t | cdeg | 航向角(百分之一度,0-35999) |
我的经验:lat/lon 用 int32_t 存 1e7 倍,是为了避免浮点精度损失。但很多新手直接打印 lat/1e7,发现小数点后第 7 位有误差。正确做法是:printf("lat: %.7f", lat / 1e7)。别问我怎么知道的——我曾在日志分析时被这个坑过。
2.3 消息收发实战——从代码到串口
理论说完了,咱们直接上手。下面是一个完整的 MAVLink 消息发送和接收示例,基于 C 语言和 MAVLink v2 库。
2.3.1 发送 HEARTBEAT
#include <mavlink.h>
void send_heartbeat(mavlink_channel_t chan) {
mavlink_message_t msg;
uint8_t buf[MAVLINK_MAX_PACKET_LEN];
// 填充 HEARTBEAT 载荷
mavlink_msg_heartbeat_pack(
1, // system_id: 飞控系统 ID
MAV_COMP_ID_AUTOPILOT1, // component_id
&msg, // 输出消息结构体
MAV_TYPE_QUADROTOR, // type
MAV_AUTOPILOT_ARDUPILOTMEGA, // autopilot
MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, // base_mode
10, // custom_mode: AUTO 模式
MAV_STATE_ACTIVE // system_status
);
// 序列化到字节流
uint16_t len = mavlink_msg_to_send_buffer(buf, &msg);
// 通过串口发送(伪代码)
uart_send_bytes(buf, len);
}
这里要注意:mavlink_msg_heartbeat_pack() 会自动计算校验和、填充序列号。你只需要填对 system_id 和 component_id。我习惯把 system_id 固定为 1,component_id 用 MAV_COMP_ID_AUTOPILOT1,这样地面站解析时不会混淆。
2.3.2 接收并解析 ATTITUDE
void parse_mavlink_message(uint8_t byte) {
mavlink_message_t msg;
mavlink_status_t status;
// 逐字节喂给解析器
if (mavlink_parse_char(MAVLINK_COMM_0, byte, &msg, &status)) {
// 成功解析出一条完整消息
switch (msg.msgid) {
case MAVLINK_MSG_ID_ATTITUDE: {
mavlink_attitude_t att;
mavlink_msg_attitude_decode(&msg, &att);
// 打印姿态(弧度转角度)
printf("Roll: %.2f deg, Pitch: %.2f deg, Yaw: %.2f deg\n",
att.roll * 180.0 / M_PI,
att.pitch * 180.0 / M_PI,
att.yaw * 180.0 / M_PI);
break;
}
// 其他消息...
}
}
}
为什么用 mavlink_parse_char() 逐字节解析?因为串口是流式数据,你不知道一帧从哪里开始。解析器内部会做帧同步、校验和验证。我曾经自己手写解析器,结果漏掉了 v2 的 INC_FLAGS 检查,导致地面站收到错误消息后直接断开——嗯,后来老老实实用官方库。
2.3.3 实战中的常见问题
- 波特率不匹配:飞控默认 115200,但很多数传模块默认 57600。我建议统一用 115200,除非你用的是老旧的 433MHz 模块。
- 消息频率过高:ATTITUDE 如果发 100Hz,串口带宽可能不够。ArduPilot 默认 50Hz,够用。你想想看,人眼 30fps 就流畅了,飞控 50Hz 足够控制。
- 字节序问题:MAVLink 使用小端序(Little-Endian)。ARM Cortex-M 系列默认小端,但如果你用某些 DSP 或 FPGA 做转发,记得检查字节序。
2.4 知识体系总览
下面这张图总结了 MAVLink 的核心逻辑:从物理层到应用层的消息流转。
2.5 小结与避坑清单
这一章的内容,说白了就是三件事:理解帧结构、记住三大消息、会写收发代码。我整理了一份避坑清单,你调试时可以直接对照:
- HEARTBEAT 的 system_status 不能为 0,否则地面站认为飞控未初始化。
- lat/lon 是 int32_t 的 1e7 倍,打印时用
%.7f格式化。 - ATTITUDE 的欧拉角是弧度,地面站显示前要转角度。
- v2 消息的 INC_FLAGS 必须检查,不理解就丢弃,别硬解析。
- 串口波特率统一,别飞控 115200、数传 57600 混着用。
我曾经踩过的坑:有一次在野外调试,地面站突然收不到 HEARTBEAT。折腾了半小时,发现是 USB 转串口线的 TX/RX 接反了。嗯,这种低级错误,犯过一次就再也不会忘了。