跳转至

内存管理

FreeRTOS 运行在资源受限的 MCU 上(如 STM32F103 只有 20KB SRAM),内存管理直接影响系统的稳定性。理解堆内存分配方案、合理配置栈大小、检测内存问题,是保障 RTOS 程序可靠运行的基础。


FreeRTOS 内存分区

RTOS 系统的 SRAM 被划分为以下几个区域:

graph TB
    subgraph "SRAM 内存布局(从低地址到高地址)"
        G["全局/静态变量<br>.data + .bss"]
        H["FreeRTOS 堆<br>(TOTAL_HEAP_SIZE)<br>任务栈、TCB、队列、信号量<br>都从这里分配"]
        MS["主栈<br>(MSP Stack)<br>main()、中断处理用"]
    end
    G --- H --- MS
区域 来源 用途
全局/静态变量 .data + .bss 全局变量、static 变量
FreeRTOS 堆 configTOTAL_HEAP_SIZE 任务栈、TCB、队列、信号量、定时器等
主栈 链接脚本定义 main() 函数和所有中断处理程序

FreeRTOS 堆 ≠ C 标准库堆

FreeRTOS 有自己的内存管理,不使用 C 标准库的 malloc/free(除了 heap_3 方案)。pvPortMalloc()vPortFree() 是 FreeRTOS 的内存分配/释放函数。


五种堆管理方案

FreeRTOS 提供了 5 种堆管理实现(heap_1 ~ heap_5),CubeMX 默认使用 heap_4。

heap_1:只分配不释放

分配前:|████████████████████████████████| 完整堆空间
分配后:|■■■TCB■■|■■栈■■■|■■TCB■■|■■栈■■■|████| 已用 | 剩余
                                                    ↑ 只能往后分配
特点 说明
分配 ✅ 简单高效
释放 ❌ 不支持
碎片 无(因为不释放)
适用场景 所有任务/队列在初始化时创建,运行期间不动态创建删除

heap_2:可释放但不合并

使用中:|■■A■■|███|■■B■■|███|■■C■■|████████|
释放B:  |■■A■■|███|     |███|■■C■■|████████|
                    ↑ 空洞,但不合并到右边的空闲区
特点 说明
分配 ✅ 支持
释放 ✅ 支持
碎片 ⚠️ 会产生(不合并相邻空闲块)
适用场景 分配/释放的块大小固定

heap_3:封装 malloc/free

直接调用 C 标准库的 malloc()free(),加上线程安全保护。

