嵌入式内存分布

本篇主要讲嵌入式中内存分布情况。

一、内存分区

内存分区示意图如下:
ddab81176bcfdfda0d35f898913149d0.png

1. 代码区

含义:

  • 这是程序的可执行代码部分的大小,通常对应于 .text 段。

内容:

  • 包含所有编译后的机器指令,即 MCU 将执行的代码。
  • 字符串常量和define定义的常量存放在代码区,它们在预处理过程就会被替换成对应常量直接嵌入到代码中,最终链接为机械指令,也就是上点所说保存在.text段。
  • 程序执行代码存放在代码区,其值不能修改(若修改则会出现错误)。

存储位置:

  • Flash(闪存)。因为 Flash 是非易失性存储器,断电后数据仍然保留,适合存储程序代码。
2. 常量区

含义:

  • 这是程序中只读数据的大小,通常对应于 .rodata 段。

内容:

包含所有的常量数据,例如 :

  • 字符串、数字等常量存放在常量区。
  • const修饰的全局变量存放在常量区。
  • 程序运行期间,常量区的内容不可以被修改。

存储位置:

  • Flash(闪存)。因为这些数据是只读的,存储在 Flash 中可以节省 RAM,并保证数据的持久性。
3. 全局(静态)区

全局(静态)区介绍

  • 编译器编译时即分配内存,全局变量和静态变量的存储是放在一块的。C语言中,已初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
  • 全局区有 .bss段 和 .data段组成,可读可写。

.bss段

  • 未初始化的全局变量和未初始化的静态变量存放在.bss段。
  • 初始化为0的全局变量和初始化为0的静态变量存放在.bss段。
  • .bss段不占用可执行文件空间,其内容由操作系统初始化(清零)。

存储位置:

  • SRAM(静态随机存取存储器)
    • **Flash 中不占用空间:**因为这些变量的初始值为零,不需要在 Flash 中存储。
    • **程序启动时:**启动代码会将对应的 RAM 区域清零。

.data段

  • 已初始化的全局变量存放在.data段。
  • 已初始化的静态变量存放在.data段。
  • .data段占用可执行文件空间,其内容由程序初始化。

存储位置:

  • Flash(闪存)和 SRAM(静态随机存取存储器)
    • **Flash 中:**存储这些变量的初始值。
    • **SRAM 中:**在程序启动时,初始值从 Flash 复制到 RAM 中,以便在运行时可以修改这些变量。
4. 堆区(heap)

堆区介绍

  • 堆区由程序员分配内存和释放。
  • 堆区按内存地址由低地址到高地址增长,用malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上。

注意:堆内存牢记不忘释放,避免内存泄漏的情况。

调用函数

  • 用malloc等函数实现动态分布内存。
void *malloc(size_t);

参数size_t是分配的字节大小。

返回值是一个void型的指针,该指针指向分配空间的首地址。

(void *型指针可以任意转换为其他类型的指针)

  • 用free函数进行内存释放,否则会造成内存泄漏。
void free(void * /ptr/);

参数是开辟的内存的首地址。

5. 栈区(stack)

栈区介绍

  • 栈区由编译器自动分配释放,由操作系统自动管理,无须手动管理。
  • 栈区上的内容只在函数范围内存在,当函数运行结束,这些内容也会自动被销毁。
  • 栈区按内存地址由高地址到低地址方向增长,其最大大小由编译时确定,速度快,但自由性差,最大空间不大。
  • 栈区是先进后出原则,即先进去的被堵在屋里的最里面,后进去的在门口,释放的时候门口的先出去。

存放内容

  • 临时创建的局部变量和const定义的局部变量存放在栈区。
  • 函数调用和返回时,其入口参数和返回值存放在栈区。

二、STM32存储器分配

在这里插入图片描述

随机存储器—RAM
  • RAM是与CPU直接交换数据的内部存储器,也叫主存(内存)。
  • 它可以随时读写,而且速度很快,通常作为操作系统或其他正在运行中的程序的临时数据存储媒介。
  • 当电源关闭时RAM不能保留数据(掉电数据消失哦)如果需要保存数据,就必须把它们写入一个长期的存储设备中(例如硬盘)。
