STM32内核精讲 | 第三章 存储器模型与 4GB 统一寻址
STM32内核精讲 | 第三章:存储器模型与 4GB 统一寻址
💡 本文是《STM32内核精讲》栏目的第三篇。前两篇我们分别认识了 Cortex-M 家族图谱和编程模型(寄存器、堆栈、操作模式)。从本篇开始,我们将走进 Cortex-M 的存储器世界——理解 4GB 地址空间如何划分、位带操作的魔法、非对齐访问的代价,以及大小端模式的影响。
📌 一、引言:为什么需要理解存储器模型?
你写过这样的代码吗?
#define PERIPH_BASE (0x40000000UL)
#define GPIOA_BASE (PERIPH_BASE + 0x00020000UL)
#define GPIOA_ODR (*(volatile uint32_t *)(GPIOA_BASE + 0x14))
或者用过位带操作:
#define PAout(n) (*(volatile uint32_t *)(0x42210184 + (n)*4))
这些地址 0x40000000、0x42210184 从何而来?为什么有些地址可以随意读写,而有些地址访问会触发 HardFault?为什么结构体指针访问有时会莫名其妙出错?
答案都藏在 Cortex-M 的存储器模型 中。
本篇将带你彻底搞懂:
- 4GB 地址空间里,Code、SRAM、外设、外部 RAM 各自住在哪里
- 位带(Bit‑Band)如何用“膨胀地址”实现单比特原子操作
- 非对齐访问的硬件行为与性能陷阱
- 大小端模式对数据存储的影响,以及何时需要关注
📌 二、4GB 统一寻址空间:Cortex-M 的“地图”
Cortex-M 使用 32 位地址总线,理论上可寻址 4GB(2^32 字节)。ARM 将这 4GB 空间划分为几个大区块,每个区块有推荐的用途。不同厂商的芯片会在这个框架下具体分配。
2.1 标准存储器映射表
| 地址范围 | 大小 | 用途区域 | 典型内容 |
|---|---|---|---|
| 0x00000000 – 0x1FFFFFFF | 512MB | Code | 代码(Flash)、只读数据、向量表 |
| 0x20000000 – 0x3FFFFFFF | 512MB | SRAM | 内部 RAM、堆、栈、变量 |
| 0x40000000 – 0x5FFFFFFF | 512MB | Peripheral | 外设寄存器(GPIO、UART、TIM 等) |
| 0x60000000 – 0x9FFFFFFF | 1GB | External RAM | 外部存储器(SDRAM、NOR Flash) |
| 0xA0000000 – 0xDFFFFFFF | 1GB | External Device | 外部设备(LCD 控制器等) |
| 0xE0000000 – 0xFFFFFFFF | 512MB | Vendor / Private | Cortex-M 内核私有外设(NVIC、SysTick、MPU 等) |
注意:具体芯片(如 STM32F1)不一定用满所有区域,比如 External RAM 可能没有,Vendor 区可能只使用一部分。
2.2 Code 区(0x00000000 – 0x1FFFFFFF)
- 通常映射到 内部 Flash,存放代码和只读数据。
- 该区域指令总线(I-Code)和数据总线(D-Code)都可以访问,但指令预取走 I-Code 总线,数据读取走 D-Code 总线。
- 向量表(Vector Table)默认位于此区域的起始地址
0x00000000。可通过 VTOR 寄存器重定位。
2.3 SRAM 区(0x20000000 – 0x3FFFFFFF)
- 存放堆、栈、全局变量、静态变量。
- 支持字节、半字、字访问,通常有较高的访问速度。
- 部分芯片将 SRAM 分为多块(如 STM32H7 的 TCM、AXI SRAM、AHB SRAM),地址可能不在同一连续段。
2.4 Peripheral 区(0x40000000 – 0x5FFFFFFF)
- 所有外设寄存器(GPIO、UART、ADC、定时器等)都映射在此区域。
- 访问外设寄存器必须使用 volatile 关键字,防止编译器优化。
- 部分外设支持 位带别名(见后文),可以原子操作单个比特。
2.5 External RAM / Device 区
- 用于扩展外部存储器(SDRAM、NOR、NAND)或外部设备(LCD 控制器)。
- 访问时序由 FSMC/FMC 控制器配置,速度较慢。
- 有些芯片将此区域映射到 QSPI Flash 或 PSRAM。
2.6 Vendor / Private 区(0xE0000000 – 0xFFFFFFFF)
- 这是 Cortex-M 内核私有外设 区域,包括:
- NVIC(0xE000E000 起)
- SysTick(0xE000E010)
- MPU(0xE000ED90)
- SCB(系统控制块,0xE000ED00)
- 调试组件(ITM、DWT、TPIU 等)
- 访问这些地址时,不需要通过外设总线,而是直接由内核处理。
在调试时,如果你看到 PC 停在
0xE000ED14附近,那很可能是在操作 CONTROL 寄存器或 SCB。
📌 三、位带(Bit‑Band):用地址膨胀实现原子位操作
3.1 为什么需要位带?
传统 MCU 要修改一个比特,通常需要:
- 读取整个寄存器
- 修改目标位
- 写回整个寄存器
这不是原子的(可能被中断打断)。虽然可以用关中断或 LDREX/STREX 实现原子性,但代码复杂。
Cortex-M 提供了 位带 机制:将 SRAM 和外设区域中的 每个比特 映射到一个 独立的 32 位别名地址。读写这个别名地址,等价于直接操作原始比特,且是原子的(由硬件保证)。
3.2 位带区域与映射公式
Cortex-M 定义了两个位带区:
- SRAM 位带区:
0x20000000 – 0x200FFFFF(1MB),映射到别名区0x22000000 – 0x23FFFFFF - 外设位带区:
0x40000000 – 0x400FFFFF(1MB),映射到别名区0x42000000 – 0x43FFFFFF
映射公式:
别名地址 = 位带基址 + (字节偏移 × 32) + (比特编号 × 4)
- 字节偏移 = 原始字节地址 - 位带区起始
- 比特编号 = 0~7
例如,要原子操作 SRAM 地址 0x2000_0004 的第 2 个比特(bit 2):
- 字节偏移 =
0x20000004 - 0x20000000 = 4 - 别名地址 =
0x22000000 + (4 × 32) + (2 × 4) = 0x22000000 + 128 + 8 = 0x22000088
读写 0x22000088 的 32 位值,等价于读写原始地址 0x20000004 的 bit 2(但注意:别名读写总是操作 32 位,且只影响目标比特)。
3.3 实现原子位操作
// 定义宏
#define BITBAND_SRAM(addr, bit) ((volatile uint32_t *)(0x22000000 + ((uint32_t)(addr) - 0x20000000)*32 + (bit)*4))
#define BITBAND_PERI(addr, bit) ((volatile uint32_t *)(0x42000000 + ((uint32_t)(addr) - 0x40000000)*32 + (bit)*4))
// 使用
uint32_t *p = BITBAND_SRAM(0x20000004, 2);
*p = 1; // 原子地设置 bit2 为 1
优点:
- 硬件原子性,无需关中断。
- 代码简洁,适合频繁操作单个 I/O 比特(如 GPIO 输出)。
缺点:
- 消耗额外的地址空间(但现代 MCU 不缺地址)。
- 只能用于位带区(1MB SRAM 和 1MB 外设空间)。
3.4 STM32 中的实际应用
STM32 标准外设库和 HAL 库中,经常用位带操作来定义 PAout(n)、PAin(n) 等宏,实现快速的 GPIO 读写。
#define GPIOA_ODR_ADDR (GPIOA_BASE + 0x14)
#define PAout(n) (*(volatile uint32_t *)(BITBAND_PERI(GPIOA_ODR_ADDR, n)))
注意:并非所有 Cortex-M 芯片都支持位带?实际上,Cortex-M3、M4、M7 支持位带;M0/M0+ 不支持位带(没有硬件位带映射)。所以如果你的项目需要可移植性,谨慎使用位带。
📌 四、非对齐访问:性能陷阱与故障
4.1 什么是对齐访问?
对于 32 位处理器:
- 字(Word):4 字节,地址必须是 4 的倍数(末两位为 0)
- 半字(Half-word):2 字节,地址必须是 2 的倍数(末位为 0)
- 字节(Byte):任意地址
对齐访问:数据地址自然对齐。非对齐访问:地址不符合对齐要求,例如从 0x20000001 读取一个 32 位整数。
4.2 Cortex-M 对非对齐的支持
- Cortex-M3、M4、M7、M33、M55、M85:硬件支持非对齐访问(通过多次总线传输实现)。但性能会下降,且可能在某些总线或外设上触发故障。
- Cortex-M0、M0+、M23:不支持 非对齐访问,任何非对齐的加载/存储都会触发 硬故障。
即使硬件支持,C 语言标准也认为非对齐指针访问是未定义行为。编译器可能假设指针是对齐的,从而优化出错误代码。
4.3 非对齐访问的性能代价
假设从地址 0x20000001 读取一个 32 位整数:
- 对齐访问:一次总线传输完成。
- 非对齐访问:处理器内部会拆分成两次对齐访问(先读
0x20000000和0x20000004的部分数据,再组合),消耗更多时钟周期。
对于 M3/M4,非对齐访问比对齐访问慢 2~3 倍。对于 M7 这种高性能内核,代价可能稍小,但仍不推荐。
4.4 如何避免非对齐访问?
- 使用结构体时,注意成员顺序和类型。编译器默认会填充(padding)以保证对齐。
- 通过
__packed属性强制紧凑布局时,要小心指针访问。 - 从网络或通信协议接收的字节流,应使用
memcpy或逐字节解析,而不是强制转换指针。
错误示例:
uint8_t buf[4] = {0x11, 0x22, 0x33, 0x44};
uint32_t *p = (uint32_t *)buf; // buf 可能未对齐
uint32_t val = *p; // 非对齐访问,可能故障或性能损失
正确做法:
uint32_t val;
memcpy(&val, buf, 4); // memcpy 会处理对齐
4.5 使能非对齐故障检测
在 SCB 的 CCR 寄存器中,有一个 UNALIGN_TRP 位。如果置 1,任何非对齐访问都会触发用法故障(UsageFault)。这在调试阶段非常有用,可以帮助你找出隐藏的非对齐访问。
SCB->CCR |= SCB_CCR_UNALIGN_TRP_Msk; // 开启非对齐捕获
之后发生非对齐访问,程序会进入 UsageFault_Handler,你可以在那里设置断点,快速定位问题。
📌 五、端模式(Endianness):默认小端,何时需大端?
5.1 小端 vs 大端
- 小端(Little-Endian):最低有效字节(LSB)存储在最低地址。例如
0x12345678在内存中存储为78 56 34 12。 - 大端(Big-Endian):最高有效字节(MSB)存储在最低地址。存储为
12 34 56 78。
Cortex-M 默认使用 小端,这是绝大多数嵌入式工具链和代码的标准假设。
5.2 能否配置为大端?
Cortex-M 内核有一个 AIRCR 寄存器的 ENDIANNESS 位(只读),指示当前端模式。对于 Cortex-M3/M4/M7 等,理论上可以在复位后通过硬件配置(某引脚)或软件修改,但 ARM 强烈建议不要改变,因为:
- 大多数外设设计为小端访问。
- 工具链(如 GCC、ARMCC)默认生成小端代码,混用会导致严重错误。
所以实际上,你几乎永远不用关心大端 —— 默认小端,且不要试图修改。
5.3 什么时候需要关注端模式?
- 与网络通信(TCP/IP)时,网络字节序是大端,需要调用
ntohl/htonl转换。 - 与某些大端设备(如旧式传感器、某些 DSP)通过内存共享通信时。
- 解析文件格式(如 BMP、ELF)时,这些格式可能固定为大端或小端。
在这些情况下,使用标准函数转换数据,而不是改变内核的端模式。
📌 六、总结与下篇预告
6.1 本篇核心要点
- 4GB 地址空间划分:Code、SRAM、Peripheral、External RAM、Private 区域,各有用途和访问特性。
- 位带(Bit‑Band):将单个比特映射到独立地址,实现原子位操作。公式
别名 = 基址 + 字节偏移×32 + 比特×4。 - 非对齐访问:M0/M0+ 不支持,会故障;M3+ 支持但有性能代价。可用
UNALIGN_TRP捕获调试。 - 端模式:默认小端,绝不更改。与外部大端设备通信时,使用转换函数。
6.2 下篇预告:《指令集基础:Thumb® 与 Thumb‑2》
下一篇我们将进入 Cortex-M 的指令世界。内容包括:
- Thumb 与 Thumb‑2 技术:16/32 位混合指令编码,为什么 Cortex-M 不支持 ARM 指令集?
- 常用指令分类:数据传送、算术逻辑、分支跳转、内存访问、屏障指令
- 条件执行与 IT 块:如何在不使用分支的情况下实现 if-then-else
- 单周期 I/O 与位带别名访问的本质:指令层面如何实现原子操作
- 内联汇编与裸函数:在 C 代码中嵌入汇编的典型场景
从下一篇开始,你将能读懂启动文件中的汇编代码,理解 __disable_irq() 底层到底执行了什么指令,为后续分析 RTOS 上下文切换打下坚实基础。
💬 读者问题专栏 · 问题征集
本篇我们走过了 4GB 地址空间、位带操作、非对齐访问和大小端模式。
你在项目开发或调试中,是否踩过这些坑:
- 结构体指针强制转换后,程序莫名 HardFault,最后发现是非对齐访问?
- 想用位带来快速控制 GPIO,但不知道别名地址怎么算?
- 从网络收到数据后,解析出来的数值总是不对,是不是大小端的问题?
- 外设寄存器地址
0x40000000是怎么来的?能自己改吗?
欢迎在评论区分享你的经历或疑问,我会在 《Cortex‑M 有问必答》 专栏中结合内核原理深入分析。
附上出错的地址和操作代码,分析会更高效。
📢 关于作者与更多内容
我是 BackCatK Chen,长期关注嵌入式底层、国产半导体与 AI 算力芯片。
如果你对芯片架构、行业趋势感兴趣,欢迎关注我的公众号,获取更多宏观技术观察。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)