4. Bootloader与启动流程:一级Bootloader、二级Bootloader、OTA分区选择、启动日志分析
ESP32 上电后到底干了些什么?为什么有时候固件刷进去却跑不起来?这些问题,说到底都跟 Bootloader 有关。我刚开始接触 ESP32 时,也被这玩意儿绕晕过——明明代码编译通过了,下载也成功了,板子就是没反应。后来啃了一遍启动流程,才算真正搞明白。
这一节,咱们就把 ESP32 的启动链条彻底捋清楚。从芯片复位到用户 main() 函数执行,中间经历了什么,每一步谁在负责,出了问题怎么看日志定位。
4.1 两级 Bootloader 架构
ESP32 的启动过程分两个阶段:一级 Bootloader(ROM Bootloader)和二级 Bootloader(Flash Bootloader)。说白了,就是芯片出厂时固化了一段代码在 ROM 里,它负责把 Flash 里的第二段加载程序拉起来,再由第二段去加载真正的用户固件。
| 层级 | 存储位置 | 职责 | 能否修改 |
|---|---|---|---|
| 一级 Bootloader | 芯片内部 ROM | 硬件初始化、检测启动模式、加载二级 Bootloader | 不可修改(出厂固化) |
| 二级 Bootloader | Flash 0x1000 偏移处 | 分区表解析、安全校验、OTA 分区选择、加载用户固件 | 可修改(由 ESP-IDF 编译生成) |
4.1.1 一级 Bootloader 干了什么
芯片上电或复位后,CPU 直接从 ROM 地址 0x4000_0000 开始执行。这段代码是乐鑫出厂前烧死的,我们动不了它。它的工作流程大致如下:
- 硬件自检:检查内部 SRAM、时钟 PLL 是否正常。
- 读取 eFuse:判断芯片的加密状态、启动模式(Flash 启动/下载模式/测试模式)。
- 检测 GPIO 电平:GPIO0 拉低进入下载模式,否则正常启动。
- 从 Flash 0x1000 读取二级 Bootloader:校验头部魔数(0xE9),加载到 IRAM。
- 跳转到二级 Bootloader:将控制权移交。
4.1.2 二级 Bootloader 的核心工作
二级 Bootloader 是 ESP-IDF 工程编译出来的,位于 Flash 的 0x1000 地址。它比一级 Bootloader 灵活得多,我们可以通过配置 menuconfig 来调整它的行为。
它的主要任务包括:
- 解析分区表(Partition Table),找到可用的 OTA 分区。
- 检查 OTA 信息分区(otadata),判断应该从哪个 OTA 槽启动。
- 验证固件签名(如果启用了安全启动)。
- 将选中的固件从 Flash 加载到 RAM,然后跳转执行。
关键点:二级 Bootloader 本身也是固件的一部分。如果你修改了分区表或者启用了安全启动,必须重新编译并烧录 bootloader。命令是:idf.py bootloader,然后 idf.py bootloader-flash。
4.2 OTA 分区选择机制
OTA 升级的核心,就是二级 Bootloader 如何决定从哪个分区启动。ESP32 的分区表里通常有两个 OTA 应用分区:ota_0 和 ota_1,外加一个 otadata 分区来记录当前应该启动哪个。
分区表示例(partitions.csv):
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
ota_0, app, ota_0, 0x10000, 0x1F0000,
ota_1, app, ota_1, 0x200000,0x1F0000,
4.2.1 otadata 分区的作用
otadata 分区只有 8KB,里面存了一个结构体,记录了当前正在运行的是哪个 OTA 槽,以及下次应该从哪个槽启动。每次 OTA 升级成功后,固件会更新 otadata 里的信息,把下次启动的槽位指向新固件所在的分区。
二级 Bootloader 启动时,会按以下逻辑选择分区:
- 读取 otadata,获取上次设置的 boot partition。
- 检查该分区的固件头部是否有效(魔数、哈希校验)。
- 如果有效,加载并启动该分区。
- 如果无效(比如升级中途断电导致固件损坏),回退到另一个 OTA 分区。
esp_ota_set_boot_partition() 并确认返回 ESP_OK,问题才解决。记住:写完固件后一定要设置下次启动分区,否则 bootloader 不会自动切换。
4.2.2 回滚机制
ESP-IDF 支持两种回滚策略:
- 无条件回滚:新固件启动后,必须调用
esp_ota_mark_app_valid_cancel_rollback()确认固件正常。如果超时未确认,下次重启自动回滚到旧固件。 - 条件回滚:通过配置 CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE 启用,适合对稳定性要求高的场景。
我个人习惯在量产项目中启用条件回滚。因为 IoT 设备一旦部署到现场,你不可能拿着串口线去刷机。回滚机制至少能保证设备在升级失败后还能恢复到上一个可用版本。
4.3 启动日志分析
启动日志是排查问题的第一手资料。ESP32 的启动日志通过 UART0 输出,波特率默认 115200。下面是一段典型的启动日志,我加了注释说明每一行的含义。
ESP-ROM:esp32s3-20210327
# 一级 Bootloader 版本信息,来自 ROM
Build:Mar 27 2021
rst:0x1 (POWERON), boot:0x8 (SPI_FAST_FLASH_BOOT)
# 复位原因:上电复位;启动模式:SPI Flash 快速启动
SPIWP:0xee
mode:DIO, clock div:1
# Flash 接口模式:DIO(双 I/O),时钟不分频
entry 0x403ce000
# 跳转到二级 Bootloader 入口地址
I (29) boot: ESP-IDF v4.4.2 2nd stage bootloader
I (29) boot: compile time 11:30:28
# 二级 Bootloader 版本和编译时间
I (30) boot: chip revision: v3.0
I (33) boot.esp32s3: SPI Speed : 80MHz
I (37) boot.esp32s3: SPI Mode : DIO
I (42) boot.esp32s3: SPI Flash Size : 16MB
I (46) boot: Enabling RNG early entropy source...
I (51) boot: Partition Table:
I (55) boot: ## Label Usage Type ST Offset Length
I (62) boot: 0 nvs WiFi data 01 02 00009000 00004000
I (70) boot: 1 otadata OTA data 01 00 0000d000 00002000
I (77) boot: 2 phy_init RF data 01 01 0000f000 00001000
I (85) boot: 3 ota_0 OTA app 00 10 00010000 001f0000
I (92) boot: 4 ota_1 OTA app 00 11 00200000 001f0000
# 分区表信息,可以看到 ota_0 和 ota_1 的偏移和大小
I (100) boot: End of partition table
I (104) boot: Loading app partition at offset 0x10000
# 二级 Bootloader 决定从 ota_0(偏移 0x10000)加载固件
I (104) boot: Checking flash encryption...
I (108) boot: Disabling watchdog...
I (112) cpu_start: Pro cpu up.
I (116) cpu_start: Starting app cpu, entry point is 0x40380000
I (116) cpu_start: App cpu up.
# 双核启动
I (126) cpu_start: Pro cpu start user code
I (126) cpu_start: cpu freq: 240000000
# 用户代码开始执行,CPU 频率 240MHz
4.3.1 常见启动失败场景
日志里出现以下信息时,基本可以定位问题:
| 日志内容 | 可能原因 | 解决方法 |
|---|---|---|
rst:0x3 (RTC_WDT_RTC) |
看门狗复位,固件卡死在初始化阶段 | 检查外设初始化代码,增加超时处理 |
boot: No bootable app partitions found |
所有 OTA 分区都无效 | 重新烧录固件,检查分区表配置 |
Invalid partition table |
分区表损坏或偏移错误 | 擦除整个 Flash 后重新烧录 |
E (xxx) esp_image: image at 0x10000 has invalid magic byte |
固件头部魔数错误,不是合法的 ESP32 固件 | 确认烧录的 bin 文件是 app 固件,不是 bootloader |
4.4 启动流程全景图
下面这张 SVG 图把整个启动流程串起来了。从芯片复位到用户代码执行,每一步的负责模块和数据流向都标清楚了。
4.5 实战:手动分析一次启动失败
讲个真实案例。有一次客户反馈说设备 OTA 升级后变砖了,串口日志只输出到 entry 0x403ce000 就卡住了。我一看就明白了——二级 Bootloader 没跑起来。
排查步骤:
- 检查 Flash 连接:用示波器量 SPI 时钟和数据线,发现 MISO 线虚焊。
- 重新焊接后:日志正常输出了分区表信息,但提示
Invalid partition table。 - 擦除全片重新烧录:
esptool.py --chip esp32s3 erase_flash,然后重新烧录 bootloader + 分区表 + 固件。 - 重启后正常:设备顺利进入用户代码。
idf.py partition-table 确认当前分区表,再决定是否需要全量烧录。