第1章:嵌入式软件安全架构:看门狗定时器、双核锁步、内存保护单元(MPU)应用

各位同学,大家好。我是老张,在医疗电子这行摸爬滚打了十五年。今天咱们聊一个硬核话题——嵌入式软件的安全架构。说白了,就是怎么让你的理疗仪在关键时刻不掉链子,不出错。

你想想看,一台理疗仪正在给患者做治疗,突然死机了,或者内存被踩坏了,那后果是什么?轻则治疗中断,重则可能对患者造成伤害。所以,安全架构不是锦上添花,而是保命用的。

我个人习惯把安全架构分成三个层次:故障检测故障容错故障隔离。今天要讲的看门狗、双核锁步和MPU,正好对应这三个层次。

1.1 看门狗定时器:最后的救命稻草

看门狗这东西,说白了就是一个定时器。你必须在它溢出之前“喂狗”,否则它就认为系统挂了,然后强制复位。

我在项目中遇到过一件事:一台理疗仪在长时间运行后,偶尔会出现死机。查了三个月,最后发现是看门狗配置有问题——喂狗间隔太短,导致正常运行时偶尔也会触发复位。嗯,这里要注意,看门狗的溢出时间必须留够余量。

关键设计原则:

  • 溢出时间 = 最长任务执行时间 × 1.5 ~ 2倍
  • 喂狗点要放在主循环末尾,而不是中断里
  • 独立看门狗(IWDG)比窗口看门狗(WWDG)更可靠

来看一个实际代码片段。这是我在STM32F4上常用的配置:

// 独立看门狗配置,溢出时间约4秒
void IWDG_Init(void) {
    // 使能写访问
    IWDG->KR = 0x5555;
    // 设置预分频器:64分频,LSI约32KHz
    IWDG->PR = IWDG_Prescaler_64;
    // 设置重装载值:2000,约4秒
    IWDG->RLR = 2000;
    // 等待寄存器更新
    while (IWDG->SR);
    // 启动看门狗
    IWDG->KR = 0xCCCC;
}

// 喂狗函数,放在主循环末尾
void IWDG_Feed(void) {
    IWDG->KR = 0xAAAA;
}

避坑指南:我曾经在一个项目中,把喂狗放在了定时器中断里。结果主循环卡死了,看门狗却一直没复位,因为中断还在正常跑。后来我改了设计——喂狗必须由主循环触发,中断只做标记。

1.2 双核锁步:让两个CPU互相监督

双核锁步,听起来很玄乎,其实原理很简单:两个核心执行同样的指令,比较器实时对比输出。如果结果不一致,说明其中一个出错了,系统立即进入安全状态。

你想想看,单核系统如果CPU内部寄存器被宇宙射线打翻了,你根本不知道。但双核锁步就不一样了——两个核同时被射线打翻的概率极低。

我参与过一款心脏除颤仪的设计,用的就是双核锁步架构。当时选型时,我坚持要用带锁步功能的MCU,比如TI的Hercules系列或者Infineon的AURIX系列。虽然成本高了30%,但安全等级直接上了两个台阶。

特性 单核系统 双核锁步
故障检测率 低(依赖软件自检) 高(硬件实时比较)
故障响应时间 毫秒级 微秒级
成本 高30%~50%
适用标准 IEC 62304 Class B IEC 62304 Class C

注意:双核锁步不是万能的。它只能检测CPU核心的故障,对外设、内存、电源等故障无能为力。所以,锁步通常要配合其他安全机制一起使用。

1.3 内存保护单元(MPU):给程序划地盘

MPU,说白了就是给内存区域设置访问权限。哪些区域可以读,哪些可以写,哪些可以执行,都由MPU说了算。

为什么要用MPU?我举个例子。理疗仪里通常有多个任务:UI显示、治疗控制、数据记录。如果UI任务不小心写到了治疗控制的内存区域,那治疗参数就可能被篡改。患者正在做治疗,突然电流变了,你说危险不危险?

MPU就是用来防止这种“越界”行为的。它把内存分成若干个区域,每个区域都有独立的权限设置。

MPU配置要点:

  1. 划分内存区域:代码区、数据区、外设区、堆栈区
  2. 设置权限:读/写/执行/不可访问
  3. 开启MPU后,所有访问都要经过检查
  4. 违反权限会触发MemManage Fault

来看一个实际的MPU配置代码,基于ARM Cortex-M4:

// 配置MPU区域0:代码区,只读可执行
void MPU_Config(void) {
    // 禁用MPU
    MPU->CTRL = 0;
    
    // 区域0:Flash代码区,0x08000000,大小256KB
    MPU->RBAR = (0x08000000) | (0 << 4);  // 区域号0
    MPU->RASR = (0x1 << 0) |   // 使能
                (0x3 << 1) |   // 全访问权限
                (0x1 << 4) |   // 可执行
                (0x0 << 8) |   // 不可共享
                (0x1 << 16) |  // 大小:256KB
                (0x0 << 24);   // TEX=0
    
    // 区域1:RAM数据区,0x20000000,大小128KB
    MPU->RBAR = (0x20000000) | (1 << 4);  // 区域号1
    MPU->RASR = (0x1 << 0) |   // 使能
                (0x3 << 1) |   // 全访问权限
                (0x0 << 4) |   // 不可执行
                (0x0 << 8) |   // 不可共享
                (0x0 << 16) |  // 大小:128KB
                (0x0 << 24);   // TEX=0
    
    // 使能MPU,使用默认内存映射
    MPU->CTRL = (0x1 << 0) |   // 使能MPU
                (0x1 << 2);    // 在异常处理中使用MPU
}

个人经验:我建议在项目初期就把MPU配置好,而不是等到调试阶段再添加。因为一旦系统跑起来,再想加MPU,可能会发现很多隐藏的越界访问——这些bug在没MPU时根本发现不了。

1.4 三种机制的协同工作

这三种机制不是孤立的,它们要协同工作才能构建完整的安全架构。

我习惯这样设计:

  • MPU负责日常的内存访问保护,防止程序越界
  • 双核锁步负责CPU核心的故障检测,确保指令执行正确
  • 看门狗作为最后一道防线,当系统完全失控时强制复位

举个例子,理疗仪的正常工作流程:

  1. MPU确保UI任务不能修改治疗参数区
  2. 双核锁步实时比较两个核的输出,发现不一致立即报警
  3. 如果系统死锁,看门狗在4秒后复位系统

重要提醒:安全架构不是堆砌功能。你装了看门狗、用了双核锁步、配了MPU,不代表系统就安全了。关键是要做故障注入测试——人为制造故障,看系统能不能正确响应。我曾经见过一个项目,看门狗配了但没测试,结果实际运行时喂狗代码有bug,看门狗从来没复位过。

好了,这一章的内容就到这里。下一章我们会深入讲解IEC 62304标准对软件安全架构的具体要求,以及如何通过认证。记住,安全设计不是选择题,而是必答题。