资讯动态

ESP32刷机不怕变砖:双分区与自动回滚机制全解析

发布时间:2026/10/2 1:23:37 来源:尧图企业网站定制
做嵌入式这么久被问得最多的问题之一就是ESP32 固件刷坏了是不是就成砖了每次听到这个问题我都想笑因为它介于对和不对之间而且绝大多数人根本没踩到真正的“砖”只是被自己吓到了。你手里那块板子大概率死不了前提是你知道ESP32的启动流程、分区表结构以及双分区加自动回滚这套玩法到底在保护什么。我最早做OTA远程升级的时候脑子里也全是“刷坏了怎么办”的焦虑。后来把双分区和自动回滚跑通之后才发现这两样东西联合起来几乎就是为“无人值守的嵌入式设备”量身定制的安全网。这篇文章不跟你谈玄乎的理论直接从我实际踩过的坑出发讲清楚三件事ESP32什么时候会真砖双分区怎么把风险兜住以及自动回滚是怎么让设备自己反悔的。文章最后会附一份完整的“故意刷坏”实验步骤你可以照着在自己板子上验证一遍比看十篇文章都管用。1. ESP32 到底会不会真的“变砖”先说结论对ESP32这种MCU来说“硬砖”的概率极低。你平时遇到的所谓刷坏了绝大多数只是把Flash里的内容写乱了芯片本身没有任何物理损伤用串口就能救回来。真正需要害怕的是少数几个特定场景比如把Flash加密密钥弄丢、或者烧错了eFuse配置。1.1 决定命运的启动链路想要理解“砖不砖”先要看懂ESP32每次上电时到底在干什么。芯片内部有个ROM引导程序这是出厂时固化在硅片里的代码你没法擦掉除非芯片烧了。每次复位CPU会从这个ROM开始执行然后根据引脚状态决定走哪条路如果GPIO0被拉低ROM引导程序就会进入串口下载模式等待上位机通过esptool发送烧写指令。如果GPIO0是高电平则从Flash地址0x1000加载第二级Bootloader再读分区表最后根据分区表里的信息和otadata区域的记录选择启动某个App分区里的固件。这条链路里最关键的一点是ROM引导程序在芯片里永远在。所以哪怕你把Flash里所有内容都擦光了只要GPIO0能拉低芯片依然能进入下载模式接受新的烧写。这就是ESP32救砖的底气。1.2 真正会砖的场景和救砖路径把“砖”分成两类来看思路就清晰了。一类是“软砖”也叫逻辑砖。App固件崩溃、分区表写错、启动死循环、Flash频率设置不对这些都属于这一类。现象是上电后串口刷日志刷个不停或者根本没有任何输出但芯片是活的。救法也很暴力按住BOOT键再按一下EN键让芯片进入下载模式然后重新烧一套完整的Bootloader、分区表和App即可。另一类是“硬砖”这需要满足很苛刻的条件。比如Flash加密或Secure Boot开启后密钥只在eFuse里烧了一次然后你又把Flash整个擦掉那加密状态和固件内容对不上启动流程直接卡死在最早期救回来的概率趋近于零。再比如误烧了eFuse把下载模式永久禁用那芯片就彻底变成一块废硅片。不过这类操作在普通开发阶段极少发生你只要别手贱去动eFuse和Secure Boot配置基本遇不到。所以说日常开发里你害怕的“刷坏”根本不会真的变砖。但话说回来对于已经部署到现场的设备就算只是“软砖”你也无法趴到设备旁边去救它。这时候双分区和自动回滚的价值就体现出来了。2. 双分区到底在保护什么双分区这个概念很多人一听就以为是把固件复制两份做备份这其实是误解。双分区的核心价值在于给OTA升级提供一个“安全着陆点”你运行在A分区把新固件写到B分区写完切换重启。如果B分区起不来A分区还完好无损地躺在那里系统随时能退回A。2.1 双分区不是“双备份”的简单复制真正的物理双备份需要整片Flash做两份冗余代价极高ESP32这种低成本MCU根本玩不起。ESP32的方案是从分区表层面把App区域切成多个分区每个分区都是一份完整的独立固件镜像。Bootloader本身支持从一个指定分区启动所以分区之间天然就是可切换的。理解了这个机制你就能明白为什么“双分区”在OTA场景下特别香升级不是直接覆盖正在运行的固件而是先写另外一份写完校验通过后再切换过去。万一新固件是个雷旧固件并没有被破坏系统还能回到旧版本继续工作。2.2 分区表示例与配置实操在ESP-IDF工程里分区表是一个CSV文件默认放在工程根目录的partitions.csv里。下面是一个典型的4MB Flash双OTA分区表# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x4000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x140000, app1, app, ota_1, 0x150000, 0x140000, spiffs, data, spiffs, 0x290000, 0x170000,这个表里有几个关键点app0和app1的Type都是appSubType分别是ota_0和ota_1这是Bootloader识别OTA分区的依据。otadata是一个专门的data分区存放“下一次启动哪个App”的记录。Bootloader每次启动前都会读它。两个app分区大小必须一致否则esp_ota_get_next_update_partition()会找不到合适的升级分区。在menuconfig里选择分区表时可以直接指定Custom partition table CSV然后把上面的内容填进去。烧录的时候Bootloader、分区表和App是分开烧的千万别只烧一个App就觉得万事大吉。2.3 Flash容量决定你能不能玩这套双分区看起来很美好但它有个前提Flash容量得够。一套完整的OTA方案最少需要两个App分区加一个otadata分区。如果你的模块是2MB Flash那每个App分区能分到的空间就非常吃紧很可能连一个带WiFi协议栈的固件都塞不下。我个人的经验是想舒舒服服玩OTA至少要用4MB Flash的模块比如ESP32-WROOM-32E这种。8MB和16MB就更宽裕了还能顺便把文件系统分区划大一点。如果你的项目里用的是上古1MB Flash模块那就别折腾双分区了老老实实把固件写好或者考虑换模块。还有一种中间方案是“factory 单个OTA分区”出厂固件放在factory分区升级固件写到ota_0分区。这种方案也能实现OTA但严格来说升级失败后的回退能力不如真正的双OTA分区。核心原因在于factory分区和ota_0分区之间不是对等的升级关系一旦ota_0固件挂了Bootloader虽然能找到factory分区但回退逻辑远没有双OTA分区那么顺滑。所以我通常建议只要Flash空间允许直接上app0app1双分区。3. 自动回滚是怎么做到让设备自己反悔的双分区解决了“旧固件还在”的问题但设备可不知道新固件到底行不行。这时候就需要自动回滚机制出场。它的本质是给Bootloader加了一个“试用期”逻辑新固件启动后必须在一段时间内证明自己是好的如果证明不了Bootloader就把启动顺序倒回去让旧固件接管。3.1 回滚机制的核心逻辑在ESP-IDF中自动回滚由Bootloader配合NVS存储共同实现需要在menuconfig里打开开关。不同版本菜单位置略有变化最简单的方法是直接在menuconfig里搜索“ROLLBACK”找到Enable app rollback support打开即可。它的工作流程可以这样理解新固件通过OTA写入B分区并设置B分区为下次启动分区。设备重启Bootloader从B分区启动同时在NVS里记一笔B分区的启动尝试次数加1。App固件正常运行后在合适的时机调用esp_ota_mark_app_valid_cancel_rollback()告诉系统“我已经验证过自己了没问题”。此时NVS里记录的尝试次数归零。如果App固件一直没调用这个函数或者启动了之后直接崩溃复位Bootloader会在下次启动时继续累加尝试次数。当尝试次数达到配置的阈值Bootloader认定当前固件是坏的不再启动它而是直接启动另一个分区。这个“次数阈值”在menuconfig里对应Number of repeated boot attempts。我习惯设成3意思是给新固件三次机会三次都没验证成功就切回旧固件。3.2 自检代码怎么写才可靠光开启回滚还不够App代码必须配合好。最忌讳的做法是在app_main()第一行就调用esp_ota_mark_app_valid_cancel_rollback()那等于告诉系统“我没检查过就宣布成功”回滚机制形同虚设。可靠的做法是先自检再标记有效。我的项目里通常是这样写的#include string.h #include freertos/FreeRTOS.h #include freertos/task.h #include esp_log.h #include esp_ota_ops.h #include nvs_flash.h static const char *TAG app; static bool board_self_check(void) { // 这里放你项目自己的自检逻辑 // 比如 GPIO、传感器、WiFi 连接、按键心跳 // 返回 false 说明当前固件有问题 return true; } void app_main(void) { esp_err_t err nvs_flash_init(); if (err ESP_ERR_NVS_NO_FREE_PAGES || err ESP_ERR_NVS_NEW_VERSION_FOUND) { nvs_flash_erase(); nvs_flash_init(); } const esp_partition_t *running esp_ota_get_running_partition(); const esp_partition_t *last_invalid esp_ota_get_last_invalid_partition(); ESP_LOGI(TAG, running partition subtype: %d, running ? running-subtype : -1); // 如果当前分区就是上一次标记过的无效分区直接回滚 if (last_invalid ! NULL running ! NULL last_invalid-subtype running-subtype) { ESP_LOGW(TAG, started from previously invalid app, rollback!); esp_ota_mark_app_invalid(); esp_restart(); return; } // 给硬件一点时间稳定同时让看门狗有个基本节奏 vTaskDelay(pdMS_TO_TICKS(2000)); if (board_self_check()) { esp_ota_mark_app_valid_cancel_rollback(); ESP_LOGI(TAG, self check passed, app marked valid); } else { ESP_LOGE(TAG, self check failed, mark invalid); esp_ota_mark_app_invalid(); esp_restart(); } // 正常业务逻辑... }这段代码里有一个很关键的细节通过esp_ota_get_last_invalid_partition()判断当前运行的分区是不是上次失败的坏分区。如果是说明这次启动的还是那个有问题的固件那就再标记无效并重启给Bootloader一个明确信号去切换分区。这种“双重保险”能避免新固件循环启动但又因为自检逻辑不严谨导致旧固件永远接管不了的情况。3.3 不要开反回滚注意 Anti-Rollback 的坑在menuconfig里rollback开关旁边往往还有个Enable anti-rollback support选项。这个功能跟安全版本号绑定开启之后固件版本只能往上升不能往下降。听起来像是“更安全了”但它带来的麻烦在调试阶段特别恶心一旦你升级到一个高版本固件哪天想刷回旧固件Bootloader会直接拒绝启动旧版本。因为NVS或者efuse里记录的secure_version比旧固件的版本还高系统认为旧固件是“被篡改的”或者“过期的”。如果你不是在搞严肃的安全产品我建议开发阶段别开这个选项。开了之后反复刷机调试会变成一个噩梦。真正生产环境要用的话也一定先把版本管理和密钥备份流程跑通再说。3.4 Arduino 场景下的可行做法很多朋友习惯用Arduino IDE开发ESP32问我在Arduino环境下怎么实现自动回滚。实话实说Arduino生态本身对自动回滚的支持比较弱ArduinoOTA库提供的是上传烧录能力并不带完善的“启动失败自动回退”机制。如果你坚持在Arduino里折腾可以在分区方案里选带OTA的那项然后自己在代码里用Preferences库在NVS中存一个升级标记升级完成后在NVS里写入updatePending true。重启后检查这个标记。如果业务初始化正常几秒后清除标记。如果业务初始化失败在代码里主动重启并在重启后检测到标记还是true就通过某种方式触发回滚。但这种方案问题很多比如Arduino库本身没有友好地暴露Bootloader回滚接口NVS标记的清除时机容易出错而且整个流程对新手来说调试成本很高。我试过几次之后果断把需要OTA的项目全部迁到了ESP-IDF。如果后续还要拓展更多可玩性比如接一个ROS2小车底盘做串口桥接ESP-IDF的环境也更顺手。所以我对Arduino用户的建议是学习阶段随便玩可以生产环境还是趁早切IDF。4. 用一次真实的“刷坏”实验验证整套机制说了这么多不如动手实验一把。我建议你找一块闲置的开发板按下面的步骤故意把新固件写成“启动即崩”然后看着Bootloader自动切回旧固件。整个过程跑完你对这套机制的信心会完全不一样。4.1 实验环境的准备准备物料一块ESP32开发板4MB Flash即可。一条能正常传数据的USB线。别小看这一步很多“刷不进去”的锅都是劣质USB线只供电不传数据引起的。装好ESP-IDF的开发环境。版本建议用v4.4以上不同版本API略有差异但核心流程一致。先把项目配置成自定义分区表放上面那个双OTA分区表的内容。然后在menuconfig里打开Enable app rollback support把Number of repeated boot attempts设成3。4.2 故意把新固件写成启动即崩准备两个固件。第一个是正常固件App A功能很简单上电后打印一行日志跑一个自检函数直接返回true调用esp_ota_mark_app_valid_cancel_rollback()然后循环打印“i am alive”。这个固件烧到app0分区模拟已经在现场稳定运行的旧版本。第二个是坏固件App B别的都不管上电后直接崩溃void app_main(void) { ESP_LOGE(bad_app, this is bad firmware, abort!); abort(); }这个坏固件就是我们要“测试升级”的目标。生产环境里的坏固件当然不会这么直白但可能因为外设初始化失败、死锁、看门狗超时等各种原因达不到同等的效果。4.3 观察回滚过程现在通过OTA方式把App B推到设备里。最直接的验证方式是在App A里留一个串口命令触发升级或者用一个简单的HTTP OTA服务。核心步骤一定是调用esp_ota_get_next_update_partition(NULL)拿到当前没运行的另一个OTA分区。调用esp_ota_begin开始写入。把App B的数据通过esp_ota_write逐块写进去。调用esp_ota_end结束并校验。调用esp_ota_set_boot_partition把启动分区切到App B。调用esp_restart()重启。重启后的现象很有意思串口日志会经历这么几个阶段第一批日志坏固件启动打印“bad firmware”然后abort。板子复位。第一批日志结束板子又重启坏固件再次启动再次abort。连续三次这就是我们设置的重复尝试次数。第四次启动时Bootloader不再启动App B而是打印类似“rollback”的信息直接切回App A。App A启动后打印“i am alive”系统恢复正常。整个过程大概也就十几秒但你在现场看着日志里Bootloader自己做出了“换人”的决定那种感觉非常踏实。尤其想到如果这块板子是部署在户外设备箱里的这个机制正在默默地保护整个系统你就会理解为什么我说双分区加自动回滚是OTA的定心丸。4.4 实验结束后的清理和备份习惯实验跑完NVS里会残留一些otadata的状态记录。如果直接在这个状态上继续开发可能会遇到“明明烧了A固件启动却跑到B分区”这类困惑。善后流程很简单按住BOOT进入下载模式执行esptool.py erase_flash然后重新烧Bootloader、分区表和你想要的App即可。我还养成了一个习惯每次要刷没验证过的固件之前先用esptool.py read_flash把当前正常的固件区域备份一份。回滚测试翻车的时候会被感恩这个动作因为它能让你在两分钟内恢复现场而不是重新编译整个工程再烧一遍。5. 常见问题排查实录写下这部分内容的时候我回忆了自己和周围朋友在实践双分区和自动回滚时踩过的各种坑。每个问题都真实发生过有的坑我现在提起来还想拍大腿。5.1 为什么会一直重启但就是不回滚这是最让人崩溃的情况新固件刷进去后不断重启但Bootloader就是不肯回滚日志一行接一行地刷。排查思路按顺序来先确认Bootloader里真的开了回滚功能。有时候你改了menuconfig但只编译烧了App忘了重烧Bootloader和分区表。回滚逻辑在Bootloader里BootLoader没更新等于白配。确认分区表里是不是真的设计成了app0app1双OTA分区而不是只有一个factory分区加一个OTA分区。确认新固件没有误调用esp_ota_mark_app_valid_cancel_rollback()。比如代码里某个库内部自动调用了或者在初始化早期就标记成功那Bootloader当然不会回滚。看看NVS和otadata是否正常。如果otadata分区被其他数据污染Bootloader可能都读不出该用哪个分区启动。5.2 救砖三件套下载模式、擦除、重烧不管固件刷得多烂只要不是硬砖救砖流程就那么三步。第一步进入下载模式。按住开发板上的BOOT键或者叫IO0键同时按一下EN键复位松开后芯片就停在ROM引导阶段等待烧写。有些板子是自动下载电路不需要手动按键但多数独立模块需要手动操作。第二步擦除整片Flash。用esptool执行esptool.py --port COM3 erase_flash这一步会把Flash里所有内容清空包括Bootloader、分区表、App和NVS。第三步重新烧写全套。用ESP-IDF工程编译产物或者手动指定地址esptool.py --port COM3 write_flash 0x1000 build/bootloader.bin 0x8000 build/partition-table.bin 0xe000 build/ota_data_initial.bin 0x10000 build/app.bin注意这里不同工程的Flash大小和分区表偏移可能不同烧写地址以你实际生成的flash_args为准。ESP-IDF编译结束后在build目录下通常会有flash_args文件烧写时直接读取这个文件最稳妥。5.3 Flash加密与安全启动的连环坑如果说前几个坑是皮肉伤这个就是真伤筋动骨。如果你开启了Flash Encryption或者Secure Boot然后又在测试中反复擦除Flash风险会急剧上升。Flash加密的密钥可能已经写死在eFuse里或者只存在于某个临时的加密过程。一旦Flash里的密文和密钥对不上设备启动到你最不想看到的阶段就直接卡死。更麻烦的是开启了Secure Boot后固件需要有合法的数字签名Bootloader才会启动它。你自己编译的App如果没走签名流程又或者签名密钥生成后没有妥善保管那基本等于给自己挖了个大坑。我的建议是开发调试阶段不要开这两样等产品成熟了、密钥备份流程完整了再考虑。真开了之后每次烧写前都要确认密钥库可访问而且一定要在设备现场模拟一次完整救砖流程别等到设备在客户机房挂了才想起没验证过。5.4 一些容易被忽略的硬件坑最后说几个跟代码无关、但能让你折腾一天的硬件问题。第一串口模块电平问题。很多便宜的USB转TTL模块默认输出5V而ESP32的IO是3.3V电平。直接把5V信号怼到ESP32的RX脚上短时间没事长时间或者信号抖动厉害的时候轻则通信不稳定重则烧坏IO。选带电平转换的串口模块或者自己做好分压。第二供电不足。ESP32开启WiFi的时候电流峰值能到四五百毫安如果全靠USB口供电很多电脑的USB口电压会被拉垮导致烧录中途掉线或者OTA过程中反复重启。我实测下来在开发阶段外接一个3.3V稳压电源或者用质量好一点的USB Hub能少掉很多头发。第三串口波特率。esptool默认烧录波特率通常是460800但有些板子用的USB转串口芯片质量一般跑高速会出错。遇到奇怪的校验错误把波特率降到115200再试很多莫名其妙的问题就消失了。我个人在实际操作中的体会是双分区加自动回滚这套组合最大的价值不是“防止变砖”而是让你在部署设备之后依然保有亲自到现场调试的底气。它在后台默默地运行平时你根本感觉不到它的存在直到某次升级出了岔子你才发现自己早就给自己买好了一份保险。最后再分享一个小技巧在你的固件启动日志里一定打一个明确的启动分区标识比如subtype16表示从ota_0启动subtype17表示从ota_1启动。回滚发生时日志里能清楚看到Bootloader是从哪个分区拉起来的这对排查问题能省下大把时间。这个习惯我从第一次做OTA实验起一直保留到现在非常值得养成。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