FreeRTOS 内存优化

内存使用情况分析

下面根据两个LED灯闪烁的程序,分析内存使用情况

1234567891011121314151617181920212223242526272829
void vLED1Task(void *pvParameters){	while(1)	{		HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);		vTaskDelay(pdMS_TO_TICKS(1000));		HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);		vTaskDelay(pdMS_TO_TICKS(1000));	}	vTaskDelete(NULL);} void vLED2Task(void *pvParameters){	while(1)	{		HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET);		vTaskDelay(pdMS_TO_TICKS(300));		HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);		vTaskDelay(pdMS_TO_TICKS(300));	}	vTaskDelete(NULL);} /* USER CODE BEGIN 2 */xTaskCreate(vLED1Task, "LED1", 128, NULL, 1, NULL);xTaskCreate(vLED2Task, "LED2", 128, NULL, 1, NULL);vTaskStartScheduler();//启用调度器/* USER CODE END 2 */

编译成功后,在下面得到编译结果:
编译结果

在STM32中,SRAM被分为以下四个区域:

.data段.bss段.heap段.stack段
未分配的内存区域
存放有初始值的
全局变量/静态变量
存放没有初始值的
全局变量/局部变量
存放堆
malloc()/free()
存放栈
函数调用的返回地址、局部变量、参数
main()/中断服务程序

编译结果有5个区域:

textdatabssdechex
只读数据的大小
包括代码/常量
存储在Flash中
.data段的大小
上方图片中data段的大小为16字节
bss段+heap段+stack段的大小

由于编译结果现实的bss包括了.bss段+.heap段+.stack段的大小,因此想计算.bss段的大小时,需要减去.heap段+.stack段的大小。
.heap段+.stack段的大小可以在CubeIDE的配置界面查看,下图中Minimum Heap Size为.heap段大小,Minimum Stack Size为.stack段大小:
heap段和stack段的大小
上图中,Minimum Heap Size设置为0x200,即512字节,Minimum Stack Size设置为0x400,即1024字节。(16进制转换为10进制)
因此,.bss段的大小为:7552 - 512 - 1024 = 6016字节

由于STM32F103ZET6的内存SRAM总大小为64KB,即64 × 1024 = 65536字节
可以计算未分配内存的大小为:65536 - .data段 - .bss段 - .heap段 - .stack段 = 65535 - 16 - 6016 - 512 - 1024 = 57967字节 ≈ 56.608 KB

刚才的两个LED闪灯的程序,使用的是heap_4内存管理方案。
heap_4创建了ucHeap数组,作为堆内存,它是一个静态全局变量:

1
PRIVILEGED_DATA static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

堆内存的大小由configTOTAL_HEAP_SIZE这个配置项决定。
由于ucHeap是一个静态全局变量,且没有赋初值,所以存在.bss段中。

ucHeap数组长度由configTOTAL_HEAP_SIZE的值决定,可以在FreeRTOS的配置文件中找到:

1
#define configTOTAL_HEAP_SIZE 4096

默认长度配置为4096,因此ucHeap数组长度为4096字节。

FreeRTOS的内核对象保存在ucHeap数组中,上方的LED闪灯程序,就有LED1、LED2、空闲任务、定时器任务、定时器命令队列任务这5个任务。
比如调用LED的任务创建时,实际动态分配了两部分的内存,一部分是TCB任务控制块(约100字节),另一部分是任务栈(任务栈深度×4个字节),即调用了两次pvPortMalloc()分配内存。
调用了两次pvPortMalloc()分配内存

因此,全部的内核对象存在ucHeap中:
全部的内核对象存在ucHeap中

堆内存管理方案的选择

heap_1~heap_5都有其特点,只能使用一种。
FreeRTOS提供的五种堆内存管理方案,只有Heap_1和Heap_4比较常用,其他的极少会用到。

heap_1

极简的堆内存管理方案,适用只分配内存,但不释放内存的场景。
基于性能考虑,可以使用。

heap_4

推荐的管理方案,既可以创建又可以删除。
默认使用heap_4。

调整堆的大小

FreeRTOS和裸机程序的一个区别是,FreeRTOS会有一个自行管理的堆ucHeap,位于.bss段,长度由配置项configTOTAL_HEAP_SIZE决定。

打开Core -> Inc -> FreeRTOSConfig.h,修改下面的配置:

123456
/* Sets the total size of the FreeRTOS heap, in bytes, when heap_1.c, heap_2.c* or heap_4.c are included in the build. This value is defaulted to 4096 bytes but* it must be tailored to each application. Note the heap will appear in the .bss* section. See https://www.freertos.org/a00111.html. */ #define configTOTAL_HEAP_SIZE 4096

