拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32 RAM深度解析:从SRAM硬件结构到内存优化实战

调试嵌入式程序的时候经常有人问我“STM32 的 RAM 是不是就是电脑内存条那种东西不够用能不能像台式机一样插一个”这个问题看似基础但如果没把 RAM 的硬件结构、内存分区、访问总线这些东西理顺后面遇到 HardFault、程序跑飞、栈溢出很容易一头雾水。我写这篇东西就是想从硬件层面把 STM32 RAM 这件事彻底拆开它到底存在芯片里哪个位置怎么被 CPU 和 DMA 访问为什么和 PC 内存条完全是两码事以及怎么在工程里把有限的 RAM 空间用到极致。适合刚入门、准备拿 STM32 做项目或者已经被内存优化折磨过的朋友参考。1. 先别急着“加内存条”STM32 的 RAM 到底是什么1.1 两个核心区别DRAM 与 SRAMPC 里的内存条用的是 DRAM也就是动态随机存储器。动态的意思是它靠电容上的电荷保存数据电容会漏电所以需要每隔一段时间刷新一次把电荷重新补满否则数据就丢了。这个刷新操作是硬件自动做的但本质上是持续耗电、持续工作的。STM32 内部集成的是 SRAM静态随机存储器。静态意味着它不需要刷新只要供电不中断数据就可以一直保持读写速度也更快。代价是 SRAM 的单元结构比 DRAM 复杂每个 bit 往往需要 6 个晶体管而 DRAM 一个 bit 只要 1 个晶体管加 1 个电容。这就是为什么 SRAM 容量做不大、成本做不低单片机上给几十 KB、几百 KB 已经算不错了电脑内存条动辄 8GB、16GB在物理原理上就不是一个量级的东西。1.2 编址方式统一编址与无 MMUPC 上 CPU 访问内存中间要经过内存控制器而且现代 PC 操作系统开启了虚拟内存进程看到的是虚拟地址真正落到物理内存是 MMU内存管理单元在管。STM32 这类 Cortex-M 内核芯片绝大多数没有 MMU也没有虚拟内存的概念所有地址都是纯物理地址。Cortex-M 内核采用的是统一编址的存储映射方式4GB 的地址空间被设计成固定的功能分区。你写 C 代码里一个指针指向 0x20000000那它就是访问 SRAM指向 0x40000000访问的就是外设寄存器。CPU 拿到地址直接走总线去访问没有中间层。这种设计对嵌入式非常合适因为你要的就是确定性这个地址能访问到什么时序如何用起来简单直接。1.3 为什么单片机 RAM 普遍那么小很多人觉得 STM32 RAM 太小其实要结合芯片定位和成本看。STM32 是面向控制领域的大部分应用不需要跑到 GB 级内存用 64KB RAM 配合 DMA、中断做电机控制、传感器采集、物联网上报完全够用。把 RAM 做大芯片面积、功耗、成本都会明显上涨对于大批量产品来说非常不划算。所以你在选型时就要有“RAM 是稀缺资源”的觉悟。很多项目翻车不是因为 Flash 不够而是 RAM 被无意中吃光了。尤其用了大数组、缓冲池、RTOS 任务栈一下就能吃掉十几 KB。想省事只有两个方向一是精打细算每一种 RAM 的用法二是遇到大缓存需求就选带 FMC 外部存储接口的型号外挂 SRAM 或者直接上串行 PSRAM但那已经是另一个话题了。2. STM32 RAM 硬件结构总线、存储映射与系列差异2.1 一张地址表看懂存储映射Cortex-M 内核定义了 4GB 地址空间半导体厂商在指定位置放自己的外设和存储器。以最常见的 STM32F103ZET6 为例核心地址分区是地址范围用途大小0x00000000 - 0x0000FFFF别名区/启动区映射64KB0x08000000 - 0x0807FFFF主 Flash512KB0x1FFFF000 - 0x1FFFF7FF系统存储器Bootloader2KB0x20000000 - 0x2000FFFFSRAM64KB0x40000000 - 0x5FFFFFFF外设寄存器区512MB0xE0000000 - 0xFFFFFFFFCortex-M 内核私有外设512MB注意 SRAM 基地址是 0x20000000这是所有主流 STM32 的默认安排。启动时 BOOT0 和 BOOT1 引脚决定从哪里取指如果从 SRAM 启动那 0x20000000 这个区域会被映射到启动区CPU 直接从 RAM 里取第一条指令。这个功能调试时偶有用到但实际项目基本都用 Flash 启动。F4 系列在 0x20000000 之外多了一个 0x10000000 起始的 CCM RAMH7 系列更复杂有 DTCM、ITCM、AXI SRAM、多个 SRAM 块后面我会专门讲。2.2 总线矩阵谁在访问 RAMSTM32 内核对 RAM 的访问不是“一根线直连”而是经过总线矩阵调度的。Cortex-M3/M4 内核有指令总线I-Bus、数据总线D-Bus、系统总线S-BusDMA 控制器也有自己的总线。这些总线通过一个 AHB 总线矩阵连接 Flash、SRAM、外设等从设备。这意味着什么CPU 取指令走 I-Bus读写数据走 D-BusDMA 搬运走 DMA 总线从硬件层面看这些通道可以并行工作。所以你在用 DMA 搬运 ADC 数据的时候CPU 同时去 Flash 取指令、去 SRAM 读写变量只要路径不冲突性能就不会互相拖死。这也是嵌入式系统能实现“高实时性”的硬件基础。不过并行不是无限并行。如果 CPU 的 D-Bus 和一个 DMA 通道同时访问 SRAM 的同一个 Bank总线矩阵就会做仲裁某一方要等待。虽然这种等待通常是几个周期的事但高负载下确实会影响实时性。工程上的对策是把 DMA 缓冲区放在一个 SRAM 块把 CPU 高频访问的变量放另一个 SRAM 块这在 H7 这种多块 SRAM 的芯片上非常有用。2.3 F1、F4、H7 RAM 资源差异不同系列的 RAM 布局差别非常大。很多人以为“STM32 内存都是 0x20000000 开始”这在 F1 上没错但到了 F4、H7 就不完全对了。系列典型型号RAM 资源特点F1F103ZET664KB SRAM单一 SRAM 块地址 0x20000000简单直接F4F407ZET6128KB SRAMSRAM1 112KB SRAM2 16KB 64KB CCMCCM 在 0x10000000DMA 无法访问H7H743DTCM 128KB ITCM 64KB AXI SRAM 512KB SRAM1/2/3/4 等多块 RAM 分散在不同地址总线配置复杂F1 的 RAM 就是个单块连续地址编译器把所有变量排进 0x20000000 到 0x2000FFFF 就行不用关心哪块是哪块。F4 的 CCM RAM 是个坑它虽然离 CPU 更近、访问速度快但是挂在 D-Bus 上DMA 控制器没法访问它。你把 DMA 缓冲区放到 CCM RAM数据根本搬不动这是很多新手踩过的最典型问题。H7 的内存系统最复杂Cortex-M7 有 TCM 接口、AXI 接口、AHB 接口。DTCM 和 ITCM 是紧耦合内存CPU 访问零等待但 DMA 访问不到AXI SRAM 才是主内存区域。用 H7 时链接脚本如果没配好内存分布乱七八糟性能也发挥不出来。选型阶段就要确认好 RAM 布局不能拿着 F1 的思路去套 H7。3. 程序眼中的 RAM 分区堆、栈、数据段的运行时布局3.1 编译产物里 RW 和 ZI 去哪了在 Keil 的 Build Output 窗口里每次编译会显示 Code、RO-data、RW-data、ZI-data 几项很多人看不懂。我用最简单的说法解释Code程序指令烧录到 Flash。RO-dataconst 常量、字符串字面量也在 Flash。RW-data有初始值的全局变量和静态变量初值存 Flash上电后由启动代码复制到 RAM。ZI-data没有初始值或者初始化为 0 的全局变量、静态变量上电后启动代码直接在 RAM 里清零。所以你评估 Flash 占用要看 Code RO-data RW-data评估 RAM 占用要看 RW-data ZI-data。很多时候你觉得“我这个程序很小”实际一查 ZI-data 里躺着几个大数组RAM 直接就爆了。3.2 堆栈到底怎么分配启动文件 startup_stm32f103xe.s 开头有 Stack_Size 和 Heap_Size 两个定义Stack_Size EQU 0x400 Heap_Size EQU 0x200Stack_Size 是栈大小默认一般是 1KB。程序里的局部变量、函数调用返回地址、中断现场都压栈1KB 对复杂中断嵌套确实不太够。Heap_Size 是堆大小给 malloc/free 用的如果你工程里从头到尾不用 malloc堆可以设为 0省下的 RAM 全部留给栈和全局变量。栈的生长方向是向下的从高地址往低地址长堆从低地址往高地址长。一旦栈长到和堆碰头程序基本就记了表现是莫名其妙跑飞、HardFault、函数返回地址被改得乱七八糟。我实际做项目时栈大小宁多勿少至少给 2KB 到 4KB如果用了 RTOS每个任务还要单独分配任务栈那个耗起来更快。3.3 CCM、TCM、备份 SRAM 这些特殊区域到底怎么用F4 的 CCM RAM 是一块 CPU 私有内存在链接脚本里需要单独划分Keil 下用 scatter 文件指定地址 0x10000000。它速度比普通 SRAM 快适合放中断服务程序里高频访问的变量、临界区数据。但是 DMA 访问不到这是硬限制不能指望绕过。H7 里 DTCM 也是同样逻辑适合放栈和热点变量ITCM 一般放代码让 CPU 以零等待取指。不过我在实际项目里很少把代码放 ITCM因为代码大部分时间在 Flash 里跑也不算慢配合 ART 加速器性能已经够了。备份 SRAM 在 STM32 的 VBAT 供电域里主电源掉电后由纽扣电池继续供电数据可以保持。比如 RTC 闹钟、掉电标志、设备校准数据放备份 SRAM 比放外挂 EEPROM 快得多又不占用普通 RAM。要注意的是访问备份 SRAM 之前一般要先把 PWR 时钟和备份域访问使能打开否则写不进去。4. RAM 空间优化实操从看报告到动手省内存4.1 先学会读内存占用报告拿到一个工程第一步是打开 Keil 编译看 Build Output 里的 Total RW Size、Total ZI Size 这两行对应你 RAM 的总需求。然后打开 .map 文件搜索 Image Symbol Table能看到每个全局符号分配的地址和大小。这个方法能快速找出“RAM 大头”到底是谁。举个例子我曾处理过一个项目编译出来 ZI-data 有 46KB但芯片只有 48KB RAM。跑起来动不动 HardFault。一查 map 文件发现是两个 8KB 的串口 FIFO 缓冲、一个 16KB 的 LCD 显存数组、一个 8KB 的协议解析缓冲全堆在全局区。这种问题不用改算法把大缓冲改成按需动态申请或者换成 DMA 半满中断的环形缓冲RAM 瞬间就降下来了。4.2 四个实用性极高的省 RAM 手段我常跟朋友说省 RAM 不是靠玄学靠的是取舍。分享几个我实测有效的招数。第一能放 Flash 的别放 RAM。查表法需要的正弦表、CRC 表、字库数据用 const 修饰直接进 RO-data跑在 Flash 里一点问题没有。不要图省事在运行时再计算生成。第二大数组按需分配不要一把梭。很多人习惯在文件顶部定义uint8_t buffer[4096];结果这个 buffer 只在某个初始化阶段用一次却整整占着 4KB。改成局部数组或者用完后覆盖影响不大。第三RTOS 任务栈按实际深度裁剪。你用 FreeRTOS 默认创建任务时栈给 128 words如果任务里嵌套调用很多可能不足但如果给得太大每个任务多占 1KB、10 个任务就是 10KB。建议每个任务里面放一个水位检测钩子跑一轮看最大栈使用深度再调整。第四合理使用位域和变量宽度。能 uint8_t 别用 uint32_t能位标志的不要整个 int。一个结构体里放了十几个 int 型状态量改造成位域后少用几十字节虽然不多但积少成多尤其在协议解析和缓存管理场合。4.3 DMA 缓冲区与内存对齐的坑DMA 传输对内存地址对齐有要求很多型号要求缓冲区首地址 4 字节对齐部分以太网 DMA 甚至要求 32 字节对齐。Cortex-M 内核如果发生非对齐访问某些场合直接 HardFault。你定义 DMA 缓冲区时最稳妥的方式是加对齐修饰__attribute__((aligned(4))) uint8_t dma_rx_buf[1024];或者用编译器关键字。还有一个我踩过的坑如果你把 DMA 缓冲区定义成结构体成员编译器可能在结构体中插入填充字节你以为是连续 1KB实际 DMA 写入时地址错位。解决办法是单独定义缓冲区变量或者用__attribute__((packed))但不能滥用packed 会影响访问性能。5. 常见问题与排查技巧实录5.1 程序跑飞、HardFault 到底是不是 RAM 问题程序跑飞的原因有很多野指针、数组越界、栈溢出、外设配置错误导致总线错误、时钟配置异常、看门狗复位等。RAM 相关问题在其中的占比非常高尤其是数组越界悄悄改掉下一个变量的值程序行为变得诡异。排查思路我一般按顺序来先看 map 文件确认几个关键全局变量在地址上是否相邻越界会不会互相覆盖然后用调试器在 HardFault_Handler 里打断点查看 LR、PC、堆栈内容看最后一次调用是在哪个函数再不行用内存断点监听特定地址看谁在非法写。这招特别有效能找到运行时才暴露的野指针。5.2 栈溢出、内存越界的定位思路栈溢出是最难定位的一类因为错误现场往往在几万次调用之后才出现。我常用的办法是在启动文件里给栈区域填充固定魔数比如 0xCC跑一段时间后用调试器检查栈尾部魔数是否被破坏一破坏就知道栈顶已经踩到下面了。另外一个定位方式是利用 MPUCortex-M 的存储保护单元可以设置 RAM 区域的访问权限把栈底设为不可写一旦越界立刻触发 MemManage Fault直接在调试器里停下比花几天猜“为什么这边明明没写变量却变了”强太多。5.3 外部 SRAM 扩展什么时候才需要当你把内部 RAM 优化到极致还是不够用才考虑外扩。STM32 的 FMCFlexible Memory Controller可以挂外部 SRAM常见型号如 IS62WV51216容量 512KB。这在跑 GUI、做音频缓冲的时候特别有用。用 FMC 外扩 SRAM 有几个要点一是地址线和数据线连接要严格对应二是时序要按芯片手册配好FMC 的地址建立时间、数据建立时间设太短跑起来会随机出错设太长又浪费性能。另外外部 SRAM 的访问速度远低于内部 SRAM高频中断里不要直接读写外部 RAM可以把数据暂存内部批量搬运再写出去。我个人的体会是外扩 RAM 是“最后手段”不是第一选择。很多项目第一版觉得内存不够仔细优化以后发现其实够用优化完还不够才是真不够。这时候选带 FMC 的型号或者直接选 RAM 更大的系列比如换到 STM32H7往往比外挂芯片省事得多。最后再分享一个小技巧如果你在工程里同时使用了 CC RAM 区、DMA 缓冲区、RTOS一定要在链接脚本里明确每个区域的用途并且给起始地址留出足够间隔。我见过有人把数组和栈首尾相连结果栈一涨就把数组踩了查了整整两天。真正的稳定性是在硬件结构和内存分区上都留足安全冗余而不是把每一字节都压到极限。希望这篇能把 STM32 RAM 这块讲透少走点弯路。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门