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();
}
我的小技巧: 在使能MPU之前,一定要先执行DSB和ISB指令。否则CPU可能还在用旧的配置执行下一条指令,直接触发MemManage Fault。

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();
}
警告: 使能MMU的那一瞬间,CPU的指令流会从物理地址切换到虚拟地址。如果你的代码段所在的虚拟地址和物理地址不一致,必须在使能MMU之前确保PC指针已经位于正确的虚拟地址空间。否则——嗯,我见过有人在这步卡了三天。

避坑指南:我踩过的几个坑

  • 坑一:MPU region重叠——我曾经配了两个重叠的region,一个设成可执行,一个设成不可执行。结果CPU执行到那块区域时,行为完全不可预测。后来我养成了习惯:配完MPU后,遍历所有region检查是否有重叠。
  • 坑二:MMU页表放在未初始化内存里——DDR刚初始化完,里面的数据是随机的。如果你直接把页表放进去,MMU读到的全是垃圾。正确做法:要么用内部SRAM放页表,要么在配MMU之前先把DDR的那块区域清零。
  • 坑三:忘了配域访问控制——MMU的域(Domain)是个好东西,但很多人配完页表就忘了设DACR。默认情况下所有域都是“无访问”,结果MMU一使能就触发异常。

一张图看懂启动阶段内存保护

下面这张图,是我自己总结的启动阶段MPU/MMU初始化流程。每次做新项目,我都会照着这个框架走一遍。

启动阶段MPU/MMU初始化流程 1. 系统复位,关MPU/MMU 2. 初始化内存控制器 是MPU还是MMU? MPU MMU 配置MPU region 建立页表,设TTB 使能MPU/MMU,DSB+ISB ✓ 内存保护生效,跳转到C代码

写在最后

MPU和MMU的初始化,说白了就是给内存区域贴上“标签”——哪块能读、哪块能写、哪块能执行、哪块需要缓存。这些标签在启动阶段就贴好,后面整个系统的稳定性就有了基础保障。

我个人建议,哪怕你的项目暂时用不到MPU/MMU的全部功能,也至少配一个最小的保护集:把外设寄存器区设成不可缓存,把关键数据区设成只读。这点工作量,换来的是少熬几个通宵查bug——我觉得挺值的。


公众号:蓝海资料掘金营,微信deep3321