16-内存管理.md 7.3 KB


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 是能跨多个不同地段管理房源的大中介。

Concept

Basic concept

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 区域

Working principle

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 并存)。

Core API

API 描述
void *pvPortMalloc(size_t xWantedSize) 申请内存,返回指针
void vPortFree(void *pv) 释放内存
size_t xPortGetFreeHeapSize(void) 获取当前空闲堆大小

Experiment

Requirements

  • task1 按键扫描:KEY1 → pvPortMalloc(20) 申请 20 字节,KEY2 → vPortFree() 释放
  • 每次操作后打印剩余堆大小 xPortGetFreeHeapSize()

Code Implementation

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);
    }
}

Key Config

// FreeRTOSConfig.h 内存相关配置
#define configTOTAL_HEAP_SIZE    ((size_t)(10 * 1024))   // 总堆大小 10KB

注意:实际使用 heap_4 时 configTOTAL_HEAP_SIZE 决定了 ucHeap[] 数组大小,所有动态创建的任务、队列、信号量等对象都从这片空间分配。

Core API Reference Table

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. 释放野指针会导致 HardFaultvPortFree 必须传入 pvPortMalloc 返回的有效指针。释放栈上变量或已释放的指针会造成不可预知的错误。

  5. 不能在中断中调用 pvPortMalloc / vPortFree:这些函数不是中断安全的。在中断中需要分配内存时,使用中断安全版的队列/信号量函数替代。

  6. heap_4 的内存碎片虽然少但仍有:长期频繁分配/释放不同大小的内存仍会导致外部碎片。定期用 xPortGetMinimumEverFreeHeapSize() 监控堆健康状态。

  7. heap_5 需要额外初始化:使用前必须调用 vPortDefineHeapRegions() 指定每个 RAM 区域的起始地址和大小。

Heap 算法对比