2. MPU硬件原理:寄存器配置、Region定义与访问权限控制

好,咱们接着聊MPU的硬件原理。说实话,这部分内容我第一次接触时也觉得有点绕,但搞明白之后你会发现——其实就那么回事。

MPU的核心工作,说白了就是三件事:定义区域、设置权限、检查访问。咱们一个一个拆开讲。

2.1 MPU的寄存器配置

MPU的配置,本质上就是往几个关键寄存器里写值。我习惯把寄存器分成两类:控制类区域类

2.1.1 控制寄存器

以ARM Cortex-M系列为例,最核心的控制寄存器是MPU_CTRL。它的位定义大致如下:

位域 名称 功能描述
[0] ENABLE 全局使能位。1使能,0禁用
[1] HFNMIENA 硬fault和NMI下是否启用MPU。我建议默认置1
[2] PRIVDEFENA 特权模式下是否使用默认内存映射。嗯,这里要注意

我在项目中遇到过一个问题:调试阶段把PRIVDEFENA置0了,结果系统一进中断就崩。后来才发现,中断服务函数默认是特权模式,但MPU没给这个模式配任何区域——直接触发异常。

避坑指南:我曾经因为忘记使能HFNMIENA位,导致在NMI中断里访问了非法地址,系统直接死锁。调试器都连不上,最后只能按复位键。所以我的习惯是:MPU_CTRL的这三个位,能置1的全置1。

2.1.2 区域寄存器

每个Region由两个寄存器描述:MPU_RBAR(基址寄存器)和MPU_RASR(属性寄存器)。

MPU_RBAR的结构:

位[31:5]  :基地址(必须32字节对齐)
位[4]     :VALID位,写1表示当前操作立即生效
位[3:0]   :REGION编号(0~15)

MPU_RASR的结构就复杂一些:

位[31:29] :属性(XN、AP等)
位[28:24] :TEX、S、C、B(缓存策略)
位[23:22] :SRD(子区域禁用)
位[21:19] :AP(访问权限)
位[18:17] :保留
位[16]    :指令访问使能
位[15:8]  :SIZE(区域大小)
位[7:0]   :ENABLE(区域使能)

你想想看,这么多位,其实真正需要你关心的就几个:基地址、大小、访问权限、使能位。其他的,大部分场景用默认值就行。

2.2 Region的定义

Region就是一块连续的内存区域。每个Region必须满足:

  • 大小是2的幂次方:比如256字节、1KB、4MB
  • 基地址必须对齐到大小:比如4KB的Region,基地址必须是0x1000、0x2000这样的
  • 最多支持16个Region:编号0~15

为什么必须是2的幂次方?我打个比方你就明白了。MPU内部是用一个掩码比较器来检查地址的。如果Region大小是2的幂,掩码就是连续的1,硬件实现起来特别简单。如果不是2的幂,那比较逻辑就复杂了,芯片面积和功耗都会增加。

Region的大小编码方式是这样的:

SIZE编码 实际大小 对齐要求
8 256字节 0x100对齐
9 512字节 0x200对齐
10 1KB 0x400对齐
11 2KB 0x800对齐
12 4KB 0x1000对齐
... ... ...
31 4GB 0x80000000对齐

公式很简单:实际大小 = 2^(SIZE编码)。比如SIZE=12,就是2^12=4096字节=4KB。

个人经验:我建议把SIZE编码记成「以2为底的对数」。比如你要配一个16KB的区域,16KB=16384字节,log2(16384)=14,所以SIZE=14。这样算起来快,不容易出错。

2.3 访问权限控制

访问权限由MPU_RASR中的AP位控制。AP位有3个比特,定义了8种权限组合:

AP[2:0] 特权模式 用户模式 典型用途
000 无访问 无访问 保留区域,谁都不让碰
001 读写 无访问 内核私有数据
010 读写 只读 系统配置参数
011 读写 读写 共享数据区
100 只读 无访问 内核代码区
101 只读 只读 只读数据(如查表)
110 只读 无访问 同100,保留
111 只读 只读 同101,保留

这里有个细节要注意:AP=000 表示完全禁止访问。我在项目中就用这个来保护未使用的内存区域,防止野指针乱窜。

另外,MPU_RASR里还有个XN位(Execute Never),用来控制能否从该区域取指令执行。对于数据区,一定要把XN位置1,否则攻击者可能通过缓冲区溢出注入代码执行。

核心要点:MPU的访问控制是「硬件级」的。一旦配置好,每次内存访问都会自动检查,不需要软件干预。这意味着:
- 检查是实时的,没有额外开销
- 检查是强制的,无法绕过
- 检查是精确的,会触发精确异常

2.4 配置示例

说了这么多理论,咱们看个实际配置。假设我们要保护一段4KB的SRAM区域,基地址0x20000000,只允许特权模式读写:

// 配置Region 0
// 基地址:0x20000000,大小:4KB,特权读写,用户无访问

MPU->RNR = 0;                    // 选择Region 0
MPU->RBAR = 0x20000000;          // 基地址
MPU->RASR = (0x01 << 19) |       // AP=001:特权读写
            (0x00 << 16) |       // XN=0:允许执行(如果是数据区,建议置1)
            (12 << 8) |          // SIZE=12:4KB
            (1 << 0);            // ENABLE=1:使能该Region

// 全局使能MPU
MPU->CTRL = (1 << 0) |           // ENABLE
            (1 << 1) |           // HFNMIENA
            (1 << 2);            // PRIVDEFENA

配置完成后,任何用户模式下的代码试图访问0x20000000~0x20000FFF这个区域,都会触发MemManage Fault。这就是MPU的保护能力。

调试技巧:我刚开始调MPU时,经常配完发现系统跑飞了。后来学了一招:先配一个「全开」的Region(AP=011,特权用户都可读写),确认系统能正常运行,再逐步收紧权限。这样出问题能快速定位是哪个Region配错了。

好了,MPU的硬件原理就讲到这里。下一节咱们聊聊怎么在实际项目中把这些Region组织起来,形成完整的内存保护方案。