本文主要针对如何合理的使用STM32的RAM角度入手,对STM32的RAM进行分配与计算。目的是降低RAM的使用率,将RAM的使用情况都弄清楚,从而合理的规划及分配内存。
本文涉及到一些堆栈方面的思考,在MDK中查看MAP文件及堆栈使用情况的文件进行分析,得出当前程序RAM的分配情况,同时对可以缩减的地方进行分析。
可编程内存基本上可以分为以下几个部分:静态存储区bss段、堆区和栈区。他们的功能不同,使用方法也就不同。
静态存储区:
静态存储区也就是BSS段,英文是Block Started by Symbol的简称,通常是指存放在未初始化的全局变量的一块内存区域,在程序载入时由内核清零。静态存储区在程序编译好的时候就已经分配好,这块内存在程序的整个运行期间都存在,主要存放静态数据全局数据和常量。
栈区:
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元自动被释放,栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内容容量有限。
堆区:
也称动态内存分配。一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。分配方式类似于数据结构中的链表。程序在运行时候调用malloc或者new申请任意大小的内存,程序员自己负责在适当的时候free或者delete释放内存。动态内存的生存期可以由程序决定。良好的使用方法是:如果某动态内存不再使用,需要将其释放掉,否则,很容易出现内存泄漏现象。
其内存的分配规律:
如果系统调用了malloc,从0X20000000开始依次为:静态存储区+堆区+栈区
如果系统未调用了malloc,从0X20000000开始依次为:静态存储区+栈区
要搞清楚stm32的堆栈机制,需要理清楚stm32的存储结构。
在stm32中,flash,SRAM寄存器和输入输出端口被组织在同一个4GB的线性地址空间内。可访问的存储器分为8个主要块,每个块为512MB。
C语言上分为栈、堆、bss、data、code段。重点分析一下STM32以及在MDK里面段的划分。
MDK下Code,RO-data,RW-data,ZI-data这几个段:
Code是存储程序代码的。
RO-data是存储const常量和指令。
RW-data是存储初始化值不为0的全局变量。
ZI-data是存储未初始化的全局变量或初始化值为0的全局变量。
Flash=Code + RO Data + RW Data;
RAM= RW-data+ZI-data;
这个是MDK编译之后能够得到的每个段的大小,也就能得到占用相应的FLASH和RAM的大小,但是还有两个数据段也会占用RAM,但是是在程序运行的时候,才会占用,那就是堆和栈。在stm32的启动文件.s文件里面,就有堆栈的设置,其实这个堆栈的内存占用就是在上面RAM分配给RW-data+ZI-data之后的地址开始分配的。
在stm32的启动文件中,statrtup_stm32l151xba.s文件中,有一句这样的函数
Stack_Size EQU 0x400
表示栈的大小为0x400也就是1024字节。这样CPU在处理任务的时候,函数局部变量最多可以占用的空间大小为1024字节。这里的栈大小包括函数的嵌套,递归等等,都是从这个栈里面分配出来的。
所以如果一个函数的局部变量过多,或者嵌套层数越深,那么程序非常容易出现崩溃的情况。所以一定不要在函数里放过多的局部变量。
堆的增长方向时向上的,而栈的增长方向时向下的,并且没有固定的界限,一旦堆栈冲突,函数就会崩溃。总体上也就是说,在使用堆栈的过程中,一定要确保堆栈的大小及使用情况。
对于OS中对堆栈的管理,主要分为两种情况:
(1)用庞大的全局变量数组来圈住一块内存,然后将这个内存拿来进行内存管理和分配。这种情况下,堆栈占用的内存就是上面说的:如果没有初始化数组,或者数组的初始化值为0,堆栈就是占用的RAM的ZI-data部分;如果数组初始化值不为0,堆栈就占用的RAM的RW-data部分。这种方式的好处是容易从逻辑上知道数据的来由和去向。目前在rtthread的内存管理上就是采用这种方式。
在board.c中,可以看到如下的代码
#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
#define RT_HEAP_SIZE 1024
static uint32_t rt_heap[RT_HEAP_SIZE]; // heap default size: 4K(1024 * 4)
用这种方式也有一定的弊端,就是在操作系统创建任务或者申请内存时,会多占用一些内存资源。
(2)就是把编译器没有用掉的RAM部分拿来做内存分配,也就是除掉RW-data+ZI-data+编译器堆+编译器栈后剩下的RAM内存中的一部分或者全部进行内存管理和分配。这样的情况下就只需要知道内存剩下部分的首地址和内存的尾地址,然后要用多少内存,就用首地址开始挖,做一个链表,把内存获取和释放相关信息链接起来,就能及时的对内存进行管理了。
如果要分析出STM32的内存使用情况,可借助KEIL中编译出来的map文件进行查阅
首先可以在map的末尾看总的内存使用情况
RO-data是 Read Only 只读常量的大小,如const型;
RW-data是(Read Write) 初始化了的可读写变量的大小;
ZI-data是(Zero Initialize) 没有初始化的可读写变量的大小。ZI-data不会被算做代码里因为不会被初始化;
其中RW Data + ZI Data表示总共需要占用的RAM的大小。而Code + RO Data + RW Data表示ROM需要的大小,根据这两个值,可以根据程序合理的选择相应的MCU。
以下是函数分配时的内存分配情况,可根据下面的map文件定位到具体的函数的内存使用情况,其中比较重要的是ZI Data,因为这些内存都是分配在RAM空间中的。
Image component sizes
Code (inc. data) RO Data RW Data ZI Data Debug Object Name
.
.
.
----------------------------------------------------------------------
31376 2302 650 400 6848 668739 Object Totals
0 0 32 0 0 0 (incl. Generated)
54 0 3 10 0 0 (incl. Padding)
从上面的表格,可以分析出(ZI Data+RW Data)正好是7248,也就是RAM的空间大小。
对于以上的数据,可以从占用RAM最大的开始计时
board.c
该文件是RT-THREAD操作系统里面的,划分了一个4KB的静态数组作为操作系统分配的内存区域。
#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
#define RT_HEAP_SIZE 3072
static uint32_t rt_heap[RT_HEAP_SIZE]; // heap default size: 4K(1024 * 4)
该文件里定义了一个全局的数组作为操作系统分配的内存区域,这块区域作为系统线程的栈空间使用,也可以用来动态的申请内存区域。在这块RAM中,合理的估算每个线程的栈大小可以有效的对该大小进行规划。
startup_stm32l151xba.S
启动函数中,会分配堆栈,这个堆栈是供C语言使用的,在进行程序跳转或者中断到来时,都会进行入栈及出栈的操作。目前分配给系统的堆栈空间时1KB。
Stack_Size EQU 0x400
usart.c
该函数是HAL库函数中定义的,其中用到了几个全局的结构体,该结构体的用处主要是供其他函数调用UART的操作句柄。这里消耗的RAM资源为600K。
idle.c
idle线程与其他的线程不一样的地方就是该线程的栈不是存在于rt_heap里面,而是单独为其申请了一块静态数组作为线程使用的堆栈。该函数消耗的RAM资源为384KB。
仅仅这四个文件就占用了6KB左右的资源。下面来分析一下具体的内存使用情况。
在操作系统中,使用RAM的情况可以通过对每个线程栈的最大深度来进行计算。
在MDK中,可以查看Static Call Graph for image文件来查看栈的使用情况
可以看出,main函数的线程栈最大,为224bytes。
那么如何计算线程的栈的最大值?
就拿main线程来分析
main函数的最深的栈的最后一个函数为_rt_timer_remove
然后看一下rt_timer_stop函数
然后是rt_thread_suspend
以此类推,可以得到main函数线程最大需要消耗的栈空间大小为224bytes。
前面分析出对于操作系统使用的内存,都是在rt_heap上,而这个内存目前是4KB。
所以对于rt_malloc内存都是在以上的资源上申请的。
目前操作系统的线程中,使用的线程如下
线程名称 | 堆栈大小 | 说明 |
---|---|---|
main | 224 | main线程 |
tsk_xxx1_entry | 184 | a线程 |
tsk_xxx2_entry | 160 | b线程 |
tsk_xxx3_entry | 168 | c线程 |
tsk_xxx4_entry | 160 | d线程 |
tsk_xxx5_entry | 176 | e线程 |
tsk_xxx6_entry | 160 | f线程 |
也就任务最少也需要分配的栈空间大小
还有邮箱与时间消耗的rt_heap空间
事件统计:
事件名称 | 占用大小 | 说明 |
---|---|---|
a_re | 32 | 回复事件 |
b_te | 32 | 发送事件 |
tcle | 32 | 其他消息 |
所以需要的大小为
信号量名称 | 占用大小 | 说明 |
---|---|---|
a_rsem | 32 | a消息 |
b_rmsg | 32 | b消息信号量 |
c_sem | 32 | c信号量 |
d_rmsg | 32 | d接收信号量 |
总结来看,消耗的rt_heap上的内存空间为
如果要优化RAM的使用,可以有以下几个办法:
OS的栈内存优化
缩小OS的堆,目前来看给OS内存空间4K还算比较的合理。但是也可以缩减到3K左右,这个就根据需求来定。这4K的资源可以做如下的分配:
线程名称 | 堆栈大小 | TCB | 内存头部 | 说明 |
---|---|---|---|---|
main | 256(min:224) | 128 | 24 | main线程 |
tsk_xxx1_entry | 256(min:184) | 128 | 24 | a线程 |
tsk_xxx2_entry | 256(min:160) | 128 | 24 | b线程 |
tsk_xxx3_entry | 256(min:168) | 128 | 24 | c线程 |
tsk_xxx4_entry | 256(min:160) | 128 | 24 | d线程 |
tsk_xxx5_entry | 256(min:176) | 128 | 24 | e线程 |
tsk_xxx6_entry | 256(min:160) | 128 | 24 | f线程 |
如果按照每个堆栈都256字节来计算,那么总共需要的RAM空间为2856字节。加上信号量与邮箱需要的资源一共是3164字节。现在分配的是4096字节。
以上是一种方式,也可以不用rt_thread的内存管理,这样就烧了内存头部信息的占用。
系统栈上的优化
对于系统栈上目前已知的消耗是2752。可以优化的地方是系统栈的空间,现在用的是1024字节。应该可以缩减到512字节,但是目前没有缩减。
然后是idle线程的,现在是分配了256字节的静态数组,这里用不了那么多,可以缩减到128字节即可。
另外具体的细节部分还可以调整,目前程序的至少可以缩小1K字节。
STM32降低RAM的使用,本质上就是降低堆栈的使用。在这个过程中,只要搞清楚内存的分配方式以及OS的堆栈使用情况即可进行内存的合理规划。
对于降低RAM的过程,可以从以下方面入手,如果用局部变量,要考虑到栈的分配问题,栈空间的计算以函数最深的入栈开始,一层一层的计算累加,得到最大的栈的大小,由此,可以计算得到栈的大小。OS上的内存有着自己的一套内存管理方式,所以可以划分出一块静态区域给OS作为堆使用。OS申请的内存都是在这块区域内进行,计算各个线程的栈空间大小,从而得出最合理的大小。
联系客服