4. 存储器映射与地址空间:ARM Cortex-M 的“地图”
各位同学,咱们今天聊点实在的。你写好的代码,烧进芯片之后,到底存在哪儿?CPU 又是怎么找到它的?这就得从存储器映射说起了。
我刚开始学 ARM 的时候,看着那个 4GB 的地址空间,心里直犯嘀咕:这玩意儿怎么用?后来被导师按着头啃了几遍手册,才明白——这其实就是一张城市地图。每个区域干什么用,ARM 公司早就给你规划好了。
4.1 ARM Cortex-M 的 4GB 地址空间
Cortex-M 系列用的是 32 位地址总线,寻址范围就是 2^32 = 4GB。这 4GB 空间被分成了几个大区,每个区有固定的用途。你想想看,如果没有这个划分,外设、内存、Flash 全挤在一起,那不乱套了?
| 地址范围 | 区域名称 | 主要用途 |
|---|---|---|
| 0x0000 0000 - 0x1FFF FFFF | Code 区 | 程序代码、异常向量表、启动代码 |
| 0x2000 0000 - 0x3FFF FFFF | SRAM 区 | 堆、栈、全局变量、数据区 |
| 0x4000 0000 - 0x5FFF FFFF | 外设区 | GPIO、UART、SPI、定时器等外设寄存器 |
| 0x6000 0000 - 0x9FFF FFFF | 外部 RAM 区 | 扩展 SDRAM、NOR Flash 等 |
| 0xE000 0000 - 0xE00F FFFF | 系统控制区 (SCS) | NVIC、SysTick、MPU 等内核外设 |
这张表你得记牢了。我见过不少新手,调试的时候发现程序跑飞了,一查地址,原来是往 0x4000 0000 这个外设区写了个数组——直接把外设寄存器给覆盖了。嗯,这种问题查起来特别头疼。
4.2 代码区与数据区:程序到底住哪儿?
咱们写的 C 代码,编译之后会分成好几个段。这些段最终会被链接器安排到不同的地址区域。
- .text 段:你的指令代码,放在 Code 区(通常是 Flash 里)。
- .data 段:已初始化的全局变量,启动时从 Flash 拷贝到 SRAM。
- .bss 段:未初始化的全局变量,启动时在 SRAM 里清零。
- 堆和栈:动态分配和函数调用用的,全在 SRAM 里。
这里有个关键点:CPU 不能直接从 Flash 里读写变量。Flash 是只读的,写操作必须通过特定的擦写流程。所以 .data 段必须在启动代码里被搬到 SRAM 中。这个搬运过程,就是咱们后面要讲的启动流程的核心步骤之一。
核心概念:代码在 Flash 里执行,数据在 SRAM 里读写。启动代码负责把 .data 段从 Flash 搬到 SRAM,并把 .bss 段清零。
4.3 外设总线地址:操作硬件的“门牌号”
操作外设,说白了就是读写寄存器。每个外设的寄存器都映射到外设区的一个固定地址。比如 STM32F103 的 GPIOA,它的端口输出数据寄存器 (ODR) 地址是 0x4001 080C。
我习惯用结构体指针来操作外设,这样代码可读性高,也不容易写错地址。举个例子:
// 定义 GPIO 寄存器结构体
typedef struct {
volatile uint32_t CRL; // 0x00
volatile uint32_t CRH; // 0x04
volatile uint32_t IDR; // 0x08
volatile uint32_t ODR; // 0x0C
// ... 其他寄存器
} GPIO_TypeDef;
// 将 GPIOA 基地址强制转换为结构体指针
#define GPIOA ((GPIO_TypeDef *) 0x40010800)
// 设置 PA0 输出高电平
GPIOA->ODR |= (1 << 0);
你看,这样写是不是清晰多了?直接操作地址的话,代码里全是魔法数字,过两个月你自己都看不懂。
小技巧:芯片厂商的 HAL 库或标准外设库,其实底层就是这么干的。你去看它们的头文件,里面全是这种结构体定义和地址宏。理解了存储器映射,你就能看懂那些头文件了。
4.4 MPU(内存保护单元)基础
MPU 是个好东西,但很多初学者觉得它可有可无。我刚开始也这么想,直到有一次产品在恶劣环境下跑飞了,数据被意外改写,整个系统崩溃。从那以后,我再也不敢省 MPU 了。
MPU 的作用,说白了就是给内存区域设置“门禁”。你可以把内存分成若干个区域,每个区域设置不同的访问权限:
- 读/写/执行权限:某个区域能不能读、能不能写、能不能执行代码。
- 特权级/用户级访问:只有内核态才能访问,还是用户态也能访问。
- 区域大小和对齐:每个区域的大小必须是 2 的幂次,且地址对齐。
举个例子,你可以把 Flash 的代码区设为“只读+可执行”,把 SRAM 的数据区设为“可读写但不可执行”。这样即使黑客注入了恶意代码,也没法在 SRAM 里执行。
注意:MPU 的配置必须在特权级下进行。一旦配置错误,系统可能直接进入 HardFault。我建议你在调试阶段先不要开 MPU,等所有功能都调通了,再逐步加上保护。
4.5 一张图看懂存储器映射
下面这张图,是我自己画的 Cortex-M 存储器映射概览。你把它存下来,以后写代码、调 bug 的时候,随时拿出来对照。
4.6 避坑指南:我踩过的那些坑
最后,分享几个我实际项目中遇到的教训,希望能帮你少走弯路。
- 数组越界写到外设区:有一次我定义了一个大数组,结果栈溢出,数据写到了 0x4000 0000 附近,直接把 UART 的波特率寄存器改了。调试了整整两天才发现。
- MPU 配置不对导致 HardFault:我刚开始用 MPU 时,把 SRAM 区域的大小设错了,结果 malloc 分配的内存刚好落在保护区外,一访问就进异常。后来我养成了习惯:每次改 MPU 配置,先打印出所有区域的边界。
- 中断向量表放错位置:如果你用 Bootloader,记得把中断向量表重映射到 SRAM 或 Bootloader 的 Flash 区。否则一触发中断,CPU 跑去读 0x0000 0000,结果读到的全是 0xFF,直接 HardFault。
嗯,存储器映射这块,说难不难,说简单也不简单。关键是要把这张“地图”刻在脑子里。下次你写代码的时候,多想想:这个变量放在哪个区?这个指针指向的地址合法吗?养成这个习惯,很多低级 bug 就能提前避免。