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 开始执行。这段代码是乐鑫出厂前烧死的,我们动不了它。它的工作流程大致如下:

  1. 硬件自检:检查内部 SRAM、时钟 PLL 是否正常。
  2. 读取 eFuse:判断芯片的加密状态、启动模式(Flash 启动/下载模式/测试模式)。
  3. 检测 GPIO 电平:GPIO0 拉低进入下载模式,否则正常启动。
  4. 从 Flash 0x1000 读取二级 Bootloader:校验头部魔数(0xE9),加载到 IRAM。
  5. 跳转到二级 Bootloader:将控制权移交。
小技巧:如果你按住 BOOT 键(GPIO0 拉低)再上电,ROM Bootloader 会进入串口下载模式,这时候 esptool.py 才能烧录固件。我见过不少新手以为芯片坏了,其实就是没进对模式。

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 启动时,会按以下逻辑选择分区:

  1. 读取 otadata,获取上次设置的 boot partition。
  2. 检查该分区的固件头部是否有效(魔数、哈希校验)。
  3. 如果有效,加载并启动该分区。
  4. 如果无效(比如升级中途断电导致固件损坏),回退到另一个 OTA 分区。
避坑指南:我曾经遇到过一个问题——OTA 升级后重启,板子反复回滚到旧固件。查了半天发现是 otadata 分区里的 CRC 校验没更新。后来在升级代码里手动调用了 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
调试技巧:如果你看不到任何日志输出,先检查串口连接和波特率。我遇到过好几次,板子其实跑起来了,但串口工具波特率设成了 9600,结果全是乱码。ESP32 默认启动日志波特率是 115200,别搞错了。

4.4 启动流程全景图

下面这张 SVG 图把整个启动流程串起来了。从芯片复位到用户代码执行,每一步的负责模块和数据流向都标清楚了。

ESP32 启动流程全景图 ESP32 启动流程全景图 芯片上电 / 复位 rst:0x1 (POWERON) 一级 Bootloader ROM 0x4000_0000 硬件初始化 → 检测 GPIO0 → 加载二级 二级 Bootloader Flash 0x1000 解析分区表 → 选择 OTA 槽 读取 otadata 分区 获取下次启动的 OTA 槽位 无效 有效 回滚到另一个 OTA 分区 自动回滚机制 加载用户固件到 RAM 校验魔数 + 哈希 用户代码开始执行 app_main() 函数入口 📌 关键日志节点 • entry 0x403ce000 → 二级 Bootloader 入口 • Loading app partition at offset 0x10000 → OTA 槽选择结果 • Pro cpu start user code → 用户代码启动

4.5 实战:手动分析一次启动失败

讲个真实案例。有一次客户反馈说设备 OTA 升级后变砖了,串口日志只输出到 entry 0x403ce000 就卡住了。我一看就明白了——二级 Bootloader 没跑起来。

排查步骤:

  1. 检查 Flash 连接:用示波器量 SPI 时钟和数据线,发现 MISO 线虚焊。
  2. 重新焊接后:日志正常输出了分区表信息,但提示 Invalid partition table。
  3. 擦除全片重新烧录:esptool.py --chip esp32s3 erase_flash,然后重新烧录 bootloader + 分区表 + 固件。
  4. 重启后正常:设备顺利进入用户代码。
重要提醒:OTA 升级时如果修改了分区表,一定要确保新固件里包含了正确的 bootloader 和分区表。我见过有人只升级了 app 分区,结果 bootloader 还是旧的,分区表对不上,直接变砖。升级前先用 idf.py partition-table 确认当前分区表,再决定是否需要全量烧录。