内存使用情况分析
下面根据两个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个区域:
| text | data | bss | dec | hex |
|---|---|---|---|---|
| 只读数据的大小 包括代码/常量 存储在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段大小:
上图中,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 | |
默认长度配置为4096,因此ucHeap数组长度为4096字节。
FreeRTOS的内核对象保存在ucHeap数组中,上方的LED闪灯程序,就有LED1、LED2、空闲任务、定时器任务、定时器命令队列任务这5个任务。
比如调用LED的任务创建时,实际动态分配了两部分的内存,一部分是TCB任务控制块(约100字节),另一部分是任务栈(任务栈深度×4个字节),即调用了两次pvPortMalloc()分配内存。
因此,全部的内核对象存在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. */ |
默认值为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. */ |
如果希望准确地获得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 */ |
串口助手接收到的结果: