3、嵌入式软件可靠性设计原则:冗余设计、容错设计、防错设计、降级设计

做火控软件这些年,我踩过最大的坑,就是以为代码能按预期跑就行。

直到有一次实弹测试,飞控在强电磁干扰下直接死机。弹头偏了,靶标没打中。嗯,那次之后,我彻底明白了——嵌入式软件,尤其是火控软件,必须把「出错了怎么办」写进代码里

今天聊的这四个原则,说白了就是一套保命手段。我一个个说。

3.1 冗余设计:多留一手,别把鸡蛋放一个篮子里

冗余设计,不是简单复制粘贴代码。你得想清楚:哪个环节最脆弱?

我个人习惯,在火控系统中至少做三层冗余:

  • 硬件冗余:双传感器、双通信总线。主路挂了,备路立刻顶上。
  • 软件冗余:关键算法保留两套实现。比如姿态解算,一套用四元数,一套用欧拉角。结果互相校验。
  • 数据冗余:关键参数存三份。读的时候三取二投票。

核心要点:冗余不是浪费,是买保险。火控系统里,一次失效的代价远高于冗余成本。

我在项目中遇到过一件事:某型火控的陀螺仪数据总线偶尔丢包。硬件冗余设计时,我们加了一条SPI备用总线。结果真有一次,主I2C总线被电机干扰拉死,备路SPI无缝接管。那次测试,弹着点偏差不到0.5米。

我的经验:冗余切换要快。我一般要求切换时间不超过10ms。超过这个数,弹道解算就飘了。

3.2 容错设计:允许出错,但别让错误扩散

容错和冗余不一样。冗余是「多准备一套」,容错是「出错了系统还能凑合跑」。

你想想看,火控系统里最怕什么?单点故障导致全系统崩溃

容错设计我一般分三步走:

  1. 错误检测:CRC校验、看门狗、心跳包。先知道出错了。
  2. 错误隔离:把故障模块关掉,别让它污染其他模块。
  3. 错误恢复:重启故障模块,或者切换到降级模式。

警告:千万别在中断里做复杂容错处理。我曾经见过一个同事,在中断服务函数里跑文件系统恢复——结果中断嵌套,直接栈溢出。系统彻底死机。

举个例子,我设计过一个火控通信协议。每个数据包带3字节CRC,接收方如果校验失败,不会直接丢弃,而是连续三次失败才报错。为什么?因为电磁环境偶尔会有单比特翻转。一次失败可能是噪声,三次失败才是真故障。

// 容错接收示例
#define MAX_RETRY 3
static uint8_t fail_count = 0;

bool receive_packet(packet_t *pkt) {
    if (crc_check(pkt) == false) {
        fail_count++;
        if (fail_count < MAX_RETRY) {
            return false;  // 暂时容忍,不报错
        } else {
            report_error("通信链路故障");
            reset_comm_module();  // 隔离并恢复
            fail_count = 0;
            return false;
        }
    }
    fail_count = 0;
    return true;
}

避坑指南:我曾经把容错阈值设得太宽松,结果通信链路已经半断了,系统还在「容忍」。后来我改成动态阈值——正常时容忍3次,高动态环境下只容忍1次。效果好很多。

3.3 防错设计:从源头掐死错误

防错设计,说白了就是让错误根本发生不了

我见过太多嵌入式软件,把防错寄托在「程序员别写错」上。这太天真了。人都会犯错,你得用代码机制来防。

常用的防错手段:

  • 参数范围检查:所有输入参数,先判断是否在合法范围内。不在?直接拒绝。
  • 状态机防抖:状态切换必须经过合法路径。非法跳转?保持原状态。
  • 写保护:关键配置寄存器,写之前先解锁。防止野指针乱写。

记住:防错设计不是限制开发,是保护系统。尤其是火控软件,一个非法参数可能导致弹道解算完全错误。

我举个例子。某次项目中,一个新手把俯仰角参数传成了弧度制,但函数期望的是角度制。结果弹道直接算到地底下去了。后来我加了一个参数合法性检查:

// 防错设计:参数范围检查
bool set_pitch_angle(float angle_deg) {
    // 防错:角度必须在 -90 到 90 度之间
    if (angle_deg < -90.0f || angle_deg > 90.0f) {
        log_error("非法俯仰角: %.2f", angle_deg);
        return false;  // 拒绝执行
    }
    // 正常处理
    pitch_angle = angle_deg;
    return true;
}

注意:防错设计不能影响正常性能。我见过有人每个函数入口都做全套参数检查,结果系统实时性下降30%。我的做法是:只在模块边界做检查,内部函数信任调用者。

3.4 降级设计:打不过就跑,但别跑死

降级设计,是最后一道防线。

当系统已经出了严重故障,冗余和容错都扛不住了,怎么办?主动降级,保住核心功能

火控系统的降级策略,我一般分三级:

降级等级 触发条件 保留功能 关闭功能
L1(轻度降级) 单个传感器故障 主火控、瞄准 自动跟踪
L2(中度降级) 通信链路半断 手动射击 自动解算、数据上报
L3(严重降级) 主控芯片过热 紧急断电、保存数据 全部火控功能

降级设计有个关键点:降级要可逆。故障恢复后,系统应该能自动升回正常模式。我见过一个系统,降级后只能手动复位才能恢复——这在战场上就是灾难。

核心原则:降级不是放弃,是战略性撤退。保住最核心的「能打出去」这个功能,其他都可以砍。

我曾经在项目中遇到一个情况:火控计算机温度飙升到85度。按照降级策略,系统自动关闭了图像处理模块(最耗CPU),只保留最基本的瞄准和射击功能。等温度降到60度以下,图像处理模块自动恢复。那次测试,虽然降级期间不能自动跟踪,但至少射手还能手动瞄准开火。

我的建议:降级策略一定要写在需求文档里,和军方客户确认好。别自己拍脑袋决定「哪些功能可以砍」。我曾经因为擅自关闭了数据记录功能,被客户追着骂了三天。

小结

这四个原则,我个人的使用优先级是:

  1. 防错设计:从源头掐死错误,成本最低。
  2. 冗余设计:关键环节多留一手。
  3. 容错设计:出错了别崩,能扛就扛。
  4. 降级设计:实在扛不住,保住核心功能。

你想想看,火控软件是直接和「打中打不中」挂钩的。代码里多一行防错,战场上可能就多一条命。嗯,这话不夸张。

下一章,我会聊具体怎么把这些原则落地到代码里。到时候拿实际项目案例来讲,更有意思。