2. 只读存储器—ROM
  • 虽然叫做“只读存储器”,但实际上像EPROM跟EEROM是支持可重复写入的,之所以叫这个名字,跟它的起源有关。
  • 读取速度相比RAM要慢
  • 断电记忆存储功能。

ROM与Flash是什么关系

(1)上个世纪,发明了两种存储器,一个是RAM,一个是ROM。当时的ROM一旦存储数据就再也无法将之改变或者删除,也就是说,程序只有一次下载机会。如果你程序写错了,那么这个芯片就报废了。很明显非常的鸡肋。
(2)为了便于使用和大批量生产,进一步发展出了可编程只读存储器(PROM)、可擦除可编程只读存储器(EPROM)。EPROM需要用紫外线长时间照射才能擦除,使用很不方便。
(3)1980s又出现了电可擦除可编程只读存储器(EEPROM),它克服了EPROM的不足,但是集成度不高、价格较贵。
(4)于是又发展出了一种新型的存储单元结构同EPROM类似的快闪存储器(FLASH MEMORY)。FLASH集成度高、功耗低、体积小,又能在线快速擦除,因而获得了快速发展。关系图如下

在这里插入图片描述

(5)因此,现在的单片机存储数据主要是使用的Flash,就是替代以前的ROM,最大的有有点是降低了芯片的成本并且可以做到电擦写。但是因为最先发展出来的是ROM,所以很多时候,人们常常愿意说芯片里面的是ROM存储代码,而不是说Flash存储代码。我们需要了解到这个即可,随便你怎么讲。我们只需要知道,现在的芯片都是用Flash存储数据,但是也要能够明白,很多人讲的ROM就是Flash

断电和通电状态下的数据存储

为什么要分断电和通电状态分析

(1)在一款MCU中,一个程序编译完成之后,需要烧录进去。这个程序有代码段,只读数据,可读可写数据。上文我们分析了,代码段和只读数据存储在ROM,那么可读可写的数据存储在哪里呢?难道是RAM吗?
(2)这些可读可写的数据有被赋予初值的变量。假设,这些变量存储在RAM,会发生什么?
(3)很明显,这些被赋予初值的变量存储在RAM,那么一旦断电了,这些初值将会消失!也就是说,我们的这些初值没了!
(4)由此可见,我们数据存储要分成断电和通电两种状态进行分析。而我们上面讲了那么多,其实都是以通电状态进行数据存储的分析的。

断电状态数据存储

(1)从上面的分析我们可以知道,虽然在通电状态下,可读可写数据是要存储在RAM的,但是在断电状态还是要存储在ROM的。所以,现在我就以STM32的断电和通电存储来进行讲解。
(2)我们对一个STM32的工程文件进行编译,会出现如下的编译结果。其中有一行是Program Size: Code=5984 RO-data=336 RW-data=56 ZI-data=1024 。
(3)
<1>Code : 代码段,存放程序的代码部分。
<2>RO-data:只读数据段,存放程序中定义的常量。
<3>RW-data:读写数据段,存放初始化为非 0 值的全局变量。
<4>ZI-data:零初始化的可读写变量,存放未初始化的全局变量及初始化为 0 的变量。

在这里插入图片描述

.bss 段不占空间的原因

  • 在编译时,.bss 段的变量并没有实际的初始值,因为这些变量在程序启动时会自动初始化为 0。因此,编译器只需要记录这些变量的大小,而不需要为它们分配额外的存储空间(比如在 Flash 中)。
  • 不占用 Flash:由于 .bss 段中的数据没有初始值,编译器不需要在 Flash 中保存它们的初始化数据。

符号表的作用

  • 编译器会为每个变量分配一个地址,这个地址被记录在符号表中,用于在程序执行时访问变量。符号表在编译过程中使用,但不会被写入最终的可执行文件中。
  • 在程序执行时,变量的地址存储在代码中,程序通过这些地址访问相应的内存。

