4. MAVLink消息结构

心跳,是无人机通信的命脉。

你想想看,地面站怎么知道飞机还活着?飞控怎么知道遥控器信号断了?全靠这条「心跳线」。MAVLink 里最基础、最重要的消息,就是 HEARTBEAT(消息 ID 0)。

我刚开始做飞控开发时,第一个调通的 MAVLink 消息就是它。当时看着 QGroundControl 上那个心跳图标从灰色变成绿色,心里踏实多了。嗯,今天我们就把它彻底讲透。

4.1 HEARTBEAT 消息结构

一条心跳消息只有 9 个字节的有效载荷。结构如下:

字段名 类型 字节数 说明
type uint8_t 1 系统类型(MAV_TYPE)
autopilot uint8_t 1 飞控类型(MAV_AUTOPILOT)
base_mode uint8_t 1 基础模式位掩码(MAV_MODE_FLAG)
custom_mode uint32_t 4 自定义模式(由飞控厂商定义)
system_status uint8_t 1 系统状态(MAV_STATE)
mavlink_version uint8_t 1 MAVLink 协议版本(固定为 3)

这里有个细节:custom_mode 占了 4 个字节,是唯一一个超过 1 字节的字段。为什么?因为飞控的模式太多了。ArduPilot 有几十种模式,PX4 也有十几二十种,1 个字节根本不够用。

核心要点:地面站判断无人机是否在线,就看心跳消息的接收间隔。如果超过 3 秒没收到,就认为连接丢失。我一般把心跳发送间隔设为 1 秒,既不会太频繁占用带宽,又能及时检测断连。

4.2 系统类型与状态枚举

系统类型 MAV_TYPE 告诉地面站:你连的是什么玩意儿?

常用的有这些:

枚举值 名称 说明
0 MAV_TYPE_GENERIC 通用类型(不推荐)
1 MAV_TYPE_FIXED_WING 固定翼
2 MAV_TYPE_QUADROTOR 四旋翼
3 MAV_TYPE_COAXIAL 共轴直升机
4 MAV_TYPE_HELICOPTER 直升机
12 MAV_TYPE_SUBMARINE 水下潜航器
16 MAV_TYPE_GCS 地面站

系统状态 MAV_STATE 则告诉地面站:飞机现在什么状况?

枚举值 名称 说明
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 正在关机

避坑指南:我曾经遇到过一个情况,飞机明明在飞,地面站却显示「待机」。查了半天,发现是飞控代码里忘了在起飞后把状态从 MAV_STATE_STANDBY 切换到 MAV_STATE_ACTIVE。地面站看到状态不对,直接触发了安全保护,把飞机给降落了。嗯,从那以后我每次都会检查状态切换逻辑。

4.3 base_mode 位掩码详解

base_mode 是一个位掩码,每个 bit 代表一个开关状态。这是 MAVLink 里比较巧妙的设计——用 1 个字节表达了 8 个布尔值。

Bit 标志位 含义
0 MAV_MODE_FLAG_CUSTOM_MODE_ENABLED 启用自定义模式
1 MAV_MODE_FLAG_TEST_ENABLED 测试模式
2 MAV_MODE_FLAG_AUTO_ENABLED 自动模式
3 MAV_MODE_FLAG_GUIDED_ENABLED 引导模式
4 MAV_MODE_FLAG_STABILIZE_ENABLED 增稳模式
5 MAV_MODE_FLAG_HIL_ENABLED 硬件在环仿真
6 MAV_MODE_FLAG_MANUAL_INPUT_ENABLED 手动输入
7 MAV_MODE_FLAG_SAFETY_ARMED 已解锁(电机可转)

举个例子:一架正在自动飞行的四旋翼,它的 base_mode 可能是这样:

// 位掩码计算
base_mode = 0;
base_mode |= (1 << 0);  // 启用自定义模式
base_mode |= (1 << 2);  // 自动模式
base_mode |= (1 << 6);  // 手动输入(遥控器信号正常)
base_mode |= (1 << 7);  // 已解锁

// 结果:base_mode = 0b11000101 = 197

我个人习惯在代码里用位操作来设置和读取这些标志位,而不是直接写死数值。这样可读性高,也不容易出错。

4.4 自定义模式(custom_mode)

这才是真正体现飞控差异的地方。

MAVLink 标准只定义了基础模式位掩码,但具体到「悬停」「返航」「跟随」这些模式,每个飞控厂商都有自己的定义。这就是 custom_mode 存在的意义——它是一个 32 位的自由空间,你想怎么用就怎么用。

