堆就是一段能动态分配的内存。
在STM32种,内存被分为4个段:.data、.bss、.heap、.stack
其中:.heap段就是能动态分配的内存区域,即单片机中的堆。
在标准的C语言中,我们可以用malloc()方法动态分配一段内存,通过free()方法释放内存。
123456789101112131415161718 | /* 代码功能: 1. 要求用户通过控制台输入n个整数 2. 把所有n个整数通过控制台打印出来 */int main(void){ // #1. 通过控制台接收用户输入的数字n int n; printf("请输入数字的个数n的值: "); scanf("%d", &n); // #2. 创建一个长度为n的int数组 int *pBuffer = (int *)malloc(sizeof(int) * n); // #3. 通过控制台接收用户输入的所有n个数字 printf("请输入所有的数字: "); for(int i=0; i<n; i++) scanf("%d", &pBuffer[i]); // #4. 把接收到的数字打印出来 printf("你输入的数字是: \n"); for(int i=0; i<n; i++) printf("%d ", pBuffer[i]); // #5. 调用free()方法释放掉刚才动态分配的内存 free(pBuffer);} |
FreeRTOS提供了5种堆内存管理方案:heap1、heap2、heap3、heap4、heap5
创建内核对象需要消耗内存,比如创建新任务时:
1 | xTaskCreate(vTask1,"Task1",128,NULL,1,NULL); |
FreeRTOS会为这个新任务,分配一段内存,来存储TCB(任务控制块,大小约100字节)和任务栈(任务栈深度*4字节=512字节),总共消耗大约600字节。
FreeRTOS使用动态方式分配这些内存,在堆中分配。
FreeRTOS为什么不使用标准C语言的malloc()和free()?
FreeRTOS自己创建了一套堆内存管理方案,即:heap1、heap2、heap3、heap4、heap5
FreeRTOS不使用malloc()和free()的原因:
1、标准C语言库的malloc()速度慢且不确定,影响实时性。
2、malloc()和free()容易产生内存碎片化的问题
3、malloc()和free()不具有可重入性
可重入:一个函数同时被多个任务调用而不出错。
例如:HAL_UART_Transmit就是一个不可重入函数,因为会竞争使用UART接口,会产生意想不到的结果。
在裸机程序中,同时只有一个程序在运行,即使一个函数不可重入,也没有影响。
但是在FreeRTOS中,包含多个任务,有可能同时使用malloc()方法分配内存,导致出错。
因此FreeRTOS自己创建了一套堆内存管理方案。
五种堆内存管理方案
FreeRTOS提供的5种堆内存管理方案分别是:heap1、heap2、heap3、heap4、heap5
| 标准C语言库 | FreeRTOS | |
|---|---|---|
| 分配内存 | malloc() | pvPortMalloc() |
| 释放内存 | free() | vPortFree() |
FreeRTOS提供的pvPortMalloc()与vPortFree()这两个编程接口和C语言标准库提供的两个堆内存管理接口使用方式是完全一致的。
例:使用STM32串口,用户通过串口发送n个整数,单片机把所有n个整数通过串口打印出来,功能参考上面的C语言代码。
123456789101112131415161718192021222324252627282930313233 | /* Private user code ---------------------------------------------------------*//* USER CODE BEGIN 0 */void vTask1(void *pvParameter){ //1.从串口接收数字n uint8_t n; HAL_UART_Receive(&huart1,&n,1,HAL_MAX_DELAY); //2.分配一个长度为n的数组 int *pBuffer = (int *)pvPortMalloc(sizeof(int) * n); //3.接收用户输入的n个整数,并保存到分配好的数组中 for(int i = 0; i < n; i++) { uint8_t num; HAL_UART_Receive(&huart1, &num, 1, HAL_MAX_DELAY); pBuffer[i] = num; } //4.打印接收到的n个数字 for(int i = 0; i < n; i++) { uint8_t num = pBuffer[i]; HAL_UART_Transmit(&huart1, &num, 1, HAL_MAX_DELAY); } //5.释放掉分配的内存 vPortFree(pBuffer); //6.删除任务自身 vTaskDelete(NULL);}/* USER CODE END 0 */ /* USER CODE BEGIN 2 */xTaskCreate( vTask1, "Task1", 128, NULL, 1 , NULL );vTaskStartScheduler();//启用调度器/* USER CODE END 2 */ |
实验现象:先通过串口发送03,代表分配长度为3个整数的数组,再发送99 88 77,串口会接收到99 88 77这三个数字。代表已经成功分配了内存。
分配内存
1 | int *pBuffer = (int *)pvPortMalloc(sizeof(int) * n); |
释放内存
1 | vPortFree(pBuffer); |
这两种方法,实际上是heap1~5提供的。
分配提供5种堆内存管理方案,分别用自己的方式实现了pvPortMalloc()和vPortFree()方法。
因此,使用FreeRTOS时,只需要保留一个,避免冲突。
应该根据自己的需要,选择一个,通常而言,heap_4.c是最常用的。
Heap_1
简单的堆内存管理方案,只能分配,不能释放。
即只能使用pvPortMalloc()分配,但是不能使用vPortFree()释放内存。
创建ucHeap数组,作为堆内存:
1 | uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; |
堆内存的大小由configTOTAL_HEAP_SIZE这个配置项决定。
适用于只创建内核对象,不销毁内核对象的程序。
比如下面的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(prvLEDTask, "LED1", 128, (void *)&led1BlinkInfo, 1, NULL);xTaskCreate(prvLEDTask, "LED2", 128, (void *)&led2BlinkInfo, 1, NULL);vTaskStartScheduler();//启用调度器/* USER CODE END 2 */ |
上面的程序中,只创建了任务,但是没有删除任务,可以使用heap_1
下面包含删除任务的程序,就不适合使用heap_1
1234567891011121314151617181920 | //LED的任务代码static void prvLEDTask(void *pvParameters){ for(;;) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); vTaskDelay(pdMS_TO_TICKS(100)); HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); vTaskDelay(pdMS_TO_TICKS(100)); }} //Task1的任务代码static void prvTask1(void *pvParameters){ //删除LED任务 vTaskDelete(xLEDTaskHandle); //所有的代码都执行完毕,删除自身 vTaskDelete(NULL); } |
上方的程序包含任务的创建和删除,不适合使用heap_1。
因为删除任务的过程中会调用vPortFree()方法来释放内存,但heap_1没有实现该方法。
heap_2
是一种过时的堆内存管理方案,不建议使用。
该方案内存碎片化问题严重,推荐使用heap_4替代。
保留该方案,是为了和老版本兼容。
创建ucHeap数组,作为堆内存:
1 | uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; |
堆内存的大小由configTOTAL_HEAP_SIZE这个配置项决定。
该方案采用最优匹配算法,不合并相邻空闲块。
最优匹配算法:每次调用pvPortMalloc(),都选择恰好满足要求的最小空闲块。
例:使用heap_2的前提下,执行下面的代码,configTOTAL_HEAP_SIZE = 256
1.p1 = pvPortMalloc(100);
2.p2 = pvPortMalloc(20);
3.p3 = pvPortMalloc(50);
4.vPortFree(p1);
5.vPortFree(p3);
6.p4 = pvPortMalloc(25);
7.p5 = pvPortMalloc(70);
8.p6 = pvPortMalloc(64);

上方的代码,到第8行时,已经没有足够的空间分配。
虽然最后的空间还足够,大于64个字节,但是heap_2不合并相邻块,剩下的空间被分割的太碎无法使用。
heap_4
采用最优匹配算法。
和heap_2的区别是:它可以合并相邻空闲块。
因此上例中,使用heap_4可以成功给p6分配内存。
heap_3
对标准库的malloc()和free()改造,让其有可重入性。
heap_3会在malloc()和free()执行期间,暂停调度器。
heap_3的pvPortMalloc实现方法:
1234567 | void * pvReturn; vTaskSuspendAll();//暂停调度器 { pvReturn = malloc( xWantedSize ); traceMALLOC( pvReturn, xWantedSize ); } ( void ) xTaskResumeAll();//恢复调度器 |
heap_3的vPortFree实现方法:
123456 | vTaskSuspendAll();//暂停调度器 { free( pv ); traceFREE( pv, 0 ); } ( void ) xTaskResumeAll();//恢复调度器 |
如果不在使用malloc期间,暂停调度器,FreeRTOS的调度器,可能在几个任务之间切换,可能会出现几个任务同时调用malloc的情况。malloc不可重入,同时执行会出现程序错误。
heap_5
类似于heap_4,采用最优匹配算法,合并相邻空闲块,但支持不连续内存。
一个单片机中,可能存在几个SRAM或外接SRAM,但这些SRAM地址不连续,需要让FreeRTOS管理的堆分散在多个不连续的内存之中,这时使用heap_5。
需要调用vPortDefineHeapRegions()提前定义好堆的位置。
例:heap_5的用法,管理不连续的内存。