程序执行时分配内存

  • 当程序启动时,启动代码(通常称为 C 运行时环境初始化代码,或 CRT)会在 RAM 中为 .bss 段的变量分配内存,并将其全部初始化为 0。因此,.bss 段中的变量在执行时才会占用 SRAM 空间,而不是在编译时占用 Flash 空间。

.data段占用空间解析:

.data这些变量在程序开始执行前就有明确的初始值,因此编译时需要在 Flash 中存储这些初始值,如果只是存储在RAM中,那么掉电会丢失。在程序启动时,启动代码会将这些初始值从 Flash 复制到 RAM 中,以便程序运行时可以修改这些变量。

因此,.data 段的初始值会占用 Flash 空间,并且在程序运行时占用 RAM 空间。

由此得知,在断电状态,数据都存储在了ROM中,如下。(注意,这张图没有出现Code,但是我们还是需要知道Code数据存储在ROM中

在这里插入图片描述

通电状态数据存储

(1)通过上面的分析我们知道了断电和通电状态下的数据存储了。那么上电的一瞬间发生了什么?
(2)RO-Data数据不变,RW-Data的数据被复制到RAM区域,RAM中划分出一个ZI-data区域。当程序执行过程中,被初始化为0的数据,CPU会向ZI-data对应的变量中写入0。没有被初始化的数据,那么就是随机值。这也是为什么我们初学C语言的时候,反复强调要进行初始化。但是keil帮我们做了很多事情,如果你变量没有被初始化,他在划分ZI-data区域的时候,会把这个区域全部清零。

bed17834e3f6a718f6861e2182ad06f8.png

(3)RO-Data的数据就是常量区的数据,RW-Data对应静态区的数据段,ZI-data对应静态区的BSS段。那么堆栈呢?
(4)堆栈是由启动文件划分的,如果你感兴趣,可以打开STM32的启动你文件startup_stm32f10x_hd.s。

单片机中的内存分布

​ 在keil中编译程序完成后,在最下面的“Build Output”输出栏,可以看到有这样一行信息“Program Size: Code=321676 RO-data=35972 RW-data=4788 ZI-data=126284”,这句话提供了程序在编译后的大小信息,包括CodeRO-dataRW-dataZI-data的大小。这些大小信息对开发人员和嵌入式系统设计者来说具有以下作用:

  1. 内存优化:通过了解不同部分的大小,可以评估程序对内存的占用情况,帮助进行内存优化。例如,可以通过减少代码大小、优化数据存储方式或优化算法来减少内存的使用,从而节约系统资源。
  2. 内存规划:了解程序的Code、RO-data、RW-data和ZI-data大小,可以帮助进行内存规划。在嵌入式系统设计中,内存资源是有限的,因此需要合理地分配内存空间来满足程序的需求。通过了解不同部分的大小,可以确定它们在内存中的布局和分配方式,以避免冲突或浪费。
  3. 系统可靠性:内存的合理管理对系统的可靠性至关重要。通过了解程序的大小,特别是RO-data和RW-data的大小,可以确保所使用的内存空间不会超出设备的可用内存范围,避免内存溢出和相关的错误。
  4. 调试和故障排查:Program Size提供的信息可以用于调试和故障排查。如果程序运行时出现问题,例如崩溃或性能问题,可以通过检查不同部分的大小和内存使用情况来定位问题所在,并分析是否存在内存相关的错误或瓶颈。

​ 能更好的帮助开发人员在嵌入式系统设计中更好地管理内存资源,并确保程序在目标设备上正常运行。

参考文档

[C语言+单片机-内存分布详解,全网最全,值得收藏保存_单片机内存划分-CSDN博客](https://blog.csdn.net/Wang_XB_3434/article/details/131318213)

[RAM明明断电会丢失数据,为什么初始化的全局变量存储在RAM?详细分析程序的存储-云社区-华为云 (huaweicloud.com)](https://bbs.huaweicloud.com/blogs/407311)

[ROM、RAM、FLASH、DDR、EMMC 百科 – 学习笔记 - 二的次方 - 博客园 (cnblogs.com)](https://www.cnblogs.com/roger-yu/p/16655061.html)

Logo

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

更多推荐