STM32F1+HAL库+FreeTOTS学习21——内存管理
- 1. 内存管理简介
- 2. 内存管理相关的API函数
- 3. 内存管理算法
- 4. 内存管理实验
- 4.1. 实验内容
- 4.2 代码实现
- 4.3 运行结果
- 5. 总结
上一期我们学习了FreeRTOS中的低功耗Tickless模式,这一期我们学习最后一个章节:内存管理
1. 内存管理简介
存管理是一个系统的基本组成部分,在 FreeRTOS 中大量地使用了内存管理,比如创建任务、信号量、队列等对象时,都可以从 FreeRTOS 管理的堆中申请内存。FreeRTOS 也向用户提供了应用层的内存申请与释放函数。
但是我们在学习C语言的时候,也会用到一些内存管理的函数,比如malloc、realloc、free、memcpy等等,只需要我们去包含否文件 <stdlib.h>即可,非常方便,为什么要单独开辟一个章节来介绍FreeRTOS中的内存管理呢,直接用C库的不好吗。其实这里面是有门道的,且听我娓娓道来:
使用FreeRTOS自己的内存管理而不用C库的原因有下面几点:
-
实时性要求:FreeRTOS 是为嵌入式系统设计的实时操作系统,内存管理的实时性至关重要。标准 C 库的动态内存分配(如 malloc 和 free)通常会引入不确定的延迟,可能导致任务的响应时间不一致,从而影响实时性能。
-
安全性:使用C库的内存管理方法,没有线程安全的相关机制,可能会引入不必要的问题。
-
内存碎片化:在嵌入式系统中,内存资源通常非常有限。标准的 C 库内存管理可能导致内存碎片化,降低可用内存。而 FreeRTOS 的内存管理机制可以通过固定大小的块分配和管理来减小碎片化的风险。
-
可定制性:FreeRTOS 提供了不同的内存分配策略,允许开发者根据特定应用的需求进行定制。这种灵活性使得开发者可以选择最适合其系统和应用场景的内存管理方案。
-
小型化和优化:FreeRTOS 设计时考虑了嵌入式系统的资源限制,因此其内存管理方案相对简洁,开销小,能够更好地适应低功耗和小型设备的需求。
-
简单性:在一些简单的应用中,FreeRTOS 的内存管理可以提供比标准库更简单的接口,降低了开发的复杂性。
鉴于以上为种种问题,FreeRTOS中提供了多种不同的内存管理方法,以适应不同嵌入式系统的需求,供用户自行选择,移植到自己的系统当中。
前面我们在介绍FreeRTOS移植的时候,就有过这样一步:
【注】:如果忘记了,可以回去看STM32F1+HAL库+FreeTOTS学习2——STM32移植FreeRTOS
这里面的MemMang存放的就是内存管理的方式,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c
下面我们来介绍以下这五种内存管理方法的特点:
- heap_1:最简单,只允许申请内存,不允许释放内存。
- heap_2:允许申请和释放内存,但不能合并相邻的空闲内存块。
- heap_3:简单封装 C 库的函数 malloc()和函数 free(),以确保线程安全。
- heap_4:允许申请和释放内存,并且能够合并相邻的空闲内存块,减少内存碎片的产生。
- heap_5:能够管理多个非连续内存区域的 heap_4。
无论我们使用的是哪一个内存管理方法,他们的API函数都是一样的,下面我们来看一下,FreeRTOS中的API函数
2. 内存管理相关的API函数
- pvPortMalloc函数
此函数用于在FreeRTOS中申请内存,在portable.h文件中有定义,下面我们来看以下函数原型:
/*** @brief pvPortMalloc:在FreeRTOS中申请内存* @param xSize : 申请内存的大小* @retval 返回值为该内存的起始地址*/
void * pvPortMalloc( size_t xSize );
- vPortFree函数
此函数用于在FreeRTOS中释放内存,在portable.h文件中有定义,下面我们来看以下函数原型:
/*** @brief vPortFree:在FreeRTOS中释放内存* @param pv : 释放内存的起始地址* @retval 无*/
void vPortFree( void * pv );
- xPortGetFreeHeapSize函数
此函数用于在FreeRTOS中获取当下剩余的内存大小,在portable.h文件中有定义,下面我们来看以下函数原型:
/*** @brief xPortGetFreeHeapSize:在FreeRTOS中获取当下剩余的内存大小* @param 无* @retval 返回值为剩余的内存大小*/
size_t xPortGetFreeHeapSize( void );
- xPortGetMinimumEverFreeHeapSize函数
此函数用于在FreeRTOS中获取历史剩余的大小最小值,在portable.h文件中有定义,下面我们来看以下函数原型:
/*** @brief xPortGetMinimumEverFreeHeapSize:在FreeRTOS中获取历史剩余的内存最小值* @param 无* @retval 返回值为历史剩余的内存最小值*/
size_t xPortGetFreeHeapSize( void );
上面就是FreeRTOS中内存管理的常用API函数。
3. 内存管理算法
前面我们说到:heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c。这5个文件中对于内存管理的实现方式各不相同,归根结底就是使用了不同的内存管理方式(算法),但是我们这里不做过多的介绍,因为实在是太多了,感兴趣的可以自己去看一下:正点原子FreeRTOS开发指南中的内存管理章节,具体链接我会放在结尾。
4. 内存管理实验
4.1. 实验内容
在STM32F103RCT6上运行FreeRTOS,通过按键控制,完成对应的内存管理操作,具体要求如下:
- 创建1个任务,用来按键扫描:
- 按键0按下,申请内存
- 按键1按下:释放内存
- 每1s打印一次系统的堆栈剩余情况
- 每完成一个步骤,通过串口打印相关信息。
4.2 代码实现
- 由于本期内容涉及到按键扫描,会用到: STM32框架之按键扫描新思路 ,这里不做过多介绍。我们直接来看代码:
- freertos_demo.c
#include "freertos_demo.h"
#include "main.h"
#include "task.h"
#include "key.h" //包含按键相关头文件/*FreeRTOS*********************************************************************************************//******************************************************************************************************/
/*FreeRTOS配置*//* TASK1 任务 配置* 包括: 任务句柄 任务优先级 堆栈大小 */
#define TASK1_PRIO 1 /* 任务优先级 */
#define TASK1_STK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t Task1Task_Handler; /* 任务句柄 */
void task1(void *pvParameters); /*任务函数*//******************************************************************************************************//*** @brief FreeRTOS例程入口函数* @param 无* @retval 无*/
void freertos_demo(void)
{taskENTER_CRITICAL(); /* 进入临界区,关闭中断,此时停止任务调度*//* 创建任务1 */xTaskCreate((TaskFunction_t )task1,(const char* )"task1",(uint16_t )TASK1_STK_SIZE,(void* )NULL,(UBaseType_t )TASK1_PRIO,(TaskHandle_t* )&Task1Task_Handler);taskEXIT_CRITICAL(); /* 退出临界区,重新开启中断,开启任务调度 */vTaskStartScheduler(); //开启任务调度
}/*** @brief task1* @param pvParameters : 传入参数(未用到)* @retval 无*/
void task1(void *pvParameters)
{size_t freeSize = 0; /* 空闲堆栈大小 */uint16_t count = 0; while(1){Key_One_Scan(Key_Name_Key0,Key0_Up_Task,Key0_Down_Task); /* 按键0扫描,按下申请内存堆栈 */Key_One_Scan(Key_Name_Key1,Key1_Up_Task,Key1_Down_Task); /* 按键0扫描,按下释放内存堆栈 */if(++count > 100){/* 1s打印一次剩余堆栈空间 */freeSize = xPortGetFreeHeapSize();printf("剩余堆栈空间:%d\r\n",freeSize);count = 0;}vTaskDelay(10);}
}
- key.c
/* USER CODE BEGIN 2 */#include "freertos_demo.h"
#include "key.h"
#include "usart.h"/* 申请的内存地址 */
uint8_t *buf = NULL;/* 按键0按下申请内存 */
void Key0_Down_Task(void)
{/* 申请大小为100的堆栈空间,返回值为堆栈空间的起点 */buf = pvPortMalloc(100);printf("bufAddress: %p\r\n",buf);
}void Key0_Up_Task(void)
{}/* 按键1按下释放内存 */
void Key1_Down_Task(void)
{/* 释放内存,如果buf为NULL,则直接返回 */vPortFree(buf);/* 正常释放之后需要将buf置为NULL,但是由于这里buf被重复使用,vPortFree()函数传入NULL,会直接返回,不会报错。为了使效果更明显,这里就补注释了*/
// buf = NULL;printf("bufDeleted\r\n");
}
void Key1_Up_Task(void)
{}
void Key2_Down_Task(void)
{}
void Key2_Up_Task(void)
{}
void WKUP_Down_Task(void)
{}
void WWKUP_Up_Task(void)
{}void Key_One_Scan(uint8_t KeyName ,void(*OnKeyOneUp)(void), void(*OnKeyOneDown)(void))
{static uint8_t Key_Val[Key_Name_Max]; //按键值的存放位置static uint8_t Key_Flag[Key_Name_Max]; //KEY0~2为0时表示按下,为1表示松开,WKUP反之Key_Val[KeyName] = Key_Val[KeyName] <<1; //每次扫描完,将上一次扫描的结果左移保存switch(KeyName){case Key_Name_Key0: Key_Val[KeyName] = Key_Val[KeyName] | (HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin)); //读取Key0按键值break;case Key_Name_Key1: Key_Val[KeyName] = Key_Val[KeyName] | (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin)); //读取Key1按键值break;case Key_Name_Key2: Key_Val[KeyName] = Key_Val[KeyName] | (HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin)); //读取Key2按键值break;
// case Key_Name_WKUP: Key_Val[KeyName] = Key_Val[KeyName] | (HAL_GPIO_ReadPin(WKUP_GPIO_Port, WKUP_Pin)); //读取WKUP按键值
// break; default:break;}
// if(KeyName == Key_Name_WKUP) //WKUP的电路图与其他按键不同,所以需要特殊处理
// {
// //WKUP特殊情况
// //当按键标志为1(松开)是,判断是否按下,WKUP按下时为0xff
// if(Key_Val[KeyName] == 0xff && Key_Flag[KeyName] == 1)
// {
// (*OnKeyOneDown)();
// Key_Flag[KeyName] = 0;
// }
// //当按键标志位为0(按下),判断按键是否松开,WKUP松开时为0x00
// if(Key_Val[KeyName] == 0x00 && Key_Flag[KeyName] == 0)
// {
// (*OnKeyOneUp)();
// Key_Flag[KeyName] = 1;
// }
// }
// else //Key0~2按键逻辑判断
// {//Key0~2常规判断//当按键标志为1(松开)是,判断是否按下if(Key_Val[KeyName] == 0x00 && Key_Flag[KeyName] == 1){(*OnKeyOneDown)();Key_Flag[KeyName] = 0;}//当按键标志位为0(按下),判断按键是否松开if(Key_Val[KeyName] == 0xff && Key_Flag[KeyName] == 0){(*OnKeyOneUp)();Key_Flag[KeyName] = 1;} }//}
/* USER CODE END 2 */
4.3 运行结果
【注1】:运行结果中的报错是因为重复释放同一块内存导致的,代码中有解释过,需要注意一下。
【注2】:如果重复申请内存,但是获取内存地址的指针是同一个,对导致申请完内存之后的上一片内存丢失,而导致内存泄漏,这种情况需要避免发生。
5. 总结
不出意外的话,这一期将会是我们FreeRTOS入门系列的最后一期内容(当然后面如果遇到了相关的问题,我也会记录在这里面的),回想一路走来,感触颇深啊,不过也确实学到了很多内容,对FreeRTOS有了较为深入的理解。我把这段时间学习的内容整理了以下,放到gitee上面去了,如果有需要的话,自行食用哈(只适用于个人学习,出了问题概不负责!!!)。
这里是仓库地址:https://gitee.com/bu-xiang-xie-dai-ma-de-wo/free-rtos-code
以上就是本期的所有内容,创造不易,点个关注再走呗。