目录
1. 预备知识
2. 内存分配方案示例
3. 与堆相关的实用函数
- 预备知识
- 从FreeRTOS V9.0.0 版本开始,可以完全静态分配FreeRTOS 应用程序,不再需要包含堆内存管理器。但是本章讨论动态内存分配,FreeRTOS 在每次创建内核对象时分配RAM,在每次删除内核对象时释放RAM。这种策略减少了系统设计和规划的工作量,简化了API,并最大限度地减少了RAM地占用。
- 可以使用标准C语言库的 malloc() 函数和 free() 函数来分配内存,但由于一些原因,这些函数可能并不适合:
- 在小型嵌入式系统中,malloc() 函数和 free() 函数并不总是可用的。
- 实现的malloc () 函数和 free() 函数可能相对较大,占用了宝贵的代码空间。
- malloc () 函数和 free() 函数很少是线程安全的。
- malloc () 函数和 free() 函数不是确定的,执行函数所需时间会因为不同的调用而有差异。
- malloc () 函数和 free() 函数可能会出现内存碎片化的情况。(比如,堆内的空闲RAM分散成彼此独立的小块,虽然可能总的空闲RAM大小是够的,但是因为其不连续,所以仍无法被分配给某个内存块。)
- malloc () 函数和 free() 函数会使链接器配置复杂化。
- FreeRTOS现在将内存分配视为可移植层部分,因为不同的嵌入式系统有不同的动态内存分配和时间要求。FreeRTOS使用pvPortMalloc() 和 vPortfree() 来分配和释放RAM。
- FreeRTOS 提供了5个pvPortMalloc() 和 vPortfree() 的实现案例,它们分别定义在heap_1.c, …, heap_5.c 源文件中,全部位于 FreeRTOS/Source/portable/MemMang 目录下。
- 内存分配方案示例
(1)heap_1
- heap_1.c 实现了基本的 pvPortMalloc() 函数,并且没有实现vPortFree() 函数。所以,从不删除任务或其他内核对象的应用程序可能会使用heap_1(有些关键系统是禁止使用动态内存分配的,因为存在非确定性)。heap_1 总是确定性的,而且不会引起内存碎片化。
- 每个创建的任务需要一个任务控制块(TaskControlBlock, TCB)和一个从堆中分配的栈。下图演示了每次创建任务时都会从heap_1数组中分配RAM。
(2)heap_2
- heap_2.c 的工作原理也是对一个大小定义为 configTOTAL_HEAP_SIZE 的数组细分。heap_2使用最佳匹配算法来分配内存,并且允许释放内存。同样数组也是静态声明的,所以会使应用程序看起来消耗大量RAM。比如,
- 堆中包含3个自由内存块,分别为5字节、25字节和100字节。
- 为请求20字节的RAM,调用pvPortMalloc() 函数。
适合所请求字节数的最小空闲RAM块是25字节的内存块,所以pvPortMalloc() 函数将25字节的内存块分割成一个20字节、一个5字节的内存块(过于理想状态,为了便于解释),然后返回一个指向20字节内存块的指针。
- 图中的C,此次创建任务引起两次调用pvPortMalloc() 函数,一次是分配新的TCB,另一次是分配任务栈。任务是使用 xTaskCreate() API函数创建的,对 pvPortMalloc()函数的调用发生在xTaskCreate() API函数内部。
(3)heap_3
- heap_3 使用标准库的 malloc() 函数和 free() 函数,所以堆的大小是由链接器配置定义的,configTOTAL_HEAP_SIZE 的设置对其没有影响。
- heap_3 通过暂停 FreeRTOS调度器使得 malloc() 函数和 free() 函数线程安全。
(4)heap_4
- 与heap_1, heap_2 一样,heap_4 的数组也是静态声明的,大小由configTOTAL_HEAP_SIZE 定义。
- heap_4 使用首次匹配算法,确保pvPortMalloc() 函数使用第一个空闲内存块。
- heap_4 可以将相邻的空闲块合并。
- 默认情况下,heap_4 使用的数组是在heap_4.c 源文件中声明的,其起始地址是由链接器自动设置的。但是,如果将 FreeRTOSConfig.h 中的 configAPPLICATION_ALLOCATED_HEAP 编译配置常量设置为1,那么这个数组便由 FreeRTOS的应用程序来声明。
/* 使用 GCC 语法声明 heap_4 将使用的数组,并将数组放入名为 my_heap 的内存区 */
uint8_t Heap [ configTOTAL_HEAP_SIZE ]__attribute__(section("my_heap"));
/* 使用 IAR 语法声明 heap_4 将使用的数组,并将数组放入0x20000000 处 */
uint8_t ucHeap [ configTOTAL_HEAP_SIZE ] @ 0x20000000;
(5)heap_5
- heap_5 用于分配和释放内存的算法与 heap_4 相同,但是可以从多个内存空间中分配内存。
- vPortDefineHeapRegions() API 函数用于指定每个独立内存区域的起始地址和大小。
void vPortDefineHeapRegions (const HeapRegion_t* const pxHeaRegions);
typedef struct HeapRegion
{
/* 内存块的起始地址,该内存块将成为堆的一部分。 */
uint8_t *pucStartAddress;
/* 内存块的大小,以字节为单位。 */
size_t xSizeInBytes;
} HeapRegion_t;
举例来说:
/* 图A,由HeapRegion_t 结构体组成的数组,这些结构体共同描述了3个RAM块的全部内容 */
/* 定义3个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 类型的数组,3个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. */
vPortDefineHeapRegion(xHeapRegions);
/* 此处添加应用程序代码。 */
}
- 图A所示,已经把所有的RAM都分配给了堆,没有空闲的RAM供其他变量使用。
- 构建工程时,构建过程的链接阶段会给每个变量分配RAM地址。可供链接器使用的RAM通常由链接器配置文件描述。如图.B,链接器脚本包含了RAM1的信息,只留下RAM1中 0x0001nnnn以上的部分供 heap_5 使用。如果仍使用上述代码,分配给heap_5 的0x0001nnnn以下的RAM将与存放变量的RAM重叠,为了避免这种情况,可以使 xHeapRegions[] 数组的第一个 HeapRegion_t 结构体使用起始地址 0x0001nnnn,但是并不推荐:
- 起始地址可能不容易确定。
- 未来的构建中,链接器使用的 RAM大小可能会改变,因此需要更新 HeapRegion_t 结构体的起始地址。
- 如果链接器使用的RAM和heap_5使用的RAM重叠,则构建工具不知道,也就不会提醒编程人员。
- 针对这个问题,下面的代码使用了更方便、更容易维护的例子。
/* 由HeapRegion_t 结构体组成的数组,描述了全部RAM2和RAM3,但只描述了部分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)
/* 声明数组,该数组将成为 heap_5 使用的堆的一部分。该数组将被链接器放在RAM1中。 */
#define RAM1_HEAP_SIZE (30 * 1024)
static uint8_t ucHeap[RAM!_HEAP_SIZE];
/* 创建HeapRegion_t 类型的数组。heap_5 将只使用RAM1 中包含 ucHeap 数组的部分。 */
const HeapRegion_t xHeapRegions[] =
{
{ucHeap , RAM1_HEAP_SIZE },
{RAM2_START_ADDRESS, RAM2_SIZE},
{RAM3_START_ADDRESS, RAM3_SIZE},
{NULL , 0 } /* 标记数组的结束。 */
};
- 上述代码所演示技术的优点是:
- 不需要硬编码的起始地址。
- HeapRegion_t 结构体使用的地址将由链接器自动设置,因此即使未来构建工程时链接器使用的RAM大小发生了变化,也将始终正确。
- 分配给 heap_5 的RAM不可能与链接器放入 RAM1 的数据重叠。
- 如果 ucHeap 太大,则应用程序将无法链接。
- 与堆相关的实用函数
- xPortGetFreeHeapSize() API 函数。
返回堆中可用的字节数,可用来优化堆的大小。例如,在创建全部内核对象后 xPortGetFreeHeapSize()API函数返回200,那么 configTOTAL_HEAP_SIZE 的值就可以减少200。
size_t xPortGetFreeHeapSize(void);
- xPortGetMinimumEverFreeHeapSize() API函数。
返回自从 FreeRTOS 应用程序开始执行以来,堆中曾经存在的未分配字节的最小数量。例如,如果 xPortGetMinimumEverFreeHeapSize()API函数返回200,那么表明自从应用程序开始执行以来的某个时间,应用程序离耗尽堆空间只有200字节。此API函数只允许heap_4, heap_5 使用。
size_t xPortGetMinimumEverFreeHeapSize(void);
- malloc 失败的钩子函数
所有堆分配方案的示例都可以配置成如果调用 pvPortMalloc()函数时返回NULL,就调用钩子(或回调)函数。
void vApplicationMallocFailedHook(void);