FreeRTOS 堆内存管理

堆就是一段能动态分配的内存。
在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提供的。
heap
分配提供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);

heap2分配方法
上方的代码,到第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的用法,管理不连续的内存。
heap5的用法

🔍 ×