单片机的内存无非就两种,内部FLASH和SRAM,最多再加上一个外部的FLASH拓展。在这里我以STM32F103C8T6为例子讲解FLASH和SRAM。
STM32F103C8T6具有64KB的闪存和20KB的SRAM。

一. Flash
1.1 定义
非易失性存储器,即使在断电后,其所存储的数据也不会丢失。它可以进行多次擦除和写入操作,但擦除和写入的速度相对较慢。
对于flash而言,其内的数据只能读,不能写。
1.2 flash存储的数据
对于flash而言,其内存储的数据有以下几种:
1.用户代码
2.中断向量表
3.全局变量:
已被初始化和未被初始化的全局变量。
4.常量:
这里的常量是只可以读,不可以被修改的常量,被const修饰的变量。
1.3 STM32的flash地址
stm32f103c8t6的flash地址为:0x08000000 ~ 0x0801 FFFF
二. SRAM
2.1 定义
是一种易失性存储器,只有在通电的情况下才能保持数据,断电数据丢失。它的读写速度非常快,能够快速地响应 CPU 的访问请求。
对于SRAM而言,其内的数据可读可写。
2.2 SRAM存储的数据
1.局部变量:
2.静态变量:
被关键字static修饰的变量/常量。
在这里需要注意一点如果 static变量被初始化为常量(如 static int x = 5;
),初始化值会被编译到 Flash 中,但变量本身(运行时的存储空间)仍在 SRAM 中。
运行时 :程序启动时,初始化值会从 Flash 复制到 SRAM 的静态数据区,之后变量的修改都发生在 SRAM 中。
3.堆Heap :动态分配的内存。
注意:堆的内存分配有用户自己实现。主要通过下面几个函数进行开辟和释放
malloc
/free
、new
/delete
在学习freertos的时候都应该知道freertos有5个heap管理的算法,有兴趣的可以查看freertos的源码查看一下其不同内存管理算法的区别。
4.栈Stack :
存放局部变量、函数参数、返回地址等,由编译器自动管理,遵循 "后进先出" 原则。
栈的空间在调用期间自动创建和释放。
我们可以打开stm32的启动文件来查看初始系统堆和栈的大小。这个大小可以自己修改,但是一定不能超出SRAM的大小。

5.寄存器reg
2.3 STM32的SRAM地址
stm32f103c8t6的SRAM地址为:0x20000000 ~ 0x20004FFF
三.存储器映像图分析

初始看见这个图,大家可能会很懵,但是一点点拆开来看,就很清晰了。
3.1 Flash memory

在表上可以明显看出其地址范围是0x0800 0000 ~ 0x0801 FFFF
3.2 SRAM

在图中可以看出SRAM的起始地址是0x2000 0000,但是图中并未标明结束地址,这是因为这个图是F103系列的存储器映像图,所以不同的型号其SRAM的结束地址不同
这个地址大家可以自己计算,计算公式为:起始地址 + 容量字节数 - 1
STM32F103C8T6 的结束地址:
0x20000000+(20×1024)−1=0x20004FFF
3.3 Peripherals(外设)


在图中可以看到,外设的地址是从 0x4000 0000开始的,TIM,SPI,I2C等外设均在此地址范围内。
在此大家可能会有疑问,外设地址是不是就是寄存器地址呢?
其实不然,外设地址是寄存器的基地址。寄存器的地址等于外设基地址+偏移量。
打开stm32的xb.h文件,里面存放了关于每个外设的地址信息,我们可以很明显的看出,每一个外设的地址都是外设基地址+偏移量。

在查看STM32芯片手册的时候,也可以看到在配置每一个外设寄存器的时候,都会显示一个地址偏移量

3.4 Cortex - M3 Internal Peripherals

Cortex - M3 内核内部外设地址:0xE000 0000 ~ 0xE010 0000
包含内核调试组件、中断控制器(NVIC)等。
3.5 APB memory space(APB 总线外设内存空间)

