3、心跳与健康监测:心跳报文设计、超时判定策略、健康状态机
各位同学,咱们今天聊一个飞控系统里特别实在的话题——心跳与健康监测。
说白了,就是怎么让两个飞控板互相知道对方还活着,活得怎么样。我当年第一次做双余度飞控时,觉得这事儿特简单:不就是定时发个包嘛?结果真上了试飞台,才发现坑多得很。嗯,咱们今天就把这些坑一个一个填上。
3.1 心跳报文设计
心跳报文,你可以理解成两个飞控之间的“打招呼”。但打招呼也有讲究,不能太啰嗦,也不能太敷衍。
报文结构,我个人习惯用固定长度、紧凑型的设计。举个例子:
typedef struct {
uint32_t timestamp; // 时间戳,单位微秒
uint8_t source_id; // 源节点ID:0x01为主控,0x02为备控
uint8_t status; // 健康状态:0x00=健康,0x01=亚健康,0x02=故障
uint8_t seq_num; // 序列号,用于检测丢包
uint8_t crc8; // 校验字节
} __attribute__((packed)) HeartbeatPacket_t;
你看,总共才8个字节。为什么这么小?因为心跳报文要高频发送,我一般设成50Hz到100Hz。你想想看,如果报文太大,CPU时间都花在打包解包上了,不值当。
这里有个细节:seq_num 字段很多人会忽略。我在项目中遇到过,单纯靠时间戳判断丢包其实不准,因为两个飞控的时钟可能有偏差。序列号就不一样了,连续递增,中间跳号了,那就是丢包了。
3.2 超时判定策略
心跳报文发出去了,对方怎么知道“没收到”呢?这就涉及到超时判定。
最简单的做法:设定一个固定超时时间,比如50ms内没收到下一包,就认为超时。但这样做有个问题——万一网络抖动呢?
我建议用滑动窗口法。具体来说:
- 维护一个长度为N的接收时间窗口(比如N=5)
- 每次收到心跳,记录当前时间
- 计算最近N个时间间隔的平均值
- 超时阈值 = 平均间隔 × 3(或4)
为什么会这样?因为飞控在不同工况下,CPU负载不一样。高负载时,任务调度可能延迟几毫秒。固定超时要么太敏感导致误判,要么太迟钝导致切换滞后。滑动窗口能自适应调整,说白了就是更聪明。
我曾经在一个项目中,固定超时设了100ms,结果飞控在高机动时偶尔丢一包,备控立马认为主控挂了,直接切换。切换过程中飞机抖了一下,试飞员差点骂娘。后来改成滑动窗口,再也没出过这种问题。
3.3 健康状态机
好了,现在能检测到心跳超时了,但光有“活着”和“死了”两种状态够吗?不够。你想想看,一个飞控可能只是某个传感器数据异常,但还能勉强工作。这时候直接切到备控,不一定是最优解。
所以我设计了三级健康状态:健康 → 亚健康 → 故障。
状态转移图如下:
你看这个状态机,核心思想是“迟判早切”。什么意思?
- 进入亚健康要快:连续2次超时,就降级。别犹豫,宁可误判也不能漏判。
- 恢复健康要慢:连续5次正常心跳,才能回到健康。防止频繁抖动。
- 进入故障要稳:连续3次超时,才判定故障。给亚健康状态一个缓冲。
我在实际项目中,还加了一个“亚健康计数器”。每次进入亚健康状态,计数器加1。如果一小时内亚健康次数超过3次,直接强制切到备控,然后主控做一次完整自检。这个机制帮我抓出过好几次间歇性硬件故障。
3.4 实际部署中的注意事项
嗯,最后聊几个实战中容易踩的坑。
- 心跳发送要独立于主任务。我见过有人把心跳发送放在控制循环里,结果控制循环卡住了,心跳也停了。备控以为主控挂了,直接切换。其实主控只是算力过载。正确做法是用一个独立定时器或RTOS任务来发心跳。
- 接收端要做去抖动。硬件中断可能连续触发两次,导致同一包心跳被处理两次。加个简单的去抖逻辑,比如10ms内只处理最新的一包。
- 注意时钟同步。两个飞控的晶振有误差,时间戳会慢慢漂移。我建议不要直接用时间戳做超时判断,而是用“收到心跳后重置看门狗”的方式。
好了,关于心跳与健康监测,今天就聊这么多。记住一句话:心跳是飞控的脉搏,状态机是飞控的神经。这两样东西设计好了,冗余切换就成功了一半。