stm32内存知识概览

发布时间:2026/6/30 4:12:38
stm32内存知识概览 文章目录一、宏观视角STM32 的内存不是一块而是一张“城市地图”二、核心内存区域详解2.1 Flash —— 你的代码和常量的家2.2 SRAM —— 运行时的变量、栈和堆2.3 外设寄存器区 —— 软硬件桥梁的“接触点”2.4 位带区Bit-banding—— 对单个比特的直接“原子”操控问题直接操作寄存器某一位不够原子位带区域映射公式2.5 系统控制区 —— 内核的“设置面板”三、内存对齐为什么不能“乱放”数据3.1 什么是内存对齐3.2 为什么需要对齐—— 硬件的物理限制3.3 Cortex-M3/M4/M7 的对齐特性3.4 结构体的内存对齐四、内存保护单元MPU—— 可选的“内存防火墙”五、从源码到二进制链接脚本与内存分配5.1 程序的各个“段”Section5.2 链接脚本的作用5.3 启动过程与内存初始化六、实践中的内存知识要点6.1 volatile 为何在寄存器访问中必须6.2 DMA 与缓存一致性M7 需要注意6.3 堆和栈的管理6.4 内存屏障指令七、总结一张小抄助你记住所有专栏目录从零开始把 STM32 的“内存世界”完整拆解一遍。读完你会明白芯片内的每一块内存都是怎么来的、如何访问、有什么规矩以及写代码时怎样避开常见的坑。一、宏观视角STM32 的内存不是一块而是一张“城市地图”STM32 使用的是统一编址的存储器映射架构。CPU 所能访问的所有东西——真正的内存、外设寄存器、内部 Flash、甚至系统控制块——都被分配到了一个 4GB 的线性地址空间里。以ARM Cortex-M3/M4为例这 4GB 空间被 ARM 官方划定了大致的“功能区”STM32 再在里面填充具体的设备。4GB 地址空间概览Cortex-M 约定 0x00000000 ────────── 0x1FFFFFFF : Code 区Flash 别名、内部 Flash 等 0x20000000 ────────── 0x3FFFFFFF : SRAM 区内部 SRAM 0x40000000 ────────── 0x5FFFFFFF : 外设区GPIO, USART, SPI... 0x60000000 ────────── 0x9FFFFFFF : 外部存储器FSMC/FMC 0xE0000000 ────────── 0xFFFFFFFF : 系统控制区NVIC, SCB, MPU...你编写的任何 C 代码、访问的任何变量、操作的任何寄存器最终都会落入这张地图的某个地址。关键认知内存 存储器映射的地址空间。它不仅仅是“RAM”还包括 Flash、外设寄存器等一切可寻址的东西。二、核心内存区域详解2.1 Flash —— 你的代码和常量的家地址范围以 F407 为例从0x0800 0000开始最大 1MB具体看芯片。特性非易失性掉电不丢只可在 CPU 视角下读取写入需通过 Flash 控制器进行特殊的“编程/擦除”操作不能像 RAM 那样随意赋值。存放了什么你的程序机器码.text段常量数据const全局变量、字符串常量.rodata段中断向量表默认放在 Flash 开头0x08000000有可能有掉电保存的用户数据如果特意编程写入为什么 Flash 也在此列因为 CPU 是取指和执行代码的它必须能从某个地址读到指令。Flash 就被映射到 Code 区。有时你会看到0x00000000也能访问到 Flash因为 STM32 上电后会根据 BOOT 引脚把 Flash 或系统存储器**别名重映射**到地址 0从而让 CPU 从 0 地址取栈顶和复位向量。2.2 SRAM —— 运行时的变量、栈和堆地址范围从0x20000000开始大小依芯片而定F407 有 128KB 常规 SRAM还有 64KB CCM RAM。特性可任意读写速度快掉电数据丢失。这是你普通变量存放的地方。内存细分以 F4 为例常规 SRAM128KB位于0x20000000可以存数据也可以执行代码尽管不常用。CCM内核耦合内存RAM64KB位于0x10000000直接挂在 CPU 数据总线上只能存数据不能存指令且DMA 无法访问。适合存放需要 CPU 频繁操作的栈或关键数据速度更快且无总线竞争。2.3 外设寄存器区 —— 软硬件桥梁的“接触点”地址0x40000000开始各种外设的寄存器组按总线和编号排布。实质这些地址背后不是 RAM 存储单元而是一组一组的硬件触发器。读这些地址会得到引脚电平或状态标志写这些地址会驱动硬件动作。这就是你之前学到的寄存器。访问规则必须用volatile指针有些寄存器只读、只写或写1清零要严格遵循手册。2.4 位带区Bit-banding—— 对单个比特的直接“原子”操控这是一个超级实用的特性值得展开。问题直接操作寄存器某一位不够原子假如你想把 GPIOA 的 ODR 第 5 位置 1经典的“读-修改-写”操作GPIOA-ODR|(15);实际是三步读整个 32 位 → 修改第 5 位 → 写回。如果在此中间发生中断也修改了 ODR就可能覆盖掉中断的改动。所以 STM32 设计了 BSRR 寄存器来原子地置位/复位。但有些外设没有 BSRR这时候位带可以解决。位带让你能用一个独立的 32 位地址去代表某个寄存器的某一个比特。你向这个地址写1就是将该位置 1写0就是清 0CPU 硬件保证这是一次原子的“读-改-写”不会被中断干扰。位带区域Cortex-M3/M4 支持两个位带区SRAM 位带区0x20000000~0x200FFFFF共 1MB外设位带区0x40000000~0x400FFFFF共 1MB它们分别对应一个位带别名区SRAM 别名区0x22000000~0x23FFFFFF外设别名区0x42000000~0x43FFFFFF映射公式要找到某个比特的别名地址bit_word_addr alias_base (byte_offset * 32) (bit_number * 4)alias_base是别名区起始地址外设是0x42000000SRAM 是0x22000000byte_offset是目标字节在 bit-band 区的偏移地址即目标寄存器地址减去 bit-band 基址bit_number是比特编号0~7 对于字节操作0~31 对于字操作例如 GPIOA ODR 是0x40020014要对 bit 5 操作bit-band 基址0x40000000偏移 0x40020014 - 0x40000000 0x20014bit 5别名地址 0x42000000 (0x20014 * 32) (5 * 4) 0x42400294随后*(volatileuint32_t*)0x424002941;// 原子置1*(volatileuint32_t*)0x424002940;// 原子清0读别名地址返回的是 0 或 1非 0 时表示该位为 1。实际上头文件或 CMSIS 提供了宏方便使用如__BITBAND_PERIPH()你甚至可以直接用标准外设库或 LL 库它们底层可能用位带优化。位带的意义原子位操作、简化代码、提高性能省去屏蔽与或运算。2.5 系统控制区 —— 内核的“设置面板”从0xE0000000开始包含NVIC中断控制器寄存器系统控制块SCB配置优先级分组、系统异常等SysTick 定时器MPU 内存保护单元这些也是通过内存映射寄存器访问的。三、内存对齐为什么不能“乱放”数据3.1 什么是内存对齐简单说数据在内存中的起始地址必须是其大小字节数的整数倍。2 字节的short必须放在偶数地址地址是 2 的倍数4 字节的int/float必须放在地址是 4 的倍数的位置8 字节的double若支持必须放在地址是 8 的倍数的位置如果地址不满足就称为非对齐访问。3.2 为什么需要对齐—— 硬件的物理限制CPU 从内存取数据时不是逐字节抓的而是通过数据总线一次抓一个字32位或半个字。总线设计时地址线的最低几位用来控制字节选通。假设没有对齐一个 4 字节的int存放在地址0x20000001CPU 需要读两次先读地址0x20000000取后 3 个字节再读0x20000004取前 1 个字节然后拼接。这会降低性能且硬件实现复杂。所以很多 CPU 架构如一些 ARM 的老版本或某些模式直接禁止非对齐访问一旦发生就触发异常。3.3 Cortex-M3/M4/M7 的对齐特性Cortex-M3/M4支持硬件级的非对齐访问仅限于普通的数据加载/存储指令如LDR/STR但有几个重要限制LDM/STM多寄存器加载/存储及堆栈操作PUSH/POP必须对齐到 4 字节否则发生用法 fault。非对齐访问是慢的可能需要多个总线周期。外设寄存器区域、强排序内存区域等可能不支持非对齐访问通常只支持字访问。编译器默认会将数据对齐所以正常写 C 代码很少遇到非对齐问题除非你强制转换指针。实践铁律永远不要让指针指向未对齐的地址除非你非常清楚自己在做什么。3.4 结构体的内存对齐编译器会自动在结构体成员之间插入填充字节来满足对齐要求。这会导致sizeof(结构体)大于各成员大小之和。例structDemo{chara;// 1字节intb;// 4字节shortc;// 2字节};// 实际内存布局在默认4字节对齐下// a 占 1 字节后跟 3 个填充字节使 b 从 4 的倍数开始// b 占 4 字节// c 占 2 字节因为结构体总大小需要是最大成员对齐的倍数所以末尾补 2 字节填充// sizeof(Demo) 很可能是 12可以通过改变成员顺序减少填充structDemoOpt{intb;shortc;chara;};// sizeof 可能是 8改变对齐方式__attribute__((aligned(n)))指定变量或结构体的最小对齐。__attribute__((packed))取消填充紧凑排列。但会导致非对齐访问影响性能甚至出错仅在移植协议或硬件映射时使用。#pragma pack(push, 1)…#pragma pack(pop)效果类似packed。编写与硬件寄存器映射相关的结构体时必须确保没有自动填充通常用__packed或__attribute__((packed))并用静态断言检查大小。四、内存保护单元MPU—— 可选的“内存防火墙”MPU 可以让你把内存分成多个区域分别设定权限读/写/执行和属性缓存、缓冲、共享等。它在以下场景有用防止堆栈溢出破坏数据区。禁止代码从 SRAM 执行安全。隔离实时任务和非实时任务的数据。MPU 在启动代码里配置或者用 RTOS如 FreeRTOS的 API 管理。默认它是关闭的所有内存都可访问。五、从源码到二进制链接脚本与内存分配5.1 程序的各个“段”Section你的 C 程序编译后会被分割成不同的段.text代码放入 Flash。.rodata只读数据const 变量、字符串放入 Flash。.data已初始化的全局/静态变量其初始值存在 Flash 里上电后由启动代码复制到 SRAM。.bss未初始化或初始化为 0 的全局/静态变量启动代码在 SRAM 中将其清零。堆heap动态内存分配malloc用的区域从 RAM 中划定。栈stack局部变量、函数调用参数、返回地址等也是从 RAM 中划定。5.2 链接脚本的作用链接脚本.ld文件决定这些段放在哪里。例如 STM32F407 的脚本MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 1024K RAM (rwx) : ORIGIN 0x20000000, LENGTH 128K CCMRAM (rw) : ORIGIN 0x10000000, LENGTH 64K } SECTIONS { .text : { *(.text) } FLASH .rodata : { *(.rodata) } FLASH .data : { *(.data) } RAM AT FLASH .bss : { *(.bss) } RAM /* 堆栈通常放在 RAM 的最后 */ }这样链接器就知道哪些东西往哪放并为全局变量生成正确的地址。5.3 启动过程与内存初始化芯片上电后从地址0x00000000重映射自 Flash 或系统存储器读取初始堆栈指针放入 MSP。读取复位向量Reset_Handler 的地址并跳转。Reset_Handler 里会做复制.data段从 Flash 到 RAM清零.bss调用SystemInit()配置时钟然后调用main()。你可以在启动文件startup_stm32f407xx.s和系统文件system_stm32f4xx.c里看到这些。六、实践中的内存知识要点6.1 volatile 为何在寄存器访问中必须因为外设寄存器值会由硬件改变编译器如果不加volatile可能会把多次读取优化成一次或者打乱写顺序导致逻辑错误。所以所有外设寄存器定义都带有__IO即volatile。6.2 DMA 与缓存一致性M7 需要注意如果使用 M7 内核如 H7 系列CPU 带有数据缓存。当 DMA 修改了 SRAMCPU 缓存里的副本不会自动更新导致读到旧数据。需要手动操作 SCB 的缓存维护指令clean/invalidate或使用 MPU 将 DMA 缓冲区设为不缓存。对于 M3/M4无缓存通常不需要考虑这个问题。6.3 堆和栈的管理栈由 MSP主栈指针管理。每个函数调用会压栈局部变量、返回地址递归过深或局部数组太大会栈溢出通常表现为 HardFault。可以在链接脚本里设置栈大小并在 startup 文件里分配。堆如果用了malloc需要在链接脚本里指定堆的大小。嵌入式系统一般不建议用动态内存分配因为容易产生碎片且不确定。通常用静态分配或内存池。6.4 内存屏障指令Cortex-M 提供了DMB、DSB、ISB等指令用于保证内存访问的顺序。在操作外设寄存器时如果编译器或硬件可能重排指令可以使用。通常外设寄存器被映射为“设备”内存类型在内核层面访问顺序已得到保证大多数应用不需要显式加屏障。七、总结一张小抄助你记住所有区域地址段里面有什么操作要点Flash0x08000000代码、常量、向量表只读不可直接赋值SRAM0x20000000变量、堆、栈可读可写掉电丢CCM RAM0x10000000高速数据缓冲区CPU 专用DMA 别碰外设0x40000000寄存器volatile按位域操作位带别名0x42…原子位操作超级好用系统控制0xE000E000NVIC、SCB、MPU内核功能配置内存对齐让数据地址是其大小的整数倍编译器自动做结构体注意排列强制指针转换要万分小心。内存模式一切皆地址CPU 只管读写地址硬件根据地址决定谁来响应。你写 C 代码时操作的一切最终都在这个统一的 4GB 地图上被精准定位。掌握了这些你就能从底层看懂为何程序能跑起来为何会进 HardFault以及如何写出更安全高效的嵌入式代码。带着这张“内存地图”去读手册和写程序你会觉得整个世界通透许多。专栏目录Click