RT-Thread中堆和栈内存的分配

在嵌入式软件开发中,我们经常会提到堆和栈,实际上堆和栈都是RAM上的物理内存空间,只是使用方式不同而已。栈和堆都是单片机RAM中一段连续的存储空间,该段空间一般在启动文件或链接脚本中指定,最后在C库的_main函数中进行初始化。

STM32中的堆栈内存空间分配就在启动文件中完成:

栈(STACK):由编译器自动分配和释放

堆(HEAP):有的地方也叫堆栈,一般由用户自行分配和释放,因此在分配好使用完成后要及时释放内存,否则会导致系统可用的内存越来越少,我们管这种情况叫做内存泄漏

使用MDK5进行STM32项目开发时,当点击全部编译后会在Build Output窗口生成如下信息:

那么这些信息究竟是代表什么意思?

  1. Code:代码段,存放程序的代码部分
  2. RO-Data:只读数据段,存放程序中定义的常量
  3. RW-Data:读写数据段,存放初始化为非0值得全局变量
  4. ZI-Data:0数据段,存放初始化为0的全局变量或未初始化的全局变量(程序运行时会对未初始化的全局变量自动清0)

需要注意的是,启动文件中定义的STACK和HEAP内存空间都是被包含到ZI-Data中的,如下图片可对比说明:

当STACK空间大小设置为0x400时ZI-Data空间大小为2908Byte,设置为0x200时ZI-Data空间大小为2396Byte。2908-2396 = 512 = 0x200,因此可以说明栈空间是被包含在ZI-Data中的。将HEAP空间大小由0x200设置为0x100时,ZI-Data空间大小由2396变为2140,2396 - 2140 = 256,因此可以证明堆空间是被包含在ZI-Data中的。

在工程编译完成后也会生成的对应.map文件,.map文件中详细描述了各个函数在ROM中的存储地址和大小,也可以看到程序中定义的全局变量、全局数组、常亮等在RAM中的存储地址和大小,因此.map文件是非常重要的一个文件。在生成的.map的最后几行,也可以看到如下信息:

那么这些信息究竟是代表什么意思?

  1. RO Size:程序在ROM中的存储大小,包括Code段和RO-Data段
  2. RW Size:程序运行时占用RAM空间的大小,包括RW-Data段和ZI-Data段
  3. ROM Size:程序烧写到ROM上时占用的空间大小,包括Code段和RO-Data段、RW-Data段(这里不包含ZI-Data段是因为ZI-Data段全部是0,将其存储在ROM中毫无意义,因此只需记录ZI-Data段占用的内存空间,在程序运行时在RAM上开辟对应大小的内存空间并将该区域清0即可)
const int ci_max_len = 100;        //RO段
static int si_loop_time = 300;     //RW
char *p1;                          //ZIint main(void)
{int i = 0;                       //STACKchar c_say_a[] = "hello world";  //c_say_a存储于STACK,"hello world"存储于ROchar *p= &s[0];                  //STACKstatic int si_i = 5;             //RWchar *p2;                        //STACKp1 = (char *)malloc(100);        //HEAPp2 = (int *)malloc(100);         //HEAP...free(p1);free(p2);return 0;
}

Cortex-M3和Cortex-M4在设计之初就考虑到了对OS的高效支持,主要有3点:

  1. 它们都具有一个内置的的简单的向下计数24位计数器SysTick。之所以要在处理器内增加一个定时器,主要是为了提高软件移植的方便性。所有基于Cortex-M3和Cortex-M4内核的处理器具有相同的SysTick定时器,在一种Cortex-M3/M4处理器上实现的OS也能适用于其它Cortex-M3/M4处理器。因此,嵌入式OS的源码可以很容易的移植到其它Cortex-M3/M4内核的处理器上,无需为设备相关的定时器做修改。
  2. Cortex-M3内核支持双堆栈机制,即主堆栈MSP和线程堆栈PSP。OS内核和系统中的中断或异常使用MSP,用户创建的多线程任务使用PSP。
  3. Cortex-M3/M4内核支持特权模式和非特权模式,可以将用户创建的多线程任务在非特权操作模式中运行,这样可以限制其对一些寄存器的访问,如NVIC、CONTROL寄存器等,以免因为不可靠的程序引起整个系统的崩溃。

