对Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide 官方文档做阅读笔记
官方网站路径: Free RTOS Book and Reference Manual
有不对的地方欢迎指正
目录
第二章
2.1章节简介及预览
FreeRTOS是由一组C源文件提供的,因此成为一名称职的C程序员是使用FreeRTOS的先决条件,因此本章假定读者熟悉以下概念:
1.C项目是如何构建的,包括不同的编译和链接阶段。
2.什么是栈和堆。
3.标准C库malloc()和free()函数。
动态内存分配及其与FreeRTOS的相关性
FreeRTOS V9.0.0内核中的对象可以在编译时静态分配,也可以在运行时动态分配:
本书的以下章节将介绍内核对象,例如任务、队列、信号量和事件组。为了使FreeRTOS尽可能易于使用,这些内核对象不是在编译时静态分配的,而是在运行时动态分配的;FreeRTOS在创建内核对象时分配RAM,并在删除内核对象时释放RAM。此策略减少了设计和规划工作,简化了API,并最大限度地减少RAM占用空间。
本章讨论动态存储分配。动态内存分配是一个C编程概念,而不是特定于FreeRTOS或多任务的概念。它与FreeRTOS相关,因为内核对象是动态分配的,通用编译器提供的动态存储分配方案并不总是适合实时应用程序。
可以使用标准的C库malloc()
和Free()
函数分配内存,但由于以下一个或多个原因,它们可能不太合适:
1. 它们并不总是在小型嵌入式系统上可用。
2. 它们的实现可能相对来说占用了宝贵的代码空间。
3. 它们很少是线程安全的。
4. 它们是不具有确定性的;执行函数所需的时间将因调用而异。
5. 它们会使链接器配置复杂化。
6. 如果允许堆空间增长到其他变量使用的内存中,它们可能是调试错误的根源。
7.它们会因为堆的碎片化导致出现问题。
如果堆中的空闲RAM被分解成彼此分离的小块,则堆被认为是碎片化的,那么如果堆中没有单个空闲内存块大到足以包含该块,即使堆中所有单独空闲块的总大小比无法分配的块的大小大很多倍,分配内存块的尝试也会失败。
动态内存分配选项
FreeRTOS V9.0.0中的内核对象可以在编译时静态分配,或在运行时动态分配内存:
早期版本的FreeRTOS使用一个内存池分配方案,从而在编译时预先分配不同大小内存块的池,然后由内存分配函数返回。尽管这是一个在实时系统中使用的常见方案,但因为它不能足够有效地使用RAM使其适用于非常小的嵌入式系统,所以该方案被放弃了。
FreeRTOS现在将内存分配视为可移植层的一部分(与核心代码库部分相反)。这是为满足不同的嵌入式系统有不同的动态存储分配和时序要求。因此,单一的动态内存分配算法只能适用于应用程序的子集。此外,从核心代码库中删除动态存储分配使应用程序编写者能够在适当的时候使用自己特定的内存分配方案。
当FreeRTOS需要RAM时,它不调用malloc()
函数,而是调用pvPortMalloc()
。当RAM被释放时,内核调用vPortFree()
而不是调用Free()
函数。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目录中。
本章的目的是让读者了解以下内容:
1. 在FreeRTOS中分配内存。
2. FreeRTOS提供的五个示例内存分配方案。
3. 内存分配方案选择。
2.2示例内存分配方案
Heap_1
对于小型专用嵌入式系统来说,在调度程序启动之前只创建任务和其他内核对象是很常见的。在这种情况下,内存仅在应用程序开始执行任何实时功能之前由内核动态分配,并且内存在应用程序的生命周期内保持分配。这意味着所选择的分配方案不必考虑任何更复杂的内存分配问题, 例如确定性和内存碎片问题,只需要考虑代码大小和复杂度等属性。
Heap_1. c
实现了一个非常基础的pvPortMalloc()
版本,并且没有实现vPortFree()
。如果在应用程序中从不删除任务或其他内核对象则可以使用heap_1。一些商业上或者安全上要求非常严格的系统可能会禁止使用动态内存分配,因此适合使用heap_1。严格的系统通常禁止动态存储分配,因为不确定性、内存碎片和分配失败相关的不确定性——Heap_1
总是确定性的,不会有内存碎片的问题。
heap_1分配方案通过调用了pvPortMalloc()将一个简单的数组细分为更小的块,该数组称为FreeRTOS堆(heap)
。
数组的总大小(以字节为单位)由FreeRTOSConfig. h
中的configTOTAL_HEAP_SIZE
定义设置。以这种方式定义一个大数组会使应用程序消耗大量RAM,即使还没有内存从数组中被分配。
每次创建的任务都需要一个任务控制块(TCB)和一个要从堆中分配的栈,下图展示了 heap_1 是怎么在创建 task 时细分一个简单的数组。
- A表示没有 task 创建之前 array 的样子,整个数组都是空的。
- B创建一个 task之后的数组
- C创建三个 task之后的数组
Heap_2
FreeRTOS发行版中保留了Heap_2以实现向后兼容,但不建议将其用于新设计,建议考虑使用heap_4
替代heap_2
,因为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适用于重复创建和删除任务的应用程序,前提是分配给创建任务的堆栈大小不变。
下图演示了当任务被创建、删除然后再次创建时,最佳拟合算法是如何工作的。
- A显示创建三个任务后的数组。一个大的空闲块保留在数组的顶部。
- B显示了其中一个任务被删除后的数组。数组顶部的大空闲块仍然存在。现在还有两个较小的空闲块,是之前分配给被删除任务的TCB和堆栈。
- C显示创建另一个任务后的情况。创建任务导致对
pvPortMalloc()
的两次调用,一次用于分配新的TCB,一次用于分配任务堆栈。任务是使用第3.4节中描述的xTaskCreate()
API函数创建的。对pvPortMalloc()
的调用发生在xTaskCreate ()
函数内部。
每个TCB大小完全相同,因此最佳拟合算法确保先前分配给已删除任务的TCB的RAM块可以被重用以分配新任务的TCB。
Heap_2不是确定性的,但比大多数标准库的malloc()和Free()更快实现。
Heap_3
Heap_3. c
使用标准库malloc()
和Free()
函数,因此堆的大小由链接器配置定义,对configTOTAL_HEAP_SIZE
设置没有影响。Heap_3通过暂时挂起FreeRTOS调度程序来使malloc()和Free()线程安全
。线程安全和调度程序暂停都是第7章资源管理中涵盖的主题。
Heap_4
像heap_1
和heap_2
一样,heap_4
通过将数组细分为更小的块来工作。同样的数组也是静态定义的,并由configTOTAL_HEAP_SIZE
确定大小。
Heap_4使用首次适应算法
来分配内存。与heap_2不同,heap_4将相邻的空闲内存块组合成一个更大的块,从而最大限度地降低内存碎片的风险。
首次适应算法确保pvPortMalloc()
使用第一个足够大的空闲内存块来保存请求的字节数。思考以下场景:
- 堆包含三个可用内存块,按照它们在数组中出现的顺序,分别为5字节、200字节和100字节。
pvPortMalloc()
请求20字节的RAM。
第一个适合请求字节数的空闲块是200字节的RAM空闲块,因此, pvPortMalloc()
将200字节的块拆分为一个20字节的块和一个180字节的块,然后返回指向20字节块的指针。新的180字节块仍然可用于以后对 pvPortMalloc()
的调用。
Heap_4将相邻的空闲块组合成一个更大的块,最大限度地降低了碎片化风险,并使其适用于重复分配和释放不同大小的RAM块的应用程序。
下图演示了heap_4首次适应算法
是如何对内存进行分配和释放工作的:
- A显示创建三个任务后的数组。一个大的空闲块保留在数组的顶部。
- B显示了其中一个任务被删除后的数组。数组顶部的大空闲块仍然存在。还有一个先前分配的已删除任务的TCB和堆栈空闲块。
请注意
:`与heap_2不同,当TCB被删除时释放的内存,以及当栈被删除时释放的内存,不会保留为两个单独的空闲块,而是组合起来创建一个更大的单个空闲块。 - C显示FreeRTOS队列创建后的情况。队列是使用
xQueueCreate ()
API函数创建的,第4.3节对此进行了描述。xQueueCreate ()
调用pvPortMalloc()
来分配队列使用的RAM。由于heap_4使用首次适应算法
,pvPortMalloc()
将从第一个足够大的空闲RAM块分配RAM,以容纳队列,在图中,这块空闲的RAM是之前任务被删除时留下的空闲RAM。然而,队列不会消耗空闲块中的所有RAM,因此该块被分成两部分,未使用的部分仍然可用于以后对pvPortMalloc()的调用。 - D显示了直接在应用程序代码调用
pvPortMalloc()
后的情况,而不是通过调用FreeRTOS API函数间接调用。用户分配的块足够小,可以放入第一个空闲块,这是分配给队列内存和分配给TCB内存之间的块。删除任务时释放的内存现在已被拆分为三个单独的块;第一个块保存队列,第二个块保存用户分配的内存,第三个块保持空闲。 - E显示队列被删除后的情况,它会自动释放分配给已删除队列的内存。现在用户分配块的上下都有空闲内存。
- F表示用户分配的内存也被释放后的情况。用户分配使用的内存块与两侧的空闲内存合并以创建更大的单个空闲块。
Heap_4不是确定性的,但比大多数标准库的 malloc() 和Free() 更快实现。
为Heap_4使用的数组设置起始地址
本小节包含进阶信息。如果使用Heap_4则不需要阅读或理解本节。有需要可阅读源文档。
Heap_5
heap_5
用于分配和释放内存的算法与heap_4
使用的相同。与heap_4
不同的是,heap_5
不限制从单个静态声明的数组分配内存;heap_5
可以从多个分离的内存空间分配内存。当运行FreeRTOS的系统提供的RAM在系统内存映射中不显示为单个连续(无空间)块时,Heap_5很有用。
heap_5
是唯一必须在调用pvPortMalloc()
之前提供显式初始化
的内存分配方案, 使用vPortDefineHeapRegions()
API 函数初始化Heap_5。使用heap_5时,必须在创建任何内核对象(任务、队列、信号量等)之前调用vPortDefineHeapRegions()
就可以了。
vPortDefineHeapRegions() API
void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );
vPortDefineHeapRegions()
用于指定每个单独内存区域的起始地址和大小,这些内存区域共同构成heap_5使用的总内存。
每个单独的存储器区域由结构类型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;
vPortDefineHeapRegions()的成员
pxHeapRegions
:
指向HeapRegion_t结构体数组开头的指针。数组中的每个结构都描述了使用heap_5时将成为堆一部分的内存区域的起始地址和长度。数组中的HeapRegion_t
结构必须按起始地址排序;
描述具有最低起始地址的内存区域的HeapRegion_t
结构必须是数组中的第一个结构体,并且描述具有最高起始地址的内存区域的HeapRegion_t
结构必须是数组中的最后一个结构体。
数组的末尾由一个HeapRegion_t结构标记
,该结构的pucStart地址成员设置为NULL
。
思考下图所示假设的内存映射,其中包含三个单独的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使用。
如果使用所示的代码,分配给heap_5地址0x0001nnnn
的RAM将与用于保存变量的RAM区域重叠。为了避免这种情况,该数组中的第一个HeapRegion_t结构可以改变起始地址为0x0001nnnn
, 而不是以0x00010000
开始的起始地址。但是,这不是推荐的解决方案,因为:
- 起始地址可能不容易确定。
- 链接器使用的RAM数量在将来的构建中可能会发生变化,因此需要更新HeapRegion_t结构中使用的起始地址。
- 如果链接器使用的RAM和heap_5使用的RAM重叠,构建工具将不知道,因此无法警告用户。
下面演示一个更方便和可维护的示例。它声明了一个名为ucHeap
的数组。 ucHeap
是一个普通变量,因此它成为链接器分配给RAM1的数据的一部分。第一个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 appearingfirst. */
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太大,应用程序将不会链接。
堆相关实用程序函数
xPortGetFreeHeapSize()
size_t xPortGetFreeHeapSize( void );
xPortGetFreeHeapSize()
API函数返回 调用该函数时堆中可用字节的数量
。它可用于优化堆大小。例如,如果xPortGetFreeHeapSize()
在创建完所有内核对象后返回2000,那么configTOTAL_HEAP_SIZE
的值可以减少2000。
在heap_3中xPortGetFreeHeap Size()
不可用。
xPortGetMinimumEverFreeHeapSize()
size_t xPortGetMinimumEverFreeHeapSize( void );
xPortGetMinimumEverFreeHeapSize()
API函数返回自FreeRTOS应用程序开始执行以来堆中存在的最小未分配字节数。
xPortGetMinimumEverFreeHeapSize()
API函数返回的值指示应用程序离堆空间耗尽还有有多少字节。例如,如果xPortGetMinimumEverFreeHeapSize() 返回值是200,说明在应用程序开始执行调用此函数后,离堆空间耗尽还有200 Byte。
此函数在heap_4 或heap_5可用。
Malloc Failed Hook Functions
void vApplicationMallocFailedHook( void );
pvPortMalloc()
API函数可直接在应用代码中调用。每次创建内核对象时,它也会在FreeRTOS源文件中调用。内核对象的示例包括任务、队列、信号量和事件组——这些都将在本书的后面章节中描述。
就像标准库malloc()
函数一样,如果pvPortMalloc()
因为请求大小的块不存在而无法返回RAM块,那么它将返回NULL
。如果由于用户正在创建内核对象而执行pvPortMalloc()
,并且对pvPortMalloc()
调用后返回NULL
,则说明创建内核对象失败。
如果对pvPortMalloc()
的调用返回NULL
,则可以将所有示例堆分配方案配置为调用钩子(或回调)函数。
如果在FreeRTOSConfig.h 中,配置宏configUSE_MALLOC_FAILED_HOOK 为1,应用程序必须提供一个malloc失败的钩子函数。该功能可以适合应用程序的任何方式实现。