一文搞懂MCU RAM的分配

2024-01-07 17:33:50

一文搞懂MCU RAM的分配

1. 前言

嵌入式MCU开发,通常受限于硬件资源,往往 RAM 和 ROM 空间有限,大一点的 MCU,RAM 空间也才 512KB,小的更是小的可怜,因此对于 RAM 空间和 ROM 空间是如何被分配走的,想要深入进阶的话是肯定需要了解的。

本文将采用STM32控制器为例,配合实验详细记录/阐述关于嵌入式MCU资源使用那些事!

2. 数据段含义

2.1 Program Size解析

比如我们使用 keil 编译完程序,编译成功之后,会在输出栏提示一行这样的信息

Program Size: Code=4620 RO-data=312 RW-data=16 ZI-data=1168 

这便是我们编译的程序的大小,但这个 Program Size 又如何理解呢?其中的 CodeRO-dataRW-dataZI-data 段分别代表的是什么含义呢?

首先,我们来看看各自段代表的都是什么含义:

  • Code 段:代表的就是我们所编写的代码所占用的 flash 空间。当然需要注意的是,代码段的内容是经过编译器优化以及汇编之后的大小,你在代码里添加注释,或者一些无意义的代码是不会增大code段的大小的。
  • RO-data段:即read only - data,只读数据段。在代码中定义的 const 常量,printf 打印的固定字符等,这些均会被存放到此数据段内。
  • RW-data段:即 read write - data,可读可写段。代码中的变量存放在此数据段内。
  • ZI-data段:即 Zero Initial - data,初始化为0的可读写变量。

明白了每个字段代表的是什么含义,那我们定义的局部变量、全局变量以及常量是如何占用的ram空间的呢?以及具体真正占用 ram 和 rom 空间又是如何计算的呢?

关于占用 ram 和 rom 空间又是如何计算,直接提供给大家以下公式:

  • ROM空间大小计算方式:Total ROM = Code + RO-data + RW-data
  • RAM空间大小计算方式:Total RAM = RW-data + ZI-data

关于局部变量、全局变量、常量如何占用ram,我们接下来采用实验验证推理。

3. 局部变量、全局变量、常量如何占用 RAM?

3.1 栈大小Stack_Size 与 堆大小Heap_Size

在弄清楚我们定义变量如何消耗 RAM 空间之前,我们先要弄清楚栈大小Stack_Size 和堆大小Heap_Size 这两个重要概念!

MCU的RAM空间被划分为多个区域,其中有两个重要组成:堆和栈(当前还有其他区域),此两个区域的大小设置可在对应的启动文件内查看并设置。
在这里插入图片描述
这里我们简单描述下栈和堆是如何使用的:

  • 栈的使用:

    • 在我们程序编码过程中,在函数内部定义一个局部变量,其实是在栈空间上申请了一个内存存放数据;
    • 程序运行过程中,进入某个函数,需要将当前函数内的变量进行现场保存,也即压栈操作,及将当前的寄存器(如r0 r1等)进行压栈操作,也是从栈空间上占用一块内存存放数据;而函数退出的时候,需要进行恢复现场,及出栈操作,也即从栈空间内读取之前进入函数时进行压栈操作的数据,恢复寄存器数据,继续跑后续代码。
  • 堆的使用:

    • 在我们程序执行过程中,调用malloc实际便是从堆上申请一块内存用于使用。使用完成之后,需要调用free释放那块内存区域。
  • 此外针对裸机程序,整个程序中使用的栈,均从启动文件startup_xxxx.s 文件中 Stack_Size 设置的栈空间中分配;而RTOS程序,各线程具有独立的栈空间,这一点需要注意。

接着我们进行如下测试验证。

3.2 验证栈大小设置,对程序编译的影响

  1. 修改栈大小,观察程序编译大小变化,设置 Stack_Size EQU 0x800,程序编译结果:Program Size: Code=3620 RO-data=380 RW-data=20 ZI-data=2124
    在这里插入图片描述
  2. 修改栈大小为Stack_Size EQU 0x400 ,程序编译大小:Program Size: Code=3620 RO-data=380 RW-data=20 ZI-data=1100
    在这里插入图片描述
    比对以上结果可知,栈大小的设置会直接影响程序RAM的消耗,由于上述我们只是简单的修改了栈的大小,缩减了栈的空间,缩减的部分栈并没有被程序使用,故看到的只是ZI-data 数据段的变化。

3.3 验证局部变量RAM分配

  1. 设置启动文件,栈大小为Stack_Size EQU 0x400 ,即分配的栈空间为 0x400 = 1024Byte
  2. 在main()函数内 定义一个局部数组a,数据大小为1Byte,编译程序,查看程序编译大小变化
  3. 注意此处为了防止被编译器优化,我们增加 __attribute__((unused)) 修饰数组
  4. 编译查看程序大小,并运行
    在这里插入图片描述
  5. 程序正常运行
  6. 之后 修改数组a大小为 400Byte,编译查看程序大小,并运行
    在这里插入图片描述
  7. 程序正常运行,编译结果显示 仅code字段发生了变化,RAM空间并未发生改变
  8. 之后再次修改数组a大小超过启动文件内分配的栈大小,设置数组a大小为 1030Byte,编译查看程序大小,并运行
    在这里插入图片描述
  9. 编译结果显示仍然 仅code字段发生了变化,RAM空间并未发生改变,但是实际运行程序会直接崩溃,仿真运行发现程序已卡死在HardFault_Handler()中断
    在这里插入图片描述

