第2章:MPU内存保护单元实战

MPU这东西,说白了就是给内存上个锁。我刚开始用STM32的时候,总觉得它多余——裸机跑跑,谁访问谁心里没数?直到有一次DMA把关键数据区给踩了,查了整整两天...从那以后,我再也不敢轻视MPU了。

本章核心:掌握MPU区域配置、属性设置、内存隔离策略,以及防止DMA与CPU访问冲突的实战方法。

2.1 MPU区域配置与属性设置

MPU把内存空间分成若干个区域,每个区域可以独立设置访问权限和属性。STM32的MPU最多支持8个区域,编号从0到7。

2.1.1 区域配置的核心参数

参数 说明 典型值
基地址 区域的起始地址 0x20000000
大小 区域的大小,必须是2的幂 32字节 ~ 4GB
可执行属性 是否允许执行代码 XN(禁止执行)或允许执行
访问权限 读/写/只读/无访问 全访问、特权访问等
子区域 将区域分成8个子区域,可单独使能 0~7

我个人习惯把SRAM分成几个段:栈区、堆区、数据区、DMA缓冲区。每个段配一个MPU区域,互不干扰。

2.1.2 配置代码实战

下面是一个典型的MPU区域配置代码。我建议你直接复制这个模板,改改地址和大小就能用。

/* MPU区域配置结构体 */
MPU_Region_InitTypeDef MPU_InitStruct;

/* 配置区域0:保护SRAM数据区 */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x20000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;

HAL_MPU_ConfigRegion(&MPU_InitStruct);
小技巧:配置完MPU后,记得调用 HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT) 使能MPU。我见过有人配了半天,结果忘了使能,MPU根本没工作。

2.2 内存隔离策略

为什么要做内存隔离?你想想看,一个跑飞的任务把另一个任务的数据踩了,系统崩溃都不知道怎么死的。内存隔离就是给每个任务画个圈,谁也别越界。

2.2.1 隔离策略设计

我在项目中常用的隔离方案是这样的:

  1. 任务栈隔离:每个任务分配独立的栈区,用MPU保护起来
  2. 共享数据区隔离:全局变量和共享缓冲区单独划一个区域
  3. 外设寄存器隔离:关键外设的寄存器映射区设为只读或特权访问
  4. DMA缓冲区隔离:DMA操作的缓冲区与CPU数据区严格分开

2.2.2 多区域配置示例

下面是一个多区域配置的例子。我习惯把区域0留给系统,区域1给任务1,区域2给任务2,以此类推。

/* 区域0:系统数据区(0x20000000 - 0x20007FFF) */
MPU_InitStruct.BaseAddress = 0x20000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.AccessPermission = MPU_REGION_PRIV_RW;
HAL_MPU_ConfigRegion(&MPU_InitStruct);

/* 区域1:任务1栈区(0x20008000 - 0x20008FFF) */
MPU_InitStruct.BaseAddress = 0x20008000;
MPU_InitStruct.Size = MPU_REGION_SIZE_4KB;
MPU_InitStruct.Number = MPU_REGION_NUMBER1;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
HAL_MPU_ConfigRegion(&MPU_InitStruct);

/* 区域2:DMA缓冲区(0x20009000 - 0x20009FFF) */
MPU_InitStruct.BaseAddress = 0x20009000;
MPU_InitStruct.Size = MPU_REGION_SIZE_4KB;
MPU_InitStruct.Number = MPU_REGION_NUMBER2;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
注意:区域配置的顺序很重要!MPU的优先级是区域编号越小优先级越高。如果两个区域重叠,编号小的区域规则生效。

2.3 防止DMA与CPU访问冲突

DMA和CPU抢内存,这是嵌入式开发的老大难问题。我曾经在一个项目中,DMA正在搬运数据,CPU突然去读同一个缓冲区,结果读到了一半新一半旧的数据...嗯,从那以后我学会了用MPU做访问仲裁。

2.3.1 冲突场景分析

常见的冲突场景有三种:

  • 读写冲突:CPU读DMA正在写的缓冲区
  • 写写冲突:CPU和DMA同时写同一个地址
  • 指令冲突:DMA把代码区覆盖了,CPU执行了错误指令

2.3.2 MPU隔离方案

我的做法是:把DMA缓冲区设为CPU不可访问,或者设为只读。这样CPU想碰DMA的数据,必须先通过MPU的检查。

/* 配置DMA缓冲区为CPU只读 */
MPU_InitStruct.BaseAddress = 0x2000A000;  /* DMA缓冲区基址 */
MPU_InitStruct.Size = MPU_REGION_SIZE_8KB;
MPU_InitStruct.AccessPermission = MPU_REGION_PRIV_RO;  /* 特权模式只读 */
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER3;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
实战经验:如果DMA和CPU必须共享数据,可以用双缓冲机制。一个缓冲区给DMA写,另一个给CPU读,通过标志位切换。MPU在这里的作用是防止误操作。

2.4 MPU调试与故障排查

MPU配置错了会怎么样?系统直接进HardFault。我第一次配MPU的时候,地址算错了一位,结果系统一启动就崩了,连调试器都连不上...

2.4.1 常见故障及解决

故障现象 可能原因 解决方法
系统启动即HardFault MPU区域覆盖了中断向量表 检查区域0的基地址,避开0x00000000
DMA传输完成后CPU读不到数据 DMA缓冲区被设为CPU不可访问 检查AccessPermission设置
任务切换时崩溃 任务栈区未正确配置 确认栈区大小和基地址对齐
外设寄存器无法访问 外设映射区被MPU保护 为外设区域单独配置MPU区域

2.4.2 调试技巧

我调试MPU问题时的步骤:

  1. 先禁用MPU,确认系统能正常运行
  2. 逐个使能MPU区域,每使能一个就测试一次
  3. 用调试器查看MPU的故障状态寄存器(MFSR)
  4. 检查访问违例地址寄存器(MMFAR)
避坑指南:我曾经在调试时发现MPU故障状态寄存器一直报错,查了半天发现是缓存一致性问题。CPU读到的MPU配置是旧的,实际硬件已经更新了。解决办法是配置完MPU后加一个DSB指令(数据同步屏障)。

2.5 本章知识体系

下面这张图总结了MPU内存保护的核心逻辑,我建议你把它打印出来贴在工位上。

MPU内存保护核心逻辑图 MPU内存保护核心逻辑 MPU控制器 区域0:系统数据区 区域1:任务1栈区 区域2:DMA缓冲区 区域3:外设寄存器 访问权限 可执行属性 缓存属性 子区域使能 内存隔离 + 访问控制 说明: 区域配置定义内存范围 属性设置定义访问规则 两者结合实现内存保护

这张图把MPU的工作流程讲清楚了:先配置区域(左侧),再设置属性(右侧),最后MPU控制器根据这些规则做访问仲裁(底部输出)。

本章要点回顾:
  • MPU最多支持8个区域,编号0~7,编号越小优先级越高
  • 每个区域需要配置基地址、大小、访问权限、可执行属性等
  • 内存隔离策略:任务栈隔离、共享数据区隔离、外设寄存器隔离、DMA缓冲区隔离
  • 防止DMA与CPU冲突:将DMA缓冲区设为CPU只读或不可访问
  • 调试技巧:逐个使能区域、检查MFSR和MMFAR寄存器、加DSB指令