相信不少朋友在用 Keil 编译工程时,都见过这个“红色炸弹”:
Error: L6220E: Execution region RW_IRAM1 size (32768 bytes) exceeds limit (32768 bytes).
Region contains 33024 bytes of data (256 bytes of padding) and code.
“我的 STM32 明明有 512KB 的 Flash,代码才写了几百行,怎么就内存溢出了?”
如果你也有过这样的困惑,那说明你踩到了一个非常普遍的“坑”——分不清存储空间和运行空间的区别。
很多同学一看到 512KB,就以为自己有 512KB 可以随便用。但实际上,Flash 是存代码的“硬盘”,而 SRAM 才是程序跑起来时真正干活的“内存条”。这两块地方,大小通常相差很远。
这篇文章将带你彻底理清 Flash 和 SRAM 的关系,并教你如何通过分析 MAP 文件,像“查户口”一样,把每一个字节的用途都管得明明白白。
概念对齐:理解 Keil 的内存划分术语
打开 Keil 编译完成后的输出窗口,你会看到类似这样的信息:
Program Size: Code=12340 RO-data=1024 RW-data=256 ZI-data=8192
这里的 RO、RW、ZI 是什么意思?我们来一一对应。
RO(Read Only)- 只读区
RO 又细分为两部分:
- RO-Code:就是你写的函数代码,编译后变成了机器指令。对应 GCC 里的
.text 段。
- RO-Data:只读数据,比如你定义的
const char* str = "Hello World",这个字符串就存在这里。
RW(Read Write)- 可读写区
- RW-Data:已经初始化的全局变量和静态变量。比如你写了
int g_count = 100;,这个 100 就是初值,需要存起来。对应 GCC 的 .data 段。
ZI(Zero Initialized)- 零初始化区
- ZI-Data:未初始化或者初始化为 0 的全局/静态变量。比如
int g_buffer[1000]; 或者 static int s_val = 0;。对应 GCC 的 .bss 段。
为什么叫“零初始化”? 因为 C 语言标准规定,未初始化的全局变量默认值是 0。
一张表帮你记住

核心揭秘:Flash 和 SRAM 的“分家”与“搬家”
这一节是全文的重点,搞懂了这里,开头的报错就能迎刃而解。
静态视图:烧录进芯片时,谁住在哪?
当你点击“Download”把程序烧进芯片后,Flash 里到底存了啥?
Flash 占用量 = RO(Code + Data)+ RW
等等,为什么 RW 变量也要在 Flash 里占地方?
原因很简单:RW 变量是有初值的!比如你写了 int g_count = 100;,这个 100 掉电之后不能丢,所以必须存在非易失的 Flash 里面。
那 ZI 呢?ZI 在 Flash 里不占空间! 因为它们反正都是 0,没必要存一堆 0 进去浪费空间,启动代码只需要知道“这块区域有多大”就行了。理解内存的静态和动态分配,是计算机科学和操作系统课程中的基础,它直接决定了程序的运行效率。
动态视图:程序跑起来时,发生了什么?
芯片上电后,程序并不是直接从 Flash 里“原地”运行变量的。在 main() 函数执行之前,有一段启动代码(__main)会做两件关键的事:
- 把 RW 数据从 Flash “拷贝”到 SRAM
- 把 SRAM 中的 ZI 区域“清零”
所以,SRAM 占用量 = RW + ZI + Heap + Stack
这就解释了开头那个报错:你的 Flash 可能还剩很多,但 SRAM 早就被全局变量和数组撑爆了!
图解:数据的“搬家”过程

