专栏首页嵌入式iotSTM32的RAM的分配与占用

STM32的RAM的分配与占用

1.介绍

本文主要针对如何合理的使用STM32的RAM角度入手,对STM32的RAM进行分配与计算。目的是降低RAM的使用率,将RAM的使用情况都弄清楚,从而合理的规划及分配内存。

本文涉及到一些堆栈方面的思考,在MDK中查看MAP文件及堆栈使用情况的文件进行分析,得出当前程序RAM的分配情况,同时对可以缩减的地方进行分析。

2.内存的基本构成

可编程内存基本上可以分为以下几个部分:静态存储区bss段、堆区和栈区。他们的功能不同,使用方法也就不同。

静态存储区:

静态存储区也就是BSS段,英文是Block Started by Symbol的简称,通常是指存放在未初始化的全局变量的一块内存区域,在程序载入时由内核清零。静态存储区在程序编译好的时候就已经分配好,这块内存在程序的整个运行期间都存在,主要存放静态数据全局数据和常量。

栈区:

在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元自动被释放,栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内容容量有限。

堆区:

也称动态内存分配。一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。分配方式类似于数据结构中的链表。程序在运行时候调用malloc或者new申请任意大小的内存,程序员自己负责在适当的时候free或者delete释放内存。动态内存的生存期可以由程序决定。良好的使用方法是:如果某动态内存不再使用,需要将其释放掉,否则,很容易出现内存泄漏现象。

其内存的分配规律:

如果系统调用了malloc,从0X20000000开始依次为:静态存储区+堆区+栈区

如果系统未调用了malloc,从0X20000000开始依次为:静态存储区+栈区

2.1 STM32的堆栈机制

要搞清楚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字节。这里的栈大小包括函数的嵌套,递归等等,都是从这个栈里面分配出来的。

所以如果一个函数的局部变量过多,或者嵌套层数越深,那么程序非常容易出现崩溃的情况。所以一定不要在函数里放过多的局部变量

堆的增长方向时向上的,而栈的增长方向时向下的,并且没有固定的界限,一旦堆栈冲突,函数就会崩溃。总体上也就是说,在使用堆栈的过程中,一定要确保堆栈的大小及使用情况。

2.2 OS系统内存分配策略

对于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内存中的一部分或者全部进行内存管理和分配。这样的情况下就只需要知道内存剩下部分的首地址和内存的尾地址,然后要用多少内存,就用首地址开始挖,做一个链表,把内存获取和释放相关信息链接起来,就能及时的对内存进行管理了。

3.STM32内存使用情况分析

如果要分析出STM32的内存使用情况,可借助KEIL中编译出来的map文件进行查阅

3.1 总体情况一览

首先可以在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。

3.2 各函数所需要的内存

以下是函数分配时的内存分配情况,可根据下面的map文件定位到具体的函数的内存使用情况,其中比较重要的是ZI Data,因为这些内存都是分配在RAM空间中的。

Image component sizes
Code (inc. data) RO Data RW Data ZI Data Debug Object Name
.
.
.

----------------------------------------------------------------------
       Object Totals
           (incl. Generated)
          (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左右的资源。下面来分析一下具体的内存使用情况。

3.3 操作系统RAM的使用情况

在操作系统中,使用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上的不只有线程的栈空间,还有线程控制块,每一个线程控制块为128字节,每申请一块内存都需要在在头部加上12字节的头部信息。所以在创建这些线程时,一共消耗rt_heap的资源为

也就是线程上消耗的rt_heap的大小为2632bytes。

还有邮箱与时间消耗的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上的内存空间为

3.优化内存使用策略

如果要优化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字节。

4.总结

STM32降低RAM的使用,本质上就是降低堆栈的使用。在这个过程中,只要搞清楚内存的分配方式以及OS的堆栈使用情况即可进行内存的合理规划。

对于降低RAM的过程,可以从以下方面入手,如果用局部变量,要考虑到栈的分配问题,栈空间的计算以函数最深的入栈开始,一层一层的计算累加,得到最大的栈的大小,由此,可以计算得到栈的大小。OS上的内存有着自己的一套内存管理方式,所以可以划分出一块静态区域给OS作为堆使用。OS申请的内存都是在这块区域内进行,计算各个线程的栈空间大小,从而得出最合理的大小。

本文分享自微信公众号 - 嵌入式IoT(Embeded_IoT),作者:bigmagic

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-04-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • rt-thread中的压栈与出栈分析

    本文主要想分析一下rt-thread中线程的压栈与入栈的相关操作。从而更好的掌握线程切换与线程恢复的相关知识。

    bigmagic
  • 嵌入式Linux上便捷开发环境搭建

    1.本文说明2.基本工具3.基本思想4.操作流程4.1 第一步ubuntu上安装tfp4.2 Windows上VS Code的操作5. 测试与使用6.总结

    bigmagic
  • RT-Thread在Cortex-A上下文切换流程分析

    这篇文章主要分析在arm cortex-a系列芯片上,上下文切换的分析,通过这篇文章,对大多数的cortex-a系列芯片的上下文切换有一定的了解。

    bigmagic
  • WMI技术介绍和应用——查询正在运行的线程信息

            本文使用了《WMI技术介绍和应用——使用VC编写一个半同步查询WMI服务的类》中代码做为基础。

    方亮
  • (经验技巧)Python中与并发的并行

    python中的并发是同时发生的事情由线程,任务,进程调用(实际上还是按顺序运行的一系列指令)。宏观上看,线程,任务和进程是相同的,细节上他们代表不同的东西。事...

    黄鸿波
  • USTC高级软件工程课程学习心得

      个人觉得,软件工程就是用工程化的思想去写代码,使得代码更加高效,这个高效不是指性能好,而是指提高开发效率,降低开发团队的成本。以前的编程的重心往往在算法的复...

    csxiaoyao
  • 探秘|为何最强人工智能比不上婴儿大脑?

    机器可以理解语音、识别面部和安全驾驶汽车。这让人们十分讶异于近期的技术方面的进步。但是,如果人工智能领域想要实现革命性的跨越,从而建造出类人式的机器,它首先将...

    灯塔大数据
  • 三分钟告诉你 1575119387982 是什么?

    说这个是数据库里字符串格式的时间戳,在网上找了两个小时没找到转为正常日期的解决方案,呆鸟一看就乐了,这不就是刚发的《Pandas 时间序列》系列文章里写过的纪元...

    石晓文
  • 一周极客热文:从分析8000条软件工程师招聘信息所学到的

    Aline Lerner 过去以编程谋生,现在从事招聘工程师的工作。去年,她通过参考全年的有效招聘数据编写了一篇文章,总结如下: 如果可以的话,尽可能让招聘信息...

    钱曙光
  • AI虽好,转行仍需谨慎

    最近不少同学在问我现在AI工程师工资这么高,想转行来学AI,让我给点意见或者建议。我在这里就集中说一下自己的看法。

    刀刀老高

扫码关注云+社区

领取腾讯云代金券