2、MCU内存架构详解:STM32内存映射、代码区、数据区(SRAM)、外设区、位带操作原理、MPU内存保护单元
说到MCU的内存架构,我其实挺感慨的。刚入行那会儿,我总觉得内存就是RAM和Flash,没什么好研究的。直到有一次项目里程序莫名其妙跑飞,查了三天才发现是内存访问越界了。从那以后,我每次选型都会先把芯片的内存映射表翻一遍。
今天咱们就好好聊聊STM32的内存架构。说白了,就是搞清楚芯片里哪些地址放了什么东西,怎么访问它们最安全、最高效。
2.1 STM32内存映射概览
STM32采用ARM Cortex-M内核,它的内存映射是固定的。什么意思呢?就是芯片出厂时,各个功能模块的地址范围已经定死了。你没法改,只能去适应它。
我习惯把整个4GB地址空间分成几个大块来看:
| 地址范围 | 区域名称 | 主要用途 |
|---|---|---|
| 0x0000 0000 - 0x1FFF FFFF | 代码区(Flash) | 存放程序代码、常量、中断向量表 |
| 0x2000 0000 - 0x3FFF FFFF | SRAM区 | 存放变量、堆栈、运行时数据 |
| 0x4000 0000 - 0x5FFF FFFF | 外设区 | GPIO、USART、SPI等外设寄存器 |
| 0x6000 0000 - 0x9FFF FFFF | 外部RAM区 | 扩展外部存储器(FSMC接口) |
| 0xE000 0000 - 0xE00F FFFF | 系统控制区 | NVIC、MPU、SysTick等内核外设 |
嗯,这里要注意:不同型号的STM32,Flash和SRAM的大小不一样,但地址范围是固定的。比如STM32F103C8T6的Flash是64KB,它就在0x0800 0000到0x0800 FFFF之间。你换个大容量的芯片,地址范围会往上扩展,但起始地址不变。
2.2 代码区(Flash)
代码区存放的是你的程序。包括函数代码、常量数据、还有中断向量表。我个人建议,所有只读数据都放在这里,别往SRAM里塞。
为什么?因为Flash掉电不丢失,而且读取速度其实不慢。Cortex-M3/M4有预取缓冲和分支预测,跑代码基本不会卡顿。
举个例子,你定义了一个查表用的数组:
// 放在Flash里,不占SRAM
const uint8_t sin_table[256] = {
0, 2, 4, 6, 8, 10, 12, 14, 16, 18, ...
};
// 如果去掉const,就会放到SRAM里
// 对于查表数据,完全没必要
我在项目中遇到过有人把大数组定义成全局变量,结果SRAM爆了。其实加个const就解决了。你想想看,256字节的常量表,放在Flash里不香吗?
2.3 数据区(SRAM)
SRAM是程序运行时的"工作台"。全局变量、局部变量、堆、栈,全挤在这里。STM32的SRAM通常从0x2000 0000开始。
SRAM分几个区域:
- .bss段:未初始化的全局变量和静态变量。上电后自动清零。
- .data段:已初始化的全局变量。初始值从Flash拷贝过来。
- 堆(Heap):动态分配的内存,malloc/free用的。
- 栈(Stack):局部变量、函数调用、中断现场保存。
我曾经踩过一个坑:在中断服务函数里定义了一个大数组作为局部变量。结果栈溢出,程序跑着跑着就挂了。后来我改成静态变量,问题就解决了。
2.4 外设区
外设区从0x4000 0000开始。每个外设都有一组寄存器,映射到特定的地址。比如GPIOA的寄存器在0x4001 0800附近。
操作外设,说白了就是读写这些寄存器。STM32的HAL库封装了这些操作,但底层还是地址访问。我个人习惯直接操作寄存器,效率更高。
举个例子,点亮一个LED:
// 直接操作寄存器,点亮PA0
#define GPIOA_BASE 0x40010800
#define GPIOA_BSRR (*(volatile uint32_t *)(GPIOA_BASE + 0x10))
GPIOA_BSRR = (1 << 0); // 设置PA0为高电平
你可能会问,为什么不直接用HAL库?其实都可以。但理解寄存器地址映射,能帮你更好地理解芯片的工作原理。
2.5 位带操作原理
位带操作是Cortex-M3/M4的一个特色功能。说白了,就是把一个32位的字映射到位带区的一个比特上。这样你就能用普通的读写指令来操作单个比特了。
位带区有两个:
- SRAM位带区:0x2000 0000 - 0x200F FFFF(1MB)
- 外设位带区:0x4000 0000 - 0x400F FFFF(1MB)
每个位带区对应一个位带别名区。别名区的地址计算公式是:
别名地址 = 位带基地址 + (字节偏移 * 32) + (位号 * 4)
举个例子,操作GPIOA的ODR寄存器的第0位:
// GPIOA ODR寄存器地址:0x4001080C
// 位带别名地址计算
#define GPIOA_ODR_ADDR 0x4001080C
#define BITBAND_PERIPH_BASE 0x42000000
// 计算第0位的别名地址
uint32_t alias_addr = 0x42000000 + (0x1080C * 32) + (0 * 4);
// 结果:0x42021060
// 直接写这个地址,就能控制PA0
*(volatile uint32_t *)0x42021060 = 1; // PA0输出高
我在项目中用过位带操作做LED流水灯。代码简洁,而且比读-改-写的方式快。不过要注意,位带操作只支持Cortex-M3/M4,M0/M0+不支持。
2.6 MPU内存保护单元
MPU是Cortex-M3/M4的可选功能。它有点像操作系统的内存管理单元,但更简单。MPU可以划分内存区域,设置访问权限。
为什么要用MPU?我遇到过这种情况:两个任务共享一个全局变量,一个任务不小心写越界了,把另一个任务的数据给覆盖了。查这种bug特别痛苦。有了MPU,就能在运行时检测到这种非法访问。
MPU的配置步骤:
- 禁用MPU(写MPU_CTRL寄存器)
- 配置区域(写MPU_RBAR和MPU_RASR)
- 启用MPU(写MPU_CTRL寄存器)
举个例子,保护SRAM中的关键数据:
// 配置MPU区域0:保护0x20001000开始的4KB区域
// 权限:只读,不允许执行代码
MPU->RBAR = (0x20001000) | (0 << 4); // 区域0,起始地址
MPU->RASR = (0x01 << 0) // 使能区域
| (0x03 << 1) // 大小:4KB(2^(1+3))
| (0x01 << 16) // 只读
| (0x01 << 28); // 禁止执行
MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
MPU的典型应用场景:
- 保护关键数据区不被意外修改
- 防止栈溢出破坏其他数据
- 在RTOS中隔离任务的内存空间
- 检测野指针访问
说实话,很多嵌入式开发者没用过MPU。但我建议你试试。它就像给你的程序加了一道防火墙,很多隐蔽的内存问题都能提前发现。
2.7 小结
好了,这一章的内容就这些。我帮你捋一下重点:
- STM32的内存映射是固定的,分代码区、SRAM区、外设区等
- 代码放Flash,数据放SRAM,这是基本原则
- 外设寄存器本质就是内存地址,直接操作效率更高
- 位带操作是Cortex-M3/M4的特色,操作单个比特很方便
- MPU能保护内存,防止非法访问,建议用起来
下一章咱们聊聊堆和栈的管理。这可是嵌入式开发的"老大难"问题,很多bug都出在这里。到时候我会分享一些实战中的避坑经验。