FreeRTOS 内存管理架构图:
heap_4 内存分配流程图:
一、FreeRTOS 内存管理概述
1.1 内存管理的重要性
FreeRTOS 提供了多种内存管理方案(heap_1 到 heap_5),用于动态内存分配。
内存管理特点:
·专门为嵌入式系统设计
·支持多种分配策略
·线程安全(在调度器启动后)
·内存碎片优化
1.2 五种内存管理方案
方案 | 特点 | 适用场景 |
heap_1 | 最简单,只分配不释放 | 不需要释放内存的应用 |
heap_2 | 支持释放,可能碎片 | 不需要碎片优化的应用 |
heap_3 | 简单包装 malloc/free | 需要标准库的应用 |
heap_4 | 相邻空闲块合并 | 需要碎片优化的应用 |
heap_5 | 支持多内存区域 | 内存不连续的系统 |
📌 要点:为什么不用标准库 malloc/free
标准库 malloc/free 在嵌入式系统中并不总是可用、占用宝贵代码空间、不是线程安全的、且执行时间不确定(非确定性)。FreeRTOS 因此把内存分配放在可移植层:内核申请内存调用 pvPortMalloc()、释放调用 vPortFree(),并提供了 heap_1 ~ heap_5 五种实现。
图:heap_1 ~ heap_5 五种内存管理方案对比
通过修改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.1 FreeRTOSConfig.h 配置
// 堆大小(字节) #define configTOTAL_HEAP_SIZE ((size_t)(40 * 1024)) // 40KB // 内存分配钩子函数 #define configUSE_MALLOC_FAILED_HOOK 1 // 应用程序任务分配钩子 #define configUSE_APPLICATION_TASK_TAG 02.2 选择内存管理方案
在工程中添加对应的 heap_x.c 文件,例如:
·使用 heap_4:添加 heap_4.c
·使用 heap_5:添加 heap_5.c
三、内存管理 API
3.1 内存分配与释放
void *pvPortMalloc(size_t xWantedSize); void vPortFree(void *pv); // 使用示例 void MallocFreeExample(void) { // 分配内存 uint8_t *pBuffer = (uint8_t*)pvPortMalloc(128); if(pBuffer != NULL) { // 使用内存 memset(pBuffer, 0, 128); // 释放内存 vPortFree(pBuffer); } else { printf("内存分配失败!\r\n"); } }3.2 堆信息查询
// 获取剩余堆大小 size_t xPortGetFreeHeapSize(void); // 获取最小剩余堆大小(历史最低) size_t xPortGetMinimumEverFreeHeapSize(void); // 使用示例 void PrintHeapInfo(void) { size_t freeHeap = xPortGetFreeHeapSize(); size_t minEverHeap = xPortGetMinimumEverFreeHeapSize(); printf("当前剩余堆: %u 字节\r\n", freeHeap); printf("历史最小剩余: %u 字节\r\n", minEverHeap); }四、heap_1 详解
4.1 heap_1 特点
·实现简单
·只支持 pvPortMalloc(),不支持 vPortFree()
·无内存碎片
·适用于不需要释放内存的系统
4.2 heap_1 工作原理
// heap_1 的内存布局 static uint8_t ucHeap[configTOTAL_HEAP_SIZE]; static size_t xNextFreeByte = (size_t)0; void *pvPortMalloc(size_t xWantedSize) { void *pvReturn = NULL; // 对齐处理 xWantedSize = (xWantedSize + (portBYTE_ALIGNMENT - 1)) & ~(portBYTE_ALIGNMENT - 1); // 检查是否有足够空间 if(xNextFreeByte + xWantedSize <= configTOTAL_HEAP_SIZE) { pvReturn = &ucHeap[xNextFreeByte]; xNextFreeByte += xWantedSize; } return pvReturn; } void vPortFree(void *pv) { // heap_1 不支持释放 (void)pv; }4.3 heap_1 适用场景
// ✅ 适用:系统启动时分配,永不释放 void InitSystem(void) { g_pBuffer = pvPortMalloc(1024); // 系统运行期间一直使用,不释放 }五、heap_2 详解
5.1 heap_2 特点
·支持 pvPortMalloc() 和 vPortFree()
·使用最佳匹配算法
·不合并相邻空闲块(可能碎片)
·适合固定大小内存分配
5.2 heap_2 工作原理
使用链表管理空闲内存块:
typedef struct A_BLOCK_LINK { struct A_BLOCK_LINK *pxNextFreeBlock; size_t xBlockSize; } BlockLink_t; static uint8_t ucHeap[configTOTAL_HEAP_SIZE];5.3 heap_2 使用示例
void Heap2Example(void) { // 分配多个固定大小块 void *p1 = pvPortMalloc(64); void *p2 = pvPortMalloc(64); void *p3 = pvPortMalloc(64); // 释放中间的块 vPortFree(p2); // 再次分配64字节,会重用p2的空间(最佳匹配) void *p4 = pvPortMalloc(64); }六、heap_3 详解
6.1 heap_3 特点
·简单包装标准库的 malloc() 和 free()
·使用调度器挂起保证线程安全
·堆大小由链接器脚本定义
6.2 heap_3 实现
void *pvPortMalloc(size_t xWantedSize) { void *pvReturn; vTaskSuspendAll(); pvReturn = malloc(xWantedSize); xTaskResumeAll(); return pvReturn; } void vPortFree(void *pv) { if(pv != NULL) { vTaskSuspendAll(); free(pv); xTaskResumeAll(); } }6.3 heap_3 注意事项
// 注意:堆大小不是由 configTOTAL_HEAP_SIZE 定义 // 而是由链接器脚本决定
七、heap_4 详解(推荐)
7.1 heap_4 特点
·支持 pvPortMalloc() 和 vPortFree()
·使用首次匹配算法
·合并相邻空闲块(减少碎片)
·适用于大多数应用
7.2 heap_4 内存合并
// 示例:释放内存时合并相邻空闲块 void FreeAndCoalesceExample(void) { // 初始状态:[已用1][已用2][已用3] void *p1 = pvPortMalloc(100); void *p2 = pvPortMalloc(100); void *p3 = pvPortMalloc(100); // 释放 p2:[已用1][空闲100][已用3] vPortFree(p2); // 释放 p1:[空闲200][已用3](合并) vPortFree(p1); }7.3 heap_4 完整示例
#include "FreeRTOS.h" // 确保使用 heap_4 // 在工程中添加 heap_4.c void Heap4Demo(void) { void *pBlock1, *pBlock2, *pBlock3; // 分配不同大小的块 pBlock1 = pvPortMalloc(200); pBlock2 = pvPortMalloc(300); pBlock3 = pvPortMalloc(100); printf("初始剩余堆: %u\r\n", xPortGetFreeHeapSize()); // 释放中间的块 vPortFree(pBlock2); printf("释放 pBlock2 后剩余: %u\r\n", xPortGetFreeHeapSize()); // 释放第一个块(会与前面的空闲块合并) vPortFree(pBlock1); printf("释放 pBlock1 后剩余: %u\r\n", xPortGetFreeHeapSize()); // 分配一个更大的块 void *pBlock4 = pvPortMalloc(400); printf("分配 pBlock4 后剩余: %u\r\n", xPortGetFreeHeapSize()); }八、heap_5 详解
8.1 heap_5 特点
·支持多个不连续的内存区域
·需要在使用前初始化
·适用于内存分散的系统(如部分在内部RAM,部分在外部RAM)
8.2 heap_5 初始化
// 内存区域描述结构 typedef struct HeapRegion { uint8_t *pucStartAddress; size_t xSizeInBytes; } HeapRegion_t; void vPortDefineHeapRegions(const HeapRegion_t *const pxHeapRegions); // 使用示例 void InitHeap5(void) { // 定义多个内存区域 const HeapRegion_t xHeapRegions[] = { // 内部RAM(64KB) { (uint8_t*)0x20000000, 0x10000 }, // 外部RAM(1MB) { (uint8_t*)0x60000000, 0x100000 }, // 结束标记 { NULL, 0 } }; // 初始化 heap_5 vPortDefineHeapRegions(xHeapRegions); printf("heap_5 初始化完成!\r\n"); }8.3 heap_5 使用示例
void Heap5Demo(void) { // 先初始化 InitHeap5(); // 分配内存(会从最合适的区域分配) void *pSmallBlock = pvPortMalloc(100); // 可能从内部RAM分配 void *pLargeBlock = pvPortMalloc(50000); // 可能从外部RAM分配 // 使用后释放 vPortFree(pSmallBlock); vPortFree(pLargeBlock); }九、内存分配失败钩子
9.1 实现钩子函数
void vApplicationMallocFailedHook(void) { // 内存分配失败时调用 printf("内存分配失败!剩余堆: %u\r\n", xPortGetFreeHeapSize()); // 可以:记录错误、复位系统、进入安全状态 while(1) { // 停止在这里 } }9.2 启用钩子函数
#define configUSE_MALLOC_FAILED_HOOK 1十、真实案例:内存池实现
10.1 案例背景
使用固定大小内存池避免碎片,提高性能。
10.2 完整实现
#include "FreeRTOS.h" #include "semphr.h" #define POOL_BLOCK_SIZE 64 #define POOL_BLOCK_COUNT 16 // 内存池 static uint8_t ucMemoryPool[POOL_BLOCK_COUNT][POOL_BLOCK_SIZE]; static uint8_t ucBlockUsed[POOL_BLOCK_COUNT]; static SemaphoreHandle_t xPoolMutex; // 初始化内存池 void InitMemoryPool(void) { xPoolMutex = xSemaphoreCreateMutex(); for(int i = 0; i < POOL_BLOCK_COUNT; i++) { ucBlockUsed[i] = 0; } } // 从池中分配 void *PoolMalloc(void) { void *pBlock = NULL; if(xSemaphoreTake(xPoolMutex, pdMS_TO_TICKS(100)) == pdTRUE) { // 查找空闲块 for(int i = 0; i < POOL_BLOCK_COUNT; i++) { if(ucBlockUsed[i] == 0) { ucBlockUsed[i] = 1; pBlock = &ucMemoryPool[i][0]; break; } } xSemaphoreGive(xPoolMutex); } return pBlock; } // 释放回池中 void PoolFree(void *pv) { if(xSemaphoreTake(xPoolMutex, pdMS_TO_TICKS(100)) == pdTRUE) { // 查找对应的块 for(int i = 0; i < POOL_BLOCK_COUNT; i++) { if(pv == &ucMemoryPool[i][0]) { ucBlockUsed[i] = 0; break; } } xSemaphoreGive(xPoolMutex); } } // 获取池使用情况 void GetPoolUsage(void) { int usedCount = 0; if(xSemaphoreTake(xPoolMutex, pdMS_TO_TICKS(100)) == pdTRUE) { for(int i = 0; i < POOL_BLOCK_COUNT; i++) { if(ucBlockUsed[i]) { usedCount++; } } xSemaphoreGive(xPoolMutex); } printf("内存池使用: %d/%d\r\n", usedCount, POOL_BLOCK_COUNT); } // 使用示例 void MemoryPoolDemo(void) { InitMemoryPool(); // 分配多个块 void *p1 = PoolMalloc(); void *p2 = PoolMalloc(); void *p3 = PoolMalloc(); GetPoolUsage(); // 3/16 // 释放 PoolFree(p2); GetPoolUsage(); // 2/16 }十一、真实案例:内存监控系统
11.1 案例背景
监控内存使用情况,记录分配/释放操作。
11.2 完整实现
#include "FreeRTOS.h" #include "task.h" #include #define MAX_ALLOCATIONS 50 typedef struct { void *pAddress; size_t size; const char *pFile; int line; uint32_t timestamp; } AllocationRecord_t; static AllocationRecord_t allocations[MAX_ALLOCATIONS]; static int allocationCount = 0; static SemaphoreHandle_t xMonitorMutex; // 初始化监控 void InitMemoryMonitor(void) { xMonitorMutex = xSemaphoreCreateMutex(); memset(allocations, 0, sizeof(allocations)); allocationCount = 0; } // 带监控的内存分配 void *MallocMonitor(size_t size, const char *file, int line) { void *p = pvPortMalloc(size); if(p != NULL && xSemaphoreTake(xMonitorMutex, 0) == pdTRUE) { // 记录分配 if(allocationCount < MAX_ALLOCATIONS) { allocations[allocationCount].pAddress = p; allocations[allocationCount].size = size; allocations[allocationCount].pFile = file; allocations[allocationCount].line = line; allocations[allocationCount].timestamp = xTaskGetTickCount(); allocationCount++; } xSemaphoreGive(xMonitorMutex); } return p; } // 带监控的内存释放 void FreeMonitor(void *p) { if(p != NULL && xSemaphoreTake(xMonitorMutex, 0) == pdTRUE) { // 查找并删除记录 for(int i = 0; i < allocationCount; i++) { if(allocations[i].pAddress == p) { // 移动后面的记录 for(int j = i; j < allocationCount - 1; j++) { allocations[j] = allocations[j + 1]; } allocationCount--; break; } } xSemaphoreGive(xMonitorMutex); } vPortFree(p); } // 打印内存分配信息 void PrintMemoryAllocationInfo(void) { if(xSemaphoreTake(xMonitorMutex, pdMS_TO_TICKS(100)) == pdTRUE) { printf("当前内存分配 (%d):\r\n", allocationCount); for(int i = 0; i < allocationCount; i++) { printf(" #%d: %p, %u 字节, %s:%d, %lu ms\r\n", i, allocations[i].pAddress, allocations[i].size, allocations[i].pFile, allocations[i].line, allocations[i].timestamp); } xSemaphoreGive(xMonitorMutex); } printf("剩余堆: %u 字节\r\n", xPortGetFreeHeapSize()); printf("历史最小剩余: %u 字节\r\n", xPortGetMinimumEverFreeHeapSize()); } // 宏定义,方便使用 #define MONITOR_MALLOC(size) MallocMonitor(size, __FILE__, __LINE__) #define MONITOR_FREE(p) FreeMonitor(p) // 使用示例 void MemoryMonitorDemo(void) { InitMemoryMonitor(); // 使用带监控的分配/释放 void *p1 = MONITOR_MALLOC(100); void *p2 = MONITOR_MALLOC(200); PrintMemoryAllocationInfo(); MONITOR_FREE(p1); PrintMemoryAllocationInfo(); }十二、内存管理最佳实践
12.1 选择合适的 heap 方案
// ✅ 大多数应用:使用 heap_4
// 自动合并空闲块,减少碎片
// ✅ 多内存区域:使用 heap_5
// 支持不连续的内存区域
// ✅ 简单应用,永不释放:使用 heap_1
// 最简单,无碎片
// ❌ 除非必要:避免 heap_2
// 不合并空闲块,碎片问题
12.2 避免内存碎片
// ✅ 好的做法:固定大小分配 for(int i = 0; i < 10; i++) { void *p = pvPortMalloc(64); // 使用 vPortFree(p); // 碎片少 } // ❌ 不好的做法:大小变化大 void *p1 = pvPortMalloc(1000); void *p2 = pvPortMalloc(100); void *p3 = pvPortMalloc(500); vPortFree(p1); vPortFree(p3); // p2 卡在中间,碎片严重12.3 检查返回值
// ✅ 总是检查返回值 void *p = pvPortMalloc(size); if(p != NULL) { // 使用内存 } else { // 处理错误 }12.4 及时释放内存
// ✅ 函数内部分配,及时释放 void Function(void) { void *p = pvPortMalloc(100); if(p != NULL) { // 使用 vPortFree(p); // 释放 } }📌 要点:选型要点
同一时间只能链接一个 heap_x.c 源文件(位于 Source/portable/MemMang/);
heap_1/2/4/5 的堆大小由 FreeRTOSConfig.h 的 configTOTAL_HEAP_SIZE 决定,heap_3 使用链接器分配的堆;
可用 xPortGetFreeHeapSize() 与 xPortGetMinimumEverFreeHeapSize() 监控堆余量,辅助优化堆大小。