在 MCU(尤其是运行 FreeRTOS 的 Cortex-M)开发中,"堆栈"这个词经常让初学者混淆,因为它其实包含了四个不同的概念。下面我们把整个内存布局梳理清楚。
1. 启动文件里定义的两个尺寸
你会在 startup_xxx.s 里看到:

assembly
Stack_Size EQU 0x00003000 ; 主栈,12KB(3*16*16*16/1024=12)
Heap_Size EQU 0x00001000 ; C 库堆,4KB(1*16*16*16/1024=4)
① Stack_Size → 主栈(MSP)
-
这是 ARM Cortex-M 的 主堆栈,系统上电后默认使用。
-
FreeRTOS 启动前:所有代码(包括 main 函数、初始化)都用这个栈。
-
FreeRTOS 启动后 :调度器会把任务切换成使用 进程栈(PSP) ,而 MSP 专门留给中断服务函数(ISR)和操作系统内核异常使用。
-
因此,
Stack_Size的大小主要取决于:-
你的中断嵌套深度和局部变量开销。
-
调试时某些库(如
printf)在中断中可能消耗较多栈。
-
-
一般设置 1KB~2KB(
0x400~0x800)即可,除非有重度中断处理。
② Heap_Size → C 库堆(系统堆)
-
这块内存是给标准 C 库的
malloc()/free()使用的。 -
FreeRTOS 的任务栈和内核对象(信号量、队列等)通常不用这块内存 ,它们使用 FreeRTOS 自带的堆管理(
heap_1.c~heap_5.c),由configTOTAL_HEAP_SIZE决定。 -
如果你的应用代码中没有调用 C 库的
malloc,这个 Heap_Size 可以设得非常小(例如0x200= 512 字节),某些编译器甚至允许为 0,但保留一个小值更安全。 -
注意:即使你不直接调用
malloc,某些库函数(如printf、文件流操作)内部也可能悄悄调用malloc,所以建议至少保留几百字节。
2. FreeRTOS 相关的堆与栈
③ FreeRTOS 堆 → configTOTAL_HEAP_SIZE

-
在
FreeRTOSConfig.h中定义,例如:c
#define configTOTAL_HEAP_SIZE ((size_t)(16 * 1024)) // 16KB -
这块内存从链接脚本的剩余 RAM 中划出,由 FreeRTOS 的
pvPortMalloc()管理。 -
所有任务栈、队列、信号量、互斥锁、定时器、任务通知等内核对象都从这里分配。
-
下图任务
InitTask_STK_SIZE 128,就是指任务栈从 FreeRTOS 堆中分配 128 * 4 字节。

④ 任务栈(每个任务独立的栈)
④.1. 动态创建(从 RTOS 堆中分配)

