3、存储器系统:存储器映射、位带操作、MPU原理与配置
大家好,欢迎来到第三章。这一章我们聊聊Cortex-M的存储器系统。说实话,很多工程师写了几年嵌入式代码,对存储器映射还是一知半解。我刚开始做项目时也踩过不少坑,比如位带操作没用好导致代码可读性极差,或者MPU配置不当让系统莫名其妙跑飞。今天咱们就把这些硬骨头啃下来。
3.1 存储器映射——Cortex-M的“地址地图”
Cortex-M系列内核采用统一的存储器映射。说白了,就是芯片上所有东西——Flash、SRAM、外设寄存器——都映射到同一个4GB地址空间里。你想想看,这就像一张城市地图,每个区域都有固定用途。
Cortex-M3/M4/M7的存储器映射基本一致,分为以下几个大区:
| 地址范围 | 区域 | 用途 |
|---|---|---|
| 0x0000 0000 - 0x1FFF FFFF | Code区 | 程序代码、异常向量表、常量数据 |
| 0x2000 0000 - 0x3FFF FFFF | SRAM区 | 堆栈、全局变量、位带别名区 |
| 0x4000 0000 - 0x5FFF FFFF | 外设区 | 片上外设寄存器、位带别名区 |
| 0x6000 0000 - 0x9FFF FFFF | 外部RAM区 | 扩展存储器(如SDRAM) |
| 0xA000 0000 - 0xDFFF FFFF | 外部设备区 | 外部外设映射 |
| 0xE000 0000 - 0xFFFF FFFF | 系统区 | NVIC、MPU、FPU等内核外设 |
嗯,这里要注意:系统区(0xE000 0000以上)是ARM Cortex-M内核私有的,不同厂商的芯片这部分完全一样。这也是为什么你的STM32代码和NXP的LPC代码,在NVIC配置上可以共用一套逻辑。
核心要点:复位后,CPU从0x0000 0000地址读取栈顶指针,从0x0000 0004读取复位向量。所以你的中断向量表必须放在Code区起始位置。
3.2 位带操作——用“魔法”操作单个比特
位带操作是Cortex-M3/M4/M7的一个特色功能。我个人习惯叫它“比特的快捷方式”。为什么这么说?
传统方式操作一个GPIO引脚,你得先读寄存器,再修改某一位,最后写回去。这个过程不是原子操作,如果被中断打断,很容易出问题。位带操作直接把一个32位的字映射到位带区的一个比特上。你往这个别名地址写1,对应的比特就置1;写0,就清0。
位带区有两个:
- SRAM位带区:0x2000 0000 - 0x200F FFFF(1MB),别名区在0x2200 0000 - 0x23FF FFFF
- 外设位带区:0x4000 0000 - 0x400F FFFF(1MB),别名区在0x4200 0000 - 0x43FF FFFF
地址转换公式其实很简单:
别名地址 = 位带区基地址 + (字节偏移 * 32) + (位号 * 4)
举个例子,你想操作GPIOA的ODR寄存器第5位。假设GPIOA的基地址是0x4002 0000,ODR偏移是0x0C。那么:
// 计算目标字节地址
目标地址 = 0x4002 0000 + 0x0C = 0x4002 000C
// 计算位带别名地址
别名地址 = 0x4200 0000 + (0x0002 000C * 32) + (5 * 4)
= 0x4200 0000 + 0x0040 0180 + 0x14
= 0x4240 0194
// 直接操作
*(volatile uint32_t *)0x42400194 = 1; // 置位
*(volatile uint32_t *)0x42400194 = 0; // 清零
我的经验:在实际项目中,我一般用宏定义封装位带操作。比如:#define BITBAND(addr, bit) (*(volatile uint32_t *)(0x42000000 + ((uint32_t)(addr) - 0x40000000) * 32 + (bit) * 4))。这样代码看起来清爽多了。
我曾经在一个电机控制项目里用过位带操作。当时需要快速切换PWM的极性,用传统方式每次都要读-改-写,延迟很大。换成位带操作后,一个赋值语句搞定,时序也稳了。不过要注意,位带操作只适用于Cortex-M3/M4/M7,M0/M0+不支持。
3.3 MPU——内存保护单元
MPU,全称Memory Protection Unit。说白了,就是给内存区域上锁。你想想看,如果任务A不小心写到了任务B的堆栈,系统可能随时崩溃。MPU就是干这个的——它定义了哪些区域可读、可写、可执行,以及谁可以访问。
MPU支持最多8个区域(Cortex-M3/M4),每个区域可以独立配置。配置参数包括:
- 基地址:区域的起始地址
- 大小:从32字节到4GB,必须是2的幂
- 权限:特权级/用户级访问控制
- 属性:可读、可写、可执行、缓冲、缓存等
3.4 MPU配置实战
配置MPU其实不复杂,但细节很多。我建议你按以下步骤来:
- 关闭MPU:修改MPU_CTRL寄存器,清0 ENABLE位
- 配置区域:设置每个区域的基地址、大小、权限
- 使能MPU:设置MPU_CTRL的ENABLE位,可选PRIVDEFENA位
- 数据同步:执行DSB和ISB指令,确保配置生效
下面是一个典型的配置代码:
void MPU_Config(void)
{
// 1. 关闭MPU
MPU->CTRL = 0;
// 2. 配置区域0:Flash区域(0x08000000,512KB)
// 特权级可读写,用户级只读,可执行
MPU->RBAR = 0x08000000 | (0 << 4); // 区域0
MPU->RASR = (0x1 << 28) | // 使能
(0x12 << 24) | // 大小:512KB
(0x1 << 18) | // 可执行
(0x3 << 16) | // 全访问权限
(0x0 << 8) | // 共享属性
(0x0 << 4); // 缓存属性
// 3. 配置区域1:SRAM区域(0x20000000,128KB)
// 特权级和用户级都可读写,不可执行
MPU->RBAR = 0x20000000 | (1 << 4); // 区域1
MPU->RASR = (0x1 << 28) | // 使能
(0x10 << 24) | // 大小:128KB
(0x0 << 18) | // 不可执行
(0x3 << 16) | // 全访问权限
(0x0 << 8) |
(0x0 << 4);
// 4. 使能MPU,允许特权级访问默认区域
MPU->CTRL = (1 << 0) | (1 << 2); // ENABLE + PRIVDEFENA
// 5. 数据同步
__DSB();
__ISB();
}
注意:配置MPU时,区域之间不能重叠。如果两个区域有重叠,优先级高的区域(区域号小的)会覆盖优先级低的。我曾经在调试时犯过这个错,两个区域都覆盖了同一段SRAM,结果MPU行为变得不可预测。排查了半天才发现是区域重叠了。
3.5 MPU的典型应用场景
在实际项目中,MPU主要有三个用途:
- 任务隔离:在RTOS中,每个任务分配独立的堆栈区域,MPU防止任务间越界访问。我做过一个航空电子项目,要求任务之间完全隔离,MPU就是关键。
- 防止栈溢出:给堆栈区域设置MPU保护,一旦栈溢出触发MemManage异常,系统可以及时处理。我曾经用这个特性抓到一个隐藏了很久的栈溢出bug。
- 代码保护:将关键代码区域设为只读,防止意外修改。比如Bootloader区域,配置为特权级只读,用户代码无法篡改。
嗯,最后说一句。MPU不是万能的,它只能检测到CPU发起的访问。DMA直接访问内存时,MPU是管不到的。这一点很多新手容易忽略。
好了,这一章的内容就到这里。存储器映射是基础,位带操作是技巧,MPU是进阶。掌握了这些,你对Cortex-M的存储器系统就有了比较完整的认识。下一章我们聊聊中断系统,那又是另一个精彩的话题。