APB 总线上的外设,涵盖 CRC、Flash Interface、RCC、DMA 等。
3.6 特殊区域

Option Bytes :位于 0x1FFFF 800
附近,用于配置芯片的一些特殊功能,如写保护、读保护、BOR级别等。
System memory :在 0x1FFFF 000
附近,用于系统自举(Bootloader)等功能。芯片启动时可从这里加载代码执行,常用于实现 ISP功能。
四.代码段分析

在STM32中,代码被存在flash和sram中,但其具体的存储位置还不明了。对于flash和sram,其中对不同的数据都有不同的存储位置,我上面给出了一个大概的逻辑图,接下来就对这些代码段进行具体分析。
在Cortex-M3权威指南中也有定义说明

4.1 .text段
也叫代码段,存储可执行的程序代码,包含函数体中的指令 。
比如定义的void main(void)
函数以及其他我们自己定义的函数代码都存放在这里。
中断向量表一般也存放在此段,位于 Flash 起始地址附近,用于中断发生时引导 CPU 找到对应的中断处理函数 。
在STM32的启动文件中,也能看到部分代码段

AREA
定义了名为.text
的区域,属性为代码段(CODE
)、只读(READONLY
) 。Reset_Handler
是复位处理函数,先调用SystemInit
进行系统初始化(如时钟配置等 ),再跳转到__main
(C 库函数,最终会调用main
函数 ) 。
在Cortex-M3权威指南中,也可以看到对.text段的定义和解释。

4.2 .rodata 段
只读数据段 ,存放只读的常量数据,以及一些字面量 。
在上文中,我们提到过,被const修饰的常量就是存放在此段的。
4.3 .bss 段
未初始化数据段。用于存储未初始化的全局变量和静态变量。
比如int globalVar;
(未初始化的全局变量)、static int staticVar;
(未初始化的静态变量)
在程序启动时,该段内存会被自动清零 ,这部分变量在运行时可被程序读写 。
4.2 .data段
已初始化数据段,存放已初始化的全局变量和静态变量。
例如int initializedGlobal = 5;
(已初始化的全局变量)、static int initializedStatic = 3;
(已初始化的静态变量)
在上面将SRAM中我们提到过,这些变量的初始值在编译时确定,程序启动时,其初始值从 Flash 复制到 SRAM 中,在程序运行过程中可被读写。
4.4 .Stack段
栈段 ,用于存储函数调用时的局部变量、函数参数、返回地址等
每当函数被调用,其局部变量等信息被压入栈中,函数执行结束后弹出 。
比如在一个函数中定义的int localVar = 1,
该变量就存储在栈中 。
若函数调用层级过深或局部变量占用空间过大,可能导致栈溢出 。此时需要在启动文件处修改栈的大小或者修改代码来避免溢出。
4.5 .heap段
堆段 。用于动态内存分配的区域。
当程序需要创建动态大小的数据结构(如链表节点、动态数组)或处理不确定大小的数据(如网络接收的可变长数据)时,可从堆中分配内存 。使用完后需手动释放,否则会造成内存泄漏 。
4.6 .reg
寄存器段。主要用于上电初始化配置外设模块的信息,此时需要操作寄存器配置一些参数。
五. .map文件分析(十分重要)
在编译代码结束的时候,我们都能看见这样一段

单片机是怎么知道我们的内存占用情况呢?我们应该如何分析每一个函数的内存占用空间呢?这个时候.map文件的作用就至关重要了。
5.1 什么是.map文件
.map文件是由连接器生成的列表文件,里详细的展示了每一段代码的占用情况。对分析和管理内存十分重要。
5.1.1符号与地址映射
记录程序中函数、全局变量等全局符号对应的起始地址 。通过它能知晓每个函数和变量在内存中的位置,便于调试和分析程序,比如可查找崩溃地址并定位到出错代码行 。
5.1.2内存使用情况
呈现程序各部分(如代码段、数据段等)在内存中的分布及占用空间大小 可查看 Flash 和 RAM 的占用情况 。
5.1.3段映射信息
明确程序中各个段(如.text、.data、.bss 等)实际映射的起始地址与长度 。与链接脚本(如.ld 文件 )中的段配置相关联,展示链接过程中段的具体布局 。
如果没有.map文件,点击下面的配置,再重现编译代码,就能看见了。