在裸机系统中,所有的变量、函数调用、中断处理等使用的栈空间均是MSP。在使用RTOS的多线程系统中,每个线程都是完全独立互不干扰的,因此需要为每个线程分配独立的栈空间,这个栈空间可以是预先分配好的全局数组(即存储于RW-Data段或ZI-Data段),也可以是动态分配的一段内存空间(注意:这里动态分配的空间不是启动文件中定义的堆空间,而是由RTOS管理的内存空间),但本质上它们都是RAM上的一段内存空间。

在RT-Thread中若需要动态内存管理,则需要先调用board.c--->rt_hw_board_init()--->rt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END);函数进行动态内存管理范围的配置,即将HEAP_BEGIN和HEAP_END之间的内存空间作为动态内存空间交由RT-Thread进行管理。

在RT-Thread Master和RT-Thread Nano版本中,对于rt_system_heap_init()函数调用时填入的参数有些许差异,下面进行对比说明。

RT-Thread Master版本:

RT-Thread Master版本默认开启RT-Thread中的动态内存分配,并将将HEAP_BEGIN和HEAP_END之间的内存空间作为动态内存空间交由RT-Thread进行管理。其中在board.h文件中有定义,具体如下:

Image$$RW_IRAM1$$ZI$$Limit是一个链接器导出的符号,代表ZI-Data段的结束,也即程序执行时所占RAM空间的结束地址,也是未使用RAM 空间的起始地址。因此RT-Thread Master版本中会默认将RAM中剩余的内存空间全部交给RT-Thread进行动态内存管理。

RT-Thread Nano版本:

RT-Thread Nano版本默认不开启RT-Thread中的动态内存分配,因此凡是涉及到使用RT-Thread动态内存分配的函数都不能使用,如在创建线程时无法使用rt_thread_create()函数,只能使用rt_thread_init()函数等。用户需开启rtconfig.h中的RT_USING_HEAP宏开启使用RT-Thread进行动态内存分配的功能,因为动态内存分配中有使用到信号量,因此还需打开RT_USING_SEMAPHORE宏。rt_system_heap_init()函数中使用的参数rt_heap_begin_get(), rt_heap_end_get()均在board.c文件中有定义,具体如下:

可以看到RT-Thread Nano版本中是将用户定义好的大小为1024 * 4个字节的内存空间交由RT-Thread进行动态内存管理,而非将RAM中剩余的内存空间全部交给RT-Thread进行动态内存管理。

这里需要明白的是,程序中并不是将RAM空间用完的,假使RAM为20KByte,实际上只使用了8560Bytes,则剩余的RAM空间全部闲置在那儿。因此在RT-Thread Nano版本中使用数组作为动态内存堆时,可能会有一部分RAM空间是闲置的。

当把RT_HEAP_SIZE由1024 * 4Byte改为256 * 4Byte时,生成的工程信息中只有ZI-Data发生变化。由11124变为8052,即11124 - 8052 = 3072Byte,和程序中的变化值相等。因此,我们可以理解为:RT-Thread Nano是通过定义全局数组的方式在ZI-Data段占据了一段空间,并将这段空间交由RT-Thread进行动态内存分配。

如果不想使用RT-Thread-Nano中使用数组作为动态内存堆的方式,也可以使用和RT-Thread-Master中同样的方法将ZI段结束作为动态内存堆起始地址,将RAM空间结束地址作为动态内存堆的结束地址。这样可以将程序运行所需RAM空间之余的全部空间作为动态内存堆使用,即RAM上没有闲置的内存空间。

将board.c中的代码作如下修改:

其中#ifndef USE_ARRAY_AS_RTT_HEAP和#else之间的为新增加的代码,当定义USE_ARRAY_AS_RTT_HEAP宏时使用用户自定义数组作为动态内存堆,否则则使用ZI段结束到RAM空间结束之间的内存作为动态内存堆。