以 ArduPilot 为例,它的自定义模式枚举:

值 模式名 说明
0 STABILIZE 增稳模式
1 ACRO 特技模式
2 ALT_HOLD 定高模式
3 AUTO 自动模式
4 GUIDED 引导模式
5 LOITER 悬停模式
6 RTL 返航模式
7 CIRCLE 绕圈模式
9 LAND 降落模式
16 POSHOLD 定点模式

PX4 的定义则完全不同,它用的是 PX4_CUSTOM_MAIN_MODE 和 PX4_CUSTOM_SUB_MODE 的组合。所以地面站在解析 custom_mode 时,必须先知道 autopilot 字段的值,才能正确映射。

实战技巧:如果你在开发自己的飞控,建议把 custom_mode 的高 16 位留给厂商自定义,低 16 位留给模式编号。这样既兼容了标准,又保留了扩展空间。我自己的飞控项目就是这么设计的,用了两年多,没出过兼容性问题。

4.5 心跳消息的发送与接收

发送心跳其实很简单。以 PX4 的代码为例:

// 发送心跳消息
mavlink_message_t msg;
uint8_t buf[MAVLINK_MAX_PACKET_LEN];

mavlink_msg_heartbeat_pack(
    1,                    // system_id(飞控自身 ID)
    1,                    // component_id(飞控组件 ID)
    &msg,                 // 输出消息
    MAV_TYPE_QUADROTOR,   // 系统类型
    MAV_AUTOPILOT_PX4,    // 飞控类型
    MAV_MODE_FLAG_MANUAL_INPUT_ENABLED |
    MAV_MODE_FLAG_SAFETY_ARMED,  // base_mode
    0,                    // custom_mode(这里用 0 简化)
    MAV_STATE_ACTIVE      // 系统状态
);

uint16_t len = mavlink_msg_to_send_buffer(buf, &msg);
// 然后通过串口/UDP 发送 buf 中的 len 个字节

接收端解析:

// 收到消息后解析
mavlink_message_t msg;
mavlink_status_t status;

// 假设从串口读到一个字节
if (mavlink_parse_char(MAVLINK_COMM_0, c, &msg, &status)) {
    if (msg.msgid == MAVLINK_MSG_ID_HEARTBEAT) {
        uint8_t type;
        uint8_t autopilot;
        uint8_t base_mode;
        uint32_t custom_mode;
        uint8_t system_status;

        mavlink_msg_heartbeat_decode(&msg, &type, &autopilot,
                                     &base_mode, &custom_mode,
                                     &system_status);

        // 检查是否解锁
        if (base_mode & MAV_MODE_FLAG_SAFETY_ARMED) {
            printf("飞机已解锁!\n");
        }
    }
}

避坑指南:我曾经在调试时发现地面站一直收不到心跳,但串口数据明明在发。最后发现是 mavlink_parse_char 的返回值判断写错了——我把 if (mavlink_parse_char(...)) 写成了 while (mavlink_parse_char(...)),导致解析完一条消息后,缓冲区里的下一个字节被当成新消息的起始字节,整个解析就乱掉了。嗯,这种低级错误,犯过一次就记住了。

4.6 知识体系总览

下面这张图把心跳消息的整个知识结构串起来了:

HEARTBEAT 消息知识体系 HEARTBEAT 消息知识体系 HEARTBEAT (ID=0) 消息结构 (9字节) type | autopilot | base_mode custom_mode (4字节) | system_status mavlink_version (固定3) 系统类型与状态 MAV_TYPE: 四旋翼/固定翼/地面站 MAV_STATE: 待机/激活/紧急 base_mode 位掩码 Bit 0-6: 模式标志位 Bit 7: 解锁标志 custom_mode 自定义 ArduPilot: 0=增稳, 6=返航 PX4: 主模式+子模式组合 发送与接收 mavlink_msg_heartbeat_pack() mavlink_msg_heartbeat_decode()

这张图把心跳消息拆成了五个维度:消息结构、系统类型与状态、base_mode 位掩码、custom_mode 自定义、以及发送与接收的 API。每个维度之间都有依赖关系——比如解析 custom_mode 之前,必须先知道 autopilot 类型。

好了,心跳消息就讲到这里。记住一句话:心跳是无人机通信的基石,它的每个字段都有明确用途。搞懂了它,后面再学其他消息就会轻松很多。