特点 说明
使用的堆 C标准库堆(不是 configTOTAL_HEAP_SIZE
碎片 取决于标准库实现
适用场景 需要兼容已有代码、使用标准库内存管理

heap_4(默认推荐):可释放 + 合并碎片

使用中:|■■A■■|■■B■■|■■C■■|████████████████|
释放B:  |■■A■■|          |■■C■■|████████████████|
                 ↑ 自动合并为更大的空闲块
特点 说明
分配 ✅ 支持
释放 ✅ 支持
碎片 ✅ 自动合并相邻空闲块,大幅减少碎片
适用场景 绝大多数项目(CubeMX 默认选择)

为什么推荐 heap_4?

  • 支持动态创建/删除任务、队列等
  • 自动合并碎片,内存利用率高
  • 实现简单高效,适合 MCU
  • CubeMX 默认方案,无需额外配置

heap_5:多内存区域

heap_4 的升级版,支持将不连续的内存区域合并为一个堆使用。

特点 说明
特殊能力 可以使用多块不连续的 SRAM(如 STM32F4 的 CCM SRAM)
适用场景 芯片有多个 SRAM 区域,想全部用于 FreeRTOS 堆

方案对比总结

方案 分配 释放 碎片合并 多区域 推荐度
heap_1 - 极简系统
heap_2 固定大小分配
heap_3 取决于库 兼容需求
heap_4 ⭐ 默认首选
heap_5 多 SRAM 芯片

CubeMX 配置内存参数

关键配置项

Middleware → FREERTOS → Config parameters 中:

参数 默认值 说明
MEMORY_ALLOCATION Dynamic 动态分配(使用堆)
TOTAL_HEAP_SIZE 15360 FreeRTOS 堆大小(字节)
Memory Management scheme heap_4 堆管理方案
CHECK_FOR_STACK_OVERFLOW Option1 或 Option2 栈溢出检测方式
INCLUDE_uxTaskGetStackHighWaterMark 1 启用栈水位检测

堆大小怎么算?

FreeRTOS 堆需要容纳所有动态分配的对象:

对象 占用内存 计算公式
每个任务 TCB + 栈 约 100 + StackSize×4 字节
每个队列 控制块 + 缓冲区 约 80 + QueueSize×ItemSize 字节
每个信号量 控制块 约 88 字节
每个互斥量 控制块 约 88 字节
每个定时器 控制块 约 48 字节

估算示例:

4个任务(栈各 256 Words)= 4 × (100 + 256×4) = 4496 字节
2个队列(大小10,每个4字节)= 2 × (80 + 10×4) = 240 字节
3个信号量 = 3 × 88 = 264 字节
1个互斥量 = 88 字节
1个定时器 = 48 字节
定时器守护任务 = 100 + 256×4 = 1124 字节
空闲任务 = 100 + 128×4 = 612 字节
─────────────────────────────────────
合计约:6872 字节 + 安全余量 ≈ 8~10KB

实际调优方法

  1. 先给一个较大的值(如 15360)
  2. 编译运行后调用 xPortGetFreeHeapSize() 查看剩余堆
  3. 根据剩余量适当缩小,留 1~2KB 余量
  4. 同时用 xPortGetMinimumEverFreeHeapSize() 查看历史最小剩余

栈空间管理

任务栈 vs 主栈

用途 大小配置
任务栈 每个任务的局部变量、函数调用 CubeMX 中每个任务单独配置(Stack Size)
主栈(MSP) main() 函数 + 所有中断处理 链接脚本中 _Min_Stack_Size(默认 0x400 = 1KB)

中断也用主栈!

所有中断处理函数(HAL 回调)都使用主栈,不是任务栈。如果中断中使用大数组或调用复杂函数,需要增大主栈。

在 CubeMX 中修改:Project Manager → Linker Settings → Minimum Stack Size

栈溢出的后果

当任务栈空间不够用时:

正常:  |──────任务栈空间──────|
         [局部变量][调用链][...]  [空闲]

溢出:  |──────任务栈空间──────|
         [局部变量][调用链][...][溢出数据→] → 写入其他内存区域!
         ↑ 可能破坏TCB、其他任务的栈、全局变量

后果:

  • HardFault:最常见,系统直接崩溃
  • 数据错误:破坏其他变量,症状随机且难以定位
  • 看门狗复位:系统无响应,被看门狗重启

栈溢出检测

CubeMX 中 CHECK_FOR_STACK_OVERFLOW 提供两种检测方法:

每次任务切换时检查栈指针是否超出边界。

  • ✅ 开销极小
  • ❌ 如果溢出后又恢复(写了又被覆盖),可能检测不到

在栈底写入特殊标记值(0xA5A5A5A5),每次切换检查标记是否被破坏。

  • ✅ 检测更可靠
  • ❌ 开销稍大,但通常可以忽略

检测到溢出后会调用钩子函数:

void vApplicationStackOverflowHook(xTaskHandle xTask,
                                    signed char *pcTaskName)
{
    /* 打印出问题的任务名 */
    printf("STACK OVERFLOW: %s\r\n", pcTaskName);

    /* 方案1:死循环,方便调试器定位 */
    while (1);

    /* 方案2:记录错误并重启 */
    // HAL_NVIC_SystemReset();
}

栈水位检测

主动检查任务栈的使用情况:

/* 获取任务栈历史最小剩余量(Words) */
void Monitor_Task(void *argument)
{
    for (;;)
    {
        UBaseType_t wm;

        wm = uxTaskGetStackHighWaterMark(LED_TaskHandle);
        printf("LED_Task  stack watermark: %lu words\r\n", wm);

        wm = uxTaskGetStackHighWaterMark(UART_TaskHandle);
        printf("UART_Task stack watermark: %lu words\r\n", wm);

        wm = uxTaskGetStackHighWaterMark(ADC_TaskHandle);
        printf("ADC_Task  stack watermark: %lu words\r\n", wm);

        osDelay(5000);  // 每5秒检查一次
    }
}

输出示例:

LED_Task  stack watermark: 98 words    ← 剩余充足,可缩小栈
UART_Task stack watermark: 32 words    ← 还行
ADC_Task  stack watermark: 8 words     ← ⚠️ 危险!马上要溢出了!

安全余量建议

栈水位 ≥ 30 Words(120 字节)才算安全。如果接近 0,必须立即增大栈。


堆使用监控

查看堆内存状态

/* 在运行时查看堆内存使用情况 */
void PrintHeapInfo(void)
{
    size_t free_heap = xPortGetFreeHeapSize();
    size_t min_free = xPortGetMinimumEverFreeHeapSize();

    printf("当前剩余堆: %u bytes\r\n", free_heap);
    printf("历史最小剩余: %u bytes\r\n", min_free);
    printf("已使用堆: %u bytes\r\n",
           configTOTAL_HEAP_SIZE - free_heap);
    printf("堆利用率: %.1f%%\r\n",
           (1.0f - (float)free_heap / configTOTAL_HEAP_SIZE) * 100);
}

输出示例:

当前剩余堆: 5120 bytes
历史最小剩余: 3840 bytes
已使用堆: 10240 bytes
堆利用率: 66.7%

内存分配失败处理

pvPortMalloc() 返回 NULL 时,默认会调用 vApplicationMallocFailedHook()

void vApplicationMallocFailedHook(void)
{
    /* 内存分配失败!通常是 TOTAL_HEAP_SIZE 不够 */
    printf("Malloc failed! Free heap: %u\r\n",
           xPortGetFreeHeapSize());
    while (1);
}

在 CubeMX 中启用

Config parameters → USE_MALLOC_FAILED_HOOK = 1


动态分配 vs 静态分配

动态分配(默认)

任务、队列等从 FreeRTOS 堆中动态分配:

/* 动态分配(CubeMX 默认) */
osThreadId_t handle = osThreadNew(MyTask, NULL, &task_attr);
// 内存从 FreeRTOS 堆中分配

静态分配

预先在全局区定义好内存,不使用堆:

/* 静态分配:自己提供栈和TCB内存 */
StaticTask_t taskTCB;
uint32_t taskStack[256];

const osThreadAttr_t task_attr = {
    .name = "StaticTask",
    .cb_mem = &taskTCB,          // 控制块内存
    .cb_size = sizeof(taskTCB),
    .stack_mem = taskStack,       // 栈内存
    .stack_size = sizeof(taskStack),
    .priority = (osPriority_t) osPriorityNormal,
};

osThreadNew(MyTask, NULL, &task_attr);
对比 动态分配 静态分配
内存来源 FreeRTOS 堆 全局变量/静态变量
灵活性 高(运行时创建/销毁) 低(编译时确定)
碎片风险
适用场景 一般项目 安全关键系统、内存紧张
CubeMX 支持 ✅ 默认 ✅ Allocation 选 Static

CubeMX 中切换分配方式

创建任务/队列时,Allocation 参数选择 Static 即可使用静态分配。CubeMX 会自动生成静态缓冲区。


内存优化技巧

1. 精确设置栈大小

不要给每个任务都分配 512 Words,按实际需求分配:

任务类型 推荐栈大小
简单 GPIO/LED 128 Words(512B)
串口通信 256 Words(1KB)
使用 printf 384+ Words(1.5KB+)
浮点运算 256+ Words
嵌套函数调用深 根据嵌套深度增加

2. 减少不必要的全局变量

// ❌ 浪费:大数组常驻内存
uint8_t big_buffer[1024];  // 即使只偶尔使用

// ✅ 改进:用 pvPortMalloc 按需分配
void SomeTask(void *argument)
{
    for (;;)
    {
        if (need_big_buffer)
        {
            uint8_t *buf = pvPortMalloc(1024);
            if (buf != NULL)
            {
                use_buffer(buf);
                vPortFree(buf);  // 用完释放
            }
        }
        osDelay(1000);
    }
}

3. 避免在栈上分配大数组

void BadTask(void *argument)
{
    for (;;)
    {
        char buf[512];  // ❌ 占用大量栈空间
        snprintf(buf, sizeof(buf), "...");
        osDelay(100);
    }
}

void GoodTask(void *argument)
{
    static char buf[512];  // ✅ 改为 static,不占用栈
    for (;;)
    {
        snprintf(buf, sizeof(buf), "...");
        osDelay(100);
    }
}

4. 合理设置 TOTAL_HEAP_SIZE

// 在初始化完成后打印堆信息,据此调整
void SystemInitDone(void)
{
    printf("初始化完成,剩余堆: %u / %u bytes (%.1f%% free)\r\n",
           xPortGetFreeHeapSize(),
           configTOTAL_HEAP_SIZE,
           (float)xPortGetFreeHeapSize() / configTOTAL_HEAP_SIZE * 100);
}

内存调试实战

完整的内存监控任务

/* 内存监控任务:定期输出系统资源状态 */
void Monitor_Task(void *argument)
{
    char task_list[512];

    for (;;)
    {
        printf("\r\n===== System Monitor =====\r\n");

        /* 1. 堆内存状态 */
        printf("[Heap] Free: %u B, Min: %u B, Used: %u B\r\n",
               xPortGetFreeHeapSize(),
               xPortGetMinimumEverFreeHeapSize(),
               configTOTAL_HEAP_SIZE - xPortGetFreeHeapSize());

        /* 2. 各任务栈水位 */
        printf("[Stack Watermark]\r\n");
        printf("  LED_Task:  %lu words\r\n",
               uxTaskGetStackHighWaterMark(LED_TaskHandle));
        printf("  UART_Task: %lu words\r\n",
               uxTaskGetStackHighWaterMark(UART_TaskHandle));
        printf("  ADC_Task:  %lu words\r\n",
               uxTaskGetStackHighWaterMark(ADC_TaskHandle));

        /* 3. 任务列表(需启用 configUSE_TRACE_FACILITY) */
        vTaskList(task_list);
        printf("[Task List]\r\n"
               "Name            State  Prio  Stack  Num\r\n"
               "%s\r\n", task_list);

        /* 4. 运行时间统计(需启用 configGENERATE_RUN_TIME_STATS) */
        // vTaskGetRunTimeStats(task_list);
        // printf("[Runtime Stats]\r\n%s\r\n", task_list);

        osDelay(10000);  // 每10秒输出一次
    }
}

常见问题

程序一启动就进 HardFault?

最常见原因:

  1. TOTAL_HEAP_SIZE 太大,超过了芯片实际 SRAM 大小。STM32F103C8T6 只有 20KB SRAM,去掉全局变量和主栈后,堆最多可能只剩 10~15KB
  2. 主栈太小,链接脚本中 _Min_Stack_Size 不够
  3. 检查方法:在调试器中查看 SP(栈指针)是否超出范围

pvPortMalloc 返回 NULL 但堆还有空间?

这是内存碎片导致的。堆中虽然总剩余空间够,但没有连续的大块空间可用。

解决方案:

  • 使用 heap_4(自动合并碎片)
  • 统一分配大小,减少碎片
  • 改用静态分配

如何确定芯片还剩多少 SRAM 可用?

编译后查看 .map 文件或编译输出:

Program Size: Code=xxxx RO-data=xxxx RW-data=xxxx ZI-data=xxxx

RW-data + ZI-data = 已用的 SRAM(不含栈和堆)。用芯片总 SRAM 减去这个值,就是栈+堆可用的空间。

任务越多越好吗?

不是。每个任务需要:

  • TCB:约 100 字节
  • 栈:至少 128×4 = 512 字节
  • 上下文切换开销

在 20KB SRAM 的芯片上,4~6 个任务是较合理的数量。