3、MPU使能与禁用:系统控制块(SCB)配置、MPU控制寄存器详解、使能/禁用流程

好,咱们接着聊。上一节我们把MPU的寄存器结构扒了个底朝天,这一节要动真格的了——怎么让MPU真正跑起来,又怎么把它关掉。

说实话,我见过不少工程师,MPU配置写得飞起,结果忘了使能,调试了一整天。嗯,这种坑我年轻时也踩过。所以今天咱们把使能和禁用这件事,掰开揉碎了讲清楚。

3.1 系统控制块(SCB)是个什么角色?

先说说SCB。系统控制块,说白了就是Cortex-M内核里的一个“总控室”。它管着异常处理、系统时钟、还有——咱们今天的主角,MPU的使能开关。

SCB的基地址是0xE000ED00,这个地址你得记住,因为后面操作MPU使能时,要往这个区域写东西。

我个人习惯把SCB相关的寄存器定义放在一个头文件里,像这样:

/* SCB寄存器映射 */
#define SCB_BASE        (0xE000ED00UL)
#define SCB_ACTLR       (*(volatile uint32_t *)(SCB_BASE + 0x008))
#define SCB_CPUID       (*(volatile uint32_t *)(SCB_BASE + 0x000))
#define SCB_ICSR        (*(volatile uint32_t *)(SCB_BASE + 0x004))
/* ... 其他寄存器 */

你可能会问:SCB和MPU使能有啥关系?别急,往下看。

3.2 MPU控制寄存器详解

MPU控制寄存器,全称是MPU Control Register,地址在0xE000ED94。它只有16位有效,但每个位都管着大事。

位域 名称 功能描述
[15:4] 保留 必须写0
[3] PRIVDEFENA 特权模式下默认内存映射使能。置1后,特权模式访问未配置区域时使用默认属性
[2] HFNMIENA 硬fault和NMI异常下MPU使能。置1表示这些异常下MPU仍然生效
[1] RESERVED 保留,必须写0
[0] ENABLE MPU全局使能位。1=使能,0=禁用

这三个位,每个都有讲究。我一个个说。

3.2.1 ENABLE位——总开关

位0,最简单也最核心。写1,MPU开始工作;写0,MPU彻底歇菜。但要注意:改变ENABLE位时,必须保证MPU已禁用。为什么?因为使能瞬间,所有区域配置会同时生效,如果配置有冲突,系统直接hard fault。

警告:千万不要在MPU使能状态下直接修改ENABLE位以外的寄存器!我曾经在一个项目里,为了省几条指令,直接在使能状态下改了区域属性,结果系统当场崩溃。后来查了ARM手册才发现,使能状态下修改MPU寄存器是未定义行为。

3.2.2 PRIVDEFENA位——特权模式的“兜底”

这个位很有意思。置1后,特权模式(比如内核异常处理)访问那些没有配置MPU区域的内存时,会使用默认的“特权可读写、不可执行”属性。说白了,就是给特权模式一个保底权限。

我建议在大多数嵌入式系统中,把这个位置1。否则,一旦你漏配了某个内存区域,特权模式访问它就会触发memmanage fault。你想想看,调试时多冤枉。

3.2.3 HFNMIENA位——异常中的MPU行为

硬fault和NMI是最高优先级的异常。这个位决定MPU在这些异常下是否还起作用。

置1:MPU照常工作,硬fault和NMI也受MPU保护。这适合安全关键系统,防止异常处理代码被破坏。

置0:MPU在硬fault和NMI下被绕过,所有内存访问都通过。这适合调试阶段,方便你定位问题。

我个人习惯在产品发布前,把这个位置1。调试阶段置0,省得异常处理里又触发异常,嵌套起来头大。

3.3 MPU使能流程

好,理论说完了,咱们上手实操。使能MPU的步骤,我总结为四步走:

  1. 先禁用MPU——写0到ENABLE位
  2. 配置所有区域——写RBAR、RASR等寄存器
  3. 数据同步屏障——执行DSB指令
  4. 使能MPU——写1到ENABLE位,同时设置PRIVDEFENA和HFNMIENA

代码实现如下:

/* MPU控制寄存器地址 */
#define MPU_CTRL    (*(volatile uint32_t *)0xE000ED94)

/* MPU使能函数 */
void MPU_Enable(uint8_t priv_defena, uint8_t hfnmi_ena)
{
    uint32_t ctrl_val = 0;
    
    /* 第一步:先禁用MPU */
    MPU_CTRL = 0;
    
    /* 第二步:配置区域(假设区域0已配置好)*/
    /* 这里调用你的区域配置函数 */
    MPU_RegionConfig(0, ®ion_config);
    
    /* 第三步:数据同步屏障 */
    __DSB();
    
    /* 第四步:使能MPU */
    ctrl_val = (1 << 0);  /* ENABLE位 */
    if (priv_defena) {
        ctrl_val |= (1 << 3);  /* PRIVDEFENA */
    }
    if (hfnmi_ena) {
        ctrl_val |= (1 << 2);  /* HFNMIENA */
    }
    MPU_CTRL = ctrl_val;
    
    /* 再次数据同步屏障,确保使能生效 */
    __DSB();
    __ISB();
}
小技巧:使能后加一条ISB(指令同步屏障)指令,能确保后续指令在MPU生效后的新上下文中执行。我见过不加ISB导致取指令异常的案例,加了就没事。

3.4 MPU禁用流程

禁用比使能简单,但同样有坑。流程如下:

  1. 写0到MPU_CTRL的ENABLE位
  2. 执行DSB和ISB——确保禁用操作完成
  3. 可选:清空区域配置——防止残留配置影响后续使能
void MPU_Disable(void)
{
    /* 直接写0禁用 */
    MPU_CTRL = 0;
    
    /* 同步屏障 */
    __DSB();
    __ISB();
    
    /* 可选:清空所有区域配置 */
    for (int i = 0; i < 8; i++) {
        /* 写0到RBAR和RASR */
        MPU->RBAR = i << 4;  /* 选择区域 */
        MPU->RASR = 0;        /* 清空属性 */
    }
}
重点:禁用MPU后,所有内存访问都回到默认的“无MPU”状态。如果你的系统依赖MPU做权限隔离,禁用后要小心——用户代码可能访问到不该访问的内存。我曾在产品中留了一个调试接口可以禁用MPU,结果测试人员不小心触发了,导致数据被破坏。后来我加了一个“禁用确认”机制,必须连续写两次特定值才能禁用。

3.5 使能/禁用时的注意事项

最后,我把自己这些年踩过的坑,总结成几条铁律:

  • 使能前必须确保所有区域配置有效——无效配置会导致使能瞬间hard fault
  • 使能/禁用操作必须在特权模式下执行——用户模式没权限碰MPU寄存器
  • 改变ENABLE位时,其他位最好同时设置——避免多次写MPU_CTRL引发意外
  • 多核系统中,每个核的MPU独立使能——别想着一个核控制所有MPU
  • 低功耗模式下,MPU状态可能丢失——唤醒后记得重新使能

嗯,关于MPU的使能与禁用,核心内容就这些。下一节我们会讲MPU的区域配置——怎么把内存切成一块一块的,每块给不同的权限。那才是MPU真正发挥威力的地方。