下面这个也建议勾选上,它可以删除冗余代码。

5.2 Component头部信息
这个时候我们打开.map文件分析,大家可以打开自己工程的.map文件查看。这里我以我的工程为例子讲解。
我工程里的.map文件头部信息如下:

显示了编译器的版本和编译工具。
.map 文件的组成部分因编译器和开发环境不同而略有差异。
有的头文件可能会出现如硬件架构(如 Cortex-M3、ARMv7 等),工程名称、输出文件路径等基本信息。
5.3 Section Cross References交叉引用表

可以很明显的看出,交叉引用表的格式是十分统一的:
每一行的格式为:源目标文件(源段) refers to 目标目标文件(目标段) for 符号名
通俗一点将就是在某某函数李调用了某某函数。
以紫色部分为例:在main函数里调用了HAL_Init();SystemClock_Config();MX_GPIO_Init()等函数。
打开我们的工程main.c函数可以看见,确实如此。

蓝色框里面的就是启动信息了,这个在启动文件里可以看见,就不过多赘述了。
(.o文件是链接器生成的目标文件。(i.main)代表的是main函数的基地址)
然后我们接着往下看。
5.4 Removing Unused input sections from the image

从生成的镜像文件中移除未使用的输入段。
简单来讲就是删除没用到的代码,节省空间。
来看红色框部分,移除了.rrx_text代码段的gpio.o文件,大小为6个字节。
在该段的最后会有这样一段

这句话的意思是:从镜像文件中总共移除了 677 个未使用段,总计 66617 字节。
5.5 Image Symbol Table 镜像符号表

Local Symbols:局部符号。
Symbol Name:符号名称,这里显示的是源文件路径,对应目标文件中定义的符号来源 。
Value :符号对应的值,此处都是0x00000000
,在不同场景下可能表示符号的地址等信息 。
Ov Type :符号的类型,这里均为Number
,表示这些符号是数值类型相关 。
Size:符号的大小,此处都是 0 。
Object(Section) :符号所在的目标文件及段,ABSOLUTE
表示符号具有绝对地址 。
那么再看紫色横线的部分,意思就清晰可见了。这里就不过多赘述了。
再看下面这个图

