4、安全单元硬件抽象层(HAL):内存保护单元(MPU)配置、看门狗定时器(WDT)集成、时钟与电源监控

好,咱们进入第四章节。这一章讲的是硬件抽象层,也就是 HAL。说白了,就是让上层软件不直接操作寄存器,而是通过一层封装来访问硬件。这样做的好处很明显——代码可移植、可维护,而且安全。

我个人习惯把 HAL 看作是「安全单元的护城河」。你想想看,如果上层应用能随便改 MPU 的配置,那安全策略就形同虚设了。所以 HAL 的设计,既要提供功能,又要限制权限。

4.1 内存保护单元(MPU)配置

MPU 这东西,我刚开始接触时觉得挺简单的——不就是划分几个区域嘛。直到有一次,我在一个项目中因为区域重叠导致系统莫名其妙地死机,排查了整整两天……嗯,从那以后我再也不敢小看 MPU 的配置了。

MPU 的核心作用,就是给不同的内存区域设置不同的访问权限。比如:

  • 代码区:只读、可执行
  • 数据区:读写、不可执行
  • 外设区:读写、不可执行、强顺序
  • 安全区:仅安全态可访问

配置 MPU 时,有几个关键参数必须搞清楚:

参数 说明 我的建议
基地址 区域的起始地址 必须对齐到区域大小
区域大小 2的幂次,最小32字节 别设太大,浪费区域数
访问权限 读/写/执行/特权级 遵循最小权限原则
子区域 可将区域分成8个子区域 灵活控制,但容易搞混

核心原则:MPU 的区域数量是有限的(通常是8个或16个)。所以你得精打细算,把最重要的区域先配置上。我一般会预留1-2个区域给动态配置用。

来看一个实际的配置代码片段:

/* 配置安全代码区域 */
MPU->RNR = 0;                    // 选择区域0
MPU->RBAR = (0x08000000) |       // 基地址:Flash起始
            MPU_RBAR_VALID_Msk | // 区域有效
            (0 << 4);           // 区域编号
MPU->RASR = (0x03 << 1) |       // 全权限(特权+用户)
            (0x01 << 24) |      // 区域大小:512KB
            MPU_RASR_XN_Msk;     // 禁止执行(数据区)

/* 配置安全数据区域 */
MPU->RNR = 1;
MPU->RBAR = (0x20000000) |
            MPU_RBAR_VALID_Msk |
            (1 << 4);
MPU->RASR = (0x03 << 1) |
            (0x00 << 24) |      // 区域大小:256KB
            MPU_RASR_XN_Msk;     // 禁止执行

避坑指南:我曾经犯过一个错——配置完 MPU 后没有做「区域有效性检查」。结果代码跑飞了都不知道是 MPU 的问题。建议你在初始化后,主动触发一次非法访问,验证 MPU 是否能正确产生异常。

4.2 看门狗定时器(WDT)集成

WDT 这东西,说白了就是一个「最后的防线」。系统正常运行时,你得定期喂狗;如果系统卡死了,狗就会咬人——也就是复位系统。

但 WDT 的集成没那么简单。我见过太多项目,要么喂狗太频繁导致 WDT 形同虚设,要么喂狗间隔太长导致系统频繁复位。

WDT 的关键参数:

  • 超时时间:从启动到复位的时间。一般设为系统最大响应时间的2-3倍。
  • 喂狗窗口:有些 WDT 有窗口机制——太早喂或太晚喂都会触发复位。
  • 复位行为:是直接复位,还是先触发中断再复位?

注意:WDT 的配置寄存器通常是一次性写入的——写完后就不能再改了,除非复位。所以初始化时一定要想清楚,别写错了就麻烦了。

我习惯把 WDT 的喂狗操作放在安全监控任务中,而不是主循环里。为什么?因为主循环可能被某个死循环卡住,但监控任务如果独立运行,就能更真实地反映系统状态。

