从FreeRTOS V9.0.0开始,FreeRTOS应用程序可以完全静态分配,无序包含堆内存管理器
从FreeRTOS V9.0.0开始,内核对象可以在编译时静态分配,也可以在运行时动态分配,内核对象有任务、队列、信号量和事件组。为了使FreeRTOS尽可能容易使用,这些内核对象不是在编译时静态分配,而是在运行时动态分配。 FreeRTOS会在每次创建内核对象时分配RAM,并在每次删除内核对象时释放RAM。该策略减少了设计和规划工作,简化了API,并最大限度减少了RAM占用空间。
动态内存分配是一个C编程概念,而不是特定于FreeRTOS或多任务处理的概念。它与FreeRTOS相关,因为内核对象是动态分配的,通用编译器提供的动态内存分配方案不总是适合应用程序。可以使用C标准库malloc()和free()分配内存,但由于一些原因,它们不合适:
FreeRTOS现在将内存分配视为可移植层的一部分(而不是核心代码库的一部分)。这是因为不同的嵌入式系统具有不同的动态内存分配和时序要求,从核心代码库中删除动态内存分配使应用程序编写者能够在适当的时候提供他们自己的特定实现。
当FreeRTOS需要RAM时,它不会调用malloc(),而是调用pvPortMalloc(),当RAM释放内存时,内核不会调用free(),而是调用vPortFree()。pvPortMalloc()与标准C库malloc函数原型相同,vPortFree()与标准C库free()函数原型相同。
pvPortMalloc()和vPortFree()是公共函数,因此也可以从应用程序代码中调用。
FreeRTOS提供了pvPortMalloc()和vPortFree()的五个示例实现,FreeRTOS应用程序可以使用示例实现之一,也可以提供自己的实现。这五个实例分别定义在heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c源文件中,均位于FreeRTOS/Source/portable/MemMang目录中。
小型专用嵌入式系统通常只在调度程序启动前,只创建任务和其他内核对象,在这种情况下,内存仅在应用程序开始执行任何实时功能之前由内核动态分配,并且内存在应用程序的生命周期内保持分配状态。 这意味着所选择的分配方案不必考虑任何更复杂的内存分配问题,例如确定性和碎片,而可以只考虑代码大小和简单性等属性。 也就是说,只分配内存,其他像把两个相邻的空闲块整合到一块,这个是不管的。
Heap_1.c实现了一个非常基本的pvPortMalloc()版本,并没有实现vPortFree(),所以从不删除任务或其他内核对象的应用程序可以使用heap_1。当调用pvPortMalloc()时,heap_1分配方案将一个堆细分为更小的块,堆的总大小由FreeRTOSConfig.h中的宏configTOTAL_HEAP_SIZE设置,但是为字节。
每个创建的任务都要两个内存块,一个是任务控制块(TCB),另一个堆栈,这两个都从堆分配。下图演示来了heap_1如何在创建任务时细分堆
为了向后兼容,FreeRTOS保留了Heap_21,但不建议使用它,使用heap_4,因为heap_4提供了增强的功能。
heap_2.c的大小也通过configTOTAL_HEAP_SIZE确定,它使用最佳拟合算法来分配内存,并且与heap_1不同,它允许释放内存。同样数组(堆)是静态声明的,因此会消耗大量的RAM,最佳拟合算法确保PVPortMalloc()使用大小与请求的字节数最接近的空闲内存块,比如,考虑以下场景:堆包含三个空闲内存块,分为为5字节、25字节和100字节,调用pvPortMalloc来请求20字节的RAM。可以容纳请求的字节数最小RAM空闲块是25字节,因此pvPortMalloc()将25字节块分为一个20字节块和一个5字节块,然后返回指向20字节的块的指针,新的5字节块依然可以被pvPortMalloc调用。
与heap_4不同,heap_2不会将相邻的空闲块合并为一个更大的块,因此容易发生碎片。 Heap_2适用于重复创建和删除任务的应用程序,前提是分配给创建的任务和堆栈大小不变。
下图演示了在创建、删除和再次创建任务时最佳拟合算法的工作原理:
Heap_3使用标准库malloc()和free()函数,因此堆的大小由链接器配置定义,configTOTAL_HEAP_SIZE设置对其没有影响,Heap_3通过暂时挂起FreeRTOS调度程序使malloc()和free()线程安全。
与heap_1和heap_2一样,heap_4的工作原理是将数组细分为更小的块,数组是静态分配的,并有configTOTAL_HEAP_SIZE确定尺寸,heap_4使用first fit算法来分配内存,与heap_2冉,heap_4将合并相邻的空闲内存块,然后组合成一个更大的块,从而最大限度的降低内存碎片的风险。
the first fit算法确保pvPortMalloc()使用第一个能够容纳请求字节数的空闲块,例如,考虑以下场景:
堆包含三个空闲内存块,按照它们在数组中出现的顺序,分别为5字节、200字节和100字节,调用pvPortMalloc()来请求20字节的RAM,第一个适合请求的字节数的空闲块是200字节,因此pvPortMalloc在返回指针之前将200字节拆分为一个20字节的块和一个180字节的块,返回的指针指向20字节的块,新的180字节块仍可以被pvPortMalloc调用。
Heap_4将合并相邻的空闲块组合成一个更大的块,最大限度的降低碎片的风险,并使其适用于重复分配和释放不同大小的RAM块的应用程序。
下图演示了具有内存合并的 heap_4 首次拟合算法的工作原理,如内存被分配和释放:
Heap_4 不是确定性的,但比 malloc() 和 free() 的大多数标准库实现要快。
有时,应用程序编写者需要将 heap_4 使用的数组放置在特定的内存地址。 例如,FreeRTOS 任务使用的堆栈是从堆分配的,因此可能有必要确保堆位于快速内部内存中,而不是位于慢速外部内存中。
默认情况下,heap_4 使用的数组在 heap_4.c 源文件中声明,其起始地址由链接器自动设置。 但是,如果 FreeRTOSConfig.h 中的 configAPPLICATION_ALLOCATED_HEAP 设置为 1,则该数组必须改为由使用 FreeRTOS 的应用程序声明。 如果数组被声明为应用程序的一部分,那么应用程序的编写者可以设置它的起始地址。
如果 configAPPLICATION_ALLOCATED_HEAP 在 FreeRTOSConfig.h 中设置为 1,则必须在应用程序的源文件之一中声明一个名为 ucHeap 并由 configTOTAL_HEAP_SIZE 设置大小的 uint8_t 数组。
heap_5用于分配和释放内存的算法和heap_4相同,不同的是heap_5不限于从单个静态声明的数组分配内存,heap_5可以从多个独立的内存空间分配内存,当运行 FreeRTOS 的系统提供的 RAM 没有在系统内存映射中显示为单个连续(没有空间)块时,Heap_5 很有用。
在撰写本文时,heap_5 是唯一提供的内存分配方案,必须在调用 pvPortMalloc() 之前显式初始化。 Heap_5 使用 vPortDefineHeapRegions() API 函数初始化。 使用 heap_5 时,必须在创建任何内核对象(任务、队列、信号量等)之前调用 vPortDefineHeapRegions()。
vPortDefineHeapRegions用于指定每个单独的内存区域的起始地址和大小,这些区域构成heap_5使用的总内存。
void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );
每个单独的内存区域由HeapRegion_t类型的结构体描述**,所有可用内存区域的描述**作为 HeapRegion_t 结构数组传递到 vPortDefineHeapRegions()。
typedef struct HeapRegion
{
/* The start address of a block of memory that will be part of the heap.*/
uint8_t *pucStartAddress;
/* The size of the block of memory in bytes. */
size_t xSizeInBytes;
} HeapRegion_t;
pxHeapRegions 指向 HeapRegion_t 结构数组开头的指针。 数组中的每个结构都描述了内存的起始地址和长度,数组中的 HeapRegion_t 结构体必须按起始地址排序; 描述起始地址最低的内存区域的 HeapRegion_t 结构必须是数组中的第一个结构,描述起始地址最高的内存区域HeapRegion_t 结构必须是数组中的最后一个结构。数组的末尾由 HeapRegion_t 结构标记,该结构的 pucStartAddress 成员设置为 NULL。
例如,考虑下图中所示的假设内存映射,其中包含三个独立的RAM块:RAM1、RAM2和RAM3,假设可执行代码放置在只读寄存器中,没有显示。
下面的代码描述了整个RAM的三个块
/* Define the start address and size of the three RAM regions. */
#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 )
/* Create an array of HeapRegion_t definitions, with an index for each of the three RAM regions, and terminating the array with a NULL address. The HeapRegion_t structures must appear in start address order, with the structure that contains the lowest start address appearing first. */
const HeapRegion_t xHeapRegions[] =
{
{ RAM1_START_ADDRESS, RAM1_SIZE },
{ RAM2_START_ADDRESS, RAM2_SIZE },
{ RAM3_START_ADDRESS, RAM3_SIZE },
{ NULL, 0 } /* Marks the end of the array. */
};
int main( void )
{
/* Initialize heap_5. */
vPortDefineHeapRegions( xHeapRegions );
/* Add application code here. */
}
虽然正确地描述了 RAM,但它没有展示一个可用的示例,因为它将所有 RAM 分配给堆,没有空闲 RAM 可供其他变量使用。
构建项目时,构建过程的链接阶段会为每个变量分配一个 RAM 地址。 可供链接器使用的 RAM 通常由链接器配置文件(例如链接描述文件)描述。假设链接描述文件包含关于 RAM1 的信息,但不包含关于 RAM2 或 RAM3 的信息。 因此,链接器将变量放置在 RAM1 中,只留下地址 0x0001nnnn 以上的 RAM1 部分可供 heap_5 使用。 0x0001nnnn 的实际值将取决于所链接的应用程序中包含的所有变量的组合大小。 链接器让所有 RAM2 和所有 RAM3 未使用,留下整个 RAM2 和整个 RAM3 可供 heap_5 使用。
如果使用上述 中所示的代码,分配给地址 0x0001nnnn 下的 heap_5 的 RAM 将与用于保存变量的 RAM 重叠。为了避免这种情况,xHeapRegions[] 数组中的第一个 HeapRegion_t 结构可以使用 0x0001nnnn 的起始地址,而不是 0x00010000 的起始地址。**但是,这不是推荐的解决方案,**因为:
linker. */
#define RAM2_START_ADDRESS ( ( uint8_t * ) 0x00020000 )
#define RAM2_SIZE ( 32 * 1024 )
#define RAM3_START_ADDRESS ( ( uint8_t * ) 0x00030000 )
#define RAM3_SIZE ( 32 * 1024 )
/* Declare an array that will be part of the heap used by heap_5. The array will be placed in RAM1 by the linker. */
#define RAM1_HEAP_SIZE ( 30 * 1024 )
static uint8_t ucHeap[ RAM1_HEAP_SIZE ];
/* Create an array of HeapRegion_t definitions. Whereas in Listing 6 the first entry described all of RAM1, so heap_5 will have used all of RAM1, this time the first entry only describes the ucHeap array, so heap_5 will only use the part of RAM1 that contains the ucHeap array. The HeapRegion_t structures must still appear in start address order, with the structure that contains the lowest start address appearing first. */
const HeapRegion_t xHeapRegions[] =
{
{ ucHeap, RAM1_HEAP_SIZE },
{ RAM2_START_ADDRESS, RAM2_SIZE },
{ RAM3_START_ADDRESS, RAM3_SIZE },
{ NULL, 0 } /* Marks the end of the array. */
};
xPortGetFreeHeapSize函数返回堆中的空闲字节数,它可用于优化堆大小。 例如,如果 xPortGetFreeHeapSize() 在所有内核对象创建后返回 2000,那么 configTOTAL_HEAP_SIZE 的值可以减少 2000。
size_t xPortGetFreeHeapSize( void );
xPortGetMinimumEverFreeHeapSize返回自FreeRTOS应用程序开始执行以来,堆中存在最小未分配字节数。xPortGetMinimumEverFreeHeapSize返回值表明应用程序接近耗尽堆空间的程序, 例如,如果 xPortGetMinimumEverFreeHeapSize() 返回 200,那么在应用程序开始执行后的某个时间,它会在 200 字节内耗尽堆空间。
xPortGetMinimumEverFreeHeapSize() 仅在使用 heap_4 或 heap_5 时可用。
size_t xPortGetMinimumEverFreeHeapSize( void );
pvPortMalloc() 可以直接从应用程序代码中调用。每次创建内核对象时,它也会在 FreeRTOS 源文件中调用。内核对象的例子包括任务、队列、信号量和事件组。
像标准库 malloc() 函数一样,如果 pvPortMalloc() 因为请求大小的块不存在而无法返回 RAM 块,那么它将返回 NULL,则不会创建内核对象。
如果对 pvPortMalloc() 的调用返回 NULL,则所有示例堆分配方案都可以配置一个调用挂钩(或回调)函数。如果在 FreeRTOSConfig.h 中将 configUSE_MALLOC_FAILED_HOOK 设置为 1,那么应用程序必须提供一个 malloc 失败的钩子函数。该函数可以以任何适合应用程序的方式实现,该函数如下:
void vApplicationMallocFailedHook( void );