//#define USE_ARRAY_AS_RTT_HEAP	//定义该宏后使用用户自定义的数组作为RT-Thread的动态内存堆#ifndef USE_ARRAY_AS_RTT_HEAP#define STM32_SRAM1_START              (0x20000000)      #define STM32_SRAM1_END                (STM32_SRAM1_START + 20 * 1024)   // 结束地址 = 0x20000000(基址) + 20K(RAM大小)#if defined(__CC_ARM) || defined(__CLANG_ARM)extern int Image$$RW_IRAM1$$ZI$$Limit;                   // RW_IRAM1,需与链接脚本中运行时域名相对应#define HEAP_BEGIN      ((void *)&Image$$RW_IRAM1$$ZI$$Limit)#endif#define HEAP_END                       STM32_SRAM1_END
#else#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)#define RT_HEAP_SIZE 1024static uint32_t rt_heap[RT_HEAP_SIZE];     // heap default size: 4K(1024 * 4)RT_WEAK void *rt_heap_begin_get(void){return rt_heap;}RT_WEAK void *rt_heap_end_get(void){return rt_heap + RT_HEAP_SIZE;}#endif
#endif/*** This function will initial your board.*/
void rt_hw_board_init()
{/* this function is defined in main.c, so use extern indicate this is an external function */extern void SystemClock_Config(void);	HAL_Init();SystemClock_Config();	/* System Clock Update */SystemCoreClockUpdate();/* System Tick Configuration */_SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);/* Call components board initial (use INIT_BOARD_EXPORT()) */
#ifdef RT_USING_COMPONENTS_INITrt_components_board_init();
#endif
#define RT_USING_USER_MAIN
#define RT_USING_HEAP
#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)#ifndef USE_ARRAY_AS_RTT_HEAPrt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END);#elsert_system_heap_init(rt_heap_begin_get(), rt_heap_end_get());#endif
#endif
}

编译后生成的.map文件如下所示,可以看到用户自定义的数组消失了,程序使用栈顶地址0x20002b90和RAM空间结束地址0x20005000之间的RAM空间作为RT-Thread的动态内存堆。

当然,RT-Thread Master中也可以使用RT-Thread Nano中默认的使用用户自定义的数组作为动态内存堆的方式。具体如何使用RAM空间,是很灵活的可以由用户自行指定的。当你深入了解RAM空间的分配原理后(一定要研究.map文件,开发中很有用的一个文件),就会发现用户就像是单片机的上帝,上帝可以任意支配手中的资源。而如何让单片机中这些资源按照用户的意愿去分配、去实现既定的功能是我们用户需要去好好研究的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/429556.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RT-Thread Nano版本和RT-Thread Master版本的关系

RT-Thread Nano版本和RT-Thread Master版本的关系: RT-Thread Master版本不仅仅是一个实时内核,还包含了丰富的中间层组件。 RT-Thread Nano是RT-Thread Master之上进行剪裁后形成的精简版,去掉了一些组件和各种开发板的BSP,仅保…

VSCODE修改文字编码格式为GB2312和TAB键为2个空格(MDK5和VSCODE联合开发STM32程序)

在MDK5中,代码、变量等的高亮显示或代码自动补全做的不是很好,经常会出现在A函数中有变量的高亮功能,在B函数中就没有了。特别是在RT-Thread创建的多个线程中,发现同一文件中的其它函数中的变量都有同一变量高亮,在RT-…

IP选路

一、IP层工作流程  二、 简单路由表 输入netstate -rn,得到如下 其中flag的标志如下 U 该路由可以使用G 该路由是到一个网管(路由器),否则,说明目的地是直接相连的 该标志区分了间接路由和…

[转]微信小程序安全浅析

本文转自:http://blog.csdn.net/baize_security/article/details/54582854 引言 近期微信小程序重磅发布,在互联网界掀起不小的波澜,已有许多公司发布了自己的小程序,涉及不同的行业领域。大家在体验小程序用完即走便利的同时&…

STM32 MDK编译后生成的 .map文件深入分析

.map文件是STM32开发中非常重要的一个文件,在该文件中可以详细的查看单个文件、函数及用户定义的全局变量等的占用RAM和ROM(一般为片内FLASH)的空间大小,通过了解这些信息可以很方便的进行代码的优化。 在MDK5中,生成…

C#程序的组织结构

C#程序的组织结构: namespace(命名空间): C#程序中的一种代码组织形式,主要用来标识类的可见范围。一个namespace中包含了一系列的类,一般一个文件中会使用多个using语句引入多个命名空间。 语法&#xff…

hihocoder1477 闰秒