关键点:RW 数据“既占 Flash 又占 RAM”,因为 Flash 存初值,RAM 存运行时的值。
隐形杀手:Heap(堆)与 Stack(栈)
除了全局变量,SRAM 里还住着两位“隐形杀手”——堆和栈。它们不在编译信息里直接显示,但一旦出问题,往往就直接导致 HardFault。
Stack(栈)—— 程序的“临时工作台”
用途: 局部变量、函数参数、函数调用时的返回地址和寄存器现场。
特点: 自动管理,向低地址生长(从高往低“长”)。
爆栈危机: 这是最难查的 Bug 之一!看下面这段代码:
void process_data(void) {
char buffer[4096]; // 4KB 的局部数组,直接分配在栈上!
// ...处理数据
}
如果你的栈空间只配置了 1KB,这一行代码执行的瞬间,栈就被击穿了。程序可能跑飞、死机,或者莫名其妙地改掉其他变量的值。
Heap(堆)—— 动态内存的“自留地”
用途: malloc() 和 free() 动态申请的内存。
特点: 手动管理,向高地址生长(从低往高“长”)。
内存泄漏风险: 嵌入式里要慎用 malloc!原因有两个:
- 内存碎片化:反复申请释放后,可能有空间却申请不出来。
- 忘记
free:单片机不像电脑,没有操作系统帮你自动回收垃圾。
图解:堆和栈的“对撞”风险

在 Keil 中如何设置堆栈大小?
打开你工程里的启动文件 startup_stm32xxxx.s,找到这两行:
Stack_Size EQU 0x00000400 ; 1KB 栈空间
Heap_Size EQU 0x00000200 ; 512B 堆空间
根据你的实际需求调整这两个值。如果你用了递归、大数组局部变量,记得把栈调大一些。
实战工具:手把手教你看 .MAP 文件
光说不练假把式。当你遇到内存问题时,MAP 文件就是你的“X 光片”,能让你看清每个字节都花在了哪里。
如何生成 MAP 文件?
在 Keil 中:Options for Target → Listing → 勾选 Linker Listing
编译完成后,在工程的 Listings 文件夹里就能找到 .map 文件。
MAP 文件的三个关键区域
① Image Symbol Table(符号表)
这里列出了每个全局变量、函数的具体地址:
Global Symbols
Symbol Name Value Ov Type
g_uart_buffer 0x20000000 Data 512
g_sensor_data 0x20000200 Data 1024
main 0x08000128 Thumb Code
当你怀疑某个变量地址被异常改写时,可以来这里查它的“户口”。
② Memory Map of the Image(内存映射)
宏观查看整个 Flash 和 RAM 的使用分布:
Execution Region ER_IROM1 (Base: 0x08000000, Size: 0x0000A000)
Execution Region RW_IRAM1 (Base: 0x20000000, Size: 0x00005000)
③ Image Component Sizes(重点!)
这是定位问题的利器,它告诉你每个 .c 文件贡献了多少内存:
Code RO Data RW Data ZI Data File
1024 64 16 4096 main.o
512 32 8 20480 gui.o ← 罪魁祸首!
256 16 4 512 uart.o
实战案例: “编译报 RAM 超限,打开 MAP 文件一看,好家伙,gui.c 里定义了一个 20KB 的全局帧缓冲数组!”
找到问题后,要么优化数据结构,要么考虑使用外部存储。在C/C++编程中,合理规划全局变量和局部变量的使用,是避免这类问题的关键。
总结与避坑指南
内存公式速记

避坑顺口溜

优化小技巧
- 能用
const 就用 const:常量存放在 Flash 中,不占用宝贵的 RAM。
- 大数组定义成全局变量:尽量避免在函数内部定义大型局部数组,以防栈溢出。
- 谨慎使用
malloc:在资源受限的嵌入式环境中,优先考虑使用静态内存分配。
- 定期检查 MAP 文件:养成编译后查看内存分布的好习惯,防患于未然。
内存管理是嵌入式开发的基本功。刚入门时可能觉得这些概念有些绕,但一旦理解了 Flash 和 SRAM 的分工、掌握了 MAP 文件的分析方法,很多看似“玄学”的问题就能迎刃而解。
你在项目里遇到过莫名其妙的 HardFault 或者内存溢出吗? 不妨打开 MAP 文件查一查,说不定罪魁祸首就藏在某个不起眼的大数组里。
想了解更多嵌入式开发中的实战技巧与深度解析,欢迎到云栈社区与其他开发者一起交流探讨。