4. 软件可靠性设计:编码规范与MISRA-C、防御性编程、状态机与看门狗协同

各位同学,咱们接着聊。前面几章讲了硬件和架构层面的可靠性,今天终于轮到软件了。很多人觉得软件嘛,能跑就行。但在航天领域,这种想法会出大问题。我见过太多因为软件小bug导致整个任务失败的案例了。

软件可靠性设计,说白了就是三件事:别写错、防出错、出错了能兜底。今天咱们就围绕这三件事展开。

4.1 编码规范与MISRA-C

先说说编码规范。你可能觉得这东西很烦,条条框框太多。但我告诉你,在航天领域,没有规范的代码就是定时炸弹。

MISRA-C是目前航天和汽车领域最权威的C语言编码标准。它不是为了限制你,而是为了帮你避开那些容易出坑的写法。

核心原则:MISRA-C强制要求代码必须清晰、可预测、无歧义。任何编译器可能产生不同行为的写法,都要禁止。

我挑几个重点说说:

  • 禁止使用动态内存分配:malloc/free在航天代码里是禁区。为什么?因为内存碎片、分配失败、释放遗漏,任何一个都够你喝一壶的。我有个项目,就因为一个malloc没检查返回值,导致卫星在轨运行三个月后突然死机。
  • 强制使用大括号:哪怕if后面只有一条语句,也必须加{}。你想想看,if (x) y = 1; 这种写法,后期维护时很容易加一行代码就出问题。
  • 禁止使用goto:这个不用多说了,goto会让代码逻辑变成一团乱麻。
  • 函数不能有超过一个出口:也就是一个函数只能有一个return。这样代码流程清晰,也方便做资源清理。

来看个例子:

/* 不符合MISRA-C的写法 */
int get_status(void)
{
    if (flag) return 1;  /* 多个出口 */
    if (error) return -1;
    return 0;
}

/* 符合MISRA-C的写法 */
int get_status(void)
{
    int status = 0;
    
    if (flag) {
        status = 1;
    } else if (error) {
        status = -1;
    } else {
        status = 0;
    }
    
    return status;  /* 单一出口 */
}

我的习惯:写代码前先配好静态检查工具,比如PC-Lint或者Coverity。让工具帮你检查MISRA-C规则,比自己肉眼强多了。

4.2 防御性编程

防御性编程,说白了就是「别相信任何人」——包括你自己。你写的函数,可能会被传进来各种奇怪的参数。你的硬件,可能会返回各种离谱的值。

防御性编程的核心思想就两条:

  1. 检查所有输入:函数入口处,先检查参数是否合法。
  2. 检查所有返回值:调用任何函数后,都要检查返回值。

我曾经在一个项目中吃过亏。一个温度传感器读取函数,正常情况下返回0~100之间的值。结果有一次硬件故障,它返回了-273。因为没有做范围检查,这个值直接参与了下游的计算,导致整个控制系统崩溃。

从那以后,我写代码都这样:

int read_temperature(void)
{
    int raw_value;
    int ret;
    
    /* 检查硬件接口返回值 */
    ret = sensor_read(&raw_value);
    if (ret != SUCCESS) {
        /* 记录错误,返回安全值 */
        log_error("sensor read failed");
        return SAFE_TEMPERATURE;
    }
    
    /* 检查数值范围 */
    if ((raw_value < TEMP_MIN) || (raw_value > TEMP_MAX)) {
        log_error("temperature out of range: %d", raw_value);
        return SAFE_TEMPERATURE;
    }
    
    return raw_value;
}

注意:防御性编程不是让你写一堆没用的检查。每个检查都要有明确的理由和对应的处理策略。如果检查到错误,是返回默认值?还是触发复位?还是进入安全模式?这些都要提前想好。

4.3 状态机与看门狗协同

状态机是航天软件中最常用的设计模式。为什么?因为航天系统的工作流程非常清晰:发射、入轨、在轨运行、姿态调整、数据回传……每个阶段都有明确的状态和转换条件。

我习惯用有限状态机(FSM)来管理系统的运行状态。每个状态对应一组行为,状态之间的转换由事件触发。这样代码结构清晰,也容易做形式化验证。

看门狗呢?它是硬件层面的最后一道防线。当软件跑飞或者死锁时,看门狗会强制复位系统。

但这里有个关键问题:看门狗和状态机怎么配合?

很多人简单地在主循环里喂狗,这其实不够。我建议的做法是:

  • 在状态机的主循环中喂狗:确保每个状态都能正常执行。
  • 在关键状态转换时喂狗:比如从「发射准备」切换到「发射中」,这个转换如果卡住了,后果很严重。
  • 设置不同的看门狗超时时间:不同状态的执行时间不同,看门狗的超时时间也要相应调整。

来看一个简单的状态机+看门狗协同的例子:

typedef enum {
    STATE_INIT,
    STATE_IDLE,
    STATE_ACTIVE,
    STATE_ERROR,
    STATE_COUNT
} system_state_t;

void main_loop(void)
{
    system_state_t state = STATE_INIT;
    
    while (1) {
        switch (state) {
            case STATE_INIT:
                /* 初始化硬件 */
                init_hardware();
                state = STATE_IDLE;
                break;
                
            case STATE_IDLE:
                /* 等待指令 */
                if (has_command()) {
                    state = STATE_ACTIVE;
                }
                break;
                
            case STATE_ACTIVE:
                /* 执行任务 */
                execute_task();
                if (error_detected()) {
                    state = STATE_ERROR;
                }
                break;
                
            case STATE_ERROR:
                /* 错误处理 */
                handle_error();
                state = STATE_IDLE;
                break;
                
            default:
                /* 非法状态,触发复位 */
                system_reset();
                break;
        }
        
        /* 在状态机主循环中喂狗 */
        watchdog_feed();
        
        /* 检查看门狗是否即将超时 */
        if (watchdog_is_about_to_expire()) {
            /* 记录日志,便于事后分析 */
            log_warning("watchdog near timeout in state %d", state);
        }
    }
}

关键点:看门狗不是用来「检测」问题的,而是用来「兜底」的。真正的问题检测,要靠状态机中的错误检测逻辑。看门狗只负责在软件完全失控时拉一把。

嗯,这里还要提一个坑。我曾经遇到过一个情况:看门狗喂狗代码写在了中断服务程序里。结果主循环死锁了,但中断还在正常触发,看门狗一直被喂,系统根本不会复位。这个教训告诉我:喂狗一定要在主循环中喂,不能放在中断里

4.4 三者如何协同工作

最后,咱们把这三者串起来看看:

层次 手段 作用 我的建议
预防层 MISRA-C编码规范 从源头减少bug 用静态检查工具强制执行
检测层 防御性编程 运行时检测异常 每个函数入口和出口都要检查
恢复层 状态机+看门狗 系统失控时自动恢复 喂狗位置要精心设计

这三层缺一不可。你想想看,如果只有编码规范,运行时出了问题怎么办?如果只有防御性编程,代码写得太复杂反而容易出错怎么办?如果只有看门狗,系统频繁复位但根本问题没解决怎么办?

所以,我的建议是:从编码阶段就开始预防,在运行阶段持续检测,最后用看门狗兜底。这样三层防护,才能保证软件在太空中稳定运行。

好了,这一章就到这里。下一章咱们聊聊实时操作系统(RTOS)在航天系统中的应用和注意事项。