第2章 单片机内存模型剖析:堆区与栈区的区别
做嵌入式开发,内存管理是绕不开的坎。我见过太多项目,功能调通了,跑几天就死机。一查,内存泄漏了。所以这一章,咱们把内存模型彻底讲透。
2.1 SRAM分区详解
单片机的SRAM,不是一整块随便用的。它被划分成几个区域,每个区域有各自的使命。我习惯把SRAM想象成一个仓库,不同货架放不同东西。
| 区域 | 存放内容 | 生命周期 |
|---|---|---|
| .bss | 未初始化的全局变量、静态变量 | 程序启动到结束 |
| .data | 已初始化的全局变量、静态变量 | 程序启动到结束 |
| 堆区 (Heap) | 动态分配的内存 (malloc/new) | 手动分配到手动释放 |
| 栈区 (Stack) | 局部变量、函数参数、返回地址 | 函数调用到函数返回 |
嗯,这里要注意。.bss和.data在编译时就确定了大小。堆和栈是动态的,它们从两端向中间生长。一旦碰上了,就是堆栈溢出。我在项目中遇到过,一个传感器驱动里用了大数组做局部变量,结果栈把堆给吃了,系统随机崩溃。
2.2 堆区与栈区的核心区别
说白了,栈是系统自动管理的,堆是程序员手动管理的。你想想看,栈上的变量,函数一返回就自动销毁了。堆上的内存,你不free,它永远在那里。
- 分配效率: 栈分配就是移动一下栈顶指针,一条指令的事。堆分配需要查找空闲链表、分割内存块,慢得多。
- 碎片问题: 栈是连续分配连续释放,没有碎片。堆频繁分配释放会产生外部碎片,就像硬盘碎片一样。
- 大小限制: 栈通常只有几KB(Cortex-M默认1-2KB)。堆可以大一些,但受总SRAM限制。
- 线程安全: 每个任务有自己的栈,天然安全。堆是所有任务共享的,需要加锁保护。
2.3 FreeRTOS的内存管理机制
FreeRTOS提供了5种内存管理方案,分别叫heap_1到heap_5。我刚开始用的时候也懵,到底选哪个?
| 方案 | 特点 | 适用场景 |
|---|---|---|
| heap_1 | 只能分配,不能释放。无碎片 | 任务、队列等只创建不删除 |
| heap_2 | 可以释放,但合并相邻空闲块 | 分配释放大小固定的场景 |
| heap_3 | 包装了标准malloc/free,依赖编译器 | 需要线程安全包装 |
| heap_4 | 合并相邻空闲块,减少碎片 | 通用场景,推荐使用 |
| heap_5 | 支持多个不连续内存区域 | 外部RAM+内部RAM混合使用 |
我个人习惯用heap_4。它在heap_2的基础上加了相邻空闲块合并,碎片控制得不错。有一次我在项目里用了heap_2,频繁创建删除小任务,结果内存越用越少。换成heap_4后,问题消失了。
// FreeRTOS heap_4 配置示例
// FreeRTOSConfig.h
#define configTOTAL_HEAP_SIZE ((size_t)(20 * 1024)) // 20KB堆空间
#define configAPPLICATION_ALLOCATED_HEAP 0 // 使用内部数组
// 使用pvPortMalloc代替malloc
void *buffer = pvPortMalloc(256);
if (buffer != NULL) {
// 使用内存
vPortFree(buffer); // 必须配对使用
}
2.4 RT-Thread的内存管理机制
RT-Thread的内存管理比FreeRTOS更丰富。它提供了静态内存池和动态内存堆两种方式。静态内存池适合实时性要求高的场景,动态内存堆适合灵活性要求高的场景。
- 静态内存池 (mempool): 预先划分固定大小的内存块,分配释放都是O(1)复杂度,无碎片。适合DMA缓冲区、网络数据包等。
- 动态内存堆 (heap): 支持小内存分配,使用dlmalloc算法,内存利用率高。适合大小不固定的内存需求。
- SLAB算法: 针对小对象分配优化,减少内部碎片。RT-Thread在动态堆里默认使用SLAB。
我记得在做一个物联网网关项目时,需要频繁分配不同大小的JSON数据包。用RT-Thread的动态堆,配合SLAB算法,内存碎片控制得很好。跑了一个月没出问题。
// RT-Thread 内存池使用示例
static rt_uint8_t mem_pool_buffer[1024];
static struct rt_mempool mp;
// 创建内存池,每个块64字节,共16块
rt_mp_init(&mp, "mp", &mem_pool_buffer, sizeof(mem_pool_buffer), 64);
// 分配内存块
void *block = rt_mp_alloc(&mp, RT_WAITING_FOREVER);
if (block != RT_NULL) {
// 使用内存块
rt_mp_free(block); // 释放回内存池
}
2.5 内存模型全景图
为了让你更直观地理解整个内存布局,我画了一张图。这张图展示了SRAM的完整分区和RTOS的内存管理层次。
从这张图可以清楚看到,堆和栈从两端向中间生长。一旦它们相遇,系统就会崩溃。这就是为什么我强调要监控堆栈使用情况。
2.6 内存泄漏的根源
内存泄漏说白了就是:你借了东西不还。在单片机里,每次malloc都要对应一个free。少了一个,那块内存就永远找不回来了。
- 函数内malloc,函数退出前忘记free
- 中断服务程序里分配了内存,但没释放
- 错误处理路径上漏了free
- RTOS任务删除前没释放任务内部分配的内存
我有个习惯:每次写malloc,立刻写对应的free。就像括号配对一样,先写闭合再写内容。这样能减少90%的泄漏问题。