外观
1.10 内存管理基础
这节要搞懂:程序和数据分别存在哪,内存被谁用掉了,程序莫名重启怎么查。 需要基础:1.9
一、先分清 ROM 和 RAM
芯片里有两种完全不同的存储器。
ROM(Read-Only Memory,只读存储器)是一类存储器的统称:断电后数据不丢失,正常运行时以读取为主,用来存放程序代码和固定不变的数据。
RAM(Random Access Memory,随机存取存储器):读写速度快,但断电后数据全部消失,用来存放程序运行时的临时数据。
二、它们的区别
| Flash | RAM | |
|---|---|---|
| 断电会丢吗 | ❌ 不丢 | ✅ 全丢 |
| 读写速度 | 慢 | 快 |
| 能随便改吗 | 不能频繁写(有寿命) | 随便读写 |
| 存什么 | 程序代码、常量、配置 | 变量、任务栈、堆 |
| ESP32-S3 上有多少 | Flash 通常 8 – 16MB | 片上 SRAM 共 512KB,实际能用约 300KB |
三、ROM、Flash、程序存储区 —— 别混为一谈
这三个词经常被混着用,但它们不是一个东西,说清楚:
| 名词 | 它是什么 | 关系 |
|---|---|---|
| ROM | 一类存储器的统称(断电不丢) | 范围最大 |
| Flash | ROM 中的一种,可以反复擦写 | 是 ROM 的一种实现 |
| 程序存储区 | 一个用途概念:专门放代码的那块地方 | 通常在 Flash 里,但不绝对 |
三条关键区别:
① ROM ≠ Flash —— ROM 是统称,Flash 只是其中一种。
早期芯片用的是掩膜 ROM:内容在出厂时就写死,永远改不了。现在的嵌入式芯片几乎都用 Flash(闪存),断电同样不丢,但可以反复擦写(约 10 万次)。
② Flash ≠ 程序存储区 —— Flash 里不只有程序代码。
它还要存 const 常量、配置参数(NVS)、OTA 升级用的备份固件等等。"程序存储区"只是 Flash 里专门放代码的那一块分区。
③ 程序存储区 ≠ 一定是 Flash —— 代码也可能在别处运行。
有些代码会从 Flash 拷到 RAM 里执行(RAM 更快),也有些代码本来就存在真正的 ROM 里。
ESP32-S3 上的实际情况
| 存储器 | 大小 | 存什么 |
|---|---|---|
| 片上 ROM | 384KB | 最早启动的那段代码,出厂写死,你改不了 |
| 外部 Flash | 通常 8 – 16MB | 按分区表切成好几块:程序代码、NVS 配置、OTA 备份…… |
| 片上 SRAM | 512KB | 运行时的变量、任务栈、堆;部分中断代码也会放这里跑,为了更快 |
所以严格来说:你烧录的程序,放在"外部 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));这个数字是历史最低剩余值。如果只剩几百字节,说明快爆了,赶紧把栈调大。
怎么定栈大小(新手照做就行):
- 先给
4096 - 把所有功能都跑一遍(按遍所有按键、连一次网……)
- 看打印出来的最低剩余是多少
- 最终大小 = 4096 - 最低剩余 + 1000(留余量)
八、三个吃栈大户
这三样特别费栈
printf和日志:调一次可能吃掉 1KB- 大的局部数组:
uint8_t buf[2048];直接吃 2KB - 联网(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配不配对
动手试试
- 定义一个
const char *字符串和一个普通全局数组,想一想哪个占 Flash、哪个占 RAM。 - 在任务里声明一个
uint8_t buf[4096];,看看程序会不会崩;加上static再试一次。 - 同时打印
esp_get_free_heap_size()和heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT),看看两个数字差多少。 - 写一个任务,交替
malloc(32)和malloc(8192)并只释放小的,跑几分钟观察这两个数字怎么变化。