内存管理
理解 FreeRTOS 五种堆方案的取舍,动态与静态分配的选择,内存碎片与栈溢出这两大隐形杀手,以及如何监控剩余内存。
理解 FreeRTOS 五种堆方案的取舍,动态与静态分配的选择,内存碎片与栈溢出这两大隐形杀手,以及如何监控剩余内存。
pvPortMalloc() 从这块堆里分配。堆的实现由你选择的 heap_x.c 决定,总大小由 configTOTAL_HEAP_SIZE 配置。用它而非标准 malloc,是为了确定性和可移植性。xPortGetFreeHeapSize 统计到。configTOTAL_HEAP_SIZE 无效)。适合已依赖 libc 堆的场景,但失去了确定性。vPortDefineHeapRegions() 初始化。适合内存分散、需要大堆的高端 MCU。configCHECK_FOR_STACK_OVERFLOW(方法 1/2)在切换任务时检测,触发 vApplicationStackOverflowHook。| 方案 | 可释放 | 合并空闲块 | 碎片风险 | 适用场景 |
|---|---|---|---|---|
| heap_1 | 否 | — | 无 | 启动全创建、永不删除;安全关键 |
| heap_2 | 是 | 否 | 高 | 已弃用,勿用于新项目 |
| heap_3 | 是 | 由 libc 决定 | 取决于 libc | 复用标准 malloc |
| heap_4 | 是 | 是 | 低 | 通用首选,动态增删对象 |
| heap_5 | 是 | 是 | 低 | 多块不连续内存合成大堆 |
不确定就选 heap_4。只在启动时创建、运行期绝不删除对象且追求极致确定性 → heap_1。需要跨内部 SRAM + 外部 SDRAM 的大堆 → heap_5。已深度依赖 C 库堆 → heap_3。heap_2 除维护老代码外不要用。把选中的那个 heap_x.c 加入工程即可,同时只能有一个。
/* FreeRTOSConfig.h */ #define configSUPPORT_DYNAMIC_ALLOCATION 1 #define configTOTAL_HEAP_SIZE ((size_t)(20 * 1024)) /* 20KB 堆 */ /* 动态创建:TCB 和栈从 FreeRTOS 堆分配 */ TaskHandle_t h; if (xTaskCreate(MyTask, "T", 256, NULL, 2, &h) != pdPASS) { /* 返回失败 = 堆不够,检查 configTOTAL_HEAP_SIZE */ } /* 用户数据也可用 FreeRTOS 分配器(线程安全、可统计) */ uint8_t *buf = pvPortMalloc(128); if (buf) { /* ... 使用 buf ... */ vPortFree(buf); /* 用完释放(heap_4/5 支持) */ }
/* FreeRTOSConfig.h */ #define configSUPPORT_STATIC_ALLOCATION 1 /* 用户提供栈和 TCB 内存,运行期不碰堆 */ #define ST_WORDS 256 static StackType_t s_stack[ST_WORDS]; static StaticTask_t s_tcb; void setup_static(void) { TaskHandle_t h = xTaskCreateStatic( MyTask, "T", ST_WORDS, NULL, 2, s_stack, &s_tcb); /* 静态创建绝不会因堆不足失败,句柄非空 */ } /* 启用静态分配时,还必须提供空闲任务和 Timer 任务的内存 */ void vApplicationGetIdleTaskMemory(StaticTask_t **ppxTCB, StackType_t **ppxStack, uint32_t *pulDepth) { static StaticTask_t idleTcb; static StackType_t idleStack[configMINIMAL_STACK_SIZE]; *ppxTCB = &idleTcb; *ppxStack = idleStack; *pulDepth = configMINIMAL_STACK_SIZE; }
动态:灵活,可运行期增删对象,代码简洁,是大多数应用的默认;风险是堆碎片和 malloc 失败。静态:内存布局编译期完全确定,绝不会因堆耗尽失败,通过 MISRA/功能安全认证更容易;代价是要手写内存缓冲、不够灵活。安全关键(汽车、医疗)常强制静态;普通消费电子多用动态 + heap_4。两者可混用。
/* FreeRTOSConfig.h:方法 2 检测更可靠(检查栈末尾填充字节) */ #define configCHECK_FOR_STACK_OVERFLOW 2 /* 触发时内核回调此钩子;用于定位是哪个任务栈爆了 */ void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { (void) xTask; printf("STACK OVERFLOW in task: %s\r\n", pcTaskName); taskDISABLE_INTERRUPTS(); for (;;) { } /* 停在此处,配合调试器抓现场 */ } /* 运行期查询任务栈"还剩多少字",用于调优栈大小 */ void check_stack(TaskHandle_t h) { UBaseType_t hw = uxTaskGetStackHighWaterMark(h); printf("min free stack = %lu words\r\n", (unsigned long)hw); }
/* 当前剩余堆(字节) */ size_t freeNow = xPortGetFreeHeapSize(); /* 历史最低剩余(水位线),判断最坏情况余量 */ size_t everMin = xPortGetMinimumEverFreeHeapSize(); printf("heap free=%u, ever-min=%u\r\n", (unsigned)freeNow, (unsigned)everMin); /* malloc 失败钩子:需 configUSE_MALLOC_FAILED_HOOK = 1 */ void vApplicationMallocFailedHook(void) { printf("pvPortMalloc FAILED — heap exhausted!\r\n"); for (;;) { } }
xTaskCreate 的栈深度和 uxTaskGetStackHighWaterMark 返回值单位都是字(word)。Cortex-M 上 1 字 = 4 字节,所以 256 是 1024 字节。若任务里用了 printf、浮点、或大局部数组,很容易吃掉几百字节,务必用高水位线实测后再留 20%~30% 余量,不要拍脑袋。
FreeRTOS 自带独立于 libc 的堆分配器,创建内核对象时通过 pvPortMalloc 从 configTOTAL_HEAP_SIZE 大小的堆分配。五种方案:heap_1 只分配不释放、无碎片、最确定;heap_2 可释放但不合并(已弃用);heap_3 包装 libc malloc;heap_4 首次适配 + 合并空闲块、碎片低,是通用首选(CubeMX 默认);heap_5 支持多块不连续内存合成大堆。内存碎片是长期稳定性头号敌人,用 heap_4/5、静态分配、内存池、少运行期 malloc 来对抗。动态分配灵活但有碎片/失败风险;静态分配(configSUPPORT_STATIC_ALLOCATION + xTaskCreateStatic)编译期确定、绝不失败,安全关键领域首选。栈溢出用 configCHECK_FOR_STACK_OVERFLOW=2 检测并回调 vApplicationStackOverflowHook,用 uxTaskGetStackHighWaterMark 实测栈余量。用 xPortGetFreeHeapSize/xPortGetMinimumEverFreeHeapSize 监控堆,vApplicationMallocFailedHook 捕获耗尽。栈深度单位是字(×4 才是字节)。