综上,可得出以下结论:局部变量会占用栈Stack空间,栈大小Stack_Size在启动文件内设置,当栈空间不足时会导致程序运行崩溃

3.4 验证全局变量的RAM分配

局部变量占用的是系统栈空间,那全局变量是不是也是如此呢?接着我们同样进行验证。

注意,采用全局变量进行验证时,为了让编译器不进行优化,故需要在main函数内增加一行对此数组的使用,我们此处在main函数内增加一行代码如下:printf("%s", a);

  1. 定义数组a为全局变量,大小1Byte,使用__attribute__((unused)) 修饰,防止被编译器优化,编译查看程序编译结果,并运行
    在这里插入图片描述

  2. 编译结果为:Program Size: Code=3600 RO-data=380 RW-data=24 ZI-data=1096 ,程序正常运行

  3. 修改数组a的大小为400Byte,继续验证
    在这里插入图片描述

  4. 编译结果为:Program Size: Code=3600 RO-data=380 RW-data=20 ZI-data=1500 ,此时我们可以发现Total RAM增大了,Add Total Ram = (The current RW-data + The current ZI-data ) - (The laster RW-data + The laster ZI-data) = (1500 + 20) - (1096+ 24) = 400Byte(注意此处400Byte != (400 - 1)Byte 与字节对齐有关) ,程序正常运行

  5. 继续修改数组a大小为1030,使其超过系统栈大小(当前配置的系统栈大小0x400=1024Byte),继续验证
    在这里插入图片描述

  6. 编译结果为:Program Size: Code=3600 RO-data=380 RW-data=20 ZI-data=2132 ,程序正常运行,并没有之前的死机现象,同时我们可以发现Total RAM继续增大,相比上次增大了 Add Total Ram = (The current RW-data + The current ZI-data ) - (The laster RW-data + The laster ZI-data) = (2132 + 20) - (1500 + 20) = 632Byte(注意此处632Byte != (1030 - 400)Byte与字节对齐有关)

综上,通过实验验证可知,定义全局变量不会占用系统栈空间,全局变量不同于局部变量,全局变量从RAM中单独申请空间。

3.5 验证全局常量和局部常量const的RAM分配

如果是const常量会怎样呢?我们在以上两个实验的基础上,增加const修饰变量

  1. 针对全局变量,采用const修饰,使其变为常量,会将此数据分配至RO-data字段,而不再再用RI-data字段
    在这里插入图片描述
  2. 针对局部常量,采用const修饰,与不采用const修饰,程序编译结果均无变化,且由于数组a大小超出系统栈大小,导致程序无法正常运行,故针对局部变量,采用const修饰,仍然占用的是系统栈空间,是不是很震惊!
    在这里插入图片描述
  3. 局部常量使用const会占用系统栈能够理解,修改为const之后,RO-data不会增加吗?这不应该吧。实际上这里又是优化导致(不确定是编译器优化还是c语言优化),由于我们将数组a的值赋值为0,所以被优化了,我们只需要简单的修改一下,让数组a里面不全是0即可。如下图所示,我们可以看到RO-data字段增加了,当然由于局部const常量仍然占用系统栈,且由于系统栈大小不够,故程序仍然无法运行!
    在这里插入图片描述

3.6 关于堆的分配

关于堆的分配及使用比栈就简单多了,如下:

  • 堆大小设置:
    • 堆的总大小,在启动文件内进行设置,Heap_Size EQU 0x200
  • 堆空间的申请:
    • 代码中使用malloc 动态申请,如果有足够大小的堆空间的话,则成功分配,并返回首地址指针;如果堆空闲空间不够,则失败,返回NULL;
  • 堆空间的释放:
    • 堆空间使用完之后,调用 free 动态释放,使用的时候注意不要重复释放,否则容易导致bug!

4. 总结

以上便是针对我们在编程中,编写代码时所会涉及到的RAM空间使用的分析,特别是变量及常量对于ram的占用,总结来说有以下几点:

  • 系统堆栈大小从启动文件进行配置
  • 全局变量不从系统栈分配,直接从ram分配
  • 局部变量从系统栈分配
  • 全局变量分配过大,超过芯片实际ram大小,编译器会帮你直接报错,局部变量分配过大,超过系统栈大小(裸机系统),编译器不会告诉你,程序只会莫名崩溃~
  • 全局常量不占用 RAM,只占用 ROM (RO-data字段)
  • 局部常量不仅占用ROM,还会占用系统栈大小!(裸机系统)

这些都是很细节的东西,想要成为一名资深开发者所必须了解的内容。希望对你有所帮助。

此外需要注意的是,以上是针对裸机系统进行的分析,在RTOS系统中,会有些许差别,主要在于在RTOS中会有系统栈和线程栈,不过也都是栈~

如果你觉得我文章写的不错,欢迎点赞、关注+收藏,谢谢~,以下我的一些推荐:


相关推荐:


创作不易,转载请注明出处!

关注、点赞+收藏,可快速查收博主有关分享!


文章来源:https://blog.csdn.net/qq_43332314/article/details/135441872
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。