4. 存储器系统与MPU:存储器映射、MPU配置与TCM实战
大家好,欢迎来到第四章。这一章我们聊聊Cortex-M的存储器系统。说实话,很多工程师写了几年嵌入式代码,对存储器映射的理解还停留在“反正能跑就行”。但一旦遇到诡异的死机、数据被莫名篡改、或者ADC采集值跳变,你才会发现——嗯,不懂存储器系统,排查问题就像蒙着眼找路。
我个人习惯,拿到一个新平台,第一件事就是看它的存储器映射表。这比看时钟树还重要。为什么?因为你的代码、数据、外设寄存器、堆栈,全都在这个地址空间里布局。搞错了,轻则跑飞,重则烧片。
4.1 Cortex-M存储器映射概览
Cortex-M使用统一的4GB地址空间(0x00000000 ~ 0xFFFFFFFF)。ARM把这片空间划分成了几个固定的区域。不同系列(M0/M3/M4/M7)的细节略有差异,但大框架是一样的。
| 地址范围 | 区域名称 | 典型用途 |
|---|---|---|
| 0x00000000 - 0x1FFFFFFF | Code区 | 程序代码、向量表、可配置为Boot ROM或Flash |
| 0x20000000 - 0x3FFFFFFF | SRAM区 | 片内RAM、位带区(Bit-banding) |
| 0x40000000 - 0x5FFFFFFF | Peripheral区 | 所有外设寄存器(GPIO、ADC、UART等) |
| 0x60000000 - 0x9FFFFFFF | External RAM区 | 外部存储器接口(FSMC/FMC) |
| 0xE0000000 - 0xE00FFFFF | System区 | 系统控制块(SCB)、NVIC、MPU、FPU等 |
这里有个容易踩的坑:Code区不仅仅放代码。我记得有一次调试一个启动异常的问题,折腾了两天,最后发现是向量表地址配置错了。Cortex-M上电后从0x00000000取栈指针,从0x00000004取复位向量。如果你的代码在0x08000000(内部Flash),必须通过硬件映射或SCB->VTOR重定向向量表。不然,一上电就飞。
4.2 位带操作(Bit-banding)
位带是Cortex-M3/M4/M7的一个特色功能。说白了,就是把一个32位字的某一位,映射到另一个地址空间的一个32位字。你操作那个映射地址,就等于直接操作原地址的某一位。这在GPIO输出、标志位设置时非常有用,而且原子操作,不会被中断打断。
SRAM区的位带区域:0x20000000 ~ 0x200FFFFF(1MB)
Peripheral区的位带区域:0x40000000 ~ 0x400FFFFF(1MB)
计算公式:
// 位带别名地址 = 位带基址 + (字节偏移 * 32) + (位号 * 4)
// 以SRAM为例:
#define BITBAND_SRAM_BASE 0x22000000
#define BITBAND_PERIPH_BASE 0x42000000
// 宏定义:将某个地址的某一位转换为位带别名地址
#define BITBAND_ADDR(reg, bit) \
((uint32_t)(((uint32_t)&(reg) - 0x20000000) * 32 + (bit) * 4 + 0x22000000))
我在项目中用过位带操作来优化一个高速PWM的占空比更新。传统读-改-写方式需要关中断,用位带直接写,省了三条指令,延迟降低了将近1微秒。对于高频控制来说,这很关键。
4.3 MPU内存保护单元实战
MPU(Memory Protection Unit)是Cortex-M3及以上内核才有的。M0/M0+没有。它的作用,说白了就是给不同的内存区域设置访问权限——谁可以读、谁可以写、谁可以执行。这在RTOS和多任务系统中尤其重要。
我曾经在一个产品中遇到一个bug:两个任务共享一个缓冲区,任务A写数据时被任务B抢占,B把缓冲区内容改了,A回来继续写,结果数据全乱套。加了MPU保护后,任务B运行时根本写不了那个缓冲区,一写就触发MemManage Fault,问题立刻暴露。
4.3.1 MPU的配置步骤
- 禁用MPU:写MPU_CTRL寄存器,清ENABLE位。
- 配置区域:设置每个区域的基址、大小、属性(读/写/执行/权限)。
- 使能MPU:重新设置MPU_CTRL,使能MPU,并选择是否使能HFNMI和PRIVDEF。
- 数据同步屏障:执行DSB和ISB指令,确保配置生效。
// 示例:配置MPU保护SRAM中的两个区域
// 区域0:任务A的私有数据区(0x20001000 ~ 0x20001FFF),只允许特权模式读写
// 区域1:任务B的私有数据区(0x20002000 ~ 0x20002FFF),只允许特权模式读写
void MPU_Config(void)
{
// 1. 禁用MPU
MPU->CTRL = 0;
// 2. 配置区域0
MPU->RNR = 0; // 选择区域0
MPU->RBAR = 0x20001000; // 基址
MPU->RASR = (0x0C << 1) | // 大小:4KB (2^(0C+1) = 4096)
(0x01 << 16) | // 使能该区域
(0x03 << 24) | // 完全访问(特权+用户读写)
(0x00 << 28); // 禁止执行(XN=0表示可执行)
// 3. 配置区域1
MPU->RNR = 1;
MPU->RBAR = 0x20002000;
MPU->RASR = (0x0C << 1) |
(0x01 << 16) |
(0x03 << 24) |
(0x00 << 28);
// 4. 使能MPU,同时使能HFNMI和PRIVDEF
MPU->CTRL = (0x01 << 0) | // ENABLE
(0x01 << 1) | // HFNMIE:在NMI和HardFault下也启用MPU
(0x01 << 2); // PRIVDEF:特权模式默认访问所有区域
// 5. 同步屏障
__DSB();
__ISB();
}
4.3.2 常见MPU配置错误
- 区域大小不对齐:MPU要求区域基址必须对齐到区域大小。比如4KB区域,基址必须是0x...000。不对齐的话,配置写进去也不生效。
- 忘记使能PRIVDEF:如果不使能PRIVDEF,特权模式访问未配置的区域也会触发Fault。很多新手在这里卡住。
- 中断服务函数里访问被保护区域:如果中断优先级高于MPU的HFNMIE配置,中断里访问受保护区域会直接HardFault。
4.4 Cache与TCM紧耦合内存
这部分主要针对Cortex-M7。M7有L1 Cache(指令Cache和数据Cache),还有TCM(Tightly Coupled Memory)。TCM和Cache的区别,我打个比方:Cache是自动管理的临时缓存,CPU觉得你常用就帮你存一份;TCM是你自己指定的专用内存,CPU访问它不需要经过Cache,延迟固定,一个周期就能拿到数据。
4.4.1 TCM的配置
M7的TCM分为ITCM(指令TCM)和DTCM(数据TCM)。ITCM通常用来放中断服务函数或实时性要求高的代码。DTCM用来放堆栈、关键变量。
// TCM基址配置(在系统初始化时设置)
// ITCM:0x00000000 ~ 0x0003FFFF(256KB,可配置)
// DTCM:0x20000000 ~ 0x2003FFFF(256KB,可配置)
// 通过SCB->ITCMCR和SCB->DTCMCR寄存器使能TCM
SCB->ITCMCR = (0x01 << 0); // EN:使能ITCM
SCB->DTCMCR = (0x01 << 0) | // EN:使能DTCM
(0x03 << 2); // RMW:读-修改-写使能
我记得在一个音频处理项目中,把FFT运算的中间缓冲区放在DTCM里,运算速度提升了将近30%。因为DTCM没有Cache的延迟和刷新开销,数据访问时间完全确定。
4.4.2 Cache的一致性问题
Cache好用,但有个大坑——一致性问题。当DMA或外设直接往内存写数据时,如果CPU的Cache里还存着旧数据,CPU读到的就是脏数据。反过来,CPU改了Cache里的数据但还没写回内存,DMA去拿就是错的。
解决方案:
- 使用非Cacheable区域:把DMA缓冲区所在的MPU区域配置为Non-Cacheable。
- 手动维护Cache:调用SCB的Cache维护函数,比如SCB_CleanInvalidateDCache()。
- 使用TCM:TCM天然没有Cache一致性问题,适合放DMA缓冲区。
4.5 本章知识体系图
下面这张图总结了Cortex-M存储器系统的核心结构,以及MPU、Cache、TCM之间的关系。建议你保存下来,调试时对照着看。
4.6 避坑指南与调试建议
最后,分享几个我这些年积累的实战经验:
- 调试MPU时,先开一个区域测试:我曾经一次性配了8个区域,结果系统直接死机。后来一个个配,发现是区域大小写错了。
- ADC采集数据异常,先查Cache:如果ADC用DMA传输数据到内存,而那块内存是Cacheable的,你读到的永远是Cache里的旧值。解决办法:把DMA缓冲区放在Non-Cacheable区域或DTCM里。
- 向量表一定要放在可执行区域:如果MPU把Code区设为不可执行(XN=1),CPU取指时会触发Fault。别问我怎么知道的——有一次我调试到凌晨三点才发现这个低级错误。
- 使用位带操作时注意地址对齐:位带别名地址必须是32位对齐的。不对齐的话,写操作可能无效。