--- tags: [source-summary] type: source source: "尚硅谷嵌入式技术之FreeRTOS实时操作系统 V1.0.3 — 内存管理" author: "尚硅谷研究院" date: 2026-07-17 created: 2026-07-17 --- # FreeRTOS 内存管理 > **用生活理解**:动态内存像租房子——heap_1 是只租不退的宿舍(简单粗暴),heap_2 是随意退租但不打扫的房间(会碎片),heap_4 是退租时帮你合并相邻空房的管家(推荐),heap_5 是能跨多个不同地段管理房源的大中介。 ## 概念介绍 ### 基本概念 FreeRTOS 在创建任务、队列、信号量等内核对象时,支持**动态创建**和**静态创建**两种方式。动态方式更灵活(调用 `pvPortMalloc` 自动从堆中分配),静态方式则需要用户预先分配内存。 标准 C 库的 `malloc()` / `free()` 在嵌入式系统中存在以下问题: - 并非所有嵌入式平台都可用 - 占用宝贵的代码空间 - 线程不安全 - 非确定性(执行时间不确定) 为此 FreeRTOS 将内存分配 API 放在可移植层(`MemMang/` 目录),提供了 5 种 heap 算法。 ### 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 | API | 描述 | |-----|------| | `void *pvPortMalloc(size_t xWantedSize)` | 申请内存,返回指针 | | `void vPortFree(void *pv)` | 释放内存 | | `size_t xPortGetFreeHeapSize(void)` | 获取当前空闲堆大小 | ## 实验 ### 需求描述 - task1 按键扫描:KEY1 → `pvPortMalloc(20)` 申请 20 字节,KEY2 → `vPortFree()` 释放 - 每次操作后打印剩余堆大小 `xPortGetFreeHeapSize()` ### 代码实现 **代码来源:**03_代码/21_内存管理/Core/Src/freertos_demo.c ```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); } } ``` ### 关键配置 ```c // 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 堆)。 ## 常见问题与避坑 1. **默认使用 heap_4**:移植 FreeRTOS 时必须从 `MemMang/` 中选择一个 heap 文件加入工程(通常选 `heap_4.c`),否则链接报错。 2. **`configTOTAL_HEAP_SIZE` 不够会导致创建任务失败**:任务栈、TCB 等全部从堆中分配。堆太小 → `pvPortMalloc` 返回 NULL → `xTaskCreate` 返回 `errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY`。 3. **`pvPortMalloc` 返回 NULL 要处理**:申请失败不会崩溃,但后续使用 NULL 指针会导致 HardFault。务必检查返回值。 4. **释放野指针会导致 HardFault**:`vPortFree` 必须传入 `pvPortMalloc` 返回的有效指针。释放栈上变量或已释放的指针会造成不可预知的错误。 5. **不能在中断中调用 `pvPortMalloc` / `vPortFree`**:这些函数不是中断安全的。在中断中需要分配内存时,使用中断安全版的队列/信号量函数替代。 6. **heap_4 的内存碎片虽然少但仍有**:长期频繁分配/释放不同大小的内存仍会导致外部碎片。定期用 `xPortGetMinimumEverFreeHeapSize()` 监控堆健康状态。 7. **heap_5 需要额外初始化**:使用前必须调用 `vPortDefineHeapRegions()` 指定每个 RAM 区域的起始地址和大小。 ![Heap 算法对比](./assets/heap_comparison.png)