3. MAVLink 心跳协议详解:HEARTBEAT 消息结构、系统状态、自定义模式
心跳,是 MAVLink 通信中最基础也最容易被忽视的协议。说白了,它就是无人机和地面站之间的「你还活着吗?」——每隔一段时间问一次,对方回一句「我还活着,状态是 XX」。但就这么一个简单的机制,背后藏着不少门道。
我刚开始做地面站的时候,觉得心跳就是个定时器发个包,没什么好研究的。直到有一次在野外测试,飞机飞出去两公里突然断连,地面站显示「连接已断开」,但飞机其实还在正常飞。后来排查发现,是心跳超时阈值设得太短,加上链路有轻微丢包,地面站误判了断连。嗯,从那以后,我对心跳协议的每一个字节都不敢马虎。
3.1 HEARTBEAT 消息结构
HEARTBEAT 的消息 ID 是 0,在 MAVLink 协议栈里排第一位。它的 payload 固定为 9 字节,结构如下:
| 字段名 | 类型 | 字节数 | 说明 |
|---|---|---|---|
type |
uint8_t | 1 | 系统类型(飞控、地面站、相机等) |
autopilot |
uint8_t | 1 | 自驾仪类型(PX4、ArduPilot 等) |
base_mode |
uint8_t | 1 | 系统基础模式(位掩码) |
custom_mode |
uint32_t | 4 | 自定义模式(飞控特有) |
system_status |
uint8_t | 1 | 系统状态(已解锁、紧急降落等) |
mavlink_version |
uint8_t | 1 | MAVLink 协议版本(目前固定为 3) |
你看,总共才 9 个字节,但信息量很大。我个人的习惯是,在调试地面站时,先把心跳包完整解析出来打印到日志里,看看每个字段的值是否合理。很多连接问题,其实从心跳数据里就能看出端倪。
关键点:心跳包是双向的。地面站也要发心跳给飞控,告诉飞控「我还连着呢」。如果飞控收不到地面站的心跳,它可能会触发 failsafe 机制——比如自动返航或降落。
3.2 系统状态(system_status)
system_status 字段只有 1 个字节,但它反映了无人机当前最核心的状态。MAVLink 标准定义了一组枚举值:
| 枚举值 | 名称 | 含义 |
|---|---|---|
| 0 | MAV_STATE_UNINIT | 未初始化,系统刚上电 |
| 1 | MAV_STATE_BOOT | 正在启动中 |
| 2 | MAV_STATE_CALIBRATING | 正在校准传感器 |
| 3 | MAV_STATE_STANDBY | 待机,已就绪但未解锁 |
| 4 | MAV_STATE_ACTIVE | 已解锁,正在飞行或运行 |
| 5 | MAV_STATE_CRITICAL | 严重告警,但还能控制 |
| 6 | MAV_STATE_EMERGENCY | 紧急状态,可能已失控 |
| 7 | MAV_STATE_POWEROFF | 正在关机 |
| 8 | MAV_STATE_FLIGHT_TERMINATION | 飞行终止(炸机或主动终止) |
我在做重连逻辑时,特别关注 MAV_STATE_CRITICAL 和 MAV_STATE_EMERGENCY。如果重连后收到的心跳显示系统处于这两个状态,地面站应该立即弹出告警,而不是默默恢复显示。你想想看,飞机都快炸了,地面站还在那慢悠悠地刷新地图,那可就出大事了。
注意:有些飞控在紧急状态下会降低心跳发送频率,甚至停止发送。地面站不能单纯依赖心跳超时来判断断连,还要结合其他消息(如 SYS_STATUS、GPS_RAW_INT)做综合判断。
3.3 自定义模式(custom_mode)
custom_mode 是 4 字节的 uint32_t,标准协议里没规定它的具体含义。说白了,这是留给飞控厂商自己发挥的空间。PX4 和 ArduPilot 对它的编码方式完全不同。
我举个例子,PX4 的自定义模式是这样编码的:
// PX4 自定义模式编码示例
// 主模式:0-15,子模式:16-31
uint32_t px4_custom_mode = (main_mode & 0xFF) | (sub_mode & 0xFF) << 8;
而 ArduPilot 则直接用枚举值:
// ArduPilot 自定义模式枚举
enum ArduPlaneMode {
MANUAL = 0,
CIRCLE = 1,
STABILIZE = 2,
TRAINING = 3,
AUTO = 10,
RTL = 11,
LOITER = 12,
// ... 更多模式
};
这就带来一个问题:地面站怎么知道当前飞控用的是哪种编码方式?答案是——看 autopilot 字段。如果 autopilot 是 MAV_AUTOPILOT_PX4,就用 PX4 的解析方式;如果是 MAV_AUTOPILOT_ARDUPILOTMEGA,就用 ArduPilot 的方式。
我的经验:在写地面站时,不要硬编码自定义模式的解析逻辑。最好做一个插件化的模式解析器,每种飞控注册自己的解析函数。这样以后支持新飞控时,只需要加一个插件,不用改核心代码。
3.4 心跳与重连机制的关系
心跳在重连机制中扮演什么角色?我画了一张图,帮你理清思路:
从这张图你可以看到,心跳是整个重连机制的「触发器」和「验证器」。没有心跳,地面站就不知道连接是否还活着;收到心跳,才能确认重连是否成功。
我个人的做法是,在地面站里维护一个心跳计数器:
// 心跳状态机伪代码
typedef struct {
uint32_t last_heartbeat_ms; // 上次收到心跳的时间戳
uint32_t heartbeat_timeout_ms; // 超时阈值,默认 5000ms
uint8_t missed_count; // 连续丢失次数
uint8_t reconnect_attempts; // 重连尝试次数
bool is_connected; // 当前连接状态
} HeartbeatMonitor;
// 每次收到心跳包时调用
void on_heartbeat_received(HeartbeatMonitor *mon) {
mon->last_heartbeat_ms = get_time_ms();
mon->missed_count = 0;
if (!mon->is_connected) {
// 之前断连,现在收到心跳,说明重连成功
mon->is_connected = true;
mon->reconnect_attempts = 0;
// 触发重连成功回调
on_reconnect_success();
}
}
// 主循环中每秒检查一次
void check_heartbeat_timeout(HeartbeatMonitor *mon) {
uint32_t elapsed = get_time_ms() - mon->last_heartbeat_ms;
if (elapsed > mon->heartbeat_timeout_ms) {
mon->missed_count++;
if (mon->is_connected) {
mon->is_connected = false;
// 触发断连回调,启动重连
on_connection_lost();
}
}
}
核心要点:重连成功不能只看一个心跳包。我曾经遇到过链路抖动,偶尔能收到一个心跳,但下一秒又断了。如果地面站收到一个心跳就认为重连成功,会导致频繁的状态切换。我建议连续收到 2-3 个心跳包,才确认重连成功。
3.5 自定义模式在重连中的特殊处理
重连成功后,地面站需要重新获取飞控的完整状态。但心跳包里的 custom_mode 只有 4 个字节,信息有限。我建议的做法是:
- 先解析心跳,获取基本的系统状态和模式信息
- 再主动请求
MAV_CMD_REQUEST_MESSAGE,获取完整的模式名称和参数 - 更新 UI,显示正确的飞行模式
为什么不能只靠心跳?因为 PX4 的 custom_mode 编码里,主模式和子模式是分开的。比如主模式是 MAV_MODE_POSITION_HOLD,子模式可能是某个具体的悬停模式。地面站需要查表才能显示成「定高悬停」这样的用户友好文字。
一个小技巧:在重连成功后,不要立即发送大量请求消息。先发一个 MAV_CMD_REQUEST_MESSAGE 请求 AUTOPILOT_VERSION,确认飞控版本和功能集,再根据版本决定后续请求哪些消息。这样可以避免在链路不稳定时造成拥塞。
好了,关于心跳协议的核心内容就这些。你可能会问:心跳超时阈值到底设多少合适?这个没有标准答案,取决于你的应用场景。室内测试可以设短一点(3 秒),野外长距离飞行建议设长一点(5-10 秒)。我个人的经验是,先设 5 秒,然后在实际测试中根据丢包率调整。