Stm32的内存管理(Code,RO-data,RW-data,ZI-data)

  单纯从C语言角度来讲内存管理,有点宏观;这里介绍具体嵌入式的内存分配管理。

一、内存分配

  对于一个C语言程序而言,内存空间主要由五个部分组成:代码段(.text)、数据段(.data)、静态区(.BSS)、堆和栈组成。

  • BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量和静态变量(这里注意一个问题:一般的书上都会说全局变量和静态变量是会自动初始化的,那么哪来的未初始化的变量呢?变量的初始化可以分为显示初始化和隐式初始化,全局变量和静态变量如果程序员自己不初始化的话的确也会被初始化,那就是不管什么类型都初始化为0,这种没有显示初始化的就是我们这里所说的未初始化。既然都是0那么就没必要把每个0都存储起来,从而节省磁盘空间,这是BSS的主要作用)的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。 BSS节不包含任何数据,只是简单的维护开始和结束的地址,即总大小,以便内存区能在运行时分配并被有效地清零。BSS节在应用程序的二进制映象文件中并不存在,即不占用磁盘空间而只在运行的时候占用内存空间,所以如果全局变量和静态变量未初始化那么其可执行文件要小很多。
  • 数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量和静态变量的一块内存区域。数据段属于静态内存分配,可以分为只读数据段和读写数据段。字符串常量等,但一般都是放在只读数据段中。
  • 代码段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等,但一般都是放在只读数据段中。
  • 栈区:由系统自动分配,栈区的分配运算内置于处理器的指令集,当函数执行结束时由系统自动释放。存放局部变量。栈的缺点是:容量有限,当相应的区间被释放时,局部变量不可再使用。查询栈容量的命令:ulimits -s。栈是一块连续的区域,向高地址扩展,栈顶和容量是事先约定好的。
  • 堆区:在程序的执行过程中才能分配,由程序员决定,编译器在编译时无法为他们分配空间,只有在程序运行时分配,所以被称为动态分配。堆是不连续的区域,向高地址扩展。由于系统用链表来描述空闲的地址空间,链表的遍历是由地地址向高地址的,故堆区是不连续的动态的存储空间。

  更多具体的C语言基础内存分配知识,可以看这篇博文:内存分配方式

二、Stm32的内存管理

编写一个空工程,BUILD后,

1
Program Size: Code=340 RO-data=252 RW-data=0 ZI-data=1632

  程序已用了1600多的RAM,要是在51单片机上,会心疼死了,这1600多的RAM跑哪儿去了???

分析完map,你会发现是堆和栈占用的。在startup_stm32f10x_md.s文件中,它的前面几行就有以下定义,这下该明白了吧。

1
2
Stack_Size  EQU     0x00000400
Heap_Size EQU 0x00000200

  一般 MCU 包含的存储空间有:片内 Flash 与片内 RAM,RAM 相当于内存,Flash 相当于硬盘。

编译器会将一个程序分为好几个部分,分别存储在 MCU 不同的存储区。Keil 工程在编译完之后,会有相应的程序所占用的空间提示信息,如下所示:

1
Program Size: Code=12266 RO-data=790 RW-data=232 ZI-data=8096

上面提到的 Program Size 包含以下几个部分:

  • Code:代码段,存放程序的代码部分;
  • RO-data:只读数据段,存放程序中定义的常量;
  • RW-data:读写数据段,存放初始化为非 0 值的全局变量;
  • ZI-data:0 数据段,存放未初始化的全局变量及初始化为 0 的全局变量;

编译完工程会生成一个. map 的文件,该文件说明了各个函数占用的尺寸和地址,在文件的最后几行也说明了上面几个字段的关系:

