通信协议基础:MAVLink 协议深度解析

做地面站开发,第一个绕不开的东西就是 MAVLink。说白了,它就是无人机和地面站之间的“共同语言”。没有它,你发指令无人机听不懂,无人机传回来的数据你也看不懂。

我个人习惯把 MAVLink 比作一套“航空专用电报码”。它不像 HTTP 那样动不动就传几百 KB 的 JSON,而是用极小的数据包,把关键信息塞进去。这在带宽有限、实时性要求高的无人机场景里,太重要了。

核心认知:MAVLink 不是协议栈,而是一种轻量级的消息序列化协议。目前主流是 v1 和 v2 两个版本,v2 增加了签名和更大的消息 ID 空间,地面站开发建议直接上 v2。

MAVLink 消息结构:拆开看看里面有什么

一个 MAVLink 消息包,就像一封信。有信封(帧头)、有正文(载荷)、还有签名(校验)。我刚开始看协议文档时,觉得这东西挺绕,后来拆开一个实际抓包的数据,一下就明白了。

下面这张图,是我自己画的消息结构拆解,你一看就懂:

MAVLink v2 消息帧结构 帧头 0xFD 长度 1 字节 不兼容 1 字节 兼容 1 字节 序列号 1 字节 系统ID 1 字节 组件ID 1 字节 消息ID 3 字节 载荷 0-255 字节 CRC 2 字节 关键字段说明: • 帧头 (0xFD):v2 版本固定标志,v1 是 0xFE,解析时先看这个字节 • 系统ID / 组件ID:用来区分不同的飞控、GPS、摄像头等设备 • 消息ID:决定载荷里装的是什么数据(心跳、姿态、GPS 等) • CRC 校验:16 位循环冗余校验,防止数据在传输中被篡改 • 载荷长度:告诉接收方“正文有多长”,方便解析

嗯,这里要注意:消息 ID 是 3 字节的,v1 只有 1 字节。这意味着 v2 最多支持 16,777,215 种消息类型,而 v1 只有 255 种。做地面站时,我建议你直接解析 v2 帧头,兼容 v1 就好。

常用 MAVLink 消息类型:你一定会用到的几个

MAVLink 协议定义了上百种消息,但日常地面站开发,常用的就那么十几个。我把它们分成三类,你记牢了:

类别 消息名称 消息 ID 主要用途
状态类 HEARTBEAT 0 系统心跳、飞行模式、系统状态
状态类 SYS_STATUS 1 电池电压、剩余电量、传感器健康度
导航类 GPS_RAW_INT 24 GPS 经纬度、海拔、卫星数
导航类 ATTITUDE 30 横滚、俯仰、偏航角
导航类 GLOBAL_POSITION_INT 33 融合后的全球位置(GPS+IMU)
控制类 COMMAND_LONG 76 发送指令(解锁、起飞、返航等)
控制类 MANUAL_CONTROL 69 遥控器通道值覆盖

我的经验:刚开始做地面站时,我先把 HEARTBEAT、ATTITUDE、GPS_RAW_INT 这三个消息解析通了,剩下的就水到渠成。你想想看,心跳通了说明连接建立,姿态和位置有了,HUD 就能动了。

心跳包与系统状态:地面站的“生命线”

心跳包(HEARTBEAT)是 MAVLink 里最基础、最重要的消息。没有之一。

为什么这么说?因为地面站就是靠心跳包来判断“无人机还活着吗”。飞控会以固定频率(通常是 1Hz)持续发送心跳包。如果地面站连续 3-5 秒没收到,基本可以判定通信断了。

心跳包的结构很简单,但信息量很大:

// HEARTBEAT 消息载荷结构(伪代码)
struct heartbeat_t {
    uint8_t  type;           // 系统类型(1=飞控, 2=地面站, 3=摄像头...)
    uint8_t  autopilot;      // 飞控类型(3=ArduPilot, 12=PX4...)
    uint8_t  base_mode;      // 基础模式标志位(是否已解锁、是否在自动模式)
    uint32_t custom_mode;    // 自定义模式(具体飞行模式:悬停、航线、返航...)
    uint8_t  system_status;  // 系统状态(0=未连接, 3=已解锁, 4=飞行中...)
};

我记得有一次在项目现场,无人机飞出去后地面站突然显示“连接丢失”。排查了半天,发现是串口线被机架夹住了,心跳包发不出来。从那以后,我养成了一个习惯:地面站启动后第一件事,就是看心跳包接收频率。如果 1 秒内没收到,先别急着操作,检查物理连接。

避坑指南:我曾经遇到过一个问题——心跳包收到了,但 custom_mode 解析出来是乱码。后来发现是字节序搞反了。MAVLink 默认使用小端字节序(Little-Endian),如果你的地面站跑在大端架构的芯片上(比如某些 ARM 核),记得做字节序转换。

系统状态机:从“未连接”到“飞行中”

地面站需要根据心跳包里的 system_status 字段,来更新 UI 上的状态显示。我一般会在代码里维护一个状态机:

地面站系统状态机 未连接 已连接 已解锁 飞行中 通信超时 / 降落

实际开发中,我建议你在地面站 UI 上做一个状态指示灯:

  • 红色闪烁:未收到心跳包,通信中断
  • 黄色常亮:已收到心跳,但系统未就绪(比如 GPS 未定位)
  • 绿色常亮:一切正常,可以起飞

这个设计虽然简单,但非常实用。操作员扫一眼就知道当前能不能飞,不用去翻日志。

如何抓包验证 MAVLink 通信

理论说完了,来点实操。我推荐两个工具:

  1. MAVLink Inspector(QGroundControl 自带):连接飞控后,在工具菜单里打开,能看到实时消息流。
  2. mavlink-router 的日志功能:在命令行里跑 mavlink-routerd -l /tmp/mavlink.log,所有消息都会存成文件。

抓包后,你可以用 Python 的 pymavlink 库来解析:

from pymavlink import mavutil

# 连接串口(波特率 115200)
master = mavutil.mavlink_connection('/dev/ttyUSB0', baud=115200)

# 等待第一个心跳包
master.wait_heartbeat()
print(f"收到心跳!系统类型: {master.target_system}")

# 持续读取消息
while True:
    msg = master.recv_match(blocking=True)
    if msg.get_type() == 'HEARTBEAT':
        print(f"模式: {msg.custom_mode}, 状态: {msg.system_status}")
    elif msg.get_type() == 'ATTITUDE':
        print(f"横滚: {msg.roll:.2f}, 俯仰: {msg.pitch:.2f}")

这段代码我用了好几年,基本没改过。你把它跑通,MAVLink 通信就算入门了。

总结一下本章核心:

  • MAVLink 是轻量级消息协议,v2 是主流,支持更多消息类型和签名
  • 消息帧由帧头、长度、ID 字段、载荷和 CRC 组成
  • 心跳包是通信基础,1Hz 频率,丢了就说明连接断了
  • 系统状态机帮助地面站 UI 准确反映无人机当前阶段
  • 用 pymavlink 抓包验证,是排查问题最快的方法