/* WDT 初始化示例 */
void WDT_Init(uint32_t timeout_ms)
{
    /* 解锁WDT配置寄存器 */
    WDT->KEY = 0x5555;
    
    /* 设置超时时间 */
    WDT->LOAD = (timeout_ms * 1000) / WDT_CLK_PERIOD;
    
    /* 配置窗口(可选) */
    WDT->WINDOW = WDT->LOAD / 2;  // 窗口下限为一半
    
    /* 使能WDT */
    WDT->CTRL = WDT_CTRL_EN_Msk |
                WDT_CTRL_RESEN_Msk;  // 使能复位
    
    /* 锁定配置 */
    WDT->KEY = 0xAAAA;
}

/* 喂狗函数 */
void WDT_Feed(void)
{
    /* 写入特定序列 */
    WDT->KEY = 0xCCCC;
}

我的经验:调试阶段可以把 WDT 的超时时间设长一些,比如30秒。这样即使代码卡住了,你也有足够时间打断点、看变量。等系统稳定了,再改回正常的超时时间。

4.3 时钟与电源监控

时钟和电源,是嵌入式系统的「心脏」和「血液」。它们出问题,系统肯定完蛋。所以安全单元必须能监控它们的状态。

时钟监控主要关注:

  • 时钟频率:是否在允许范围内?
  • 时钟源:是否切换到了备份时钟?
  • PLL 锁定:PLL 是否稳定输出?

电源监控则包括:

  • 电压监测:核心电压、IO电压是否正常?
  • 掉电检测:是否检测到电源即将掉电?
  • 温度监测:芯片温度是否过高?

我做过一个项目,设备在高温环境下频繁死机。查了半天,发现是电源监控模块检测到电压波动,触发了复位。后来加了一个滤波电容,问题就解决了。你看,硬件问题有时候就是这么简单。

时钟监控的典型实现:

/* 时钟监控初始化 */
void CLK_Monitor_Init(void)
{
    /* 使能时钟安全系统(CSS) */
    RCC->CR |= RCC_CR_CSSON_Msk;
    
    /* 配置时钟失效中断 */
    NVIC_EnableIRQ(CSS_IRQn);
}

/* 时钟失效中断处理 */
void CSS_IRQHandler(void)
{
    /* 清除中断标志 */
    RCC->CIR |= RCC_CIR_CSSC_Msk;
    
    /* 切换到内部RC振荡器 */
    RCC->CR |= RCC_CR_HSION_Msk;
    while(!(RCC->CR & RCC_CR_HSIRDY_Msk));
    
    /* 重新配置系统时钟 */
    SystemClock_Config();
    
    /* 记录故障日志 */
    FaultLog_Write(FAULT_CLOCK_FAILURE);
}

关键点:时钟失效时,系统必须能自动切换到备份时钟源。否则,系统会直接「死机」。我建议你在设计时,至少保留一个内部 RC 振荡器作为最后的「救命稻草」。

电源监控方面,很多 MCU 都内置了 POR(上电复位)和 BOR(掉电复位)模块。但光靠硬件还不够,软件也要配合:

监控项 硬件支持 软件处理
上电复位 POR 模块自动检测 初始化所有外设
掉电检测 BOR 模块触发中断 保存关键数据到备份寄存器
电压波动 PVD 模块可编程阈值 触发警告或安全操作
温度过高 内部温度传感器 降低频率或关机

警告:电源监控的中断优先级一定要设得最高。为什么?因为掉电时系统随时可能停止运行,你必须抢在最后一刻把关键数据保存好。我曾经因为优先级设低了,导致数据丢失,教训深刻。

好了,这一章的内容就这些。MPU 配置、WDT 集成、时钟与电源监控,这三块是安全单元 HAL 的核心。你想想看,如果这三块都做好了,上层应用的安全性就有了坚实的基础。

下一章我们会讲安全通信协议,到时候见。