Global Symbols:全局符号
5.5.1 局部符号和全局符号的区别
他们的区别主要有以下几点
1.作用域
- 局部符号 :作用域局限于定义它的特定模块、函数或代码块内部 。比如 C 语言中函数内部定义的局部变量、局部静态变量 ,以及函数内部声明的局部函数(如使用
static
修饰的内部函数 ),仅在该函数内可见和可访问 。像在一个main
函数中定义的int localVar = 10;
,在main
函数外部无法访问localVar
。 - 全局符号 :作用域为整个程序 。在程序的任何位置,只要满足访问权限等要求,都能对其进行访问 。例如 C 语言中在所有函数外部定义的全局变量、全局函数 ,如
int globalVar = 5;
,在其他源文件中通过extern
声明后也可访问 。
2.可见性
- 局部符号 :仅在定义它的代码区域内可见 。当程序执行流程离开该区域(如函数执行完毕返回 ),局部符号就不再可见 。比如函数中定义的临时变量,函数执行结束后其生命周期结束,无法再被访问 。
- 全局符号 :在整个程序范围内可见 。只要程序在运行,全局符号始终存在且可被访问(需符合访问控制规则 ) 。
3.存储位置
- 局部符号 :一般存储在栈(stack)中,像函数的局部变量,随着函数调用入栈,函数结束出栈 。局部静态变量存储在静态存储区 。
- 全局符号 :全局变量通常存储在静态存储区 。未初始化的全局变量存放在
.bss
段,已初始化的全局变量存放在.data
段 。全局函数的代码存放在代码段(.text
) 。
4.生命周期
- 局部符号 :自动局部变量生命周期随函数调用开始,函数返回结束 。局部静态变量在程序启动时初始化,程序结束时销毁 。
- 全局符号 :全局变量和函数在程序启动时创建并初始化,程序运行期间一直存在,直到程序结束才销毁 。
那么它有什么作用呢?
5.5.2 镜像符号表的作用
5.5.2.1 调试定位
查找符号地址:在调试程序时,如果遇到程序崩溃或异常,通过错误提示中的地址信息,可在符号表中查找对应的符号名称 。比如知道一个错误地址,可在 "Symbol Name" 列找到该地址对应的源文件及符号,进而定位到具体代码位置,快速排查问题 。
函数和变量追踪:对于大型项目,函数和变量众多。符号表能帮助了解每个源文件中定义的符号情况,追踪函数调用关系和变量使用位置 。例如,想知道某个函数在哪些源文件中被引用,可借助符号表梳理 。
5.5.2.2 程序分析与优化
代码结构理解:通过查看符号表中不同源文件对应的符号,能清晰了解项目的代码结构 。比如哪些源文件属于底层驱动(如stm32f1xx_hal_xxx.c
相关 ),哪些是应用层代码(如main.c
等 ),有助于新开发者快速熟悉项目架构 。
在我们实际开发中写代码也应该分层编写,利于梳理。
未使用符号检测:可以发现一些未被使用的符号 。如果某个源文件对应的符号在整个项目中都没有被引用,可能是冗余代码,可考虑清理以优化代码体积和提高编译效率 。
5.5.2.3 链接与编译检查
符号冲突排查:在多文件编译链接过程中,可能出现符号重名冲突 。符号表能展示所有符号信息,方便开发者检查是否存在同名符号,避免因符号冲突导致的编译或运行错误 。
链接正确性验证:符号表中的信息与链接过程紧密相关 。它能反映链接器是否正确解析和处理了各个目标文件中的符号,验证链接的正确性 。如果符号表中符号信息异常,可能意味着链接过程出现问题 。
5.6 Memory Map of the image存储器映射

先看第一句:Image Entry point :表示程序镜像的入口点地址,这里是0x080000ed
,即程序开始执行时的起始地址 。
然后再往下看
5.6.1 Load Region加载区域
- Base :加载区域的起始地址为
0x08000000
,一般对应芯片 Flash 的起始地址 。 - Size :大小为
0x0000a088
,表示该区域占用的字节数 。 - Max :最大允许大小为
0x00010000
,用于限定该加载区域可使用的最大空间 。 - 属性 :
ABSOLUTE
表示该区域地址是绝对地址 。
接着就是下面的
5.6.2 Execution Region执行区域
- Exec base :执行基地址为
0x08000000
,即程序执行时的起始地址 。 - Load base :加载基地址也是
0x08000000
,说明加载地址和执行地址相同 。 - Size :大小为
0x00009fa4
,是执行区域实际占用空间 。 - Max :最大允许大小
0x00010000
。 - 属性 :
ABSOLUTE
。
然后就是执行区域的具体解释
- Exec Addr:执行地址 。
- Load Addr:加载地址 。
- Size:对应段的大小 。
- Type :段的类型,
Data
表示数据段,Code
表示代码段 。 - Attr :属性,
RO
表示只读(Read - Only) 。 - Idx:索引值 。
- Section Name :段名,如
RESET
是存放复位向量等的段;.emb_text
是包含部分代码的文本段 。 - Object :该段所属的目标文件,如
startup_stm32f103xb.o
、port.o
等 。
看蓝色箭头,RO代表执行区内容都是只读的,都存在Flash里。看起始地址0x80000000也可以看出,符合Flash的基地址。
++执行区域对于分析内存占用情况是十分重要的。++
我们接着往下看。

