前言
学 FreeRTOS 第一道坎不是任务,是搞懂内存。堆和栈分不清,后面任务栈大小、内存管理全都会懵。
目录
[1.1 堆的概念](#1.1 堆的概念)
[1.2 堆的模拟实现](#1.2 堆的模拟实现)
[2.1 栈的概念](#2.1 栈的概念)
[2.2 栈的大小实验](#2.2 栈的大小实验)
[四、FreeRTOS 的五种内存管理方案](#四、FreeRTOS 的五种内存管理方案)
一、堆(Heap)
1.1 堆的概念
堆是由程序员通过动态内存分配函数(malloc()、new 等)分配的内存区域,当你不再需要这块内存时,必须显式释放它(free() / delete)。
简单理解:堆是一块空闲内存,可以从中取出一部分,用完再还回去。
在 FreeRTOS 里,堆的作用是给"动态创建的任务、队列、信号量"提供内存。比如 xTaskCreate 动态创建任务时,任务控制块(TCB)和任务栈就是从堆里分配的。
1.2 堆的模拟实现
用代码模拟一下堆的工作方式,能帮你彻底理解它:
cpp
/* 模拟堆:一个固定大小数组 + 一个分配函数 */
static char heap_buf[1024]; // 模拟堆空间
static int pos = 0; // 当前分配位置
void *my_malloc(int size)
{
if (pos + size > 1024)
return NULL; // 空间不够,返回空
void *p = &heap_buf[pos]; // 取走一块内存
pos += size; // 指针后移
return p;
}
这段代码的运行逻辑:
-
heap_buf[1024]是一个大小为 1024 字节的静态数组,用来模拟堆内存空间; -
my_malloc()函数负责从heap_buf中按 size 参数分配指定大小的内存; -
分配后
pos的值增加,以便下次分配时不会覆盖之前的内存空间; -
由于没有实现
my_free()函数,当前设计是单向分配,无法回收已经分配的空间。
⚠️ 这就是新手写内存管理最容易犯的错:只分配不释放,最后内存耗尽。真实开发中 malloc 和 free 必须成对出现。
二、栈(Stack)
2.1 栈的概念
栈是一种后进先出(LIFO)的数据结构 。可以把栈想象成一摞盘子:后放上去的盘子先拿走。
在 FreeRTOS 里,每个任务都有自己独立的栈空间,用于保存:
-
任务运行时的上下文信息(寄存器值)
-
局部变量
-
函数调用信息(返回地址)
cpp
void func_b(void)
{
int b = 2; // b 压入栈
} // func_b 返回,b 出栈(自动释放)
void func_a(void)
{
int a = 1; // a 压入栈
func_b(); // 调用 func_b,b 压入 a 的上面
} // 全部返回,栈恢复原状
关键点:栈由操作系统自动分配释放,无需手动控制。函数被调用时,局部变量压入栈;函数结束时,变量自动释放。
2.2 栈的大小实验
FreeRTOS 创建任务时,xTaskCreate 的 usStackDepth 参数就是任务栈大小(单位:字,不是字节!)。
如果任务栈定小了会发生什么?任务里的局部变量一多,就会栈溢出------程序跑飞、乱跳、HardFault,这是 RTOS 开发最经典的坑。
排查手段:
-
看运行现象:任务执行到某个函数后开始乱跑、复位,多半是栈不够;
-
开栈溢出检测 :在
FreeRTOSConfig.h中开启:
cpp
#define configCHECK_FOR_STACK_OVERFLOW 2
开启后,系统会在任务切换时检查栈是否越界,一旦越界就调用钩子函数:
cpp
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
/* 栈溢出!在这里打印出错任务名,或直接停机 */
printf("Stack overflow: %s\n", pcTaskName);
while (1);
}
三、堆和栈的核心区别
| 维度 | 堆 | 栈 |
|---|---|---|
| 谁管理 | 程序员手动(malloc/free) | 操作系统自动 |
| 分配方向 | 从低地址向高地址 | 从高地址向低地址 |
| 生命周期 | 直到 free 才释放 | 函数返回即释放 |
| 速度 | 慢(要查找空闲块) | 快(压栈弹栈) |
| 容量 | 大(通常几百 KB~几 MB) | 小(每任务几百字节~几 KB) |
| 典型问题 | 内存泄漏、碎片 | 栈溢出 |
四、FreeRTOS 的五种内存管理方案
FreeRTOS 在 portable/MemMang/ 下提供了 5 种堆实现(heap_1 ~ heap_5),它们各有适用场景:
| 方案 | 特点 | 适用场景 |
|---|---|---|
| heap_1 | 只能分配,不能释放 | 程序启动时创建好所有对象,之后不再删除 |
| heap_2 | 可分配可释放,但不合并碎片 | 对象大小固定、反复创建删除的场景 |
| heap_3 | 包装标准库 malloc/free,线程安全 | 与标准库混用的场景 |
| heap_4 | 可分配可释放,合并相邻空闲块(最常用) | 通用推荐,碎片少 |
| heap_5 | 同 heap_4 + 支持跨内存区 | 多块不连续 RAM 的芯片(如带外部 SRAM) |
实际开发中 90% 的工程用 heap_4,它兼顾了灵活性和防碎片能力。
总结
-
堆 = 手动管理的大仓库,给任务、队列动态分配内存;
-
栈 = 系统自动管理的临时区,函数调用、局部变量放这里;
-
任务栈大小是 FreeRTOS 开发第一坑 ------宁大勿小,配合
configCHECK_FOR_STACK_OVERFLOW使用; -
内存管理 5 种方案,默认优先 heap_4。