我们完成了基于FreeRTOS的第一个简单的项目,目的是让读者能有个感性的认识。现在开始我将就一些FreeRTOS的一些具体技术细节进行讲解,这一讲是关于堆栈管理。

虽然“堆栈“这个词大多数时候是连在一起使用的,但堆和栈其实是不同的概念。

栈(stack):由编译器自动分配和释放,如存放函数的参数值,局部变量的值等
堆(heap):一般由程序员分配和释放,分配方式类似于数据结构中的链表

栈的空间有限,堆有很大的自由存储区(最大值由SRAM区决定),程序在编译器和函数分配内存都是在栈上进行的,同时程序运行中函数调用时参数的传递也是在栈上进行的。通常我们习惯用malloc和free等API申请分配和释放内存,但频繁使用会造成大量的内存碎片进而降低系统的整体性能。同时这些API不是线程安全的,有机率会导致系统的不稳定。

FreeRTOS早期的版本使用内存池分配方案(memory pool allocation)。内存池分配是指在程序编译阶段就分配一定数量的内存块留作备用。当有新的内存需求时,就从内存块分出一部分内存块,若内存块不够了就继续申请新的内存。这样做的一个显著优点是尽量避免了内存碎片,使得内存分配效率得到了提高。然而因为内存池分配对RAM的使用效率底下,在新的版本这个方案已经不被采用。

现在FreeRTOS最新的版本把内存分配当成可以由用户定制的部分。由于不同的嵌入式系统可以有不同的动态内存分配方案和时间要求,所以并不能有一个万能的内存分配方案满足所有需求。当FreeRTOS需要RAM的时候,它会调用pvPortMalloc这个函数而不是Malloc这个系统函数;当它需要释放内存的时候,会调用vPortFree这个函数而不是free这个系统函数。

FreeRTOS提供了五个pvPortMalloc和vPortFree的实现方案,分别是heap_1.c, heap_2.c, heap_3.c, heap_4.c 和 heap_5.c。读者可以根据自己项目的要求选择这几个方案中的一种或者也可以定制实现方法。

下面将分别介绍这五个预先准备的方案。

  • Heap_1

本方案适用于小型的嵌入式系统,并且这个系统只会在调度器启动之前创建任务和其它内核对象。内存只需要在程序启动调度器前对内存进行动态分配,之后内存分配在程序的运行周期中保持不变。heap_1.c实现了一个基础版本的pvPortMalloc函数,并没有实现vPortFree这个函数。如果系统运行后不用删除任务或者内核对象就可以采用这个方案。一些不需要动态分配内存的安全相关的系统也可以采用这个方案,因为这个方案是可确定性的(deterministic)并且不会导致内存碎片化。这个方案中堆由一个数组实现,数组的大小由FreeRTOSConfig.h文件中configTOTAL_HEAP_SIZE定义,如下图

FreeRTOS 从入门到精通4--堆栈管理知多少_stm32

 

内存分配示例如下,A表示没有任何任务创建时的内存;B表示一个任务(每个任务有自己的TCB块和栈区)被创建时的内存分配情况;C表示三个任务被创建时的分配情况。

FreeRTOS 从入门到精通4--堆栈管理知多少_stm32_02

  • Heap_2

这个方案用于保持FreeRTOS的向下兼容性,并不推荐使用。内存管理也由一个数组实现,大小由FreeRTOSConfig.h文件中configTOTAL_HEAP_SIZE定义。它通过一套优化算法(best fit algorithm)对内存进行分配,并允许释放内存。Heap_4是Heap_2的功能强化版本。

best fit 算法确保pvPortMalloc函数分配大小最接近所需要字节的内存空间。它会对大的内存块进行分割,但无法合并相连的内存块。Heap_2适用于重复添加和删除相同任务的系统,但这种系统应该十分少见。

内存分配示例如下,A表示有三个任务被创建时的内存分配情况;B表示有一个任务被删除时的分配情况,此时有两个小的内存块空闲出来;C表示另一个任务被创建时的分配情况,因为这个对TCB块和栈区大小的要求和之前被删除任务的大小一样,best fit 算法便把之前被释放的内存块分配给它。

 

FreeRTOS 从入门到精通4--堆栈管理知多少_数组_03

  • Heap_3

本方案使用标准库里的malloc和free函数,所以堆的大小由链接器配置决定,不受configTOTAL_HEAP_SIZE大小影响。

  • Heap_4

此方案同第一个和第二个方案一样,由一个数组表示堆,并把数组分割成小的内存块。堆的大小由FreeRTOSConfig.h文件中configTOTAL_HEAP_SIZE定义。

Heap_4采用的first fit算法可以把相邻的空闲的内存块合并成更大的内存块,减少了内存碎片化的风险。所以这个方案适用于通用的应用(系统会添加和删除不同大小的任务)

内存分配示例如下,可以看到内存可以自由分配和合并空闲的内存块,并使得整体的性能最优。Heap_4的分配方案是不确定性的,但速度要比malloc和free函数快。

FreeRTOS 从入门到精通4--堆栈管理知多少_内存碎片_04

 在STM32CubeIDE的项目中,内存管理默认采用了heap_4.c 作为实现方案。

FreeRTOS 从入门到精通4--堆栈管理知多少_内存分配_05

  • Heap_5

这个方案使用类似于Heap_4的内存分配技术,但不同于Heap_4只用一个连续的数组表示堆,Heap_5可以用不同的数组空间对内存进行分配。在本方案要使用vPortDefineHeapRegions这个函数对不同的数组进行申明。下面这张图定义了三个不同内存空间用于模拟堆。程序如下,首先定义了每个区域的开始地址START_ADDRESS和空间大小SIZE,然后用一个结构体xHeapRegions指向了这些区域,最后使用vPortDefineHeapRegions函数申明堆的空间。

FreeRTOS 从入门到精通4--堆栈管理知多少_内存碎片_06

 

 

FreeRTOS 从入门到精通4--堆栈管理知多少_stm32_07

 

FreeRTOS中内存管理相关的函数

size_t xPortGetFreeHeapSize( void );

这个函数会返回当前堆中的空闲空间,可以用来优化堆空间大小。比如在系统运行起来后调用xPortGetFreeHeapSize如果返回了3000,就可以把堆大小configTOTAL_HEAP_SIZE设置为3000。

size_t xPortGetMinimumEverFreeHeapSize( void );

这个函数会返回在系统运行过程中堆空间的最小空闲空间,如果最小空闲空间很小的话可以考虑提高堆大小configTOTAL_HEAP_SIZE的值。

void vApplicationMallocFailedHook( void );

这是一个回调函数,需要用户自己实现。如果配置文件中configUSE_MALLOC_FAILED_HOOK 设置为1的话,当堆分配内存失败时会调用此函数。用户可以在此函数中进行错误处理。