大家好,我是杂烩君。今天分享FreeRTOS堆管理方案的内容。
1. 如何选择堆方案
FreeRTOS有5种堆管理方案(heap_1~heap_5):
| 方案 | 释放 | 合并 | 链表排序 | 碎片 | 核心特点 |
|---|---|---|---|---|---|
| heap_1 | 不支持 | — | 无链表 | 无 | 顺序分配,最简单 |
| heap_2 | 支持 | 不合并 | 按大小 | 严重 | 最佳匹配 |
| heap_3 | 支持 | 依赖系统 | 无(用系统堆) | 依赖系统 | 封装malloc/free |
| heap_4 | 支持 | 合并 | 按地址 | 少 | 首次匹配,官方推荐 |
| heap_5 | 支持 | 合并 | 按地址 | 少 | 同heap_4,多RAM区域 |
通过修改FreeRTOSConfig.h中的宏定义选择,核心配置如下:
// 选择堆管理方案(仅需开启其中一个)
// #define configUSE_HEAP_1 1 // 启用heap_1方案
// #define configUSE_HEAP_2 1 // 启用heap_2方案
#define configUSE_HEAP_3 1 // 启用heap_3方案
// #define configUSE_HEAP_4 1 // 启用heap_4方案
// #define configUSE_HEAP_5 1 // 启用heap_5方案
// 配置堆内存总大小(heap_1/2/4/5适用,单位:字节)
#define configTOTAL_HEAP_SIZE (10 * 1024) // 10KB堆内存
5种堆方案互斥,只能启用其中一种;heap_3方案无需配置configTOTAL_HEAP_SIZE(依赖系统malloc/free),其余4种均需配置堆总大小。
2. 5种堆方案详解
2.1 heap_1:只分配,不释放
最简单的方案。在一个静态数组上维护一个偏移量,分配时向后移动,不提供释放功能。
源码中堆的核心数据结构只有两个变量:
/* heap_1.c */
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
static size_t xNextFreeByte = ( size_t ) 0U;
分配时检查剩余空间,够就返回 pucAlignedHeap + xNextFreeByte 并后移偏移量。逻辑像切蛋糕——只能从头往后切,切下来的不能放回去:
偏移量只往右走,永远不回头——这就是 heap_1 不支持释放的根本原因。
需要注意的是,heap_1 的 vPortFree() **并非"什么都不做"**,而是会触发断言:
void vPortFree( void * pv )
{
( void ) pv;
configASSERT( pv == NULL ); /* 传入非NULL指针会触发断言失败 */
}
在开了 configASSERT 的工程里,误调 vPortFree 会直接卡死。
适用场景:启动时一次性创建所有内核对象,运行期间不删除。RAM极小(<10KB)的MCU优先考虑。
2.2 heap_2:支持释放,不合并空闲块
引入链表管理空闲块,每个内存块前面挂一个 BlockLink_t 头部:
/* heap_2.c / heap_4.c 共用此结构 */
typedef struct A_BLOCK_LINK {
struct A_BLOCK_LINK * pxNextFreeBlock;
size_t xBlockSize;
} BlockLink_t;
heap_2 的空闲链表按块大小从小到大排序,分配时从链表头开始遍历,找到第一个能满足需求的块——因为链表按大小排序,所以找到的就是最小够用的块,本质是最佳匹配算法。
释放时将块插回链表对应位置,但不合并相邻空闲块。长期分配释放不同大小的内存,碎片会越来越严重。连续内存不够,想分配 120B —— 失败!
这就是"内存碎片"——剩余总空间够,但全是不连续的小块,分不出大块。
适用场景:每次分配的大小固定(比如所有任务栈大小一样),此时释放后的块刚好能被下次分配复用,不会产生碎片。
2.3 heap_3:封装系统malloc/free
不自己管理堆,直接用编译器提供的 malloc()/free(),套一层调度器挂起来保证多任务安全:
/* heap_3.c */
void * pvPortMalloc( size_t xWantedSize )
{
void * pvReturn;
vTaskSuspendAll(); /* 挂起调度器,不是关中断 */
{
pvReturn = malloc( xWantedSize );
}
( void ) xTaskResumeAll();
return pvReturn;
}
vTaskSuspendAll()挂起调度器防止任务切换,中断仍然可以响应。这也意味着中断中不能调用 heap_3 的分配/释放。
heap_3 本质就是一个"线程安全的壳",内存管理能力完全取决于编译器自带的 malloc 实现。
适用场景:快速原型开发、对内存效率要求不高的场合。
2.4 heap_4:支持空闲块合并(大多数项目首选)
和 heap_2 结构类似,但有两个关键区别:
空闲链表
按地址从低到高排序(heap_2 按大小排序)释放时合并相邻空闲块
分配采用首次匹配——遍历链表找到第一个够大的块就用。合并逻辑是 heap_4 的精华,释放时检查前后相邻块是否也空闲:
/* heap_4.c - prvInsertBlockIntoFreeList 合并逻辑(简化) */
puc = ( uint8_t * ) pxIterator;
if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
{
/* 前一块的尾部恰好接上当前块的头部,合并 */
pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
pxBlockToInsert = pxIterator;
}
/* 同样检查与后一个空闲块是否地址连续,连续则合并 */
用一个例子说明合并过程——假设释放 B 块,它恰好夹在两个空闲块 A、C 之间:
正因为链表按地址排序,释放时才能判断前后块是否在地址上连续,从而合并。这就是 heap_2 做不到合并的根本原因——它按大小排序,地址关系全丢了。
适用场景:分配大小不固定、频率高、对碎片敏感的通用场景。绝大多数项目直接选它。
2.5 heap_5:多块不连续RAM
在 heap_4 基础上支持多块不连续 RAM 统一管理。使用前必须先调用 vPortDefineHeapRegions() 注册内存区域,数组按地址从低到高排列,以 {NULL, 0} 结尾:
const HeapRegion_t xHeapRegions[] = {
{ (uint8_t *) 0x20000000UL, 64 * 1024 }, /* 内部RAM 64KB */
{ (uint8_t *) 0x68000000UL, 1024 * 1024 }, /* 外部SDRAM 1MB */
{ NULL, 0 } /* 结束标记 */
};
/* 必须在创建任何内核对象之前调用 */
vPortDefineHeapRegions( xHeapRegions );
vPortDefineHeapRegions() 内部会把这些不连续的区域串成一条按地址排序的空闲链表:
注册完成后,分配/释放逻辑与 heap_4 完全一致——分配时遍历整条链表,无论内存在哪块 RAM 上都能分配到。
适用场景:MCU有内部RAM + 外部SRAM/SDRAM,需要统一管理所有RAM资源。
3. 注意事项
heap_1 调 vPortFree 会断言失败,不是静默忽略
heap_3 中断不可用:它靠挂起调度器保护,中断里调用会出问题
heap_2 碎片陷阱:分配大小不固定时,碎片快速积累导致分配失败
configTOTAL_HEAP_SIZE 别设太满:给任务栈和全局变量留空间,建议预留 RAM 的 10%~20%
heap_5 别滥用:只有一块连续RAM时,heap_5比heap_4多了配置复杂度,没有任何优势
heap_4 覆盖了绝大多数嵌入式场景,也是 FreeRTOS 官方推荐的通用方案。
332