这段执行区里的内容都是可以读写的,看红色框RW即可看出。说明该段位于SRAM中。
我们从他的基地址也可以看出。从0x20000000开始,符合我们前面讲到的。
我们再看第一个 size:0x00000004,代表freertos.o占用了四个字节
然后我们再看一下我们前面提到的代码段,在.map文件里都能找到。

这个.constdata意思是常量数据段,保存在flash里

.bss数据段,它主要用于存放未初始化的全局变量和静态变量,该段位于 SRAM 中。

.text代码段,存放在Flash里

.STACK栈段,位于SRAM里
5.7 Image component sizes镜像组件大小

- Code (inc. data):包含代码以及与代码相关数据的大小总和 ,这里的代码指编译后的机器指令 。
- RO Data :只读数据的大小,如程序中用
const
修饰的常量等 。存放在 Flash里。 - RW Data :可读写数据的大小,一般是已初始化的全局变量和静态变量 。存放在SRAM里。
- ZI Data :未初始化的全局变量和静态变量,程序启动时会被初始化为 0 。存放在SRAM里。
- Debug:调试信息的大小,用于调试器辅助调试程序 。
- Object Name:目标文件名称,代表这些数据所属的编译后文件 。
以alert.o
这一行数据64 32 0 0 28 1132 alert.o
为例:
alert.o
目标文件中,包含代码及相关数据的大小为 64 字节 ;
只读数据大小是 32 字节 ;
没有可读写数据(RW Data 为 0 )
零初始化数据(ZI Data 为 0 )
调试信息大小为 28 字节
在目标文件后面会有一个内存总计。其实就是自己写的代码的内存占用情况。

其他行数据同理,分别对应不同目标文件的各类数据大小情况 。
++镜像组件大小有助于我们去了解每个目标文件对存储空间的占用情况,分析程序的内存使用布局,排查是否存在不合理的内存占用等问题 。++

这个图里展示的是库文件统计。
再看最后一段

Total RO Size (Code + RO Data) 为 40868 ( 39.91kB) :是代码和只读数据大小之和,即存储在 ROM 中不会被修改的内容总大小为 40868 字节,约 39.91kB 。
Total RW Size (RW Data + ZI Data) 为 15872 ( 15.50kB) :是可读写数据(已初始化和未初始化)的总大小,这部分数据在程序运行时可能会存放在 SRAM 中,大小为 15872 字节,约 15.50kB 。
Total ROM Size (Code + RO Data + RW Data) 为 41096 ( 40.13kB) :是存储在 ROM 中的代码、只读数据和已初始化可读写数据的总大小,为 41096 字节,约 40.13kB 。
六:总结
在分析代码的内存占用情况时,查看镜像组件中用户代码的内存占用是首要步骤。用户代码涵盖了开发者编写的各类源文件经编译后生成的目标文件内容。通过关注镜像组件中如 "Code (inc. data)""RO Data""RW Data""ZI Data" 等不同类别数据的大小,能清晰知晓每部分代码和数据在内存中的占用情况。
对于内存占用较大的代码,也需要慎重评估处理。
一方面,可深入分析代码逻辑,查找冗余部分进行修改精简。例如,检查是否存在重复的计算逻辑、不必要的变量定义等,通过优化算法和代码结构来降低内存开销。
另一方面,考虑将不常变动且对读取速度要求相对不高的部分存放在 Flash 中。因为 Flash 具有非易失性,可用于存储程序代码和一些只读数据。而 SRAM 作为程序运行时用于快速读写数据的区域,需确保其空间充足,以保障程序运行时变量的读写操作能高效进行,避免因 SRAM 空间不足导致程序运行出错或性能下降。