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。
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的步骤,我总结为四步走:
- 先禁用MPU——写0到ENABLE位
- 配置所有区域——写RBAR、RASR等寄存器
- 数据同步屏障——执行DSB指令
- 使能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();
}
3.4 MPU禁用流程
禁用比使能简单,但同样有坑。流程如下:
- 写0到MPU_CTRL的ENABLE位
- 执行DSB和ISB——确保禁用操作完成
- 可选:清空区域配置——防止残留配置影响后续使能
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; /* 清空属性 */
}
}
3.5 使能/禁用时的注意事项
最后,我把自己这些年踩过的坑,总结成几条铁律:
- 使能前必须确保所有区域配置有效——无效配置会导致使能瞬间hard fault
- 使能/禁用操作必须在特权模式下执行——用户模式没权限碰MPU寄存器
- 改变ENABLE位时,其他位最好同时设置——避免多次写MPU_CTRL引发意外
- 多核系统中,每个核的MPU独立使能——别想着一个核控制所有MPU
- 低功耗模式下,MPU状态可能丢失——唤醒后记得重新使能
嗯,关于MPU的使能与禁用,核心内容就这些。下一节我们会讲MPU的区域配置——怎么把内存切成一块一块的,每块给不同的权限。那才是MPU真正发挥威力的地方。