Heap_1
适用于小型嵌入式系统。应用程序在执行之前动态分配好内存,生命周期直到运行结束。不会考虑过于复杂的内存管理,如碎片等问题。
Heap_1.c是最基本的版本,只用pvPortMalloc()分配内存,没有实现pvPortFree释放内存。因此不会删除任务及其内核应用程序资源。一些商业和安全相关的关键系统,通常需要禁止内存动态分配、碎片划分等,会用到heap_1.c,因为它始终是确定性的,不会对内存进行分片处理。
Heap_1将一个数组分成多个小的块,由pvPortMalloc()调用使用。这个数组就是FreeRTOS的堆。
数组的总大小(以字节为单位),由FreeRTOSConfig.h中的configTOTAL_HEAP_SIZE定义设置。 以这种方式定义大阵列可以使应用程序看起来消耗大量的RAM,甚至在从数组中分配任何内存之前就已经消耗。
每个创建的任务需要一个任务控制块(TCB)和堆栈从堆中分配。如下图所示:
A:创建任务之前。
B:创建一个任务后。
C:创建三个任务后。
Heap_2
Heap_2保留在FreeRTOS发行版中,以实现向后兼容性,但其使用不推荐用于新设计。 考虑使用heap_4而不是heap_2,因为heap_4提供了增强的功能。
Heap_2.c还可以通过细分数据并由configTOTAL_HEAP_SIZE定义数组大小。 它使用最佳拟合算法分配内存,与heap_1不同,它允许释放内存。 同样,数组是静态声明的,所以即使在分配了阵列的任何内存之前,应用程序都将显示为消耗大量的RAM。
pvPortMalloc()使用与请求的字节数最佳匹配大小区块分配内存。 例如,考虑以下情况:
- 堆包含三个可用内存块,分别为5个字节,25个字节和100个字节。
- 调用pvPortMalloc()来请求20字节的RAM。
与heap_4不同,Heap_2不将相邻的空闲块组合成一个更大的块,所以它更容易碎裂。 但是,如果被分配并随后释放的块总是相同的大小,那么碎片不是问题。 Heap_2适用于重复创建和删除任务的应用程序,前提是分配给已创建任务的堆栈大小不会更改。
如下图所示:
A:创建三个任务后。
B:释放掉第二个任务后。
C:再次新创建一个任务后。
每个TCB的大小是完全相同的,所以最合适的算法确保了块的先前分配给已删除任务的TCB的RAM被重新用于分配新任务的TCB。分配给新创建的任务的堆栈的大小与分配给先前删除的任务的堆栈的大小相同,因此最佳拟合算法确保先前分配给已删除任务的堆栈的RAM块被重新使用以分配 新任务。因此顶部的空闲区域空间保持不变。
Heap_3
Heap_3.c使用标准库malloc()和free()函数,所以堆的大小由链接器配置定义,configTOTAL_HEAP_SIZE设置没有影响。
通过临时挂起FreeRTOS调度程序,来保证Heap_3使malloc()和free()时线程的安全性。
Heap_4
像heap_1和heap_2一样,heap_4通过将数组细分为更小的块来工作。 像以前一样,数组被静态地声明,并且由configTOTAL_HEAP_SIZE来定义,所以即使在从数组中实际分配任何内存之前,应用程序也将消耗大量的RAM。
Heap_4使用第一个拟合算法分配内存。 与heap_2不同,heap_4将相邻空闲的内存块组合为单个较大的块,从而最大限度地减少内存碎片的风险。
第一个拟合算法确保pvPortMalloc()使用足够大的第一个空闲块来保存所请求的字节数。 例如,考虑以下情况:
- 堆包含三个可用内存块,按照它们在数组中出现的顺序分别为5个字节,200个字节和100个字节。
- 调用pvPortMalloc()来请求20字节的RAM。
Heap_4将相邻空闲块的块组合到一个较大的块中,最大限度地降低了碎片的风险,并使其适用于重复分配和释放不同大小的RAM块的应用程序。
如下图所示:
A:创建三个任务后。
B:释放掉第二个任务后。
C:创建一个队列后。
D:再创建一个用户数据空间。
E:释放之前创建的队列。
F:释放用户数据空间。
有时,应用程序写入程序需要将heap_4使用的数组放在特定的内存地址上。 例如,FreeRTOS任务使用的堆栈从堆中分配,因此可能需要确保堆位于快速内部存储器中,而不是缓慢的外部存储器。
默认情况下,heap_4使用的数组在heap_4.c源文件中声明,其起始地址由链接器自动设置。 但是,如果在FreeRTOSConfig.h中将configAPPLICATION_ALLOCATED_HEAP编译时配置常量设置为1,则该数组必须由使用FreeRTOS的应用程序声明。 如果数组被声明为应用程序的一部分,则应用程序的写入程序可以设置其起始地址。
如果在FreeRTOSConfig.h中将configAPPLICATION_ALLOCATED_HEAP设置为1,则必须在其中一个应用程序的源文件中声明一个名为ucHeap的uint8_t数组,并由configTOTAL_HEAP_SIZE设置确定。
将变量置于特定内存地址所需的语法取决于正在使用的编译器,因此请参阅编译器的文档。 两个编译器的示例如下:
- 清单2显示了GCC编译器声明数组所需的语法,并将数组放置在名为.my_heap的内存部分中。
uint8_t ucHeap[ configTOTAL_HEAP_SIZE ] __attribute__ ( ( section( ".my_heap" ) ) );
- 清单3显示了IAR编译器声明数组所需的语法,并将数组放置在绝对内存地址0x20000000。
uint8_t ucHeap[ configTOTAL_HEAP_SIZE ] @ 0x20000000;
Heap_5
heap_5用于分配和释放内存的算法与heap_4使用的算法相同。 与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结构数组的开头的指针。 阵列中的每个结构都描述了当使用heap_5时将作为堆的一部分的内存区域的起始地址和长度。数组中的HeapRegion_t结构必须按起始地址排序; 描述具有最低起始地址的存储区域的HeapRegion_t结构必须是数组中的第一个结构,并且描述具有最高起始地址的存储区域的HeapRegion_t结构必须是数组中的最后一个结构。
数组的末尾由HeapRegion_t结构标记,该结构的pucStartAddress成员设置为NULL。
每个单独的内存区域由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;
作为示例,如下图 所示的假想存储器映射,其包含三个单独的RAM块:RAM1,RAM2和RAM3。 假设可执行代码被放置在只读存储器中,这里 未显 示出来 。
下面清单显示了一组HeapRegion_t结构,它们整体描述了三块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通常由链接器配置文件(如链接描述文件)描述。在上图B中,假设链接描述文件包含在RAM1的信息中,但不包含于RAM2或RAM3中的信息。因此,链接器将变量放在RAM1中,只有RAM1 0x0001nnnn上面的部分可以由heap_5使用。实际值0x0001nnnn将取决于所链接的应用程序中包含的所有变量的组合大小。链接器已经将所有的RAM2和所有的RAM3都没有使用,留下了整个RAM2和整个RAM3可供heap_5使用。
如果使用上面程序清单所示的代码,则分配给地址0x0001nnnn以下的heap_5的RAM将与用于保存变量的RAM重叠。 为了避免这种情况,xHeapRegions []数组中的第一个HeapRegion_t结构可以使用0x0001nnnn的起始地址,而不是起始地址0x00010000。 但是,这不是推荐的解决方案,因为:
- 起始地址可能不容易确定。
- 链接器使用的RAM量可能会在将来的版本中更改,因此需要更新HeapRegion_t结构中使用的起始地址。
- 如果链接器使用的RAM和heap_5使用的RAM重叠,构建工具将不会知道,因此无法警告应用程序写入程序。
下面的程序清单演示了一个更方便和可维护的示例。 它声明一个名为ucHeap的数组。 ucHeap是一个普通变量,所以它成为链接器分配给RAM1的数据的一部分。 xHeapRegions数组中的第一个HeapRegion_t结构描述了ucHeap的起始地址和大小,所以ucHeap成为由heap_5管理的内存的一部分。 可以增加ucHeap的大小,直到链接器使用的RAM消耗所有RAM1,如上图中C所示。
/* Define the start address and size of the two RAM regions not used by the
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. */
};
上面程序清单所示的技术的优点包括:
- 没有必要使用硬编码的起始地址。
- HeapRegion_t结构中使用的地址将由链接器自动设置,所以始终都是正确的,即使连接器使用的RAM的数量在将来的版本中更改。
- 分配给heap_5的RAM不可能通过链接器重叠放入RAM1中的数据。
- 如果ucHeap太大,应用程序将不会链接。