Skip to content

1.10 内存管理基础 ​

这节要搞懂:程序和数据分别存在哪,内存被谁用掉了,程序莫名重启怎么查。 需要基础:1.9

一、先分清 ROM 和 RAM ​

芯片里有两种完全不同的存储器。

ROM(Read-Only Memory,只读存储器)是一类存储器的统称:断电后数据不丢失,正常运行时以读取为主,用来存放程序代码和固定不变的数据。

RAM(Random Access Memory,随机存取存储器):读写速度快,但断电后数据全部消失,用来存放程序运行时的临时数据。

二、它们的区别 ​

FlashRAM
断电会丢吗❌ 不丢✅ 全丢
读写速度慢快
能随便改吗不能频繁写(有寿命)随便读写
存什么程序代码、常量、配置变量、任务栈、堆
ESP32-S3 上有多少Flash 通常 8 – 16MB片上 SRAM 共 512KB,实际能用约 300KB

三、ROM、Flash、程序存储区 —— 别混为一谈 ​

这三个词经常被混着用,但它们不是一个东西,说清楚:

名词它是什么关系
ROM一类存储器的统称(断电不丢)范围最大
FlashROM 中的一种,可以反复擦写是 ROM 的一种实现
程序存储区一个用途概念:专门放代码的那块地方通常在 Flash 里,但不绝对

三条关键区别:

① ROM ≠ Flash —— ROM 是统称,Flash 只是其中一种。

早期芯片用的是掩膜 ROM:内容在出厂时就写死,永远改不了。现在的嵌入式芯片几乎都用 Flash(闪存),断电同样不丢,但可以反复擦写(约 10 万次)。

② Flash ≠ 程序存储区 —— Flash 里不只有程序代码。

它还要存 const 常量、配置参数(NVS)、OTA 升级用的备份固件等等。"程序存储区"只是 Flash 里专门放代码的那一块分区。

③ 程序存储区 ≠ 一定是 Flash —— 代码也可能在别处运行。

有些代码会从 Flash 拷到 RAM 里执行(RAM 更快),也有些代码本来就存在真正的 ROM 里。

ESP32-S3 上的实际情况

存储器大小存什么
片上 ROM384KB最早启动的那段代码,出厂写死,你改不了
外部 Flash通常 8 – 16MB按分区表切成好几块:程序代码、NVS 配置、OTA 备份……
片上 SRAM512KB运行时的变量、任务栈、堆;部分中断代码也会放这里跑,为了更快

所以严格来说:你烧录的程序,放在"外部 Flash 中的 app 分区"里。

四、程序是怎么用这两块的 ​

      Flash(断电不丢,4MB)                RAM(断电就没,300KB)
   ┌──────────────────────────┐        ┌──────────────────────────┐
   │  你写的程序代码            │  上电   │  全局变量                 │
   │  const 常量、字符串        │ ────►  │  每个任务的栈             │
   │  NVS 配置区(3.4 讲)      │  拷贝   │  堆(malloc 用的)        │
   └──────────────────────────┘        └──────────────────────────┘

上电时:芯片从 Flash 里读出程序开始执行,需要用到的变量在 RAM 里开辟空间。

断电时:RAM 全部清空,Flash 里的东西原封不动。

c
const char *g_version = "v1.0";   // const 常量 → 存在 Flash,不占 RAM
int g_counter = 0;                // 全局变量 → 占 RAM

为什么闹钟设置要存 NVS

因为普通变量在 RAM 里,一断电就没了。

要断电还在,就必须写进 Flash。NVS(3.4 节)干的就是这件事。

五、RAM 被谁用掉了 ​

我们真正要精打细算的是那 300KB 的 RAM。它被这三样分掉:

谁在用存什么大概多大
全局变量写在函数外面的变量你自己定
每个任务的栈任务里的局部变量每个任务 2 – 4 KB
堆(Heap)malloc 出来的剩下的都是

关键认知

你每创建一个任务,就吃掉 2KB 以上的 RAM。