地址:http://hihocoder.com/problemset/problem/1477 题目: 闰秒 时间限制:10000ms单点时限:1000ms内存限制:256MB描述 计算机系统中使用的UTC时间基于原子钟,这种计时方式同“地球自转一周是24小时”的计时方式有微小的偏差。为了弥补这种偏…

C#命名空间namespace中不能直接包含字段(变量)或方法(函数)之类的成员

C#命名空间即namespace中不能直接包含字段(变量)或方法(函数)之类的成员,须将字段或方法放到类class中,否则编译器会报错。 C#命名空间中不能直接定义字段(变量): 将val…

Lucene实战之初体验

前言 最早做非结构化数据搜索时用的还是lucene.net,一直说在学习java的同时把lucene这块搞一搞,这拖了2年多了,终于开始搞这块了。 开发环境 idea2016、lucene6.0、jdk1.8 使用lucene准备条件 1、pom.xml 2、测试数据。 我从博客园首页拿了几…

C#中变量(成员变量、局部变量、全局变量)的作用域

不管在任何编程语言中都有变量的定义,变量就像是一个容器,不同的变量会在内存中占据不同大小的内存空间。定义变量后会将分配的地址绑定在这个变量名上,以后对该变量名的操作就是对该内存地上存储内容的操作。 namespace test {class myTest…

Servelt中的ServletContext对象

转载于:https://www.cnblogs.com/yxh-only/p/6548046.html

export function函数传参_从底层看前端(七)—— JavaScript到底有多少种函数?

在上篇文章中我们了解到了执行上下文是什么,也知道了任何语句的执行都会依赖特定的上下文。一旦上下文被切换,整个语句的效果可能都会发生变化。那么,切换上下文的时机就显得非常重要。在JavaScript中,切换上下文最主要的场景就是…

liunx常用命令0

1 开启Linux操作系统,要求以root用户登录GNOME图形界面,语言支持选择为汉语 点击“未列出?”-->输入root和密码 2 使用快捷键切换到虚拟终端2,使用普通用户身份登录,查看系统提示符 ctrlaltf2 3 使用命令退出虚拟终…

2个td合成一个td_18个月16个爆款,合成类玩法的下一个机会在哪?

18个月16个爆款!近日,编者体验了近18个月爆款小程序榜单之中的游戏,体验之后编者发现,在这200多款游戏中,有16款都应用了合成类玩法,而且部分游戏是数次登榜,比如枪火工厂、全民养鲲、世界争霸等…

在线代码图片生成工具carbon

在日常工作中时常需要和同事间进行代码的沟通和交流,有时只是需要讨论某一段代码的内容,因此不必将整个文件发给同事。通常可以将部分代码进行截图,或者直接将部分代码复制粘贴发送给同事。但以上方法或因为代码太长需要多次截图,…

antd 日期时间选择_Excel最全时间类函数总结,有必要收藏一下哦

Excel数据格式中,一共分十一类,其中两类分别是日期与时间。Excel中存在大量公式用于处理这两个类型的数据,下面一一介绍与之相关的函数。年月日函数Excel函数中分别用year()、month()、day()函数返回一日期的年、月、日,这三个函数…

C#中的变量类型(值类型、引用类型)

C#中的变量类型: 值类型:值类型直接存储的是变量的值,变量空间在栈上分配,分配速度比较快。给变量赋值时需注意变量类型的取值范围,给变量赋不合理的值会导致编译器报错。布尔类型的变量只有两种可选择的值true/false&…

Java:IDEA下使用JUNIT

单元测试的基本使用 一、环境配置 使用idea IDE 进行单元测试,首先需要安装JUnit 插件。 1.安装JUnit插件步骤 File-->settings-->Plguins-->Browse repositories-->输入JUnit-->选择JUnit Generator V2.0安装。 2.使用JUnit插件 在需要进行单元测试…

arcore之路-unity开发从入门到实践_Unity游戏开发——单例模式的最佳实践

0.前言StarryFun:Unity游戏开发——关于单例模式的理解​zhuanlan.zhihu.com之前一篇文章讲了单例模式的简单理解,自知其中有很多不严谨的地方,由于本萌新也是在学习阶段,所以去翻看了开源的项目都是怎么实现的,发现了…