17、MPU/MMU初始化:在启动阶段保护内存区域
说实话,MPU和MMU这俩东西,刚入行那会儿我总觉得它们是操作系统的活,跟我写启动代码没啥关系。直到有一次,一个同事在bootloader里不小心写穿了某个关键数据区,整块板子直接变砖——嗯,从那以后,我再也不敢轻视启动阶段的内存保护了。
今天我们就聊聊,在系统刚上电、C环境还没完全跑起来的时候,怎么用MPU或MMU把内存区域管起来。
为什么启动阶段就需要内存保护?
你想想看,芯片上电后,CPU跑的第一段代码通常是汇编写的。这时候没有操作系统,没有任务调度,甚至连栈指针都可能还没设好。如果这时候某个外设的DMA突然写了一块不该写的地方,或者你的代码里一个野指针乱飞——后果就是死机、跑飞、甚至烧外设。
我个人习惯,在跳转到main()之前,就把MPU/MMU配好。哪怕只是简单配几个region,也能挡住绝大多数低级错误。
MPU vs MMU:你该选哪个?
很多初学者分不清这俩。我简单说下:
- MPU(内存保护单元):只做保护,不做地址映射。常见于Cortex-M系列MCU。配置简单,开销小。
- MMU(内存管理单元):既能保护,又能做虚拟地址到物理地址的映射。常见于Cortex-A系列应用处理器。配置复杂,但功能强大。
在启动阶段,如果你用的是MCU,基本就是MPU;如果是跑Linux的SoC,那肯定是MMU。但不管哪种,初始化的思路是相通的。
| 特性 | MPU | MMU |
|---|---|---|
| 地址映射 | 不支持 | 支持 |
| 保护粒度 | 通常32字节起 | 4KB页起 |
| 典型硬件 | Cortex-M3/M4/M7 | Cortex-A系列 |
| 启动阶段配置难度 | 低 | 中高 |
MPU初始化实战:以Cortex-M7为例
我记得第一次在STM32H7上配MPU,踩了个坑——没配内存属性,结果CPU访问外部SRAM时总是卡死。后来才发现,MPU不光管权限,还管缓存策略和内存类型。
下面是一个典型的启动阶段MPU配置流程:
/* 启动阶段MPU配置示例 */
void mpu_early_init(void)
{
/* 1. 先禁用MPU,防止配置过程中产生异常 */
MPU->CTRL = 0;
/* 2. 配置region 0:内部SRAM,全权限,可缓存 */
MPU->RBAR = (0x20000000UL | MPU_REGION_VALID | 0 << 1);
MPU->RASR = (MPU_AP_PRIV_RW_USER_RW |
MPU_MEM_NORMAL_WBWA |
MPU_REGION_SIZE_64KB |
MPU_REGION_ENABLE);
/* 3. 配置region 1:外设寄存器区,禁止缓存,只允许特权访问 */
MPU->RBAR = (0x40000000UL | MPU_REGION_VALID | 1 << 1);
MPU->RASR = (MPU_AP_PRIV_RW_USER_NO |
MPU_MEM_DEVICE_NGnRE |
MPU_REGION_SIZE_1MB |
MPU_REGION_ENABLE);
/* 4. 配置region 2:Boot ROM,只读 */
MPU->RBAR = (0x08000000UL | MPU_REGION_VALID | 2 << 1);
MPU->RASR = (MPU_AP_PRIV_RO_USER_RO |
MPU_MEM_NORMAL_NC |
MPU_REGION_SIZE_1MB |
MPU_REGION_ENABLE);
/* 5. 使能MPU,并启用默认背景region */
MPU->CTRL = MPU_CTRL_ENABLE | MPU_CTRL_PRIVDEFENA;
/* 6. 数据同步屏障,确保配置生效 */
__DSB();
__ISB();
}
MMU初始化:页表是个体力活
MMU的初始化比MPU复杂得多,因为你要先建页表。页表说白了就是一张大表,告诉MMU每个虚拟地址对应哪个物理地址,以及有什么权限。
在启动阶段,我一般只建一级页表(section映射),每个条目覆盖1MB空间。这样页表只有4096个条目,16KB大小,放在一个固定的物理地址上。
/* 启动阶段MMU一级页表初始化(简化版) */
#define PAGE_TABLE_BASE 0x40000000 /* 页表基址,需16KB对齐 */
void mmu_early_init(void)
{
uint32_t *pte = (uint32_t *)PAGE_TABLE_BASE;
uint32_t va;
/* 清空页表 */
for (va = 0; va < 4096; va++) {
pte[va] = 0;
}
/* 映射0x00000000 - 0x3FFFFFFF:设备内存,不可缓存 */
for (va = 0; va < 1024; va++) {
pte[va] = (va << 20) | /* 物理地址 = 虚拟地址 */
(0x2 << 10) | /* 域 = 2 */
(3 << 10) | /* 访问权限:全权限 */
(0 << 3) | /* 不可缓存 */
(1 << 1) | /* 段映射 */
1; /* 有效 */
}
/* 映射0x40000000 - 0x7FFFFFFF:DDR内存,可缓存,可写 */
for (va = 1024; va < 2048; va++) {
pte[va] = (va << 20) |
(0x2 << 10) |
(3 << 10) |
(1 << 3) | /* 可缓存,写回 */
(1 << 1) |
1;
}
/* 设置TTB(Translation Table Base)寄存器 */
__set_TTBR0(PAGE_TABLE_BASE);
/* 设置域访问控制:域2为客户端模式(检查权限) */
__set_DACR(0x55555555);
/* 使能MMU */
uint32_t sctlr = __get_SCTLR();
sctlr |= (1 << 0); /* M位:使能MMU */
__set_SCTLR(sctlr);
__DSB();
__ISB();
}
避坑指南:我踩过的几个坑
- 坑一:MPU region重叠——我曾经配了两个重叠的region,一个设成可执行,一个设成不可执行。结果CPU执行到那块区域时,行为完全不可预测。后来我养成了习惯:配完MPU后,遍历所有region检查是否有重叠。
- 坑二:MMU页表放在未初始化内存里——DDR刚初始化完,里面的数据是随机的。如果你直接把页表放进去,MMU读到的全是垃圾。正确做法:要么用内部SRAM放页表,要么在配MMU之前先把DDR的那块区域清零。
- 坑三:忘了配域访问控制——MMU的域(Domain)是个好东西,但很多人配完页表就忘了设DACR。默认情况下所有域都是“无访问”,结果MMU一使能就触发异常。
一张图看懂启动阶段内存保护
下面这张图,是我自己总结的启动阶段MPU/MMU初始化流程。每次做新项目,我都会照着这个框架走一遍。
写在最后
MPU和MMU的初始化,说白了就是给内存区域贴上“标签”——哪块能读、哪块能写、哪块能执行、哪块需要缓存。这些标签在启动阶段就贴好,后面整个系统的稳定性就有了基础保障。
我个人建议,哪怕你的项目暂时用不到MPU/MMU的全部功能,也至少配一个最小的保护集:把外设寄存器区设成不可缓存,把关键数据区设成只读。这点工作量,换来的是少熬几个通宵查bug——我觉得挺值的。
公众号:蓝海资料掘金营,微信deep3321