4. MAVLink协议基础:消息结构、常用类型与收发原理
MAVLink 这东西,说白了就是无人机世界的“普通话”。
不管你是用 PX4、ArduPilot,还是自己写的飞控,只要想跟地面站、机载电脑或者另一架飞机说话,就得走 MAVLink。我最早接触它的时候也觉得不就是个通信协议嘛,后来被坑了几次才明白——不理解消息结构,你连日志都读不懂。
4.1 MAVLink 消息结构
MAVLink 分两个版本:v1 和 v2。现在主流是 v2,但很多老设备还在用 v1。我个人习惯直接上 v2,兼容性更好,而且支持消息加密和更大的 payload。
先看一个 v2 消息帧长什么样:
| 起始标志 | 负载长度 | 不兼容标志 | 兼容标志 | 序列号 | 系统ID | 组件ID | 消息ID | 负载数据 | 校验和 |
| 0xFD | 1 byte | 1 byte | 1 byte | 1 byte | 1 byte | 1 byte | 3 byte | 0-255 B | 2 byte |
嗯,这里要注意几个关键字段:
- 起始标志 (0xFD):v2 的标识,v1 是 0xFE。解析器靠这个区分版本。
- 系统ID 和 组件ID:系统ID 代表一架飞机或地面站,组件ID 代表飞机上的某个模块(飞控、GPS、摄像头)。
- 消息ID:3 字节,决定了这条消息是什么内容。比如 0 是 HEARTBEAT,30 是 ATTITUDE。
- 校验和:CRC16,包含整个消息头+负载,还额外混了一个密钥字节。我曾经因为校验和算错,折腾了一整天才发现是 CRC 种子没对齐。
核心要点:MAVLink 消息是定长头 + 变长负载的结构。解析时先读头,根据负载长度字段再读后续数据。不要一次性读固定长度,否则遇到变长消息会崩。
4.2 常用消息类型
MAVLink 定义了上百种消息,但日常 SITL 仿真和真机调试,你打交道最多的就下面几个。
4.2.1 HEARTBEAT (消息ID: 0)
心跳消息。每架飞机、每个地面站都必须定期发送。它的作用就一个:告诉对方“我还活着”。
HEARTBEAT 的负载结构:
typedef struct {
uint8_t type; // 飞行器类型(四旋翼、固定翼...)
uint8_t autopilot; // 飞控类型(PX4、ArduPilot...)
uint8_t base_mode; // 基础模式(是否已解锁、是否在遥控...)
uint32_t custom_mode; // 自定义模式(具体飞行模式)
uint8_t system_status; // 系统状态(STANDBY、ACTIVE、EMERGENCY...)
} mavlink_heartbeat_t;
我在项目中遇到过一个问题:地面站收不到心跳,但飞控明明在跑。后来发现是 system_status 字段没更新,地面站认为飞控已经挂了。所以心跳不仅要发,字段值也要对。
调试技巧:SITL 仿真时,用 mavlink console 或者 QGroundControl 的 MAVLink Inspector 看心跳频率。正常是 1Hz,如果低于 0.5Hz 说明链路有问题。
4.2.2 ATTITUDE (消息ID: 30)
姿态消息。包含飞机的滚转、俯仰、偏航角,以及对应的角速度。这是飞控最核心的状态之一。
typedef struct {
uint32_t time_boot_ms; // 系统启动时间
float roll; // 滚转角 (rad)
float pitch; // 俯仰角 (rad)
float yaw; // 偏航角 (rad)
float rollspeed; // 滚转角速度 (rad/s)
float pitchspeed; // 俯仰角速度 (rad/s)
float yawspeed; // 偏航角速度 (rad/s)
} mavlink_attitude_t;
注意单位是弧度,不是度。我见过有人直接把角度塞进去,结果地面站显示飞机在疯狂翻转。SITL 仿真里你可以用 --airframe 参数模拟不同机型,看看姿态数据的变化规律。
4.2.3 LOCAL_POSITION_NED (消息ID: 32)
本地位置消息。NED 坐标系:北 (North)、东 (East)、地 (Down)。
typedef struct {
uint32_t time_boot_ms;
float x; // 北向位置 (m)
float y; // 东向位置 (m)
float z; // 地向位置 (m,注意向下为正)
float vx; // 北向速度 (m/s)
float vy; // 东向速度 (m/s)
float vz; // 地向速度 (m/s)
} mavlink_local_position_ned_t;
这里有个容易踩的坑:z 轴向下为正。也就是说,飞机高度 10 米时,z 是 -10。我第一次写 SITL 仿真脚本时,把 z 当成了高度直接传正值,结果飞机在仿真里一直往地下钻。
避坑指南:我曾经在真机测试时,因为 LOCAL_POSITION_NED 的 z 符号搞反,导致高度控制反向。飞机一推油门就往地上砸。后来我在代码里加了一个断言:if (z > 0) print("警告:z 应为负值")。这个习惯一直保留到现在。
4.3 消息收发原理
MAVLink 的收发其实不复杂,核心就三步:打包 → 发送 → 解析。
4.3.1 打包与发送
以 PX4 的 SITL 为例,发送一条 HEARTBEAT 的流程:
// 1. 创建消息对象
mavlink_message_t msg;
uint8_t buf[MAVLINK_MAX_PACKET_LEN];
// 2. 打包
mavlink_msg_heartbeat_pack(
system_id, // 你的系统ID
component_id, // 你的组件ID
&msg, // 输出消息
type, autopilot, base_mode, custom_mode, system_status
);
// 3. 序列化到字节流
uint16_t len = mavlink_msg_to_send_buffer(buf, &msg);
// 4. 通过串口/UDP/TCP 发送
write(fd, buf, len);
在 SITL 里,默认通过 UDP 端口 14550 发送。你可以用 nc -u localhost 14550 抓包看看原始字节流。
4.3.2 接收与解析
接收端需要做的是:从字节流里一字节一字节地喂给解析器。
mavlink_status_t status;
mavlink_message_t msg;
while (1) {
uint8_t byte = read_byte_from_channel();
if (mavlink_parse_char(MAVLINK_COMM_0, byte, &msg, &status)) {
// 解析成功,处理消息
switch (msg.msgid) {
case MAVLINK_MSG_ID_HEARTBEAT:
mavlink_heartbeat_t heartbeat;
mavlink_msg_heartbeat_decode(&msg, &heartbeat);
// 处理心跳...
break;
case MAVLINK_MSG_ID_ATTITUDE:
// 类似处理...
break;
}
}
}
这里有个关键点:mavlink_parse_char 是状态机驱动的,每次只吃一个字节。你不需要自己维护帧头、校验和,库函数全包了。但要注意——不要在多线程里同时调用同一个通道的解析函数,否则状态机乱掉,你会收到一堆 CRC 错误。
个人经验:SITL 仿真时,我习惯在接收循环里加一个超时计数器。如果连续 5 秒没收到 HEARTBEAT,就打印一条警告。这在调试网络延迟或链路断开时特别有用。
4.4 MAVLink 消息流全景图
下面这张图展示了 SITL 仿真中 MAVLink 消息的完整流动路径:
4.5 常见问题与排查思路
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 收不到任何消息 | 端口/波特率不匹配 | 检查 SITL 启动参数中的 UDP 端口,用 Wireshark 抓包确认 |
| CRC 错误频繁 | 数据损坏或版本不匹配 | 确认收发双方都用 v2 协议,检查链路噪声 |
| 姿态数据跳变 | 单位错误(度 vs 弧度) | 打印原始值,看范围是否在 ±π 内 |
| 心跳超时 | 飞控卡死或线程阻塞 | 在飞控循环中加调试输出,确认主循环在跑 |
最后提醒一句:MAVLink 协议本身不保证可靠传输。如果你需要确保每一条消息都到达(比如上传航点),要么用 MAVLink 的确认机制,要么自己在应用层做重传。我在一次竞速无人机测试中,就因为没做重传,航点上传丢了一条,飞机直接飞偏了。从那以后,凡是涉及安全的关键指令,我都加了确认和超时重试。