所以别一口气创建十几个任务 —— 六七个足够做一个完整项目(第三章就是 6 个)。

六、什么是栈 ​

栈(Stack)是每个任务独占的一块 RAM,用来存放该任务的局部变量、函数调用的返回地址,以及任务切换时保存的寄存器数据。

回忆 1.1 说的:栈是每个任务的私人小本子。

本子写满了会怎样?—— 写到别人的地盘上去,程序莫名其妙就崩了。

c
void task_my(void *pvParameters)
{
    uint8_t buffer[4096];         // ❌ 一下子占掉 4KB 栈!
    // ...
}

如果创建这个任务时只给了 2048 字节,必爆。

七、什么是栈溢出 ​

栈溢出(Stack Overflow)指的是任务使用的栈空间超过了分配给它的大小,导致数据写到了相邻的内存区域,破坏其他数据甚至导致系统崩溃。

这是嵌入式最常见的死机原因。

典型症状:

  • 跑一会儿就重启
  • 变量的值莫名其妙变了
  • 串口打印 Guru Meditation Error

怎么查:在任务里加一行

c
printf("stack remain: %d bytes\n", uxTaskGetStackHighWaterMark(NULL));

这个数字是历史最低剩余值。如果只剩几百字节,说明快爆了,赶紧把栈调大。

怎么定栈大小(新手照做就行):

  1. 先给 4096
  2. 把所有功能都跑一遍(按遍所有按键、连一次网……)
  3. 看打印出来的最低剩余是多少
  4. 最终大小 = 4096 - 最低剩余 + 1000(留余量)

八、三个吃栈大户 ​

这三样特别费栈

  1. printf 和日志:调一次可能吃掉 1KB
  2. 大的局部数组:uint8_t buf[2048]; 直接吃 2KB
  3. 联网(HTTPS):加密握手很吃栈,这类任务栈至少给 6144

大数组怎么办?两个办法:

c
// 办法1:加 static,挪出栈(推荐,简单)
static uint8_t buffer[4096];

// 办法2:用 malloc 从堆上要
uint8_t *buffer = malloc(4096);
// ...
free(buffer);                    // ⚠️ 用完一定要释放

九、什么是堆 ​

堆(Heap)是一块公共的 RAM 区域,程序运行时可以用 malloc 从里面按需申请任意大小的内存,用完再用 free 还回去。

栈和堆的区别:

栈 Stack堆 Heap
归谁每个任务独占一块全局共用一大块
谁管理系统自动(函数进出自动分配回收)你自己(malloc / free)
大小创建任务时定死剩下的 RAM 都是
忘了还会怎样不存在这个问题内存泄漏

十、什么是内存碎片化 ​

内存碎片化(Memory Fragmentation)指的是:反复申请和释放大小不一的内存后,堆里的空闲空间被切割成许多不连续的小块。此时空闲总量可能还很多,但没有一块足够大的连续空间,导致大块申请失败。

打个比方:停车场一共 10 个空位,但它们零零散散分布在各处。

来了一辆需要三个连续车位的大巴车 —— 虽然还有 10 个空位,它就是停不进去。

堆的状态:
┌───┬─────┬───┬─────┬───┬─────┐
│空 │ 占用 │空 │ 占用 │空 │ 占用 │      空闲总共 30KB
└───┴─────┴───┴─────┴───┴─────┘
 10K       10K       10K

这时候 malloc(20KB) → ❌ 失败!
虽然空着 30KB,但没有一块连续的 20KB

十一、碎片化的症状和排查 ​

典型症状:

程序跑了几个小时后,malloc 突然开始失败,可是打印剩余内存还有一大堆。

怎么确认是碎片化:对比这两个数字

c
// 剩余内存总量
printf("free total : %lu\n", esp_get_free_heap_size());

