ESP32-S3 内存体系与启动流程
第一部分:内存体系
Q1: 为什么不能像电脑一样,所有代码和数据都放一个地方?
电脑有 GB 级的 RAM,可以把程序全部加载进去跑。ESP32-S3 片内 RAM 总共只有 512KB,但你的固件可能几百 KB 甚至几 MB。
所以 ESP32 的方案是:代码留在 Flash 里直接执行,只把必须快速访问的东西搬到 RAM。
这就引出了两种存储:
| 类型 | 特点 | 类比 |
|---|---|---|
| Flash(片外) | 大(常见 4~16MB),掉电不丢,读取较慢,不能随机写 | 硬盘 |
| RAM(片内) | 小(512KB),掉电就没,但读写极快 | 内存 |
Q2: 片内 RAM 只有一种吗?
不是,ESP32-S3 的 RAM 分成几块,各有用途:
┌─────────────────────────────────────────────┐
│ 片内 RAM(512KB) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │ IRAM │ │ DRAM │ │ RTC RAM │ │
│ │ 指令RAM │ │ 数据RAM │ │ 深睡保持 │ │
│ │ ~32KB │ │ ~390KB │ │ ~8KB │ │
│ └──────────┘ └──────────┘ └───────────┘ │
└─────────────────────────────────────────────┘
| 类型 | 存什么 | 为什么单独分 |
|---|---|---|
| IRAM | 需要极快执行的代码(中断处理函数等) | CPU 取指令走专用总线,不跟数据抢通道 |
| DRAM | 全局变量、堆(malloc)、任务栈 | 普通数据存取 |
| RTC RAM | 深度睡眠期间需要保留的数据 | 深睡时只有 RTC 模块供电,其他 RAM 断电丢失 |
启动日志中可以直接看到这些区域的分配情况:
heap_init: At 3FC952B8 len 337 KiB: DRAM ← 主数据区,最大的一块
heap_init: At 3FCE9710 len 21 KiB: STACK/DRAM ← 也是数据区
heap_init: At 3FCF0000 len 32 KiB: DRAM ← 也是数据区
heap_init: At 600FE010 len 7 KiB: RTCRAM ← 深睡保持区
这些是初始化完成后剩余的空闲 RAM,被注册成堆,供程序动态分配使用。
Q3: 代码在 Flash 里怎么执行?不是说 Flash 很慢吗?
ESP32 有一个硬件机制叫 Flash MMU(内存管理单元):
CPU 要执行地址 0x3c020020 的代码
│
▼
┌───────┐
│ MMU │ 把虚拟地址映射到 Flash 的物理地址
└───┬───┘
│
▼
┌────────┐
│ Cache │ 把 Flash 内容缓存到片内,CPU 从缓存读
└────────┘
Bootloader 加载固件时,日志里能看到两个关键字:
segment 0: ... size=0af20h (44832) map ← 映射,代码留在 Flash,通过 Cache 执行
segment 1: ... size=02704h ( 9988) load ← 搬运,真正把数据复制到 RAM
map— 建立映射关系,代码留在 Flash,通过 Cache 读取执行,不占 RAMload— 真正把数据从 Flash 复制到 RAM
这就解释了为什么几百 KB 的固件能跑在只有 512KB RAM 的芯片上——代码不占 RAM,只有变量和栈占 RAM。
Q4: 写代码时,什么东西会占 RAM?
| 占 RAM 的 | 例子 | 说明 |
|---|---|---|
| 全局/静态变量 | uint8_t ucLed[1] |
编译时就确定大小 |
| FreeRTOS 任务栈 | xTaskCreate(..., 4096, ...) |
每个任务独立分配 |
| 动态分配 | malloc(256) / UART 接收缓冲 |
运行时从堆分配 |
| 局部变量 | 函数里的 int i |
在任务栈上分配,函数返回后释放 |
一个实际项目的 RAM 消耗估算:
LED 任务栈: 4096 字节
按键任务栈: 4096 字节
UART 任务栈: 4096 字节
UART 接收缓冲: 256 字节
全局变量: 几十字节
─────────────────────
总计约 12.5 KB(剩余 ~390KB 可用)
Q5: 地址空间里那些 0x3FCxxxxx、0x4037xxxx 是什么规律?
ESP32-S3 的地址空间是固定划分的,每个地址段对应不同的存储区域:
地址范围 对应什么
────────────────────────────────────
0x3C00_0000 ~ Flash 映射区(代码通过 Cache 执行)
0x3FC0_0000 ~ DRAM(数据)
0x4037_0000 ~ IRAM(快速代码)
0x4200_0000 ~ Flash 映射区(另一段)
0x600F_E000 ~ RTC RAM
不需要记住这些地址,但看到日志时能对应上即可:
3FC开头 → 变量在 DRAM 里403开头 → 代码在 IRAM 里3C0/420开头 → 代码映射到 Flash
第二部分:启动流程
Q1: 按下电源键后,芯片第一件事做什么?
芯片上电后,CPU 从一个固定的地址开始执行代码。这段代码烧死在芯片内部的 ROM 里(出厂就有,不可修改)。
ROM 代码做两件事:
- 判断复位原因 — 是上电?看门狗超时?还是软件复位?
- 决定从哪里启动 — 读取几个 GPIO 引脚的电平,决定启动模式
对应日志:
rst:0x1 (POWERON) ← 复位原因:上电
boot:0xb (SPI_FAST_FLASH_BOOT) ← 启动模式:从 Flash 启动
Q2: 启动模式有哪些?为什么有多种?
| 模式 | 用途 |
|---|---|
| SPI Flash Boot | 正常运行,从外部 Flash 加载程序 |
| Download Boot | 烧录模式,等待电脑通过串口下载固件 |
按住 BOOT 键再按 RST 键,就会进入下载模式。这就是开发板上有两个按键的原因。
开发板上用电阻把相关 GPIO 拉到了固定电平,默认就是 Flash 启动。按住 BOOT 键会改变电平,切换到下载模式。
Q3: ROM 决定从 Flash 启动后,直接就能运行我的代码了吗?
不能。 中间还需要一个 Bootloader(引导程序)。
原因:ROM 代码很简单(芯片出厂就固化了),它不知道你的 Flash 里怎么分区、固件放在哪个地址、用什么加密方式。所以 ROM 只负责把 Flash 最前面的一小段程序加载运行,这段程序就是 Bootloader。
ROM(芯片固有) → Bootloader(编译生成的) → 你的应用
这就是日志里说的 “2nd stage bootloader”(第二阶段引导程序),ROM 是第一阶段。
Q4: Bootloader 具体做了什么?
按顺序做以下几件事:
4.1 读取分区表
Flash 就像一块硬盘,需要"分区"来管理。分区表告诉 Bootloader 每个区域放了什么:
地址 内容
0x009000 nvs(键值对存储,比如 WiFi 密码)
0x00F000 phy_init(射频校准数据)
0x010000 factory(你的应用程序)← Bootloader 要加载这个
4.2 校验应用固件
检查 factory 分区里的固件是否完整(SHA256 校验),防止 Flash 损坏导致运行乱码。
4.3 把固件加载到 RAM
结合第一部分的内存知识,Bootloader 把固件的各个**段(segment)**搬到对应位置:
segment 0 → 3c020020(代码段,map 映射到 Flash,通过 Cache 执行,不占 RAM)
segment 1 → 3fc92300(全局变量,load 复制到 DRAM)
segment 2 → 40374000(关键代码,load 复制到 IRAM)
segment 3 → 42000020(大段代码,map 映射到 Flash)
segment 4 → 403769c4(更多关键代码,load 复制到 IRAM)
4.4 跳转到应用入口
一切准备好后,Bootloader 把控制权交给应用程序。
Q5: 应用程序拿到控制权后,到 app_main() 之间还发生了什么?
还有一段系统初始化,这是 ESP-IDF 框架帮你做的:
5.1 启动双核
cpu_start: Pro cpu up. ← CPU0(协议核)启动
cpu_start: Starting app cpu ← CPU1(应用核)启动
cpu_start: cpu freq: 240000000 ← 主频设为 240MHz
ESP32-S3 有两个核,默认都启动。
5.2 初始化堆内存
heap_init: At 3FC952B8 len 337 KiB: DRAM
把空闲 RAM 注册为堆,之后使用 malloc 或创建 FreeRTOS 任务时,内存就从这里分配。
5.3 启动 FreeRTOS 调度器
app_start: Starting scheduler on CPU0
调度器启动后才能运行多任务。app_main() 本身就是被当作一个任务来调用的。
Q6: 整个流程串起来是什么样?
上电,RAM 内容全是随机值
│
▼
┌──────────────┐
│ ROM │ 芯片固有代码
│ │ 判断复位原因 + 启动模式
└──────┬───────┘
│ 从 Flash 地址 0x0 加载
▼
┌──────────────┐
│ Bootloader │ 编译生成的引导程序
│ │ 读分区表 → 校验固件 → 加载到 RAM
└──────┬───────┘
│ 跳转到应用入口
▼
┌──────────────┐
│ 系统初始化 │ ESP-IDF 框架代码
│ │ 启动双核 → 初始化堆 → 启动调度器
└──────┬───────┘
│ 调用
▼
┌──────────────┐
│ app_main │ 你写的代码
│ │ 初始化外设 → 创建任务 → 开始运行
└──────────────┘
内存视角下的同一过程:
上电,RAM 内容全是随机值
│
▼
ROM 运行(用芯片内部固定的一小块 RAM)
│
▼
Bootloader 把固件各段搬到对应位置:
├── .text(大段代码) → map 到 Flash,不占 RAM
├── .data(有初始值的全局变量) → load 到 DRAM
├── .bss(零初始化全局变量) → 在 DRAM 中清零
└── 关键代码 → load 到 IRAM
│
▼
heap_init 把剩余 RAM 注册为堆
│
▼
FreeRTOS 启动,开始从堆中分配任务栈
│
▼
app_main() 运行,你的代码开始分配资源
附录:日志等级说明
启动日志中每条信息前面的字母表示日志等级:
| 前缀 | 含义 | 说明 |
|---|---|---|
E |
Error | 错误,必须处理 |
W |
Warning | 警告,可以运行但有隐患 |
I |
Info | 信息,正常流程提示 |
D |
Debug | 调试,默认不显示 |
V |
Verbose | 详细,默认不显示 |
可以在 menuconfig → Component config → Log output → Default log verbosity 中调整显示等级。
附录:常见启动警告
Flash 大小不匹配
W: Detected size(16384k) larger than the size in the binary image header(2048k)
实际 Flash 是 16MB,但 sdkconfig 配置的是 2MB。解决方法:menuconfig → Serial flasher config → Flash size 改为实际大小。不影响运行,但会浪费 Flash 空间。
复位原因一览
| 代码 | 含义 |
|---|---|
rst:0x1 (POWERON) |
正常上电 |
rst:0x3 (SW_RESET) |
软件复位 |
rst:0x5 (DEEPSLEEP) |
从深度睡眠唤醒 |
rst:0x7 (TG0WDT_SYS_RST) |
看门狗复位(通常意味着程序卡死) |
rst:0x8 (TG1WDT_SYS_RST) |
看门狗复位 |
rst:0xc (RTC_SW_CPU_RST) |
RTC 看门狗复位 |