资讯详情

ESP32固件防砖指南:双分区与自动回滚机制详解

📅 2026/10/28 9:16:49 | 华诺云谱 👁 阅读
ESP32固件防砖指南:双分区与自动回滚机制详解
1. 从一次刷废了的深夜事故说起凌晨两点我盯着串口监视器里不断滚动的rst:0x3 (SW_RESET), boot:0x13 (SPI_FAST_FLASH_BOOT)发呆。一块刚焊好的 ESP32-S3 核心板因为我在 OTA 升级过程中手贱拔了电源现在上电就重启串口日志刷得比弹幕还快连一个完整的启动信息都抓不到。那一刻我脑子里只有一个念头这板子是不是彻底变砖了后来我把这个问题彻底研究了一遍也踩了不少坑才搞明白一件事——ESP32 的变砖和传统单片机意义上的变砖完全是两码事。绝大多数情况下只要芯片本身没烧、Flash 没物理损坏它都能救回来。而真正让 ESP32 在固件升级这件事上抗造的是它内置的一套机制双分区Dual OTA Partition 自动回滚Rollback。这篇内容就是围绕这套机制展开的。我会讲清楚它到底怎么工作、为什么能防砖、怎么在 ESP-IDF 里配置、OTA 升级时有哪些坑、以及当你的板子真的看起来像砖时该怎么一步步把它救回来。不管你是刚接触 ESP32 的新手还是已经在做量产固件的老手这套东西都值得你花时间吃透——因为它直接决定了你的设备在用户手里能不能自己救自己。关键词里提到的 ESP32、固件、双分区、自动回滚、OTA这几个词其实就是一条完整的技术链路。下面我按实际项目里的思考顺序一层层拆开讲。2. 先搞清楚变砖在 ESP32 上到底指什么2.1 传统单片机为什么会真变砖在聊 ESP32 之前得先理解变砖这个词的来源。早期的 AVR、STM32 这类单片机程序烧录走的是 ISP/JTAG/SWD 接口Flash 里只有一份固件。如果这份固件在写入过程中断电或者写进去的程序一上电就把调试接口禁用、把时钟配错、把看门狗喂崩那芯片就会陷入一种跑不起来又连不上的状态。这时候你只能靠外部编程器硬擦运气不好连编程器都进不去那就真成砖了。这种单分区 无恢复机制的架构本质上是把设备的生死全押在唯一一份固件上。固件一旦损坏没有任何退路。2.2 ESP32 的启动链路决定了它天生不容易死ESP32 的启动流程和传统单片机不一样。上电后芯片内部固化的 ROM Bootloader 会先跑起来它负责最基础的初始化然后去 Flash 的固定偏移0x1000 附近读取二级引导程序Second Stage Bootloader再由二级引导程序去读分区表最后根据分区表找到要启动的应用程序分区。这个链路里有两个关键点第一ROM Bootloader 是固化在芯片里的你刷不掉它所以芯片永远有最低限度的意识第二二级引导程序和分区表是独立于应用程序存在的只要它们没坏你就有机会通过串口重新烧录。换句话说ESP32 的砖通常只发生在应用层而不是芯片层。这就给双分区和回滚机制留出了发挥空间。2.3 什么情况下 ESP32 才可能真的救不回来我整理了几种真正棘手的情况供你对照排查故障类型是否可救原因说明应用固件崩溃、反复重启可救进下载模式重新烧录即可OTA 升级中断电可救双分区机制会保留旧固件分区表被写坏多数可救手动指定分区表重新烧录二级引导程序损坏多数可救串口强制下载模式重烧Flash 物理损坏不可救硬件问题换芯片eFuse 误烧导致启动模式锁死基本不可救一次性熔丝无法逆转可以看到真正意义上的不可救几乎都和硬件或一次性熔丝有关和固件本身关系不大。这也是为什么我敢说ESP32 刷固件正常操作下几乎不会变砖。3. 双分区机制给固件留一条退路3.1 分区表长什么样要理解双分区先得看分区表。ESP32 的 Flash 布局是由一张分区表描述的它告诉引导程序哪块区域放引导程序、哪块放应用、哪块放 NVS、哪块放 OTA 数据。一个典型的支持 OTA 的分区表大概是这样# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, otadata, data, ota, 0xf000, 0x2000, phy_init, data, phy, 0x11000, 0x1000, factory, app, factory, 0x20000, 0x140000, ota_0, app, ota_0, 0x160000,0x140000, ota_1, app, ota_1, 0x2A0000,0x140000,这里有几个关键角色factory是出厂固件分区ota_0和ota_1是两个 OTA 应用分区otadata则是一块专门记录当前该启动哪个分区的小区域。双分区的核心思想就是同一时刻Flash 里至少存在两份可启动的应用固件。当你通过 OTA 升级时新固件被写进当前没在运行的那个分区写完之后再更新otadata告诉引导程序下次从新分区启动。整个过程旧固件始终完好无损。3.2 otadata 是怎么决定启动哪个分区的otadata这块区域不大通常 0x2000 字节里面存的是两个ota_select结构体对应两个 OTA 槽位。每个结构体里有一个seq序号和一个state状态。引导程序启动时会比较两个槽位的seq选序号更大、状态为有效的那个来启动。这个设计很巧妙升级时新固件写入后seq加一状态先标记为待验证如果新固件启动成功并主动确认状态才变成有效如果新固件启动失败或没确认引导程序下次就会回退到旧分区。这就是自动回滚的底层依据。3.3 为什么必须用双分区而不是单分区有人会问我就用一个 OTA 分区不行吗技术上可以但风险极高。单分区 OTA 意味着新固件直接覆盖旧固件写入过程中一旦断电两份都没了那就真的只能靠串口救。而双分区是写新不碰旧把升级风险降到了最低。代价是什么Flash 空间。双分区意味着应用可用空间减半。比如 4MB Flash 的模组如果两个 OTA 分区各占 1.25MB那你的固件就不能超过这个大小。这是选型时必须提前算清楚的账。提示如果你的固件体积接近分区上限优先考虑换更大 Flash 的模组而不是压缩分区。分区压得太紧后续加功能会非常痛苦。4. 自动回滚让设备自己判断新固件行不行4.1 回滚的触发条件是什么自动回滚不是新固件一崩就回退这么简单它有一套明确的判定逻辑。在 ESP-IDF 里回滚的触发主要看两件事新固件有没有在启动后调用esp_ota_mark_app_valid_cancel_rollback()来报到以及有没有在限定时间内完成报到。如果新固件启动后一直没调用这个函数或者调用之前就崩溃重启了引导程序在下次启动时就会发现这个分区状态还是待验证于是自动切回上一个有效分区。这个机制的本质是把固件是否健康的判定权交给固件自己。4.2 在代码里怎么正确报到很多新手踩的坑就在这里写了 OTA 升级逻辑但忘了在新固件里加报到代码结果设备每次升级后都会莫名其妙回滚到旧版本还以为是升级失败。正确的做法是在应用初始化完成后确认关键功能比如网络、传感器、外设都正常了再调用确认函数。#include esp_ota_ops.h void app_main(void) { // 先做关键初始化 init_network(); init_sensors(); // 确认关键功能正常后再标记固件有效 const esp_partition_t *running esp_ota_get_running_partition(); esp_ota_img_states_t ota_state; if (esp_ota_get_state_partition(running, ota_state) ESP_OK) { if (ota_state ESP_OTA_IMG_PENDING_VERIFY) { // 这里可以加一些自检逻辑 if (self_test_passed()) { esp_ota_mark_app_valid_cancel_rollback(); } else { esp_ota_mark_app_invalid_rollback_and_reboot(); } } } }这段代码的意图很明确只有自检通过才告诉系统这个固件没问题别回滚了。如果自检没过主动触发回滚并重启让设备回到上一个能用的版本。4.3 回滚窗口期该设多长ESP-IDF 默认给了新固件一个观察期但这个时间不是无限的。如果你在观察期内一直不报到系统会认为固件有问题。实际项目里我一般把报到逻辑放在网络连接成功、关键外设初始化完成之后而不是一上电就报。因为有些固件的问题恰恰是在联网或跑业务逻辑时才暴露的。但也不能拖太久。如果报到逻辑依赖网络而网络本身可能因为固件 bug 连不上那就死锁了。我的经验是报到逻辑要分层——先确认不依赖网络的核心功能再确认网络功能任何一层失败都可以选择回滚。这样既给了固件充分的验证机会又不会因为单一依赖卡死。5. 从零配置一套能防砖的 OTA 流程5.1 分区表怎么改默认的 ESP-IDF 工程用的是factory单分区要启用双分区 OTA得换成partitions_two_ota.csv或者自己写一份。我一般直接基于官方模板改把应用分区大小按实际固件体积留出 20% 余量。改完之后用idf.py partition-table确认一下布局别等到烧录时才发现分区重叠。5.2 OTA 升级的核心代码逻辑一个最小可用的 OTA 升级流程大概分四步连接服务器、下载固件、写入目标分区、切换启动分区。下面是我常用的骨架esp_ota_handle_t ota_handle; const esp_partition_t *update_partition esp_ota_get_next_update_partition(NULL); esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, ota_handle); while (还有数据) { esp_ota_write(ota_handle, data, len); } esp_ota_end(ota_handle); esp_ota_set_boot_partition(update_partition); esp_restart();注意esp_ota_get_next_update_partition(NULL)这个调用它会自动帮你选出当前没在运行的那个 OTA 分区你不需要手动指定 ota_0 还是 ota_1。这是双分区机制自动化的体现。5.3 升级过程中断电会怎样这是大家最关心的问题。假设你正在往 ota_1 写新固件写到一半断电了。重新上电后引导程序读otadata发现当前有效分区还是 ota_0因为 ota_1 的写入还没完成状态没更新于是照常从 ota_0 启动。ota_1 里那半截数据就是垃圾下次升级会被覆盖不影响使用。这就是双分区最核心的价值升级失败不会破坏正在运行的固件。我实测过很多次包括故意在写入过程中拔电设备都能正常回到旧版本。5.4 版本号与防降级怎么处理量产项目里还有一个容易被忽略的点版本管理。如果 OTA 服务器配置不当设备可能反复升级到同一个版本或者被降级到有漏洞的旧版本。我的做法是在固件里嵌入版本号升级前先比对只有新版本号大于当前版本才执行升级。同时服务器端也要做校验避免推送错误的固件包。6. 那些让我熬夜的坑OTA 实战踩坑记录6.1 坑一忘记调用确认函数导致无限回滚这是我第一次做 OTA 时踩的坑。升级逻辑写得没问题新固件也能启动但每次重启后都回到旧版本。排查了半天才发现新固件里根本没调用esp_ota_mark_app_valid_cancel_rollback()。系统一直认为新固件没报到观察期一过就回滚了。这个坑的隐蔽之处在于设备表面上看是升级失败但实际升级是成功的只是没被确认。后来我养成了一个习惯只要用了 OTA第一件事就是检查确认函数有没有被调用。6.2 坑二分区表大小算错导致写入越界有一次我给一个 4MB 模组配分区两个 OTA 分区各给了 1.5MB加上引导程序、分区表、NVS、otadata总大小超了。编译时没报错烧录时才发现问题。ESP-IDF 其实会在编译阶段检查分区是否超出 Flash 大小但如果你用的是自定义烧录脚本这个检查可能被绕过。教训是分区表配完一定要用工具核对总大小别凭感觉。我现在的习惯是在分区表注释里直接写上每个分区的偏移和结束地址一眼就能看出有没有重叠或越界。6.3 坑三回滚后 NVS 数据不兼容这个坑比较深。假设旧固件用 NVS 存了一个结构体新固件改了结构体字段升级后新固件写入新格式数据然后因为某个原因回滚到旧固件。旧固件读 NVS 时发现数据格式对不上可能直接崩溃于是又触发回滚……形成死循环。解决办法是给 NVS 数据加版本号读取时先检查版本不兼容就重置或迁移。这个问题在双分区场景下尤其要注意因为回滚意味着新旧固件会交替运行数据格式必须双向兼容。6.4 坑四看门狗与回滚的相互作用ESP32 有任务看门狗和中断看门狗。如果新固件因为 bug 导致看门狗频繁触发重启而重启次数又没达到回滚判定条件设备就会陷入重启—看门狗—重启的循环看起来像砖其实还在观察期内。这时候如果你手动断电再上电反而可能加速回滚判定。我的处理方式是在固件里加一个启动计数器存在 RTC 内存里如果连续重启超过阈值主动调用回滚函数别等系统判定。这样能更快地把设备拉回可用状态。7. 板子真的像砖了怎么一步步救回来7.1 先判断是哪种砖遇到设备不启动别急着下结论。先看串口日志如果能看到 ROM Bootloader 的输出类似rst:0x1 (POWERON_RESET)说明芯片还活着只是应用层有问题大概率能救。如果串口完全没输出再检查供电、晶振、Flash 焊接最后才怀疑芯片。7.2 强制进入下载模式重烧ESP32 进入下载模式的标准操作是按住 BOOT 键点一下 RESET 键再松开 BOOT 键。这时候串口会打印waiting for download说明芯片在等你烧录。然后用idf.py flash或者 esptool 重新烧录完整固件即可。如果板子上没有 BOOT 键可以手动把 GPIO0 拉低再上电。这个操作的本质是让芯片在启动时采样到 GPIO0 为低电平从而进入 UART 下载模式。7.3 擦除 Flash 的两种力度有时候重烧还不够需要先擦除。esptool 提供了两种擦除方式erase_flash是全片擦除把整个 Flash 清空erase_region是擦除指定区域。全片擦除最彻底但会丢掉 NVS 里的配网信息、校准数据等。如果这些数据重要优先用区域擦除只擦应用分区和 otadata。我一般先用区域擦除试不行再全片擦。全片擦之后记得重新烧录引导程序、分区表、应用固件三件套缺一不可。7.4 用 esptool 手动救砖的完整命令# 查看芯片信息确认连接正常 esptool.py --port /dev/ttyUSB0 chip_id # 擦除 Flash esptool.py --port /dev/ttyUSB0 erase_flash # 烧录引导程序 esptool.py --port /dev/ttyUSB0 write_flash 0x1000 bootloader.bin # 烧录分区表 esptool.py --port /dev/ttyUSB0 write_flash 0x8000 partition-table.bin # 烧录应用固件 esptool.py --port /dev/ttyUSB0 write_flash 0x10000 app.bin这套命令我在救砖时用过无数次只要硬件没坏基本都能救回来。关键是偏移地址要对不同芯片型号的引导程序偏移可能不同ESP32 是 0x1000ESP32-S3 也是 0x1000但具体以编译输出为准。8. 把防砖能力做成产品级设计8.1 出厂固件与 OTA 固件的关系量产设备一般会烧一个出厂固件到 factory 分区这个固件通常功能最简、最稳定只负责联网和触发 OTA。设备第一次联网后OTA 到正式固件。这样设计的好处是即使正式固件有问题factory 分区始终是最后一道防线。不过要注意factory 分区不是必须的。如果你的分区表里没有 factory只有 ota_0 和 ota_1设备也能正常工作只是第一次启动时两个分区都是空的需要先烧一个固件进去。8.2 双分区之外的额外保险除了双分区和回滚我还会加几个额外措施一是固件签名校验防止刷入被篡改的固件二是 Flash 加密保护固件不被读取三是安全启动确保只有签名的引导程序能运行。这几项在 ESP32 上都有官方支持配置起来不算复杂但对产品安全性提升很大。当然这些安全功能会带来一些代价比如加密后烧录速度变慢、调试变麻烦。是否启用要看产品定位消费类产品可以适当取舍工业类产品建议全上。8.3 监控与远程诊断设备部署出去之后你不可能每台都拆开看。所以我会在固件里加一个健康上报机制设备每次启动、每次 OTA、每次回滚都上报一条日志到服务器。这样你能在后台看到哪些设备在回滚、回滚原因是什么从而快速定位固件问题。这个机制配合双分区使用效果特别好。因为回滚本身是设备自救的行为但如果你不知道它在自救就永远发现不了固件里的隐患。9. 我个人的几条经验总结做 ESP32 OTA 这几年我最大的体会是防砖的核心不是防止刷坏而是刷坏了也能自己回来。双分区和自动回滚就是这套思路的落地。你不需要保证每次升级都成功你只需要保证每次失败都能退回可用状态。另外几个实操建议第一OTA 相关的代码一定要在真机上反复测试包括断电、断网、服务器返回错误固件等异常场景第二分区表要留足余量别把空间算得太死第三确认函数和回滚逻辑要写对这是整个机制能跑起来的关键第四NVS 数据格式要考虑新旧兼容避免回滚后数据错乱。最后分享一个小技巧如果你在调试 OTA 时不想每次都真的联网下载可以先把固件写到本地 HTTP 服务器让设备从局域网升级。这样迭代速度快很多也方便复现各种异常情况。等逻辑跑通了再切到真实的 OTA 服务器。这套流程我用了很久能省下大量等待时间。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