ESP32-S3内存体系与启动流程

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 读取执行,不占 RAM
  • load — 真正把数据从 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: 地址空间里那些 0x3FCxxxxx0x4037xxxx 是什么规律?

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 代码做两件事:

  1. 判断复位原因 — 是上电?看门狗超时?还是软件复位?
  2. 决定从哪里启动 — 读取几个 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 看门狗复位