默认值为4096,即FreeRTOS堆的大小为4096字节。
应该根据项目的需要,去修改这个值。存储的内核对象越多,占用的堆内存越多,因此configTOTAL_HEAP_SIZE越大。因该设置一个合适的值,如果设置过大,会有大量空间用不到,造成浪费;如果设置过小,内存不够,任务创建会失败。

创建任务时使用的xTaskCreate,返回值BaseType_t,会有两种结果,一种是pdPASS代表任务创建成功,另一种返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY内存创建失败。
参考下方程序:

1234567891011
BaseType_t xReturn;//声明一个变量接收返回值 //创建LED1闪灯任务xReturn = xTaskCreate(vLED1Task, "LED1", 128, NULL, 1, NULL);//使用configASSERT()进行断言,断言失败会卡在这一行,调试时及时发现configASSERT( xReturn == pdPASS ); //创建LED2闪灯任务xReturn = xTaskCreate(vLED2Task, "LED2", 128, NULL, 1, NULL);//使用configASSERT()进行断言,断言失败会卡在这一行,调试时及时发现configASSERT( xReturn == pdPASS );

此时,有五个任务:空闲任务、定时器任务、LED1、LED2、定时器命令队列。
定时器任务和定时器命令队列可以关闭,如果没有使用到,通过configUSE_TIMERS关闭:

1234567
/* Set configUSE_TIMERS to 1 to include software timer functionality in the* build. Set to 0 to exclude software timer functionality from the build. The* FreeRTOS/source/timers.c source file must be included in the build if* configUSE_TIMERS is set to 1. Default to 0 if left undefined. See* https://www.freertos.org/RTOS-software-timer.html. */ #define configUSE_TIMERS 1

如果希望准确地获得configTOTAL_HEAP_SIZE的值,通过下方接口获得:

123
//作用:获取当前堆内存的剩余量//返回:获取堆内存的大小,单位是字节size_t xPortGetFreeHeapSize( void )
123
//作用:获取历史以来堆内存的最小剩余量//返回:历史以来堆内存的最小剩余量,单位是字节size_t xPortGetMinimumEverFreeHeapSize( void )

使用串口打印当前堆内存的剩余量和历史以来堆内存的最小剩余量:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
/* Private user code ---------------------------------------------------------*//* USER CODE BEGIN 0 */void vLED1Task(void *pvParameters){	while(1)	{		HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);		vTaskDelay(pdMS_TO_TICKS(1000));		HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);		vTaskDelay(pdMS_TO_TICKS(1000));	}	vTaskDelete(NULL);} void vLED2Task(void *pvParameters){	while(1)	{		HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET);		vTaskDelay(pdMS_TO_TICKS(300));		HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);		vTaskDelay(pdMS_TO_TICKS(300));	}	vTaskDelete(NULL);} void vSystemInfoTask(void *pvParameters){	char buffer[64];	size_t current,minimum; 	for(;;)	{		//打印当前堆内存剩余的字节数		current = xPortGetFreeHeapSize();		sprintf(buffer,"当前堆内存剩余 = %d\n",current);		HAL_UART_Transmit(&huart1, (const uint8_t *)buffer, strlen(buffer), HAL_MAX_DELAY); 		//打印历史最小剩余堆内存		minimum = xPortGetMinimumEverFreeHeapSize();		sprintf(buffer,"历史最小剩余堆内存 = %d\n",minimum);		HAL_UART_Transmit(&huart1, (const uint8_t *)buffer, strlen(buffer), HAL_MAX_DELAY); 		//间隔1s打印一次		vTaskDelay(pdMS_TO_TICKS(1000));	}}/* USER CODE END 0 */    /* USER CODE BEGIN 2 */  BaseType_t xReturn;//声明一个变量接收返回值   //创建LED1闪灯任务  xReturn = xTaskCreate(vLED1Task, "LED1", 128, NULL, 1, NULL);  //使用configASSERT()进行断言,断言失败会卡在这一行,调试时及时发现  configASSERT( xReturn == pdPASS );   //创建LED2闪灯任务  xReturn = xTaskCreate(vLED2Task, "LED2", 128, NULL, 1, NULL);  //使用configASSERT()进行断言,断言失败会卡在这一行,调试时及时发现  configASSERT( xReturn == pdPASS );   //创建System Info任务  xReturn = xTaskCreate(vSystemInfoTask, "System Info", 128, NULL, 1, NULL);  //使用configASSERT()进行断言,断言失败会卡在这一行,调试时及时发现  configASSERT( xReturn == pdPASS );   vTaskStartScheduler();//启用调度器  /* USER CODE END 2 */

串口助手接收到的结果:
串口助手接收到的结果

🔍 ×