资讯动态

STM32 RAM深入解析:从内存分区到嵌入式优化实战

发布时间:2026/9/7 12:25:31 来源:尧图企业网站定制
搞嵌入式的人第一次从PC世界转进STM32最容易被吓到的就是内存。看着电脑里插着16GB DDR5内存条再看看手里的STM32F103C8T6只有20KB RAM第一反应是这玩意儿能跑得动啥更离谱的是20KB就20KB吧还得被编译器拆成栈、堆、全局变量好几块动不动就编译报错说RAM溢出。这篇文章我就从硬件结构、内存分区两个层面把STM32的RAM彻底拆开讲一遍顺便聊聊它和PC内存条到底差在哪、以及实际项目里怎么查RAM占用、怎么优化。1. PC内存条与STM32 RAM不只是容量差了几个量级1.1 DRAM和SRAM根本就是两种存储原理先说最本质的区别PC内存条用的是DRAMSTM32内部用的是SRAM。这俩虽然都叫“随机存取存储器”但工作原理完全不同。DRAM的存储单元是“一个晶体管加一个电容”靠电容里存不存电荷来表示0和1。问题来了电容会漏电电荷放久了就没了所以DRAM必须定期刷新也就是不停地重新充电读改写否则数据就丢了。这也是为什么内存条控制器里始终有个刷新逻辑在后台忙活。SRAM就不一样了每个存储单元由6个晶体管组成一个双稳态触发器只要通电状态就稳定保持不需要刷新。一个是漏水的水桶得不停补水一个是写字板写上去就不掉。这个特性决定了二者在不同场景下的地位。对比项DRAMPC内存条SRAMSTM32内部RAM存储单元结构1晶体管1电容6晶体管触发器是否需刷新需要毫秒级刷新周期不需要集成度高单位面积容量大低同样面积容量小得多单颗容量GB级别起步KB到几MB级别访问速度几十ns级别几ns级别单位成本便宜贵典型应用PC主内存、显卡显存CPU缓存、MCU内部RAMPC选择DRAM是因为需要GB级别的容量成本、功耗、面积的账都得算。STM32选择SRAM是因为MCU芯片面积本身就很金贵而且实时控制场合需要快速、确定的访问时间不需要刷新带来的额外延迟和功耗。芯片内部集成大容量DRAM不是做不到而是成本和复杂度不可控所以在主流MCU上你基本见不到片内大容量DRAM。1.2 一个可插拔、一个焊死在芯片里PC内存条是独立模块通过主板上的DIMM插槽和CPU连接你可以8GB换16GB坏了拔下来换一根。它跟CPU之间还有一套复杂的内存控制器、地址总线、时序训练流程甚至内存条上还有一颗SPD芯片专门告诉主板“我是谁、能跑多快”。STM32的RAM直接做在MCU芯片内部通过内部总线与Cortex-M内核相连整个生命周期不可更换、不可升级。你要多少RAM选型那一刻就定死了。选错了只能换芯片型号不可能像PC那样加个内存条。还有一个很多人忽略的差异PC有虚拟内存机制内存不够时操作系统会把一部分数据换到磁盘上程序感觉“内存好像很多”。STM32的Cortex-M系列没有MMU内存管理单元程序访问的全部是物理地址不存在“虚”的映射。RAM不够就是不够编译器直接给你报错。提示Cortex-M7带的是MPU内存保护单元它是做访问权限控制的不是做地址虚拟映射的。这点必须先搞清楚不然会拿PC思维去理解MCU内存越学越乱。1.3 为什么MCU不把RAM做大点很多人问过既然SRAM快为什么不直接给STM32配个几MB的RAM答案就两个字不值。芯片面积是MCU成本的决定因素之一。SRAM的六管结构很占面积1MB SRAM占的硅片面积可能比一颗F103的整个芯片还大。面积大了成本上去了功耗上去了良率还下降。所以MCU厂商的平衡策略是内部放刚好够用的那几十到几百KB如果你确实需要大内存通过FSMC/FMC总线在外面挂一颗SRAM或者SDRAM自己按需扩展。这就是另外一个话题了后文我会细讲。2. 从地址地图看STM32内部RAM的几种形态SRAM、CCM、TCM与备份域STM32不同系列的RAM组织形式差别非常大这也是新手容易懵的地方。我从最常用的F1、F4、H7三个系列出发带你看清楚它们各自的RAM地图。2.1 不同系列的RAM地图差异F1系列最简单比如F103C8T6只有20KB SRAM统一挂在0x20000000起始的地址段。所有数据都在这一个连续区域里CPU和DMA都能访问没有额外的花头。F4系列开始出现分化。以F407为例内部RAM分为两部分主SRAM128KB地址0x20000000起CPU和DMA都能访问CCM RAMCore Coupled Memory64KB地址0x10000000起只有CPU内核能访问DMA和外设总线摸不到它另外还有4KB备份SRAM放在备份域里由VBAT引脚供电主电源断电后数据不丢。H7系列是最复杂的。以H743为例内部RAM资源非常丰富AXI SRAM512KB挂在0x24000000SRAM1/2/3等若干块总共还有256KB左右挂在0x30000000附近ITCM128KB直接连内核指令总线DTCM128KB直接连内核数据总线这么多块RAM各司其职有的带ITCM/DTCM接口速度极快但不经过Cache有的挂在AXI总线上所有主机都能访问有的只能CPU访问搞错了一块都得踩坑。2.2 CCM和TCM这类特殊RAM到底特殊在哪CCM和TCM本质上都是“紧耦合内存”指的是它们直接连到CPU内核的私有总线上不经过总线矩阵不经过AHB/APB外设总线也不经过Cache。好处是访问速度极快CPU访问它的时候不会和其他总线事务发生仲裁冲突实时性稳定。代价是访问路径受限。F4的CCM只有CPU能访问DMA控制器没法从CCM搬数据也没法往CCM写数据。很多新手写DMA采集把缓冲区用__attribute__定义到了CCM地址区域结果发现DMA压根不工作就是这个原因。H7的TCM同理ITCM通常用来放关键代码或中断服务函数DTCM用来放要求极高确定性访问的数据。但同样如果某个外设的DMA要访问这块数据缓冲区你就不能放在TCM里得放到AXI SRAM这类能全局访问的区域。实际项目里最常见的场景是音频或AD采集。比如用I2S录音DMA从I2S外设把数据搬到内存缓冲区缓冲区必须放在DMA可达的内存区域如果这块缓冲区同时又要被CPU高频处理那么对H7来说建议放在普通SRAM里然后配合MPU把该区域配置为write-through缓存模式既有速度又不会产生Cache一致性问题。如果你直接放DTCMDMA访问不了这缓冲区就废了。2.3 备份SRAM掉电也不丢的那一小块“RAM”F4和L4系列带备份SRAM最典型的就是F4的4KB备份SRAM。它由VBAT引脚供电当主电源VDD掉电时只要VBAT还接着电池或者超级电容里面的数据就不会丢。这个特性很实用。举个我自己的例子之前做一款带RTC时钟的设备需要在设备重启或掉电后知道上一次的校准参数、复位次数和运行模式。用Flash存储的话写入次数有限而且擦写麻烦放在备份SRAM里随时改写掉电不丢重启还在完全贴合需求。使用备份SRAM有几个步骤使能PWR时钟通过PWR_CR寄存器置DBP位使能备份域访问读写备份SRAM需要注意的是DBP位置位后备份域读写就放开了做完操作没必要一直开着的话可以清除防止程序跑飞时误改备份区。F1系列没有备份SRAM只有80字节的备份寄存器功能类似但容量小很多。3. 编译器与链接脚本眼里的RAM分区栈、堆、data和bss到底怎么分讲了硬件的RAM形态接下来该看软件视角了。这里涉及嵌入式开发最核心的一块内存分区。你写的C语言代码最终怎么落在RAM里是编译器、链接器、启动代码三者协作的结果。3.1 从启动文件到main函数RAM经历了什么Cortex-M内核芯片上电后硬件会自动从向量表起始地址读取栈顶地址存入SP再读取复位向量存入PC跳到Reset_Handler。Reset_Handler随后做几件事设置初始SP其实上电时硬件已经做了但有些启动代码会再覆盖一遍调用SystemInit配置时钟调用C库的初始化函数__main它完成把.data段从Flash拷贝到RAM把.bss段清零初始化堆和栈跳转执行main启动文件里有一段固定的宏定义定义栈和堆的大小Stack_Size EQU 0x400 AREA STACK, NOINIT, READWRITE, ALIGN3 Stack_Mem SPACE Stack_Size __initial_sp Heap_Size EQU 0x200 AREA HEAP, NOINIT, READWRITE, ALIGN3 __heap_base Heap_Mem SPACE Heap_Size不用小看你修改的这个Stack_Size它定义的是主栈大小。启动后SP指向栈顶C语言里所有局部变量、函数调用时压栈的参数、返回地址都从这块区域里出。链接脚本MDK里对应分散加载文件.sct里的IRAM1设置则决定了编译器认为的RAM起始地址和总大小LR_IROM1 0x08000000 0x00010000 { ER_IROM1 0x08000000 0x00010000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00005000 { .ANY (RW ZI) } }RW_IRAM1的0x20000000就是F103C8T6的SRAM起始地址0x00005000对应20KB。如果你把RAM地址或大小写错链接器可能照样编译通过但下到板子上程序直接跑飞因为栈和变量的实际位置跟芯片物理RAM地址对不上。3.2 C内存分区在STM32上的实际落地PC程序的内存分区概念在MCU上同样存在但地址和特性完全不同。我们用一张表说清楚段名存放位置内容掉电或复位后.textFlash代码、常量字符串、const变量保留.rodataFlash只读数据const变量、字符串保留.dataFlash保存初值运行在RAM已初始化且初值非0的全局/静态变量上电后从Flash拷贝到RAM.bssRAM未初始化或初值为0的全局/静态变量上电后清零.heapRAMmalloc/free动态分配区域不保证内容.stackRAM局部变量、函数调用栈不保证内容这里有一个嵌入式新人特别容易误解的点const变量到底占不占RAM答案是不占。const修饰的变量会被放到Flash的只读数据区MCU可以直接对Flash地址进行随机读取。不过要注意编译器在某些情况下可能仍会在RAM里创建const数组的副本比如你用memcpy把一个const数组复制到RAM再处理那RAM复制品当然占RAM。另外static修饰的const如果在函数内部且编译器无法静态确定只读性可能也有例外需要看编译器的具体行为。但绝大多数情况下const在MCU上就是纯Flash占用。再看MDK编译输出的经典四行数据Code52376 RO-data4200 RW-data1200 ZI-data15000Code代码占的FlashRO-data只读数据占的FlashRW-data初值非0的全局/静态变量既占Flash保存初值又占RAM运行时要被拷过去ZI-data零初始化变量只占RAMFlash总占用 Code RO-data RW-data RAM总占用 RW-data ZI-data很多人看编译输出只关心“还剩多少Flash”其实RWZI就是RAM的基准占用。一个F103C8T6有20KB RAM如果上面RWZI已经干到18KB留给栈和堆的空间就很少了运行起来稍不注意就溢出。3.3 用.map文件揪出占RAM的大户MDK编译后会在工程目录生成.map文件这是排查RAM占用的利器。打开后找到Image Symbol Table部分能看到每个模块、每个全局符号的地址和大小。拿我之前一个工程举例ZI-data占了大头我就去.map文件里搜一是看所有目标文件贡献的ZI大小.bss 0x20000b58 0x200 lwip.o .bss 0x20000d58 0x400 eth_buffer.o .bss 0x20001158 0x30 main.o这么一看哪个模块开了一堆大缓冲区、哪个模块定义了巨型数组一目了然。排查完再做针对性优化比瞎猜效率高得多。还有个功能容易被忽略“Maximum Stack Usage”。如果你的编译器工具链开启了栈使用量分析Keil AC5里在Options→Target→Compiler里勾选相关选项AC6有--stack_usage选项编译后的.map文件末尾会生成每个函数的栈最大占用估算以及调用路径的栈深度分析。这对计算任务栈大小非常有参考价值。4. 实测项目RAM不够用时先压缩、再外扩、后调优RAM不够是STM32项目里最经典的问题之一。我按优先级从高到低把实际项目中最常用的解决路径完整梳理一遍。4.1 先搞清楚自己还有多少RAM可用动手优化前先看基准数据。每次Rebuild后MDK有输出Program Size: Code52376 RO-data4200 RW-data1200 ZI-data15000RAM占用 1200 15000 16200字节约15.8KB。如果芯片是20KB RAM留给栈和堆的只有约3.8KB。这个数值相当激进大概率隐藏风险。再看Keil里的配置“Options for Target → Target”中的IRAM1地址范围和你芯片手册对一下。很多人从别处拷贝工程IRAM1可能被人改成过或者不从0x20000000开始导致编译器无法利用全部RAM。这个细节排查起来很隐蔽但非常重要。4.2 第一刀把能放Flash的数据全赶去Flash绝大多数项目的RAM浪费都出在“用RAM存了一堆不该存RAM的东西”。最典型的操作就是加const。把命令列表、状态机跳转表、字符串提示语、7段码表、数学常量这些只读数据全部加上const它们就会从ZI区域/DATA区域搬到RO区域。举个例子一个协议解析模块里的命令映射表// 优化前 typedef struct { char *name; uint8_t id; } cmd_map_t; cmd_map_t cmd_table[] { {read, 0x01}, {write, 0x02}, {erase, 0x03}, }; // 优化后 const char READ_CMD[] read; const char WRITE_CMD[] write; const char ERASE_CMD[] erase; typedef struct { const char *name; uint8_t id; } cmd_map_t; const cmd_map_t cmd_table[] { {READ_CMD, 0x01}, {WRITE_CMD, 0x02}, {ERASE_CMD, 0x03}, };改动只是加const但cmd_table和所有字符串立刻从RAM挪到Flash。一个中小型协议解析模块动辄能省下几百字节甚至几KB RAM收益非常直接。还有一类隐蔽的开销是“大数组初始化”。如果你定义一个大小为4KB的数组并带有初始化值uint8_t buffer[4096] {0x55};它不仅是ZI变量还可能是RW变量Flash里要存一份4KB的初值RAM里再放一份可读写的副本。对于大数组通常乖乖定义成不加初始化的全局变量或者显式用__attribute__((section(.bss)))把需求归到清零初始化即可。4.3 第二刀给大数组找个“外置仓库”内部RAM不够就需要外扩。STM32的FSMC/FMC接口可以外挂并行SRAM或SDRAM把这些芯片映射到MCU的地址空间里。外扩并行SRAM以F103ZET6搭配IS62WV51216512KB SRAM16位数据线为例硬件上把数据线D0-D15、地址线A0-A18、片选NE1、读写NOE/NWE接到SRAM芯片的对应引脚。软件上FSMC配置为NOR/PSRAM模式片选使能Bank1 NE1。初始化完成后这块SRAM自动映射到0x60000000地址开始的空间。如果你用MDK定义一个全局数组指向外部映射地址uint8_t ext_buffer[524288] __attribute__((at(0x60000000)));这个数组就落在外部SRAM里。编译后记入RAM的是0因为它的地址不在IRAM区域不占内部RAM。但注意__attribute__((at()))只是把变量定位到指定地址FSMC必须在访问之前完成初始化否则程序一碰这个地址就会HardFault。外扩SDRAMF4和H7的FMC接口支持SDRAM。SDRAM容量大、价格比并行SRAM便宜但使用复杂需要配置GPIO复用为FMC功能初始化FMC SDRAM控制器的时序参数列地址位数、行地址位数、CAS延迟、刷新周期上电后先发NOP命令、预充电、自动刷新、配置模式寄存器完成SDRAM初始化之后SDRAM映射地址如F429的0xC0000000区域才可正常读写SDRAM有一个必须注意的点它保留了DRAM需要刷新和初始化的特性。掉电数据丢失上电后必须等待初始化完成才能访问。这意味着你不能把栈或者启动早期就要用的变量放到SDRAM里。外拓RAM的速度代价外扩的RAM通过外部总线访问速度远不及内部SRAM而且会占用更多总线周期和功耗。对F407这类没有Cortex-M7 Cache的芯片外扩SRAM访问比内部SRAM慢好几倍是常态。到H7上因为带Cache还得考虑Cache一致性建议为外部RAM单独配置MPU把区域设置为write-back或write-through策略否则很容易出现“SDRAM里数据写了但读回来不对”的诡异问题。4.4 第三刀把软件设计往“内存友好”方向收敛压缩完数据、扩完容其实还有大量内存浪费在软件结构上。拿RTOS任务栈来说很多工程师的习惯是先给个512字节或1024字节的栈运行稳定后就不管了。但每个任务的栈静态占用RAM任务一多累计很可观。用FreeRTOS的uxTaskGetStackHighWaterMark接口可以实时查看每个任务历史最低剩余栈空间拿这个数据砍掉20%-30%的冗余一款8任务系统轻松省出几KB RAM。还有一类很典型全局缓冲区反复覆盖使用。比如同一个通信项目接收缓冲区、解析缓冲区、发送缓冲区各自开一个4KB总计12KB。但如果数据流是流水线的可以共用同一个4KB缓冲解析完就清理下一包数据继续用。前提是明确谁在什么时间点持有缓冲区做好访问互斥。对于malloc这类的动态内存我个人的看法是在STM32这样的小RAM芯片上能不用就不用。动态分配会带来碎片化问题分配次数多了之后可能一个大对象都分配不出来而且malloc的执行时间不确定对实时性要求高的任务不友好。如果确实需要建议用FreeRTOS的heap方案它内部做了内存池管理比C标准库的malloc可预测性强得多。5. MDK中查看内部RAM值与那些把RAM搞挂的坏习惯最后一个重要主题如何在实际开发中查看RAM值以及最常见的RAM翻车现场。5.1 调试状态下如何看内部RAM和变量如果你听到“Keil查看内部RAM值”这个问题它可能指的是两件事一是编译时看RAM剩余多大二是运行时看某块地址的数据。前者前面已经讲了看Program Size这里说后者。进入MDK调试界面后菜单栏点击“View → Memory Windows → Memory 1”在Address输入框里输入0x20000000回车就能看到内部SRAM起始地址往后的所有原始数据。你随便定义一个变量uint8_t test_buffer[64] {0};在Memory窗口地址栏输入test_buffer就能实时观察这个数组每个字节的当前值。调试器暂停时窗口数据会刷新。如果你想知道某个变量的值和它的地址用Watch窗口右键添加变量名可以看到它的类型、地址、当前值。程序跑飞后变量的值已经一团乱麻可以在Memory窗口里观察其地址附近的字节被写成了什么模式判断是哪块代码污染了它。更高级的用法是设置数据访问断点。在MDK的Breakpoint窗口里输入表达式和访问类型比如test_buffer[0], AccessWrite当程序执行写操作改动这个地址时调试器立刻停在触发指令处。这一招追查“某个变量被神秘修改”极其好用之前我把一个索引变量改错导致数组越界写坏下一个变量用普通方式根本找不到上了数据访问断点之后一条指令就定位了。5.2 几个把RAM搞翻车的典型操作栈设置过大或过小启动文件里栈设大了吧编译阶段会直接报错因为RAM空间不够这还算好的。栈设小了编译器不报错程序跑着跑着栈溢出栈区往下生长覆盖了全局变量区出现“变量值随机被改”“函数返回之后跳飞到HardFault”之类的诡异现象。查这种问题需要对栈水位线做检测后面我会给一个非常实用的方法。函数内部定义超大的局部数组默认栈大小只有1KB或者2KB你在函数里写一个void process_data(void) { uint8_t local_buf[2048]; // ... }这个数组就要占2KB栈空间直接击穿协调好的栈/堆分配。哪怕编译没报错一执行到这个函数栈指针就冲到比自己栈区更低的地址把其他变量全踩烂了。正确做法是大数组定义为全局变量或static变量并且做好互斥能复用就复用。DMA缓冲区放到了DMA访问不到的内存F4的CCM RAM0x10000000区域是CPU私有的DMA访问不了。如果你在CCM里定义一个数组来做ADC DMA的接收缓冲区DMA永远不会往这个地址搬数据或者传输配置时直接报参数错误。同样是定义缓冲区放0x20000000的主SRAM就没问题。H7上类似TCM区域不能给DMA用。看芯片内存地图时一定注意哪些区域标注“not accessible by DMA”或“AHB/APB not accessible”配置大缓冲区前先过一遍内存可达性。外扩RAM地址没初始化就被访问用了__attribute__((at(0x60000000)))但你在main函数之前或者FSMC配置之前就去写这个地址程序直接HardFault。任何外扩地址必须先初始化对应的内存控制器再开始读写。5.3 栈水位线检测法我用了无数次的土办法这个办法没有高级调试器的栈覆盖检测也能用。原理是给栈区填充一个特殊值程序跑完目标功能后从栈顶往下扫描看从哪个地址开始特殊值被覆盖了就能估算历史最大栈用量。做法不复杂在启动代码栈区初始化时填充0xA5// 伪代码假设__initial_sp之外预留了足够RAM void stack_watermark_init(void) { extern uint32_t __initial_sp; extern uint32_t __stack_limit; uint8_t *start (uint8_t *)__stack_limit; uint8_t *end (uint8_t *)__initial_sp; for (uint8_t *p start; p end; p) { *p 0xA5; } }程序运行到你想评估的时刻比如跑完一轮完整业务、各中断都触发过再统计一下0xA5残留的边界uint32_t stack_max_usage(void) { uint8_t *start (uint8_t *)__stack_limit; uint8_t *end (uint8_t *)__initial_sp; uint32_t used 0; for (uint8_t *p start; p end; p) { if (*p ! 0xA5) { used (uint32_t)(end - p); break; } } return used; }调用stack_max_usage()得到的数值就是历史最大栈深度。如果这个值已经逼近或超过栈大小说明要扩容或者优化局部大数组。多跑几个不同的功能场景取最大值再留出30%以上的余量栈大小设置就相当稳了。提示调用这个函数本身也会消耗栈空间所以测量结果会比实际偏大一点点但这点偏大在实际工程判断中属于“误差可接受”不用纠结。我个人在实际项目里一直习惯把RAM相关的检查前置到开发流程里选型时先估算RAM需求写代码时保持对const关键字的敏感建网调试时先用.map文件过一遍RAM分布最后用栈水位线法验证栈大小。这套流程跑下来真正因为RAM问题翻车的概率低了很多。如果你现在手头正有一个编译通过但跑起来疯了的工程别急着查逻辑先打开.map文件看看RAM分布再给栈填个0xA5跑一遍。很多时候问题根本不在算法和逻辑而是内存这块地基已经塌了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价