这里的 InitTask_STK_SIZE 就是任务栈大小 (单位:word,4 字节),实际会从 FreeRTOS 堆中 pvPortMalloc(128 * 4) 分配。
任务运行在自己的栈上,它和启动文件的 Stack_Size(MSP)完全没有关系。
内部发生了什么:
-
RTOS 调用自己的内存分配器(如
pvPortMalloc) -
从 RTOS 堆 中划出
128 * sizeof(StackType_t)字节的连续内存 -
这块内存被永久标记为"Task Stack"
-
任务控制块(TCB)记录栈的起始地址和边界
关键理解:
-
分配来源是 RTOS 堆 ✓
-
分配结果是任务私有栈 ✓
-
分配完成后,逻辑上不再是堆 ✓
就像你从银行取了一笔钱,这笔钱曾经属于银行的资产池,但取出来后就是你的现金,不再属于银行。
④.2. 静态创建(不从堆中分配)
static StackType_t myStack[128]; // 编译时分配,在 BSS/Data 段
xTaskCreateStatic(taskFunc, "Name", 128, NULL, 1, myStack, &tcb);
-
栈空间是用户预定义的全局/静态数组
-
完全不经过 RTOS 堆
-
内存布局上属于 BSS/Data 段,与堆无关
④.3.分配后的本质变化
分配前(RTOS 堆中的空闲块):
┌─────────────────────────┐
│ 空闲内存块 #5 │ ← 由堆管理器跟踪(大小、前后块指针)
│ [未使用] │
└─────────────────────────┘
分配后(转化为 Task Stack):
┌─────────────────────────┐
│ 任务 A 的私有栈 │ ← 由任务调度器管理(栈顶指针 SP)
│ [栈帧1][栈帧2][...] │ ← 压栈/出栈操作,与堆管理器无关
└─────────────────────────┘
3. 内存布局全景图
3.1典型 Cortex-M + FreeRTOS
text
RAM 低地址
├── 全局/静态变量区(.data、.bss) ← 编译器分配
├── C 库堆(Heap_Size) ← malloc/free 使用,很少使用
├── FreeRTOS 堆(configTOTAL_HEAP_SIZE)
│ ├── 任务 A 栈
│ ├── 任务 B 栈
│ ├── 队列、信号量等控制块
│ └── ...(剩余作为动态内存)
│
├── 主栈(Stack_Size,MSP) ← 用于中断和启动,不与任务栈重叠
│ (中断嵌套向下增长 ⬇️)
│
RAM 高地址
注:具体的排列顺序取决于链接脚本(
.ld文件),但通常 FreeRTOS 堆会用ucHeap大数组占住一块,或者放在.bss末尾,主栈放在 RAM 顶端。这确保了它们不会互相覆盖。
3.2裸机
在裸机(无 RTOS)环境下,情况要简单得多------只存在启动文件里定义的那两个内存区域:系统栈和堆
text
RAM 低地址
├── 全局/静态变量区(.data、.bss) ← 编译器分配
├── C 库堆(Heap_Size) ← malloc/free 使用,很少使用
├── 主栈(Stack_Size,MSP) ← 用于中断和启动,不与任务栈重叠
│ (中断嵌套向下增长 ⬇️)
│
RAM 高地址
3.3裸机与 FreeRTOS 的关键区别
| 对比项 | 裸机 | FreeRTOS |
|---|---|---|
| 程序栈 | 只有一个系统栈(MSP),所有代码和中断共用 | MSP 专用中断,每个任务有独立的任务栈(PSP) |
| 动态内存 | 只有 C 库堆(Heap_Size),通过 malloc 使用 |
除了 C 库堆,还有 FreeRTOS 堆(configTOTAL_HEAP_SIZE),用 pvPortMalloc |
| 需要配置的大小 | 仅需设置 Stack_Size 和 Heap_Size |
还需额外配置 FreeRTOS 堆及每个任务的栈大小 |
| 栈溢出风险 | 一次溢出,整个系统崩溃 | 溢出通常只影响单个任务,且可通过高水位监测发现 |
4. 常见问题及排查
| 问题 | 可能原因 |
|---|---|
| 任务创建返回 NULL | FreeRTOS 堆不足(configTOTAL_HEAP_SIZE 太小) |
| 进入 HardFault,栈指针异常 | 某个任务栈爆了(监测高水位!),或主栈太小导致中断溢出 |
调用 malloc() 返回 NULL |
C 库堆(Heap_Size)不足或未初始化 |
printf 在中断中导致死机 |
主栈(MSP)过小,printf 内部可能使用大缓冲区 |
5. 如何确定大小?实战建议
✅ 主栈(Stack_Size)
-
如果你的中断服务程序里没有浮点运算、没有调用复杂库,1KB(0x400) 几乎总是够用。
-
若使用了
printf调试、或中断中调用重型第三方库,可适当提高到 2KB(0x800)。
✅ C 库堆(Heap_Size)
-
如果你全程使用 FreeRTOS 的
pvPortMalloc,那么Heap_Size可以设为 0x200(512B) 甚至更小,只为防止某些库隐式调用。 -
如果你在任务里用了标准
malloc,请统计最大动态分配量然后扩大。
✅ FreeRTOS 堆(configTOTAL_HEAP_SIZE)
-
等于 所有任务栈大小之和 + 内核对象预估 (队列长度×消息大小等) + 可能通过
pvPortMalloc动态拿的内存。 -
留 20% 余量。
-
编译后检查 map 文件,确保
ucHeap数组大小合适,且 RAM 总用量不超过芯片 RAM。
✅ 任务栈大小
-
先用我们一直使用的方法:运行时打印高水位,然后乘以 1.3~1.5 作为最终栈大小(为功能扩展和中断栈帧预留)。
-
理想剩余量:至少 20% 空闲。
总结
-
启动文件 Stack_Size:主栈,给中断用。
-
启动文件 Heap_Size :C 库堆,给
malloc用,很少用,可设很小。 -
FreeRTOS
configTOTAL_HEAP_SIZE:FreeRTOS 自己的堆,所有任务栈和内核对象从这里分配,需要足够大。 -
每个任务栈:属于 FreeRTOS 堆的一部分,大小由你指定。