// 最大的那块连续空闲内存
printf("largest blk: %u\n", heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
  • 两个数字差不多 → 内存很完整,没问题
  • 总量还有 100KB,但最大连续块只剩 8KB → 严重碎片化

十二、怎么避免碎片化 ⭐ ​

好消息:ESP-IDF 会自动合并

相邻的空闲块会被自动合并成一大块,所以只要你 free 得干净,碎片化不会无限恶化。

但"申请大小差别很大、又频繁申请释放"的场景仍然会出问题。

四条实用建议,按有效程度排序:

① 能不用 malloc 就不用(最有效)

c
// ❌ 每次都申请、释放
void task_work(void *pv) {
    while (1) {
        uint8_t *buf = malloc(1024);
        do_work(buf);
        free(buf);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// ✅ 用固定的 static 数组,一次都不用申请
void task_work(void *pv) {
    static uint8_t buf[1024];          // 编译时就定死了,永不碎片化
    while (1) {
        do_work(buf);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

② 要用就在启动时一次分配好,之后别再释放

大的缓冲区(屏幕缓冲、音频缓冲)在初始化时申请一次,程序跑到关机都不释放。这样它永远占着固定位置,不会造成碎片。

③ 别让大小差异很大的块交替申请释放

一会儿申请 32 字节、一会儿申请 8KB,最容易把堆切碎。

④ 队列里传固定大小的数据

用 xQueueCreate(10, sizeof(my_data_t)) 传结构体本身,比"malloc 一块再传指针"更安全,因为队列的空间是创建时一次性分配好的。

十三、什么是内存泄漏 ​

内存泄漏(Memory Leak)指的是程序申请了内存却没有释放,导致可用内存越来越少,最终耗尽而崩溃。

碎片化和泄漏的区别:

内存泄漏内存碎片化
剩余总量一直变少基本不变
最大连续块一直变少一直变少
原因malloc 后忘了 free申请释放的大小不规律

怎么查泄漏:在主循环里每分钟打印一次

c
printf("free heap: %lu\n", esp_get_free_heap_size());

如果这个数字一直在变小,就是漏了。 去检查所有 malloc 是不是都有对应的 free。

一个更好用的指标

c
esp_get_minimum_free_heap_size();     // 从开机到现在,剩余内存的历史最低值

它只会降不会升。跑一晚上如果这个值还很健康,说明没漏。

十四、ESP32 特有:申请特殊内存 ​

有些外设(屏幕、音频)要求数据放在特定类型的内存里,得用专门的函数申请:

c
// 屏幕、音频的缓冲区要这样申请
uint8_t *dma_buffer = heap_caps_malloc(1024, MALLOC_CAP_DMA);
// ...
heap_caps_free(dma_buffer);

用普通 malloc 的话,屏幕可能花屏、音频可能有杂音,而且时好时坏特别难查。

第二章用到屏幕和音频时会再提醒你。现在只要有个印象。

本节要点 ​

  • ROM 是统称,Flash 是其中一种(可反复擦写);程序存储区只是 Flash 里放代码的那块分区
  • Flash 断电不丢,存程序、常量、配置;RAM 断电就没,存运行数据
  • 要断电还在 → 必须写 Flash(NVS)
  • RAM 被三样分掉:全局变量、每个任务的栈、堆
  • 栈 = 任务私有,系统自动管;堆 = 全局共用,你自己 malloc / free
  • ⭐ 栈溢出是最常见的死机原因,用 uxTaskGetStackHighWaterMark 查
  • 碎片化 = 空闲总量够,但没有连续的大块;对比"剩余总量"和"最大连续块"就能确认
  • 少用 malloc、大块在启动时一次分配好,是避免碎片最有效的办法
  • 泄漏 = 剩余总量一直变少,检查 malloc / free 配不配对

动手试试 ​

  1. 定义一个 const char * 字符串和一个普通全局数组,想一想哪个占 Flash、哪个占 RAM。
  2. 在任务里声明一个 uint8_t buf[4096];,看看程序会不会崩;加上 static 再试一次。
  3. 同时打印 esp_get_free_heap_size() 和 heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT),看看两个数字差多少。
  4. 写一个任务,交替 malloc(32) 和 malloc(8192) 并只释放小的,跑几分钟观察这两个数字怎么变化。