tags: [source-summary] type: source source: "尚硅谷嵌入式技术之FreeRTOS实时操作系统 V1.0.3 — 内存管理" author: "尚硅谷研究院" date: 2026-07-17
用生活理解:动态内存像租房子——heap_1 是只租不退的宿舍(简单粗暴),heap_2 是随意退租但不打扫的房间(会碎片),heap_4 是退租时帮你合并相邻空房的管家(推荐),heap_5 是能跨多个不同地段管理房源的大中介。
FreeRTOS 在创建任务、队列、信号量等内核对象时,支持动态创建和静态创建两种方式。动态方式更灵活(调用 pvPortMalloc 自动从堆中分配),静态方式则需要用户预先分配内存。
标准 C 库的 malloc() / free() 在嵌入式系统中存在以下问题:
为此 FreeRTOS 将内存分配 API 放在可移植层(MemMang/ 目录),提供了 5 种 heap 算法。
| 算法 | 策略 | 释放 | 碎片 | 适用场景 |
|---|---|---|---|---|
| heap_1 | 简单数组分配 | ❌ 不支持 | 无碎片 | 从不删除任务/队列的简单应用 |
| heap_2 | 最佳适应(Best-fit)链表 | ✅ | ❌ 会碎片 | 动态创建/删除但大小固定的场景 |
| heap_3 | 包装标准 malloc/free | ✅ | 取决于 C 库 | 需要线程安全的 malloc |
| heap_4 | 首次适应(First-fit)+合并相邻块 | ✅ | 碎片少 | 默认推荐 |
| heap_5 | 同 heap_4 | ✅ | 碎片少 | 多个非连续 RAM 区域 |
heap_1:最简单的实现。一个大数组作为堆空间,每次分配从空闲位置递增指针。不允许释放,因此不会产生碎片。适用于系统启动时一次性创建所有内核对象、永不删除的场景。
heap_2:使用最佳适应(Best-fit)算法——在所有空闲块中选择与请求大小最接近的块进行分配。支持释放,但不会合并相邻空闲块。当频繁分配和释放不同大小的内存时,会产生严重碎片。适用于创建/删除任务大小始终相同的场景。
heap_3:简单包装 C 库的 malloc() / free(),通过挂起调度器实现线程安全。内存分配行为完全取决于 C 库实现。
heap_4:使用首次适应(First-fit)算法——在空闲块链表中选择第一个足够大的块进行分配。与 heap_2 的关键区别:释放时会合并相邻空闲块,显著减少碎片。是 FreeRTOS 官方推荐的默认算法。
heap_5:算法与 heap_4 完全相同,区别在于支持跨多个非连续 RAM 区域的堆。适用于内存地址不连续的复杂硬件场景(如外扩 SRAM + 内部 SRAM 并存)。
| API | 描述 |
|---|---|
void *pvPortMalloc(size_t xWantedSize) |
申请内存,返回指针 |
void vPortFree(void *pv) |
释放内存 |
size_t xPortGetFreeHeapSize(void) |
获取当前空闲堆大小 |
pvPortMalloc(20) 申请 20 字节,KEY2 → vPortFree() 释放xPortGetFreeHeapSize()File: 21_内存管理/Core/Src/freertos_demo.c
#include "freertos_demo.h"
#include "FreeRTOS.h"
#include "task.h"
#include "LED.h"
#include "Key.h"
#define START_TASK_STACK 128
#define START_TASK_PRIORITY 1
TaskHandle_t start_task_handle;
void start_task(void *pvParameters);
#define TASK1_STACK 128
#define TASK1_PRIORITY 2
TaskHandle_t task1_handle;
void task1(void *pvParameters);
void freertos_start(void)
{
xTaskCreate((TaskFunction_t)start_task,
(char *)"start_task",
(configSTACK_DEPTH_TYPE)START_TASK_STACK,
(void *)NULL,
(UBaseType_t)START_TASK_PRIORITY,
(TaskHandle_t *)&start_task_handle);
vTaskStartScheduler();
}
void start_task(void *pvParameters)
{
taskENTER_CRITICAL();
xTaskCreate((TaskFunction_t)task1,
(char *)"task1",
(configSTACK_DEPTH_TYPE)TASK1_STACK,
(void *)NULL,
(UBaseType_t)TASK1_PRIORITY,
(TaskHandle_t *)&task1_handle);
vTaskDelete(NULL); // 启动任务自我删除
taskEXIT_CRITICAL();
}
void task1(void *pvParameters)
{
uint8_t key = 0;
void *buff = 0;
while (1)
{
key = Key_Detect();
if (key == KEY1_PRESS)
{
// KEY1 按下 → 申请 20 字节内存
buff = pvPortMalloc(20);
printf("申请 20 字节内存,当前剩余堆大小=%d\r\n",
xPortGetFreeHeapSize());
}
else if (key == KEY2_PRESS)
{
// KEY2 按下 → 释放内存
vPortFree(buff);
printf("释放内存,当前剩余堆大小=%d\r\n",
xPortGetFreeHeapSize());
}
vTaskDelay(500);
}
}
// FreeRTOSConfig.h 内存相关配置
#define configTOTAL_HEAP_SIZE ((size_t)(10 * 1024)) // 总堆大小 10KB
注意:实际使用 heap_4 时 configTOTAL_HEAP_SIZE 决定了 ucHeap[] 数组大小,所有动态创建的任务、队列、信号量等对象都从这片空间分配。
| API | 参数 | 返回值 | 说明 |
|---|---|---|---|
pvPortMalloc(xWantedSize) |
xWantedSize=请求字节数 | 成功返回指针,失败返回 NULL | 从 FreeRTOS 堆中分配内存 |
vPortFree(pv) |
pv=要释放的内存指针 | 无 | 释放内存回堆 |
xPortGetFreeHeapSize() |
无 | size_t 空闲字节数 | 获取当前堆空闲大小 |
xPortGetMinimumEverFreeHeapSize() |
无 | size_t 历史最小空闲 | 追踪最小剩余堆(需 configUSE_TRACE_FACILITY=1) |
注意:
xPortGetFreeHeapSize()只在 heap_1/2/4/5 中有效(heap_3 包装 C 库 malloc,无法统计 FreeRTOS 堆)。
默认使用 heap_4:移植 FreeRTOS 时必须从 MemMang/ 中选择一个 heap 文件加入工程(通常选 heap_4.c),否则链接报错。
configTOTAL_HEAP_SIZE 不够会导致创建任务失败:任务栈、TCB 等全部从堆中分配。堆太小 → pvPortMalloc 返回 NULL → xTaskCreate 返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY。
pvPortMalloc 返回 NULL 要处理:申请失败不会崩溃,但后续使用 NULL 指针会导致 HardFault。务必检查返回值。
释放野指针会导致 HardFault:vPortFree 必须传入 pvPortMalloc 返回的有效指针。释放栈上变量或已释放的指针会造成不可预知的错误。
不能在中断中调用 pvPortMalloc / vPortFree:这些函数不是中断安全的。在中断中需要分配内存时,使用中断安全版的队列/信号量函数替代。
heap_4 的内存碎片虽然少但仍有:长期频繁分配/释放不同大小的内存仍会导致外部碎片。定期用 xPortGetMinimumEverFreeHeapSize() 监控堆健康状态。
heap_5 需要额外初始化:使用前必须调用 vPortDefineHeapRegions() 指定每个 RAM 区域的起始地址和大小。