FreeRTOS 基础系列文章
基本对象
FreeRTOS——任务
FreeRTOS——队列
FreeRTOS——信号量
FreeRTOS——互斥量
FreeRTOS——任务通知
FreeRTOS——流和消息缓冲区
FreeRTOS——软件定时器
FreeRTOS——事件组
内存管理
FreeRTOS——静态与动态内存分配
FreeRTOS——堆内存管理
FreeRTOS——栈溢出保护
代码组织
FreeRTOS——源代码组织
FreeRTOS——创建新的项目
FreeRTOS——配置文件
每次创建任务、队列、互斥量、软件计时器、信号量或事件组时,RTOS 内核都需要 RAM。RAM 可以在 RTOS API 对象创建函数中从 RTOS 堆自动动态分配,也可以由应用程序设计者提供。
如果 RTOS 对象是动态创建的,那么有时可以使用标准 C 库 malloc()
和 free()
函数来实现此目的,但是······
······所以通常需要另一种内存分配实现。
一个嵌入式/实时系统可能与另一个系统具有非常不同的 RAM 和时序要求 —— 因此单个 RAM 分配算法将只适用于应用程序的一个子集(还可以有其他分配算法)。
为了解决这个问题,FreeRTOS 将内存分配 API 保留在其可移植层中。可移植层位于实现核心 RTOS 功能的源文件之外,允许提供适用于正在开发的实时系统的特定于应用程序的实现。当 RTOS 内核需要 RAM 时,它不会调用 malloc(),而是调用 pvPortMalloc()。当 RAM 被释放时,RTOS 内核不会调用 free(),而是调用 vPortFree()。
FreeRTOS 提供了多种堆管理方案,其复杂性和功能各不相同。也可以提供您自己的堆实现,甚至可以同时使用两个堆实现。同时使用两个堆实现允许将任务堆栈和其他 RTOS 对象放置在快速的内部 RAM 中,并将应用程序数据放置在较慢的外部 RAM 中。
FreeRTOS 下载中包括五个示例的内存分配实现,每个都在以下小节中进行了描述。这些小节还包括关于提供的每个实现最适合在哪些情况下使用的信息。
每个提供的实现都包含在一个单独的源文件中(分别为 heap_1.c
、heap_2.c
、heap_3.c
、heap_4.c
和 heap_5.c
),它们位于主 RTOS 源代码下载的 Source/Portable/MemMang
目录中。可以根据需要添加其他实现。在一个项目中一次只应该包含这些源文件中的一个 [ 由这些可移植层函数定义的堆将由 RTOS 内核使用,即使使用 RTOS 的应用程序选择使用自己的堆实现 ]。
如下:
注意:
heap_1 不太有用,因为 FreeRTOS 添加了对静态分配的支持。
heap_1 是最简单的实现。一旦分配了内存,它不允许释放内存。尽管如此,heap_1.c 适用于大量嵌入式应用程序。这是因为许多小型且深度嵌入的应用程序会在系统启动时创建所有所需的任务、队列、信号量等,然后在程序的生命周期内使用所有这些对象(直到应用程序再次关闭或重新启动)。什么都不会被删除。
该实现只是在请求 RAM 时将单个阵列细分为更小的块。数组的总大小(堆的总大小)由 configTOTAL_HEAP_SIZE
设置 —— 它在 FreeRTOSConfig.h
中定义。提供configAPPLICATION_ALLOCATED_HEAP
FreeRTOSConfig.h 配置常量以允许将堆放置在内存中的特定地址。
xPortGetFreeHeapSize()
API 函数返回未分配的堆空间总量,允许优化 configTOTAL_HEAP_SIZE
设置。
heap_1 实现:
heap_2 现在被认为是遗留的,因为 heap_4 是首选。
heap_2 使用最佳拟合算法,并且与方案 1 不同,它允许释放先前分配的块。它不会将相邻的空闲块组合成单个大块。参阅 heap_4.c了解合并空闲块的实现。
可用堆空间的总量由 configTOTAL_HEAP_SIZE
设置 —— 它在 FreeRTOSConfig.h
中定义。提供configAPPLICATION_ALLOCATED_HEAP
FreeRTOSConfig.h 配置常量以允许将堆放置在内存中的特定地址。
xPortGetFreeHeapSize()
API 函数返回未分配的堆空间总量(允许优化 configTOTAL_HEAP_SIZE
设置),但不提供有关如何将未分配的内存分成更小的块的信息。
这个实现:
即使在应用程序重复删除任务、队列、信号量、互斥量等情况时也可以使用,下面是关于内存碎片的注意事项。
如果分配和释放的内存大小是随机的,则不应使用。例如:
如果应用程序动态创建和删除任务,并且分配给正在创建的任务的堆栈大小始终相同,那么在大多数情况下可以使用 heap2.c。但是,如果分配给正在创建的任务的堆栈大小并不总是相同,那么可用的空闲内存可能会碎片化为许多小块,最终导致分配失败。在这种情况下,heap_4.c 将是更好的选择。
如果应用程序动态创建和删除队列,并且每种情况下队列存储区都相同(队列存储区是队列项大小乘以队列长度),那么大多数情况下可以使用 heap_2.c。但是,如果每种情况下的队列存储区域都不相同,那么可用的空闲内存可能会分成许多小块,最终导致分配失败。在这种情况下,heap_4.c 将是更好的选择。
应用程序直接调用 pvPortMalloc()
和 vPortFree()
,而不是仅通过其他 FreeRTOS API 函数间接调用。
如果您的应用程序队列、任务、信号量、互斥量等以不可预测的顺序排列,可能会导致内存碎片问题。这对几乎所有的应用程序都不太可能,但应该记住。
不是确定性的 —— 但是比大多数标准 C 库的 malloc 实现更高效。
heap_2.c 适用于许多必须动态创建对象的小型实时系统。有关将空闲内存块组合成单个更大块的类似实现,请参见 heap_4。
这为标准 C 库 malloc() 和 free() 函数实现了一个简单的封装器,在大多数情况下,这些函数将随您选择的编译器一起提供。封装器只是使 malloc() 和 free() 函数线程安全。
这个实现:
请注意,当使用 heap_3 时,FreeRTOSConfig.h
中的 configTOTAL_HEAP_SIZE
设置无效。
该方案使用快速拟合算法,与方案 2 不同的是,它将相邻的可用内存块合并成一个大的块(它包含合并算法)。
可用堆空间的总量由 configTOTAL_HEAP_SIZE
设置 —— 它在 FreeRTOSConfig.h
中定义。提供configAPPLICATION_ALLOCATED_HEAP
FreeRTOSConfig.h 配置常量以允许将堆放置在内存中的特定地址。
xPortGetFreeHeapSize()
API 函数返回调用该函数时未分配的堆空间总量,而 xPortGetMinimumEverFreeHeapSize()
API 函数返回 FreeRTOS 应用程序自启动以来系统的历史最小可用堆空间量。这两个函数都没有提供有关如何将未分配的内存分成更小的块的信息。
vPortGetHeapStats()
API 函数提供附加信息。它填充 heap_t 结构的成员,如下所示。
/* vPortGetHeapStats()函数的原型。 */
void vPortGetHeapStats( HeapStats_t *xHeapStats );
/* Heap_stats_t结构的定义。 */
typedef struct xHeapStats
{
/* 当前可用的堆大小——这是所有空闲块的总和,而不是可以分配的最大块。 */
size_t xAvailableHeapSpaceInBytes;
/* 调用vPortGetHeapStats()时堆中所有空闲块的最大大小(以字节为单位)。 */
size_t xSizeOfLargestFreeBlockInBytes;
/* 调用vPortGetHeapStats()时堆中所有空闲块的最小大小(以字节为单位)。 */
size_t xSizeOfSmallestFreeBlockInBytes;
/* 调用vPortGetHeapStats()时堆中可用内存块的数量。 */
size_t xNumberOfFreeBlocks;
/* 自系统启动以来堆中的总空闲内存(所有空闲块的总和)的历史最小值。 */
size_t xMinimumEverFreeBytesRemaining;
/* 返回有效内存块的pvPortMalloc()调用次数。 */
size_t xNumberOfSuccessfulAllocations;
/* 成功释放内存块的vPortFree()调用次数。 */
size_t xNumberOfSuccessfulFrees;
} HeapStats_t;
heap_4:
heap_4.c 对于想要在应用程序代码中直接使用可移植层内存分配方案的应用程序特别有用(而不是通过调用本身调用 pvPortMalloc() 和 vPortFree() 的 API 函数间接使用)。
该方案使用与 heap_4 相同的快速拟合和内存合并算法,并允许堆跨越多个非相邻(非连续)内存区域。
Heap_5 是通过调用 vPortDefineHeapRegions()
初始化的,并且在 vPortDefineHeapRegions()
执行之后才能使用。创建 RTOS 对象(任务、队列、信号量等)将隐式调用 pvPortMalloc(),因此在使用 heap_5 时,必须在创建任何此类对象之前调用 vPortDefineHeapRegions()
。
vPortDefineHeapRegions()
接受单个参数。该参数是一个 HeapRegion_t
结构数组。HeapRegion_t
在portable.h
中定义为
typedef struct HeapRegion
{
/* 将成为堆的一部分的内存块的起始地址。*/
uint8_t *pucStartAddress;
/* 内存块的大小。 */
size_t xSizeInBytes;
} HeapRegion_t;
使用 {NULL, 0} 的区域定义结束数组,数组中定义的内存区域必须按地址从低地址到高地址的顺序出现。以下源代码片段提供了一个示例。
/* 分配两个内存块供堆使用。第一个块是从地址0x80000000开始的0x10000字节,
第二个块是从地址0x90000000开始的0xa0000字节。从0x80000000开始的块具有
较低的起始地址,因此首先出现在数组中。 */
const HeapRegion_t xHeapRegions[] =
{
{ ( uint8_t * ) 0x80000000UL, 0x10000 },
{ ( uint8_t * ) 0x90000000UL, 0xa0000 },
{ NULL, 0 } /* 终止该数组。 */
};
/* 将数组传递给vPortDefineHeapRegions()。 */
vPortDefineHeapRegions( xHeapRegions );
xPortGetFreeHeapSize()
API 函数返回调用该函数时未分配的堆空间总量,而 xPortGetMinimumEverFreeHeapSize()
API 函数返回 FreeRTOS 应用程序自启动以来系统的历史最小可用堆空间量。这两个函数都没有提供有关如何将未分配的内存分成更小的块的信息。
vPortGetHeapStats()
API函数提供关于堆状态的附加信息。