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 防御性编程
防御性编程,说白了就是「别相信任何人」——包括你自己。你写的函数,可能会被传进来各种奇怪的参数。你的硬件,可能会返回各种离谱的值。
防御性编程的核心思想就两条:
- 检查所有输入:函数入口处,先检查参数是否合法。
- 检查所有返回值:调用任何函数后,都要检查返回值。
我曾经在一个项目中吃过亏。一个温度传感器读取函数,正常情况下返回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)在航天系统中的应用和注意事项。