1
2
3
Total RO Size (Code + RO Data) 13056 ( 12.75kB)
Total RW Size (RW Data + ZI Data) 8328 ( 8.13kB)
Total ROM Size (Code + RO Data + RW Data) 13288 ( 12.98kB)
  • RO Size = (Code + RO-data):表示程序占用 Flash 空间的大小;
  • RW Size = (RW-data + ZI-data):表示运行时占用的 RAM 的大小;
  • ROM Size = (Code + RO Data + RW Data):表示烧写程序所占用的 Flash 空间的大小。

  这个是MDK编译之后能够得到的每个段的大小,也就能得到占用相应的FLASH和RAM的大小。

  • Flash = Code + RO Data + RW Data;
  • RAM = RW-data+ZI-data;

Ps:堆和栈都存在RAM里,他两各分多少看函数需求,但是他两的总值不能超过单片机硬件的实际RAM尺寸!(可以外扩RAM)

三、分散文件加载

  内存分配在嵌入式开发,有另外一个名字:分散文件加载。

该章节内容主要来自这位博主:Solaris_超,想更了解分散文件加载的可以看这位博主链接文章。

3.1 什么是分散加载

  简单来说就是让编译器高速MCU内核哪里存的是代码、哪里存的是数据,去哪个特定的地址找到下一步需要运行的函数,就是高速编译器把每一个编译好的函数、数据放到具体的哪一个物理地址。

3.2 分散加载常见应用场景

  • Bootloader & 程序升级
    • Bootloader的原理就简单来说在MCU的Flash里面同时摆放2个(或多个)不同工程的程序,一个Bootloader程序和一个用户程序,那么这就需要调整分散加载文件,以达成在一个Flash里面同时摆放两个不同程序的目的。
    • 程序升级都是为了增加一个小功能或修复一个小BUG,不需要全部升级而是只升级一点点。当然要实现这个功能同样需要分散加载的配合,把可能会后续升级的部分函数或数据事先分配好空间,留好空间上的余量,这些都需要分散加载来完成。
  • 加速程序运行速度(如:对速度有较高要求的算法等、RTOS kernel)
    • 在SRAM中运行的程序要比在XIP Flash中执行要快,性能提升明显。
  • 访问扩展存储&对存储区的划分
    • 如果要把外扩的存储用于运行代码/扩展RW数据段等用途,简单来说就是把片内地址映射到片外,需要按照寻址空间的方式来访问扩展存储的话,比如扩展Nor-Flash、扩展SDRAM、扩展SRAM等,那就需要分散加载配合。(只作存储数据的话,分散加载不是必要的!!!)

3.3 分散加载的基本结构定义以及分散加载的目的

  • Code段:表示程序代码部分
  • RO-data段:程序定义的所有常量以及const类型数据
  • RW-data段:已经初始化的所有静态变量
  • ZI-data段:未初始化的静态变量

所以分散加载的根本目的就是:

  • 指引把RO-data数据段、RW数据段从片内程序存储区里面(一般是片内Flash),搬到片内程序运行区(一般是片内SRAM);
  • 在片内程序运行区(一般是片内SRAM)内分配ZI数据段运行需要的空间并把这段数据初始化为0;
  • 初始化堆栈;
  • 对于有些指定加载到程序运行区(一般是片内SRAM)的RO数据段,把他们加载到程序运行区(一般是片内SRAM)里面。

Ps: 这个和使用的电脑运行操作系统或者软件原理类似,电脑就是把硬盘里面的操作系统加载到内存里面,然后CPU从内存里面取数据以及程序指令来运行的。

Ps: RW以及ZI数据段的初始化是在分散加载过程中完成的,也就是在__main中完成的,比如你定义一个全局变量,并给它赋值,只有在__main结束后你才能看到这个全局变量被赋值成功的,也就是说在__main之前,使用全局变量是行不通的。


  我们可以在编译链接完层的代码后,链接器的输出打印上看到这部分信息,如下图,就是一个Hello World工程的输出打印,其中链接器打印出了这几个段的大小(蓝色底纹部分):

如果大家想看更详细编译结果,可以双击工程名查看.map文件

  .map文件最后有关于编译结果的详细介绍,我是用的这个hello world工程中的所有被编译&链接的文件都会在.map(链接器的工作报表)文件里面详细记述,每一个文件编译后产生的Code、RO-Data、RW、ZI的大小,以及加在一起的总大小,如下图:

