从FreeRTOS V9.0.0开始内核对象(如tasks, queues, semaphores ,event groups等)需要的ram空间既可以在编译的时候静态分配,也可以在运行时动态分配。
一、动态分配内核对象
FreeRTOS 每次创建内核对象时分配 RAM,删除的时候释放RAM。 该策略减少了设计和规划工作,简化了 API,并最大限度地减少 RAM 占用。
当FreeRTOS调用pvPortMalloc()
动态分配内存,调用vPortFree()动态释放内存。
pvPortMalloc()
和标准C库的malloc()
有同样的函数原型(prototype),vPortFree()
和标准C库的free()
有同样的函数原型。注意,只是原型一样,内部实现不一样!
FreeRTOS对于pvPortMalloc()
和vPortFree()
提供了5种实现,后续章节会讲到。FreeRTOS应用程序可以使用其中的一种,或者使用自己的实现。5种实现分别在heap_1.c
, heap_2.c
, heap_3.c
, heap_4.c
和 heap_5.c
文件中,都存在于文件夹 FreeRTOS/Source/portable/MemMang
下。
二、Heap_1
这是5个内存管理策略中最简单的一个,它简单到只能申请内存,一旦申请成功后,这块内存再也不能被释放。因此它适用于从来不会删除任务的应用。
基本原理:
在内存中开辟一段空间,空间大小为configTOTAL_HEAP_SIZE字节。
如何开辟的这段空间的呢?其实就是定义了一个全局数组,因此,总空间肯定是连续的,并且这段空间可以由编译器自动指定位置,也可以用户使用链接脚本(或散列文件)强制指定位置。
在创建内核对象(下图以创建任务为例)的时候,从ucHeap数组中取空间即可。详情见heap_1.c中pvPortMalloc函数的实现。
每个创建的任务都需要一个任务控制块 (TCB) 和一个从堆中分配的栈。
ref:
FreeRTOS动态内存管理(heap_1)_jiang_2018的博客-CSDN博客
三、Heap_2
为了向后兼容,FreeRTOS 发行版中保留了 Heap_2,但不推荐用于新设计。 建议使用增强版本的heap_4。再次不做过多介绍
四、Heap_3
这种管理方法其实就是调用的标准库中的malloc和free。
因此,configTOTAL_HEAP_SIZE宏不再起作用,可用的最大空间由链接器指定。
这种方法是线程安全的,因为使用malloc前后分别调用vTaskSuspendAll()和xTaskResumeAll();
其源代码实现如下:
void * pvPortMalloc( size_t xWantedSize )
{
void * pvReturn;
vTaskSuspendAll();
{
pvReturn = malloc( xWantedSize );
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll();
#if ( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
/*-----------------------------------------------------------*/
void vPortFree( void * pv )
{
if( pv )
{
vTaskSuspendAll();
{
free( pv );
traceFREE( pv, 0 );
}
( void ) xTaskResumeAll();
}
}
五、Heap_4
和heap1类似,heap4也是通过定义一个全局数组来决定总的heap空间。
heap4使用了first-fit算法来分配内存。把临近的空闲的内存拼凑成一个更大的内存块,这就减少了内存碎片化的风险。该算法的描述如下:
- A演示了创建3个任务之后的数组的样子,一大块空的块存在于数组的顶端。
- B演示了删除1个任务之后的数组,一大块空的块存在于数组的顶端。被删除的那个任务占据的TCB和栈存储空间现在是空的,并且它们拼接成一个大的空的块。
- C演示了FreeRTOS创建了一个Queue。队列是通过xQueueCreate() API 创建的,它是调用pvPortMalloc() 来分配存储空间的。由于heap_4采用first-fit算法,pvportMalloc()将会使用第一块大的足够容纳队列的RAM块来分配(采用之前删除任务的那一块)。然而队列并不完全消耗那个空闲的区块,所以那个RAM块会分成两个部分,未使用的部分将会由后续的pvPortMalloc()占用。
- D演示了应用程序直接调用pvPortMalloc()而不是间接地由FreeRTOS API调用之后的情形。用户分配的区块足够小,能够放在第一个空闲的区块中,这个区块就是队列占用的区块和后面的TCB占用的区块之间的那一块。
- E 演示了Queue删除之后,存储空间也自动释放了。现在用户分配的区块两边都是空闲区块。
- F 演示了用户分配的存储空间释放的情形。这个区块现在和两边的空闲区块拼接成了一个更大的空闲区块。
六、Heap_5
heap5也是使用first-fit算法来分配内存。但是总内存的使用方式和heap4不同:heap4指定的总空间必须是连续的,而 heap_5 可以从多个独立的内存空间分配内存。因此,在heap5调用pvPortMalloc()分配内存之前,必须调用vPortDefineHeapRegions()指定每个单独的起始地址和大小,以此来共同构成 heap_5 使用的总内存。
如果嵌入式系统有三个ram,地址分别如A图所示:
如果freertos将这三块空间全部用作heap,那么代码就应该如下所示:
/ *定义三个RAM区域的起始地址和大小。* /
#define RAM1_START_ADDRESS((uint8_t *)0x00010000)
#define RAM1_SIZE(65 * 1024)
#define RAM2_START_ADDRESS((uint8_t *)0x00020000)
#define RAM2_SIZE(32 * 1024)
#define RAM3_START_ADDRESS((uint8_t *)0x00030000)
#define RAM3_SIZE(32 * 1024)
/ *创建一个HeapRegion_t定义的数组,为三个RAM区域中的每个区域创建一个索引,并以NULL地址终止该数组。HeapRegion_t结构必须按起始地址顺序出现,包含最低起始地址的结构首先出现。* /
const HeapRegion_t xHeapRegions [] =
{
{RAM1_START_ADDRESS,RAM1_SIZE},
{RAM2_START_ADDRESS,RAM2_SIZE},
{RAM3_START_ADDRESS,RAM3_SIZE},
{NULL,0} / *标记数组的结尾。* /
};
int main(void)
{
/ *初始化heap_5。* /
vPortDefineHeapRegions(xHeapRegions);
/ *在此处添加应用程序代码。* /
}
实际上,上述三块空间不可能全部用于heap,因为c语言定义变量等也需要内存。假如我们通过链接脚本指定这些变量保存在RAM1区域,那么代码就可以这么写:
/ *定义链接器未使用的两个RAM区域的起始地址和大小。* /
#define RAM2_START_ADDRESS((uint8_t *)0x00020000)
#define RAM2_SIZE(32 * 1024)
#define RAM3_START_ADDRESS((uint8_t *)0x00030000)
#define RAM3_SIZE(32 * 1024)
/ *定义了一个静态数据,数组的地址将由编译器指定。大小我们自己指定,这样一来,ram1中剩余的空间就可以确定地分配给heap5用了* /
#define RAM1_HEAP_SIZE(30 * 1024)
static uint8_t ucHeap [RAM1_HEAP_SIZE];
const HeapRegion_t xHeapRegions [] =
{
{ucHeap,RAM1_HEAP_SIZE},
{RAM2_START_ADDRESS,RAM2_SIZE},
{RAM3_START_ADDRESS,RAM3_SIZE},
{NULL,0} / *标记数组的结尾。* /
};
这样做的好处是,我们可以通过调整宏RAM1_HEAP_SIZE来调整ram1用作heap的大小。当总大小超过了RAM1的可用空间的时候,编译阶段就会报错!
七、Heap管理其他的函数
1、size_t xPortGetFreeHeapSize( void )
这个函数可以获取调用时堆中空闲内存的大小,以字节为单位。使用它可以优化堆的大小。例如,当内核对象都创建完毕后调用xPortGetFreeHeapSize()
返回2000,那么可以把configTOTAL_HEAP_SIZE
减小2000.
note:当使用heap_3
时是不能调用这个函数的。
2、size_t xPortGetMinimumEverFreeHeapSize( void );
此函数返回FreeRTOS应用程序开始运行之后曾经存在的最小的未被分配的存储空间的字节数。它的返回值指示了应用程序离将要耗尽堆空间的接近程度。例如该函数
返回200个字节,那么从应用程序开始运行之后的某个时间,在使用200个字节就会把堆空间用完。
note:xPortGetMinimumEverFreeHeapSize()
只在使用heap_4
或者heap_5
时生效。
3、malloc失败的钩子函数
用户可以直接调用pvPortMalloc(),当用户创建内核对象时,pvPortMalloc()也会被间接调用。
钩子函数可以理解为回调函数,当pvPortMalloc()
返回NULL
时(表示分配内存失败)调用这个钩子函数。
将FreeRTOSConfig.h
中的configUSE_MALLOC_FAILED_HOOK
设置为1,同时实例化钩子函数即可,其函数原型如下:
void vApplicationMallocFailedHook( void );
ref:
FreeRTOS--堆内存管理 - 林特斯9527 - 博客园FreeRTOS堆内存管理_Rookie on the road的博客-CSDN博客_freertos堆栈设置