引言

        在 Keil 开发 STM32 时,编译完成后会生成一个 .map 文件,文件末尾列出了 Total RO Size、Total RW Size、Total ROM Size 这几行数据。

        这篇文章以 STM32F103C8T6 为例,把自己梳理的结果记录下来,主要搞清楚以下几个问题:

        1.怎么看 .map 文件末尾的 Flash 和 RAM 占用?

        2.这些数字分别代表什么?

        3.RAM 里的 .data、.bss、堆、栈是怎么划分的?

第一部分:Flash 和 RAM 的分工

存储介质

存放内容

是否掉电保存

Flash

程序代码(.text)、常量(.rodata)、已初始化变量(.data 的初值)

是

RAM

全局变量(.data / .bss)、堆、栈

否

第二部分:如何计算程序占用的 Flash 和 RAM

        以 STM32F103C8T6(20KB RAM,64KB Flash)为例,计算程序占用的 Flash 和 RAM。程序编译完成后,查看工程中 Listings 文件夹内的 .map 文件,滚动到末尾即可看到汇总数据,例如:

Total RO  Size (Code + RO Data)                 7572 (   7.39kB)
Total RW  Size (RW Data + ZI Data)              4664 (   4.55kB)
Total ROM Size (Code + RO Data + RW Data)       7612 (   7.43kB)

项目

公式

说明

RAM 占用

RW Data + ZI Data

程序运行时占用 RAM 的总量

Flash 占用

Code + RO Data + RW Data

烧录时占用 Flash 的总量(包括代码和初始值的备份)

        上面的数据中涉及几个缩写,具体含义如下:

        1.Code(代码段):程序编译后生成的指令码,也就是 CPU 要执行的机器指令。包括写的 C 代码、调用的库函数等。存在 Flash 中,只读。

        2.RO Data(只读数据段):程序中使用到的常量数据,例如字符串常量、const 修饰的变量、中断向量表等。存在 Flash 中,只读。

        3.RW Data(读写数据段):已初始化为非零值的全局变量和静态变量(如 int a = 5;)。这部分数据比较特殊:初始值备份存在 Flash 中,变量本身在程序启动时被拷贝到 RAM 中运行。所以在 Flash 和 RAM 的统计中,它都会出现。

        4.ZI Data(零初始化数据段):未初始化或初始化为零的全局变量和静态变量。例如 int b;、int c = 0;、结构体 {0} 等。它只存在于 RAM 中,程序启动时由启动代码清零,不需要在 Flash 中备份初值。

        为什么 RW Data 在 Flash 占用和 RAM 占用中都会出现?

        以 int a = 5; 为例:

        这个 5 在烧录程序时,作为初始值备份存放在 Flash 里。所以计算 Flash 占用时,必须加上这部分空间。

        程序启动时,启动代码会把 Flash 里备份的 5 拷贝到 RAM 中 a 所在的地址。所以计算 RAM 占用时,也包含 a 这个变量本身的大小。

第三部分:RAM 内部的四大区域

        RAM 内部被划分成四个逻辑区域,每个区域存放不同类型的数据。

区域

存放内容

生命周期

举例

.data 段

已初始化为非零值的全局/静态变量

整个程序运行期间

int a = 5;

.bss 段

未初始化或初始化为零的全局/静态变量

整个程序运行期间

int b; 或 int c = 0;

堆(Heap)

动态分配的内存(malloc)

手动 free 之前

malloc(100)

栈(Stack)

局部变量、函数返回地址、中断现场

函数调用期间

uint8_t buf[100];

        注意:= {0} 会被编译器归入 .bss 段,因为它不需要在 Flash 中备份初值。

第四部分:栈和堆的大小如何调整?

        栈大小 决定一次函数调用最多能容纳多少局部变量。如果局部变量过大或函数嵌套过深,栈空间不足会导致栈溢出,程序跳转到 HardFault。

        堆大小 决定 malloc 最多能分配多少内存。如果堆空间不足,malloc 返回 NULL,程序访问空指针会死机。栈和堆的大小由启动文件(startup_stm32f10x_md.s)中的宏定义控制:

Stack_Size   EQU  0x00000400   ; 1KB
Heap_Size    EQU  0x00000200   ; 512B

        实际场景举例: 当程序中使用了 cJSON 库解析 JSON 数据时,cJSON 内部会通过 malloc 动态分配内存来构建解析树。如果接收到的 JSON 数据较长,解析树占用的堆空间就会相应增大。此时如果 Heap_Size 仍然保持默认值(如 512B),堆空间可能被耗尽,导致 malloc 返回 NULL,程序访问空指针从而触发 HardFault 死机。因此,在使用 cJSON 等动态内存分配较多的库时,需要根据实际数据量适当增大 Heap_Size。

