本文面向准备自己开发地面站的开发者,目标是解释 MAVLink 是什么、MAVLink 2 包含什么、QGroundControl 为什么围绕 MAVLink 2 工作,以及在自己的地面站里应该怎样使用 MAVLink。
参考资料:
- MAVLink 官方文档:https://mavlink.io/en/
- MAVLink 协议概览:https://mavlink.io/en/about/overview.html
- MAVLink 微服务:https://mavlink.io/en/services/
- MAVLink 消息定义:https://mavlink.io/en/messages/common.html
- QGroundControl MAVLink 定制说明:https://docs.qgroundcontrol.com/master/en/qgc-dev-guide/custom_build/mavlink.html
1. MAVLink 是什么
MAVLink,全称 Micro Air Vehicle Link,是无人机、地面站、云台、相机、伴随计算机等组件之间常用的轻量级通信协议。
它解决的问题是:不同设备之间如何用统一格式交换飞行状态、控制命令、任务航点、参数、日志、相机控制、视频状态等信息。
常见通信关系如下:
text
QGC / 自研地面站
|
| MAVLink over UDP / TCP / Serial
|
飞控 PX4 / ArduPilot
|
| MAVLink
|
相机 / 云台 / 伴随计算机 / 遥测电台
MAVLink 不是传输链路本身。它可以跑在多种链路上:
- 串口:USB、UART、数传电台
- UDP:仿真、Wi-Fi、以太网
- TCP:网络连接、代理转发
- 其他链路:只要能收发字节流,理论上都可以承载 MAVLink
MAVLink 的核心特点:
- 二进制协议,包很小,适合低带宽链路。
- 消息是结构化的,每种消息都有固定 ID 和字段定义。
- 支持多系统、多组件通信,例如一台地面站同时连接多架飞机。
- 支持任务、参数、命令、日志、相机、云台等上层微服务。
- MAVLink 2 支持更大的消息 ID 空间、扩展字段和消息签名。
2. MAVLink 1 和 MAVLink 2 的区别
QGroundControl 当前默认使用 MAVLink 2 的 C 语言生成库,也就是 mavlink/c_library_v2。MAVLink 2 是现在开发新地面站时应该优先考虑的版本。
主要区别:
| 能力 | MAVLink 1 | MAVLink 2 |
|---|---|---|
| 起始字节 | 0xFE |
0xFD |
| Message ID | 8 bit,最多 256 个消息 ID | 24 bit,空间大很多 |
| 扩展字段 | 不支持 | 支持 |
| 消息签名 | 不支持 | 支持 13 字节签名 |
| 兼容性标志 | 不支持 | 支持 incompat_flags 和 compat_flags |
| 新项目推荐 | 不推荐作为首选 | 推荐 |
MAVLink 2 仍然可以处理很多 MAVLink 1 场景,但新功能应该按 MAVLink 2 设计。
3. MAVLink 2 数据包格式
一个 MAVLink 2 包大致由包头、载荷、校验和、可选签名组成。
text
+------------+----------------+---------+------------+
| Header | Payload | CRC | Signature |
+------------+----------------+---------+------------+
| 10 bytes | 0 - 255 bytes | 2 bytes | 0 / 13 B |
+------------+----------------+---------+------------+
MAVLink 2 头部字段:
| 字段 | 长度 | 说明 |
|---|---|---|
| magic | 1 | 起始字节,MAVLink 2 是 0xFD |
| len | 1 | payload 长度 |
| incompat_flags | 1 | 不兼容标志。接收方不理解时必须拒绝该包 |
| compat_flags | 1 | 兼容标志。接收方不理解也可以继续处理 |
| seq | 1 | 发送序号,用于检测丢包 |
| sysid | 1 | 系统 ID,例如某一架飞机或地面站 |
| compid | 1 | 组件 ID,例如飞控、相机、云台、地面站 |
| msgid | 3 | 消息 ID,决定 payload 应按哪种消息结构解析 |
| payload | 0-255 | 消息载荷 |
| checksum | 2 | CRC 校验 |
| signature | 0 或 13 | 可选签名,启用签名时存在 |
开发时通常不要手写这些字节。正确做法是使用 MAVLink 官方生成库或成熟封装库,让库负责打包、解析、CRC、字段布局和字节序。
4. MAVLink 的核心概念
4.1 System ID
sysid 表示一个系统。常见例子:
- 飞控:通常是
1 - 地面站:常用
255 - 多机时:不同飞机使用不同
sysid
地面站连接多架飞机时,应该用 sysid 区分每一架飞机。
4.2 Component ID
compid 表示系统里的组件。常见例子:
- 飞控自驾仪组件
- 地面站组件
- 相机组件
- 云台组件
- 伴随计算机组件
同一个 sysid 下可以有多个 compid。
4.3 Message ID
msgid 表示消息类型。例如:
| 消息 | 用途 |
|---|---|
HEARTBEAT |
心跳包,发现设备、判断连接是否在线 |
SYS_STATUS |
系统状态、电池、电压、电流等 |
GPS_RAW_INT |
GPS 原始信息 |
ATTITUDE |
姿态角、角速度 |
GLOBAL_POSITION_INT |
全球位置、高度、航向等 |
COMMAND_LONG |
发送通用命令 |
COMMAND_ACK |
命令执行结果 |
PARAM_REQUEST_LIST |
请求全部参数 |
PARAM_VALUE |
参数值返回 |
MISSION_ITEM_INT |
航点任务项 |
MISSION_COUNT |
任务数量 |
MISSION_ACK |
任务传输结果 |
4.4 Dialect
MAVLink 消息由 XML 文件定义,这些 XML 被称为 dialect。
常见 dialect:
common.xml:通用消息,大部分地面站和飞控都会支持。ardupilotmega.xml:ArduPilot 扩展消息。standard.xml:标准消息集合。- 自定义 dialect:给自己的设备扩展新消息。
开发地面站时,建议先支持 common.xml。如果要兼容 ArduPilot,再加入 ardupilotmega.xml。如果要支持自定义硬件,再设计自己的 XML dialect。
5. MAVLink 里有什么内容
MAVLink 不只是一些单独消息,它还定义了一组常见通信流程,也就是 microservices。
地面站最常用的内容如下。
5.1 心跳与设备发现
核心消息:
HEARTBEAT
地面站通过接收飞控发来的 HEARTBEAT 判断:
- 是否连接成功
- 飞控类型
- 飞行器类型
- 是否解锁
- 当前飞行模式
- MAVLink 版本
地面站也应该周期性发送自己的 HEARTBEAT,让飞控知道地面站在线。
5.2 遥测状态
常见消息:
SYS_STATUSBATTERY_STATUSGPS_RAW_INTGLOBAL_POSITION_INTLOCAL_POSITION_NEDATTITUDEVFR_HUDRC_CHANNELS
这些消息用于显示:
- 电池
- GPS
- 姿态
- 高度
- 速度
- 位置
- 航向
- 遥控输入
- 飞行状态
5.3 命令协议
常见消息:
COMMAND_LONGCOMMAND_INTCOMMAND_ACK
常见命令:
- 解锁 / 上锁
- 起飞
- 降落
- 返航
- 设置模式
- 设置航向
- 控制相机
- 控制云台
基本流程:
text
地面站发送 COMMAND_LONG
|
v
飞控执行或拒绝命令
|
v
飞控返回 COMMAND_ACK
地面站不能只管发送命令,还必须处理 COMMAND_ACK,否则无法知道命令是否成功。
5.4 参数协议
常见消息:
PARAM_REQUEST_LISTPARAM_REQUEST_READPARAM_VALUEPARAM_SET
用途:
- 读取飞控全部参数
- 读取单个参数
- 修改参数
- 保存参数缓存
基本流程:
text
地面站发送 PARAM_REQUEST_LIST
|
v
飞控连续返回多个 PARAM_VALUE
|
v
地面站根据 param_count 和 param_index 判断是否收齐
参数协议容易遇到丢包,所以地面站通常需要:
- 记录已收到的参数索引
- 对缺失参数重新请求
- 设置超时重试
- 区分参数类型
- 本地缓存参数
5.5 任务协议
常见消息:
MISSION_COUNTMISSION_REQUEST_INTMISSION_ITEM_INTMISSION_ACKMISSION_CLEAR_ALLMISSION_REQUEST_LISTMISSION_CURRENTMISSION_ITEM_REACHED
用途:
- 上传航点任务
- 下载飞控已有任务
- 清空任务
- 跟踪当前执行到哪个航点
- 显示航点是否到达
上传任务的典型流程:
text
地面站发送 MISSION_COUNT
|
v
飞控请求第 0 个任务项:MISSION_REQUEST_INT
|
v
地面站发送第 0 个 MISSION_ITEM_INT
|
v
飞控继续请求下一个任务项
|
v
全部发送完成后,飞控返回 MISSION_ACK
任务协议也需要超时、重试和状态机。不要用简单的 for 循环一次性把所有航点发出去。
5.6 文件传输协议
常见用途:
- 下载日志
- 访问飞控文件系统
- 获取任务或配置文件
相关协议通常称为 MAVLink FTP。它比普通消息流程复杂,建议等基础连接、参数、任务稳定后再实现。
5.7 日志协议
常见能力:
- 查询日志列表
- 下载日志
- 实时日志流
QGC 里有 PX4 的 MAVLink 2 日志相关功能,但这类功能依赖飞控支持和带宽条件。
5.8 相机与云台协议
常见能力:
- 拍照
- 开始 / 停止录像
- 设置相机模式
- 获取相机信息
- 控制云台角度
- 跟踪目标
如果你的地面站后续要做专业载荷控制,需要单独设计 Camera 和 Gimbal 模块。
5.9 消息签名
MAVLink 2 支持消息签名,用于认证消息来源,降低被伪造命令控制飞机的风险。
签名会给消息追加 13 字节签名数据。启用后,收发双方需要共享密钥,并检查时间戳和签名。
注意:
- 签名是认证,不是加密。
- 签名不能隐藏消息内容。
- 签名配置错误会导致通信失败。
- 初期开发可以先不启用签名,等基础链路稳定后再加入。
6. 地面站如何使用 MAVLink
开发一个最小地面站,可以按下面流程做。
6.1 选择 MAVLink 库
推荐路线:
- C++/Qt:使用官方生成的 C MAVLink 头文件,外层用 C++ 封装。
- Python 原型:使用
pymavlink快速验证流程。 - Rust/Go/Java:选择对应生态里成熟的 MAVLink 库。
如果你做 Qt 地面站,建议后续在 libs/mavlink/ 放 MAVLink 生成代码或作为子模块引入。
6.2 建立通信链路
先实现两种链路就够:
- UDP:用于 PX4 SITL、仿真和本机测试。
- Serial:用于真实飞控、USB、数传。
建议模块划分:
text
src/Comms/
LinkInterface 抽象链路接口
UdpLink UDP 实现
SerialLink 串口实现
src/MAVLink/
MAVLinkProtocol MAVLink 解析、打包、路由
MAVLinkMessageRouter 消息分发
6.3 解析字节流
MAVLink 是字节流协议。串口或 UDP 收到的是 bytes,不是一条条自动分好的消息。
处理方式:
text
链路收到 bytes
|
v
逐字节喂给 MAVLink parser
|
v
parser 解析出完整 mavlink_message_t
|
v
根据 msgid 分发给对应模块
伪代码:
cpp
for (uint8_t byte : receivedBytes) {
mavlink_message_t message;
mavlink_status_t status;
if (mavlink_parse_char(channel, byte, &message, &status)) {
handleMavlinkMessage(message);
}
}
6.4 发送心跳
地面站应该定时发送 HEARTBEAT。常见周期是 1 Hz。
心跳用于告诉飞控:
- 我是地面站
- 我还在线
- 我支持 MAVLink
伪代码:
cpp
mavlink_message_t msg;
mavlink_msg_heartbeat_pack(
groundStationSystemId,
groundStationComponentId,
&msg,
MAV_TYPE_GCS,
MAV_AUTOPILOT_INVALID,
0,
0,
MAV_STATE_ACTIVE
);
sendMavlinkMessage(msg);
6.5 处理飞控心跳
收到 HEARTBEAT 后,地面站应该创建或更新 Vehicle 对象。
建议逻辑:
text
收到 HEARTBEAT
|
v
读取 sysid / compid
|
v
如果是新 sysid,创建 Vehicle
|
v
更新飞控类型、飞行器类型、解锁状态、飞行模式、最后在线时间
如果超过一定时间没有收到某个 Vehicle 的心跳,比如 3-5 秒,可以标记为失联。
6.6 请求数据流
不同飞控的数据流控制方式不完全一样。
常见方式:
- 老方式:
REQUEST_DATA_STREAM - 新方式:
MAV_CMD_SET_MESSAGE_INTERVAL
新项目建议优先使用 MAV_CMD_SET_MESSAGE_INTERVAL 请求指定消息频率。
例子:
text
请求 ATTITUDE 以 20 Hz 发送
请求 GLOBAL_POSITION_INT 以 5 Hz 发送
请求 SYS_STATUS 以 1 Hz 发送
6.7 分发消息
不要在一个巨大函数里处理所有 MAVLink 消息。建议按模块分发:
text
MAVLinkProtocol
|
+-- Vehicle HEARTBEAT / SYS_STATUS / ATTITUDE
+-- MissionManager MISSION_*
+-- ParameterManager PARAM_*
+-- CameraManager CAMERA_*
+-- LogManager LOG_*
+-- TerrainManager TERRAIN_*
这样后续模块会更清楚,也更接近 QGC 的组织方式。
6.8 发送命令并等待 ACK
命令发送必须带状态跟踪。
推荐流程:
text
创建 CommandTransaction
|
v
发送 COMMAND_LONG
|
v
启动超时定时器
|
v
收到 COMMAND_ACK 后结束
|
v
超时则重试或返回失败
你需要处理这些结果:
MAV_RESULT_ACCEPTEDMAV_RESULT_TEMPORARILY_REJECTEDMAV_RESULT_DENIEDMAV_RESULT_UNSUPPORTEDMAV_RESULT_FAILEDMAV_RESULT_IN_PROGRESS
6.9 做好丢包和重试
MAVLink 常跑在无线链路上,丢包是正常情况。
地面站必须设计:
- 消息序号统计
- 丢包率计算
- 超时重试
- 请求缺失数据
- 连接断开检测
- 自动重连
- 对关键命令显示失败原因
7. 一个最小 MAVLink 地面站应支持什么
第一阶段建议只做这些:
| 功能 | 必要消息 |
|---|---|
| 发现飞控 | HEARTBEAT |
| 显示连接状态 | HEARTBEAT、链路状态 |
| 显示电池 | SYS_STATUS、BATTERY_STATUS |
| 显示姿态 | ATTITUDE |
| 显示 GPS | GPS_RAW_INT |
| 显示位置 | GLOBAL_POSITION_INT |
| 解锁 / 上锁 | COMMAND_LONG、COMMAND_ACK |
| 起飞 / 降落 / 返航 | COMMAND_LONG、COMMAND_ACK |
| 设置模式 | COMMAND_LONG 或固件适配接口 |
第二阶段再做:
- 参数读取和修改
- 航点任务上传下载
- 地图航线显示
- 飞行模式完整适配
- 日志下载
第三阶段再做:
- 视频
- 相机
- 云台
- 多机
- MAVLink 签名
- 自定义消息
8. 在本项目里的建议模块设计
结合当前目录结构,建议这样安排:
text
src/Comms/
负责 UDP、TCP、Serial 等链路,不理解具体 MAVLink 消息含义。
src/MAVLink/
负责 MAVLink parser、message encode/decode、协议版本、签名、消息路由。
src/Vehicle/
负责飞行器对象、连接状态、飞行模式、解锁状态、基础遥测。
src/FirmwarePlugin/
负责 PX4、ArduPilot 差异,比如飞行模式映射、命令差异、参数差异。
src/MissionManager/
负责 MISSION_* 协议状态机。
src/Settings/
负责地面站设置,比如默认 UDP 端口、系统 ID、组件 ID、自动连接策略。
qml/FlightDisplay/
显示飞行状态、姿态、高度、速度、电池、GPS。
qml/PlanView/
编辑和上传航线任务。
关键原则:
Comms只负责收发字节,不解析业务含义。MAVLink只负责协议层,不直接控制界面。Vehicle负责把零散 MAVLink 消息聚合成地面站可用的飞行器状态。FirmwarePlugin处理 PX4 和 ArduPilot 的差异,不要把差异写得到处都是。- QML 只绑定状态和触发动作,不直接写 MAVLink 协议状态机。
9. 常见端口和连接方式
常见默认值:
| 场景 | 地址 / 端口 |
|---|---|
| PX4 SITL 到地面站 | UDP 14550 |
| 地面站主动连仿真 | UDP 或 TCP,按仿真配置 |
| ArduPilot SITL | UDP/TCP 端口视启动参数而定 |
| USB 飞控 | 串口设备,例如 Linux /dev/ttyACM0,Windows COMx |
| 数传电台 | 串口,波特率常见 57600 或 115200 |
实际项目里不要把端口写死,应该放入 Settings。
10. 自定义 MAVLink 消息
只有在通用消息不够用时才建议自定义消息。
流程:
- 新建自己的 XML dialect。
- 在 XML 里定义 enum 和 message。
- 使用
mavgen生成目标语言代码。 - 地面站和设备端使用同一份 dialect。
- 给消息 ID 留好范围,避免和已有消息冲突。
注意:
- 能用
common.xml解决的,不要自定义。 - 自定义消息会降低和其他地面站、飞控、工具的兼容性。
- 自定义协议必须写清版本和字段含义。
11. 安全注意事项
MAVLink 本身默认不加密。真实飞行时要注意:
- 不要在开放网络里裸跑可控飞机的 MAVLink。
- 关键命令必须检查 ACK。
- UI 上要对解锁、起飞、降落、返航做确认和状态反馈。
- 对外部输入的 MAVLink 消息做来源和目标检查。
- 多机时必须严格区分
sysid。 - 有安全要求时考虑 MAVLink 2 signing、VPN、专用链路或其他加密通道。
12. 学习和实现路线
建议按下面顺序写:
- UDP 收发字节。
- 接入 MAVLink parser。
- 收到并打印
HEARTBEAT。 - 地面站发送自己的
HEARTBEAT。 - 创建
Vehicle,显示在线 / 离线状态。 - 解析
ATTITUDE、GPS_RAW_INT、GLOBAL_POSITION_INT、SYS_STATUS。 - 实现
COMMAND_LONG和COMMAND_ACK。 - 实现参数协议。
- 实现任务协议。
- 做固件适配层,区分 PX4 和 ArduPilot。
- 加入串口连接。
- 加入日志、相机、云台、视频等高级功能。
13. 最小调试方法
开发初期推荐使用仿真,不要一开始就连真实飞机。
推荐组合:
- PX4 SITL + 自研地面站
- ArduPilot SITL + 自研地面站
- Wireshark MAVLink dissector 抓包
- QGroundControl 对照同一飞控的行为
调试时重点看:
- 是否收到
HEARTBEAT sysid和compid是否正确- 是否使用 MAVLink 2 包头
0xFD - 是否有 CRC 错误
- 是否有丢包
- 命令是否收到
COMMAND_ACK - 参数和任务流程是否有超时重试
14. 结论
MAVLink 是地面站和飞控之间的核心通信协议。开发自己的 QGC 风格地面站时,不要把 MAVLink 当作简单的收发字符串,而要把它当作一套完整的协议系统:
- 底层是字节流解析和消息打包。
- 中层是心跳、命令、参数、任务、日志等微服务状态机。
- 上层是 Vehicle、Mission、Camera、Settings 等业务对象。
- UI 只应该使用业务对象提供的状态和动作。
先把心跳、遥测、命令 ACK 做稳,再逐步扩展参数、任务和高级功能。这样项目结构会更清楚,也更容易接近 QGroundControl 的工程质量。