第二讲:内存基础——RAM与Flash的区别、堆与栈、静态内存与动态内存、内存碎片化问题

大家好,欢迎来到第二讲。今天咱们聊聊嵌入式开发里最基础、也最容易被忽视的东西——内存。

说实话,我见过太多工程师,代码写得飞起,一上板子就崩。查来查去,最后发现是内存问题。要么是栈溢出了,要么是动态分配把堆搞碎了。嗯,今天这一讲,就是帮你把这些坑提前填上。

一、RAM vs Flash:它们到底差在哪?

先问个问题:你写的代码,最终存在哪?

答案是:代码存在Flash里,运行时数据存在RAM里

Flash是非易失性的,掉电不丢。RAM是易失性的,一断电就清零。这个大家都知道。但实际项目中,很多人会忽略它们的性能差异。

核心区别速览:

  • Flash:读快写慢(擦写寿命约10万次),适合存代码和常量
  • RAM:读写都快,但掉电丢失,适合存变量和堆栈

我在一个低功耗项目里遇到过这么个事:为了省RAM,把一个大数组定义成const放在Flash里。结果每次访问都要等几十个时钟周期,整个系统的响应速度直接拉胯。后来改成运行时从Flash拷贝到RAM再操作,速度才上来。

所以,我的建议是:频繁读写的数据,别放Flash里。除非你用的是XIP(eXecute In Place)模式,但那又是另一个话题了。

二、堆与栈:两个容易打架的兄弟

堆和栈,是RAM里最重要的两个区域。很多初学者搞不清它们的关系,其实说白了:

  • 栈(Stack):由编译器自动管理,存局部变量、函数调用信息。先进后出,效率极高。
  • 堆(Heap):由程序员手动管理,用malloc/free或new/delete操作。可以动态分配,但容易出问题。

我习惯把栈想象成一个叠盘子的架子——你只能从最上面拿盘子,放盘子也只能放最上面。而堆就像一个大仓库,你可以随便找个空位放东西,但得自己记住东西在哪。

栈的陷阱:溢出

嵌入式里栈空间通常很小,比如STM32默认就几KB。如果你在函数里定义了一个大数组:

void my_function(void) {
    char buffer[4096];  // 4KB的局部变量
    // 如果栈总共才4KB,这一下就满了
}

嗯,这就是典型的栈溢出。程序可能跑着跑着就HardFault了。我曾经在一个RTOS项目里,就是因为某个任务的栈分配少了,导致系统运行几小时后随机崩溃。查了整整两天才定位到。

避坑指南: 我曾经在调试一个电机控制程序时,发现只要一调用某个函数,系统就重启。后来用调试器看栈指针,发现已经越界到堆区域了。从那以后,我每个任务都会预留至少20%的栈余量。

堆的陷阱:泄漏与碎片

堆的问题更隐蔽。你malloc了,但忘了free,这叫内存泄漏。你反复malloc和free,堆里就会出现很多小空洞,这叫内存碎片化。

举个例子:

void bad_example(void) {
    char *p1 = malloc(100);
    char *p2 = malloc(100);
    free(p1);
    char *p3 = malloc(150);  // 可能失败!因为100的空洞放不下150
}

为什么会这样?因为堆管理器把释放的100字节标记为可用,但下一个申请150字节时,它发现没有连续150字节的空闲块,就返回NULL了。

你想想看,这种问题在长期运行的嵌入式设备里有多致命。我见过一个智能电表项目,运行三个月后突然死机,就是因为堆碎片导致malloc失败。

三、静态内存 vs 动态内存:选哪个?

这个问题,我的答案很明确:能静态分配,就别动态分配

静态内存是在编译时就确定好的,比如全局变量、static局部变量。它们存在RAM的.bss或.data段,生命周期贯穿整个程序。

动态内存是运行时才申请的,灵活但不可控。

对比项 静态内存 动态内存
分配时机 编译时 运行时
大小确定 固定 可变
碎片风险
执行效率 高(无分配开销) 低(有分配开销)
适用场景 大小已知、固定不变 大小不确定、需要灵活

我个人习惯是:在嵌入式GUI里,所有缓冲区、帧缓冲、控件对象,全部用静态分配。只有在处理可变长度的字符串或数据流时,才考虑动态分配,而且会加上严格的失败处理。

小技巧: 如果你非要用动态内存,可以考虑用内存池(Memory Pool)代替标准malloc。内存池预先划分好固定大小的块,分配和释放都很快,而且不会产生外部碎片。我在多个RTOS项目里都用过,效果很好。

四、内存碎片化:看不见的杀手

内存碎片化,说白了就是RAM里到处都是小空洞,每个空洞都不大,但加起来不少,而且没有一个空洞能装下你新申请的大块内存。

碎片化有两种:

  • 外部碎片:空闲块不连续,分散在堆的各处
  • 内部碎片:分配的内存块比实际需要的大,浪费了空间

我参与过一个车载仪表盘项目,GUI需要频繁创建和销毁一些临时控件。刚开始测试没问题,但连续运行48小时后,界面开始出现绘制异常。用调试工具一看,堆里全是几十字节的小空洞,最大的连续空闲块只剩不到200字节了。

怎么解决?

  1. 避免频繁分配释放:能用对象池就别用malloc/free
  2. 固定大小分配:所有动态申请都按2的幂次对齐(如16、32、64字节)
  3. 使用内存池:预分配一大块,自己管理
  4. 定期检查:在调试版本里加入堆状态打印

核心原则: 嵌入式系统里,内存不是用来「玩」的,是用来「规划」的。你花在内存布局上的每一分钟,都会在后期调试时加倍回报给你。

五、实战建议:如何规划你的内存

最后,分享一个我常用的内存规划流程:

  1. 先算总账:芯片有多少RAM?多少Flash?
  2. 固定分配:把帧缓冲、大缓冲区、关键数据结构全部静态分配
  3. 预留栈空间:每个任务至少1KB,复杂任务2-4KB
  4. 堆作为最后手段:只用于不确定大小的临时数据
  5. 加保护:在链接脚本里设置栈保护区,溢出时触发异常

嗯,这一讲的内容就到这里。下一讲我们会深入具体的内存优化技术,包括如何用链接脚本精确控制内存布局、如何用内存池替代动态分配等。到时候见。