第五部分:基于.map画出 RAM 分布图

        FSC8T6 的 RAM 大小是 20KB,地址范围为 0x20000000 ~ 0x20004FFF。 基于 .map 文件的实际占用数据,可以画出精确的分布图:

RAM 低地址 (0x20000000)
    ↓
┌─────────────────────────────────────────────────────────────────────┐
│ **.data 段**           0x20000000 ~ 0x20000027  (40 字节)         │
│  ├─ stm32f10x_rcc.o    0x20000000 ~ 0x20000013  (20 字节)         │
│  │  └─ APBAHBPrescTable (16B), ADCPrescTable (4B)                │
│  ├─ fs800.o            0x20000014 ~ 0x20000018  (5 字节)          │
│  │  └─ time_count (2B), timer_running_flag (2B), current_state (1B)│
│  ├─ PAD                0x20000019 ~ 0x2000001B  (3 字节)          │
│  ├─ stdout.o           0x2000001C ~ 0x2000001F  (4 字节)          │
│  │  └─ __stdout                                                    │
│  ├─ mvars.o            0x20000020 ~ 0x20000023  (4 字节)          │
│  │  └─ __microlib_freelist                                        │
│  └─ mvars.o            0x20000024 ~ 0x20000027  (4 字节)          │
│      └─ __microlib_freelist_initialised                           │
├─────────────────────────────────────────────────────────────────────┤
│ **.bss 段**            0x20000028 ~ 0x20000C33  (3084 字节)       │
│  ├─ serial1_rx         0x20000028 ~ 0x2000042D  (1030 字节)       │
│  ├─ serial2_rx         0x2000042E ~ 0x20000833  (1030 字节)       │
│  └─ String             0x20000834 ~ 0x20000C33  (1024 字节)       │
├─────────────────────────────────────────────────────────────────────┤
│ **PAD**                0x20000C34 ~ 0x20000C37  (4 字节)          │  ← 地址对齐填充
├─────────────────────────────────────────────────────────────────────┤
│ **HEAP (堆)**          0x20000C38 ~ 0x20000E37  (512 字节)        │
│  起始: 0x20000C38  结束: 0x20000E37                              │
│  __heap_base = 0x20000C38                                        │
│  __heap_limit = 0x20000E38(边界值,指向堆结束后的第一个字节)   │
├─────────────────────────────────────────────────────────────────────┤  ← 堆结束 / 栈起始(紧挨着)
│ **STACK (栈)**          0x20000E38 ~ 0x20001237  (1024 字节)      │
│  起始: 0x20000E38  结束: 0x20001237                              │
│  __initial_sp = 0x20001238(栈顶边界值)                         │
│  向下增长                                                        │
├─────────────────────────────────────────────────────────────────────┤
│ **空闲 RAM**            0x20001238 ~ 0x20004FFF  (~15.8 KB)       │
└─────────────────────────────────────────────────────────────────────┘
RAM 高地址 (0x20004FFF)

第六部分:变量存储规则速查表

代码写法

存储段

是否占 Flash 备份空间

int a = 5;

.data

是(初始值需要备份)

int b;

.bss

否(只占 RAM)

int c = 0;

.bss

否(全零初始化,不需要备份)

static char buf[100];

.bss

否(只占 RAM)

局部变量 uint8_t arr[100];

栈

否(函数返回后自动释放)

第七部分:总结

        1. 两个核心公式

        Flash 占用大小 = Code + RO Data + RW Data(烧录时占用的总空间)

        RAM 占用大小 = RW Data + ZI Data(运行时占用的总空间)

        2. 三种数据段的划分

        .data:非零初始化的全局/静态变量(Flash 和 RAM 各占一份)

        .bss(即 ZI Data):未初始化 / 零初始化的全局/静态变量(只在 RAM 中)

        堆 + 栈:运行时动态使用的内存(由启动文件中的宏定义控制大小)

        3. 内存管理思路

        Flash 只要总量不超就行:具体内部怎么排布由编译器处理。

        RAM 需要重点规划:.data 和 .bss 是静态占用的,堆和栈是动态使用的。如果堆或栈不够,编译不报错,但运行时会死机。

        4. 关于 .map 文件

        需要查看更详细的内存分布(如具体变量的地址、各段在 Flash 中的位置),可以直接打开 .map 文件,搜索 Memory Map of the image 部分。里面的 Execution Region ER_IROM1 对应 Flash 分布,Execution Region RW_IRAM1 对应 RAM 分布。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