第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 隔离策略设计
我在项目中常用的隔离方案是这样的:
- 任务栈隔离:每个任务分配独立的栈区,用MPU保护起来
- 共享数据区隔离:全局变量和共享缓冲区单独划一个区域
- 外设寄存器隔离:关键外设的寄存器映射区设为只读或特权访问
- 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问题时的步骤:
- 先禁用MPU,确认系统能正常运行
- 逐个使能MPU区域,每使能一个就测试一次
- 用调试器查看MPU的故障状态寄存器(MFSR)
- 检查访问违例地址寄存器(MMFAR)
避坑指南:我曾经在调试时发现MPU故障状态寄存器一直报错,查了半天发现是缓存一致性问题。CPU读到的MPU配置是旧的,实际硬件已经更新了。解决办法是配置完MPU后加一个DSB指令(数据同步屏障)。
2.5 本章知识体系
下面这张图总结了MPU内存保护的核心逻辑,我建议你把它打印出来贴在工位上。
这张图把MPU的工作流程讲清楚了:先配置区域(左侧),再设置属性(右侧),最后MPU控制器根据这些规则做访问仲裁(底部输出)。
本章要点回顾:
- MPU最多支持8个区域,编号0~7,编号越小优先级越高
- 每个区域需要配置基地址、大小、访问权限、可执行属性等
- 内存隔离策略:任务栈隔离、共享数据区隔离、外设寄存器隔离、DMA缓冲区隔离
- 防止DMA与CPU冲突:将DMA缓冲区设为CPU只读或不可访问
- 调试技巧:逐个使能区域、检查MFSR和MMFAR寄存器、加DSB指令