Ps: 我们可以通过查看 .map 文件来规划Bootloader程序的存储空间的大小。

四、嵌入式的内存分配

  • 内存管理:是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。内存管理的实现方法有很多种,他们其实最终都是要实现 2 个函数: malloc 和 free(不可重入函数,上操作系统注意调用,很危险); malloc 函数用于内存申请, free 函数用于内存释放。
  • 内存碎片:通常应用程序可以调用ANSI C编译器的malloc()和free()函数来动态的分配和释放内存,但多次这样的操作会把原来很大的一块连续存储区域逐渐地分割成许多非常小并且彼此不相邻的存储区域,这就是存储碎片。

  malloc( )属于标准C语言函数,当然可以在单片机上使用。

  但是在嵌入式(裸机)中最好不要这么做!一般单片机的内存都比较小,而且没有MMU(内存管理管理单元),多次的mallocfree的使用容易造成内存碎片。没有MMU的管理,当后面因为空间不足而分配失败,从而导致系统崩溃,因此应该慎用,或者自己实现内存管理。除了UCOS或FREERTOS等嵌入式操作系统有自带的MMU处理外,裸机长时间连续工作产生的内存碎片为系统工作稳定埋下隐患。


嵌入式内存的分配方式,常见如STM32可以先在启动文件中设置heap的大小,再使用动态内存分配:

1
Heap_Size     EQU    0x00000200     //也就是 512字节

  嵌入式系统的堆栈,不管是用什么方法来得到内存,感觉他的方式都和编程中的堆差不多。目前我知道两种获得内存情况:

  1. 用庞大的全局变量数组来圈住一块内存,然后将这个内存拿来进行内存管理和分配。这种情况下,堆栈占用的内存就是上面说的:如果没有初始化数组,或者数组的初始化值为0,堆栈就是占用的RAM的ZI-data部分;如果数组初始化值不为0,堆栈就占用的RAM的RW-data部分。这种方式的好处是容易从逻辑上知道数据的来由和去向。
  2. 就是把编译器没有用掉的RAM部分拿来做内存分配,也就是除掉RW-data+ZI-data+编译器堆+编译器栈后剩下的RAM内存中的一部分或者全部进行内存管理和分配。这样的情况下就只需要知道内存剩下部分的首地址和内存的尾地址,然后要用多少内存,就用首地址开始挖,做一个链表,把内存获取和释放相关信息链接起来,就能及时的对内存进行管理了。

正点原子的方法即是上面的方法一,详情介绍可以看这篇文章:正点原子例程_内存管理

五、内存管理的好处

  内存管理,即进行内存分配、释放操作。

  内存管理的核心好处,就是释放内存;开辟内存并不难,问题是及时把不用到的内存释放出来。当嵌入式项目用到文件系统、操作系统时,就需要内存管理了。一般如果做的嵌入式项目不涉及文件系统、操作系统,是不需要进行内存管理的。
  因为不涉及文件系统、操作系统的话,嵌入式项目内部采用所需的变量内存大小都是大致可知的;声明定义采用 足够长的局部数组类型,既分配了临时内存。离开后又自动及时释放;就根本不需要内存管理。
  但当涉及到文件系统时,项目程序开始有了不确定的输入,例如插入的SD卡、U盘有多个文件。例如我想实现一个文件(名)浏览功能:

  1. 需要读取所有文件名到内存,然后显示到LCD。
  2. 用上述不用内存管理的方法,是定义一个数组来存储所有文件名。
    • 需要知道其中最大文件名的长度。设为255字节。
    • 需要知道文件个数。 100?1000?10000 ?
  3. 如果没有内存管理:
    • 则要定义一个:u8 filenametbl[10000][255];的数组!!
    • 要2550K字节内存!(MCU表示压力山大…)
-------------本文结束感谢您的阅读-------------