4. HEARTBEAT报文深度解析:心跳机制、系统状态、模式切换、自定义模式

HEARTBEAT,说白了就是飞控的「我活着」信号。在MAVLink协议里,它是所有通信的基础。没有心跳,地面站根本不知道飞机在哪、什么状态、能不能响应指令。

我刚开始做SITL仿真时,第一件事就是抓心跳报文。为什么?因为只要心跳正常,整个链路就通了。心跳断了,后面所有报文都是白搭。

4.1 心跳报文的结构

HEARTBEAT的报文ID是0,优先级最高。它的载荷只有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字节的字段。为什么?因为飞控的模式太多了,1个字节根本不够用。

4.2 心跳机制:1Hz的约定

MAVLink标准规定,心跳报文以1Hz的频率发送。也就是每秒一条。这个频率是经过权衡的——太快浪费带宽,太慢又无法及时感知系统状态变化。

我在项目中遇到过一个问题:某个定制飞控的心跳频率被改成了0.5Hz,结果地面站一直报「连接超时」。查了半天才发现是心跳间隔超标了。所以,不要轻易改心跳频率,除非你同时改了地面站的超时阈值。

小技巧:在SITL仿真中,可以用 mavlink console 或 mavproxy --heartbeat 来监控心跳。如果看到心跳中断,先检查串口波特率,再检查飞控是否卡死在某个循环里。

4.3 系统状态:MAV_STATE枚举

system_status字段告诉你飞控当前的整体健康状态。常用的值有:

  • MAV_STATE_UNINIT (0):未初始化,刚上电
  • MAV_STATE_BOOT (1):正在启动
  • MAV_STATE_CALIBRATING (2):校准中
  • MAV_STATE_STANDBY (3):待机,已就绪但未解锁
  • MAV_STATE_ACTIVE (4):活跃,已解锁或正在执行任务
  • MAV_STATE_CRITICAL (5):严重警告,比如电池电压过低
  • MAV_STATE_EMERGENCY (6):紧急状态,比如遥控器失控
  • MAV_STATE_POWEROFF (7):正在关机
  • MAV_STATE_FLIGHT_TERMINATION (8):飞行终止,比如触发了急停

我个人习惯在SITL仿真中,用Python脚本轮询心跳的system_status字段,来判断飞控是否真的进入了预期状态。比如解锁后,状态应该从STANDBY变成ACTIVE。如果没变,说明解锁指令没生效。

4.4 模式切换:base_mode与custom_mode

模式切换是心跳报文最核心的功能之一。它通过两个字段配合实现:

base_mode是一个位掩码,定义了一些通用标志:

位 标志 含义
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 已解锁

custom_mode则是飞控特有的模式编码。比如ArduPilot里,每个模式都有一个数字ID:

  • STABILIZE = 0
  • ACRO = 1
  • ALT_HOLD = 2
  • AUTO = 3
  • GUIDED = 4
  • LOITER = 5
  • RTL = 6
  • ...

你想想看,如果只靠base_mode,你只能知道「是否启用了自动模式」,但不知道具体是AUTO还是RTL。而custom_mode补全了这个信息。

关键点:判断飞控当前模式,必须同时看base_mode和custom_mode。只看一个,信息是不完整的。

4.5 自定义模式:飞控的「方言」

MAVLink标准只定义了通用框架,具体模式由各飞控自己定义。这就是custom_mode存在的意义。

我曾经在PX4和ArduPilot之间做桥接,发现同一个custom_mode值,在两个飞控里代表完全不同的模式。比如值4,在ArduPilot里是GUIDED,在PX4里可能是OFFBOARD。所以,解析custom_mode时,一定要知道飞控类型。

在SITL仿真中,我们可以用Python直接读取心跳报文:

# 伪代码示例:解析心跳报文
def parse_heartbeat(msg):
    base_mode = msg.base_mode
    custom_mode = msg.custom_mode
    system_status = msg.system_status
    
    # 检查是否解锁
    armed = bool(base_mode & 0x80)  # 第7位
    
    # 检查是否启用了自定义模式
    custom_enabled = bool(base_mode & 0x01)
    
    if custom_enabled:
        # 根据飞控类型解析custom_mode
        if msg.autopilot == MAV_AUTOPILOT_ARDUPILOTMEGA:
            mode_name = ardumode_to_name(custom_mode)
        elif msg.autopilot == MAV_AUTOPILOT_PX4:
            mode_name = px4mode_to_name(custom_mode)
        else:
            mode_name = f"Unknown({custom_mode})"
    
    return armed, mode_name, system_status
避坑指南:我曾经在解析custom_mode时,直接用了ArduPilot的映射表去解析PX4的心跳,结果地面站显示的模式全是错的。后来我加了一个判断:先检查autopilot字段,再选择对应的解析表。这个坑,你早晚会遇到。

4.6 心跳与模式切换的流程

模式切换的完整流程是这样的:

  1. 地面站发送MAV_CMD_DO_SET_MODE指令
  2. 飞控收到指令,尝试切换模式
  3. 飞控在下一条心跳中更新base_mode和custom_mode
  4. 地面站收到心跳,确认模式已切换

整个过程是异步的。地面站不能假设指令发出后模式立刻改变,必须等心跳确认。我见过有人写代码时,发完指令立刻读模式,结果读到的还是旧模式。正确的做法是:发指令,然后等心跳,直到模式字段变成目标值。

下面这张图展示了心跳报文在模式切换中的核心作用:

心跳报文与模式切换流程 地面站 飞控(SITL) ① 发送 DO_SET_MODE ② 处理模式切换 ③ 发送HEARTBEAT ④ 心跳含新 base_mode + custom_mode ⑤ 确认模式已切换 关键点: 模式切换是异步的,地面站必须通过心跳报文确认切换结果,不能假设指令发出后立即生效。

4.7 实战:在SITL中抓取并解析心跳

我们直接用Python在SITL仿真中抓心跳。假设你已经启动了SITL并连接了MAVLink:

from pymavlink import mavutil

# 连接SITL
master = mavutil.mavlink_connection('udp:127.0.0.1:14550')

# 等待第一个心跳
master.wait_heartbeat()
print("已收到心跳,系统启动成功")

# 持续监听心跳
while True:
    msg = master.recv_match(type='HEARTBEAT', blocking=True)
    if msg:
        base_mode = msg.base_mode
        custom_mode = msg.custom_mode
        system_status = msg.system_status
        
        armed = (base_mode & 0x80) != 0
        mode = custom_mode
        
        print(f"状态: {system_status}, 解锁: {armed}, 模式: {mode}")

这段代码会每秒打印一次心跳信息。你可以试着在SITL里切换模式,看看custom_mode的变化。

个人经验:我习惯在调试时,把心跳的原始字节也打印出来。有时候解析库有bug,原始字节不会骗人。用 msg.get_msgbuf() 可以拿到原始数据。

4.8 常见问题与避坑

  • 心跳中断:检查网络连接、串口波特率、飞控是否卡死。SITL中可以用 --heartbeat 参数强制发送。
  • 模式解析错误:确认autopilot字段,选择正确的模式映射表。不要混用ArduPilot和PX4的映射。
  • 解锁状态不更新:检查base_mode的第7位。有些地面站只看custom_mode,忽略了base_mode的解锁标志。
  • 版本不匹配:mavlink_version字段固定为3。如果收到其他值,说明协议版本不对,需要升级库。

好了,心跳报文的核心内容就这些。它虽然简单,但却是整个MAVLink通信的基石。理解透了,后面所有报文解析都会顺手很多。