
1. 从一次刷机翻车说起ESP32 到底会不会变砖很多人第一次给 ESP32 刷固件手都是抖的。尤其是用 USB-TTL 接上开发板、敲下esptool.py write_flash回车的那一刻脑子里全是“万一刷坏了怎么办”。我当年也是这么过来的第一块 ESP32-WROOM-32 因为分区表填错上电之后串口只吐乱码当时真以为它彻底废了。先把结论摆在前面ESP32 在绝大多数情况下不会真正变砖。它和手机、路由器那种一旦引导程序损坏就只能拆机上编程器的设备不太一样。ESP32 芯片内部固化了一段 ROM Bootloader这段代码是出厂就烧死在硅片里的用户改不了也擦不掉。只要芯片没被物理损坏、供电没出大问题你永远可以通过正确的引脚组合把它拉进下载模式重新烧录。那为什么还有那么多人喊“变砖”因为大家说的“砖”其实分两种。一种是假砖程序跑飞了、分区表错了、OTA 升级到一半断电了表现是串口刷屏乱码或者一直重启但芯片本身还能进下载模式重新烧一次就好。另一种是真砖比如烧录时把 eFuse 里的加密位、安全启动位误烧了或者供电不稳导致 Flash 芯片损坏这种才是真的麻烦往往需要换 Flash 甚至换芯片。我这次要聊的就是怎么用**双分区Dual OTA Partition加自动回滚Rollback**这套机制把“假砖”的概率压到几乎为零。这套方案的核心思路很朴素固件永远留一份能跑的备份新固件启动失败就自动退回旧版本。听起来简单但里面的分区表设计、回滚标志位、看门狗配合每一步都有坑。下面我把整个设计思路、实操步骤和踩过的坑一次性讲清楚。这套内容适合谁看如果你正在用 ESP-IDF 或者 Arduino 框架做 ESP32 的 OTA 升级或者你手上有个部署在户外、上墙、装进设备里的 ESP32不方便每次升级都拆下来插 USB那这套机制基本是刚需。哪怕你只是想让自己的开发板刷不死也值得花时间把双分区和回滚搞明白。2. 双分区与自动回滚的整体设计思路2.1 为什么单分区 OTA 是个危险游戏先说说最朴素的 OTA 做法整个 Flash 只放一个 app 分区新固件下载下来直接覆盖旧固件。这种做法在实验室里跑跑没问题一旦放到真实环境就是灾难。原因很简单覆盖写 Flash 的过程中如果断电、网络中断、或者新固件本身有 bug你手上就只剩一个半残的固件设备再也起不来只能拆机重刷。我见过太多项目栽在这一步。有个做温湿度采集的朋友设备装在冷库顶上OTA 升级到 90% 的时候路由器重启了结果二十多台设备全部失联最后请了吊车才把设备取下来。这种代价一次就够记一辈子。双分区的思路就是永远保留一份完整的、验证过能跑的固件。Flash 里划出两个 app 分区比如ota_0和ota_1当前运行的在ota_0新固件就写到ota_1。写完之后改一下启动标志下次重启从ota_1启动。如果ota_1启动失败自动回滚到ota_0。这样无论升级过程出什么问题设备至少还有一个能跑的版本兜底。2.2 自动回滚到底靠什么判断“启动失败”这是整套机制里最容易被误解的地方。很多人以为“回滚”是芯片自己判断固件好不好其实不是。ESP32 的回滚机制依赖两个东西Bootloader 的启动计数和应用层的主动确认。具体流程是这样的Bootloader 每次尝试从新分区启动时会先检查这个分区是否被标记为“待验证”。如果是它就把启动次数加一然后跳转过去执行。应用启动之后如果一切正常需要在规定时间内调用esp_ota_mark_app_valid_cancel_rollback()告诉系统“我跑起来了别回滚了”。如果应用启动后崩溃、卡死、或者根本没来得及调用这个函数Bootloader 在下次重启时发现启动次数超了阈值就会自动切回上一个正常分区。这个设计的关键在于**“确认”这个动作必须由应用主动完成**。也就是说你得在代码里显式地告诉系统“我活着”。如果你忘了写这一句那设备会陷入“启动—计数—回滚—再启动”的循环表现就是反复重启。我第一次做的时候就在这里卡了半天串口日志里一直看到rollback字样后来才发现是忘了加确认函数。2.3 分区表怎么划才合理分区表是整套机制的骨架划错了后面全白搭。一个典型的支持双分区 OTA 的分区表大概长这样名称类型子类型偏移大小说明nvsdatanvs0x90000x6000存储 WiFi 配置等otadatadataota0xf0000x2000记录当前启动分区和状态phy_initdataphy0x110000x1000射频校准数据ota_0appota_00x200000x180000第一个应用分区ota_1appota_10x1A00000x180000第二个应用分区nvs_keydatanvs_keys0x3200000x1000加密密钥可选这里有几个数字值得掰开说。otadata分区只有 8KB但它极其关键里面存着两个 OTA 槽的状态哪个是当前启动的、哪个是待验证的、启动计数是多少。这个分区一旦损坏Bootloader 就不知道该从哪启动所以它必须放在靠前且稳定的位置。两个 app 分区大小要一致而且必须能装下你最大的固件。ESP32 常见的 4MB Flash扣掉 Bootloader、分区表、NVS 和各种数据区留给每个 app 分区大概 1.5MB 左右。如果你的固件带 WiFi、蓝牙、还有一堆库很容易就顶到 1.2MB 以上这时候就得考虑换 8MB 或 16MB 的模组。我手上这块 ESP32-S3-N16R8 是 16MB Flash 加 8MB PSRAM分区可以划得很宽裕每个 app 分区给 2MB 都行。注意分区表里otadata的偏移和大小不能随便改它和 Bootloader 里的逻辑是绑定的。改错了会导致 Bootloader 找不到 OTA 状态直接回退到出厂分区或者报错。3. 核心细节解析与实操要点3.1 otadata 里的状态机到底怎么转otadata分区里其实存了两份esp_ota_select_entry_t结构每份 4KB互为备份。这个结构里最关键的两个字段是ota_seq和ota_state。ota_seq是个递增的序号用来判断哪个分区更新ota_state则有几种取值对应不同的启动状态。当你第一次烧录固件时如果用的是idf.py flash它会把ota_0设为出厂分区otadata里两个槽都是空的。第一次 OTA 升级时系统把新固件写到ota_1然后把ota_1对应的ota_seq设为比ota_0大 1状态设为ESP_OTA_IMG_NEW。重启后 Bootloader 看到ota_1是 NEW 状态就把它标记为ESP_OTA_IMG_PENDING_VERIFY启动计数加一然后跳过去执行。应用跑起来之后调用确认函数状态变成ESP_OTA_IMG_VALID回滚取消。如果应用没确认就重启了Bootloader 发现PENDING_VERIFY且计数超限就把ota_1标记为ESP_OTA_IMG_ABORTED切回ota_0。整个过程的状态流转是严格定义的你可以在 ESP-IDF 的esp_ota_ops.h里看到完整的枚举。3.2 启动计数阈值和看门狗的配合启动计数的阈值默认是 3 次这个值可以通过CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK相关的配置调整但一般不建议改。3 次的意思是新固件连续启动失败 3 次就判定为坏固件执行回滚。为什么是 3 次而不是 1 次因为有些固件启动慢或者第一次启动要做一些初始化偶尔会超时重启一次给 3 次机会能避免误判。但这里有个坑如果你的应用启动后卡在某个死循环里既没崩溃也没重启Bootloader 是没机会计数的。这时候就需要看门狗Task Watchdog 或 Interrupt Watchdog出手。我通常会在应用启动的关键路径上喂狗如果主任务卡死超过设定时间看门狗触发重启重启后 Bootloader 才会把这次算作一次失败。所以看门狗和回滚机制是配套使用的缺一不可。实操心得确认函数不要放在app_main的第一行。我一般会等 WiFi 连上、主要外设初始化完成、系统稳定运行 10 秒后再调用确认。这样能确保固件是真的能干活而不是仅仅“能启动”。3.3 固件签名与安全启动对回滚的影响如果你启用了安全启动Secure Boot和 Flash 加密回滚机制依然有效但流程会多一层签名校验。Bootloader 在跳转前会验证新固件的签名签名不对直接拒绝启动连计数都不会加直接回滚。这其实是好事能防止别人往你设备里刷恶意固件。但要注意启用安全启动后eFuse 里的密钥位是不可逆的。一旦烧录这块芯片就只能跑签名过的固件。如果你在开发阶段不小心烧了安全启动的 eFuse后面想刷自己的测试固件就会很麻烦。我的建议是开发阶段绝对不要碰 eFuse 的安全相关位等产品定型量产时再统一烧录。这个坑我踩过一块 ESP32-S3 因为误烧了密钥最后只能当普通芯片用安全启动功能再也开不了。4. 实操过程与核心环节实现4.1 环境准备与分区表配置我这次用的是 ESP-IDF v5.1开发板是 ESP32-S3-N16R8 核心板。如果你用 Arduino 框架思路一样但分区表的配置方式不同后面我会单独说。先看 ESP-IDF 下的操作。第一步是创建自定义分区表。在项目根目录新建partitions.csv内容如下# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, otadata, data, ota, 0xf000, 0x2000, phy_init, data, phy, 0x11000, 0x1000, ota_0, app, ota_0, 0x20000, 0x200000, ota_1, app, ota_1, 0x220000,0x200000,然后在menuconfig里把分区表指向这个文件Partition Table - Custom partition table CSV填上文件名。同时确认Bootloader config - Enable app rollback support是打开的这个选项默认就是开的但值得检查一遍。编译之后用idf.py partition-table可以查看最终的分区布局确认两个 app 分区大小一致、偏移正确。这一步千万别跳过我有一次因为 CSV 里多打了个空格导致分区表解析失败烧录后设备一直重启查了半天才发现是格式问题。4.2 OTA 升级代码的完整实现下面是我实际项目里用的 OTA 升级核心代码基于 ESP-IDF 的esp_https_ota组件。为了通用性我把 URL 和证书校验部分做了简化重点看回滚相关的逻辑。#include esp_ota_ops.h #include esp_http_client.h #include esp_https_ota.h #include esp_log.h static const char *TAG OTA; void ota_task(void *pvParameter) { esp_http_client_config_t config { .url http://your-server/firmware.bin, .timeout_ms 10000, .keep_alive_enable true, }; esp_https_ota_config_t ota_config { .http_config config, }; ESP_LOGI(TAG, 开始 OTA 升级); esp_err_t ret esp_https_ota(ota_config); if (ret ESP_OK) { ESP_LOGI(TAG, OTA 成功准备重启); esp_restart(); } else { ESP_LOGE(TAG, OTA 失败: %s, esp_err_to_name(ret)); } vTaskDelete(NULL); }这段代码负责把新固件写到备用分区。注意esp_https_ota内部会自动处理分区切换和otadata更新你不需要手动去写otadata。升级完成后调用esp_restart()Bootloader 就会从新分区启动。接下来是应用启动后的确认逻辑这个必须加void app_main(void) { // 初始化各种外设 init_nvs(); init_wifi(); init_sensors(); // 等待系统稳定 vTaskDelay(pdMS_TO_TICKS(10000)); // 确认固件有效取消回滚 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) { ESP_LOGI(TAG, 固件运行正常标记为有效); esp_ota_mark_app_valid_cancel_rollback(); } } // 启动主循环 main_loop(); }这里的关键是esp_ota_get_state_partition先判断当前分区是不是处于PENDING_VERIFY状态。只有在这个状态下才需要确认。如果固件是出厂烧录的状态是VALID就不需要重复确认。这个判断能避免一些边界情况下的误操作。4.3 模拟升级失败与回滚验证代码写完了怎么验证回滚真的有效总不能真等设备出故障。我的做法是故意制造一次启动失败。具体操作是在 OTA 升级完成后先不调用确认函数然后在应用启动后 5 秒主动调用esp_restart()。这样 Bootloader 会认为新固件启动失败连续 3 次之后自动回滚。验证步骤我整理成了一张表步骤操作预期现象1烧录 v1 固件正常运行串口打印 v1 版本号2触发 OTA 升级到 v2下载完成设备重启3v2 启动后不确认5 秒后重启串口打印 v2然后重启4重复 3 次第 3 次重启后Bootloader 回滚5观察最终运行版本串口打印 v1 版本号回滚成功这个测试我跑了不下十遍每次都能稳定回滚。唯一需要注意的是回滚之后otadata里 v2 的状态会变成ABORTED下次再 OTA 升级时系统会优先选择另一个分区不会重复往坏分区写。注意测试回滚时确保设备供电稳定。如果测试过程中断电otadata可能处于中间状态虽然 Bootloader 有容错机制但多一事不如少一事。4.4 Arduino 框架下的双分区配置如果你用 Arduino IDE 或 PlatformIO双分区 OTA 也能做但配置方式不同。Arduino 框架默认的分区方案是Default只有一个 app 分区。你需要切换到Minimal SPIFFS (1.9MB APP with OTA)或者自定义分区表。在 Arduino IDE 里工具 - Partition Scheme选择带 OTA 的方案即可。PlatformIO 则需要在platformio.ini里指定board_build.partitions min_spiffs.csv或者自己写一个partitions.csv放到项目目录然后在配置里引用。Arduino 框架的Update库内部也实现了类似的回滚逻辑但确认函数需要手动调用Update.markValid()否则同样会回滚。很多人用 Arduino 做 OTA 时忘了这一步结果设备升级后反复重启还以为是固件问题。5. 常见问题与排查技巧实录5.1 设备升级后反复重启串口一直打印回滚日志这是最常见的问题九成以上的原因是应用没有调用确认函数。串口日志里通常会看到Rollback或者Pending verify字样。解决办法就是在应用启动稳定后调用esp_ota_mark_app_valid_cancel_rollback()。如果你用的是 Arduino检查有没有调用Update.markValid()。还有一种可能是确认函数调用了但调用得太早应用还没真正稳定就确认了结果后面崩溃了。这种情况回滚机制救不了你因为已经标记为 VALID 了。所以确认时机很重要我一般等 10 秒以上。5.2 OTA 下载到一半失败设备还能正常启动吗能。因为新固件是写到备用分区的下载失败只会导致备用分区数据不完整otadata不会被更新Bootloader 依然从原分区启动。下次重新 OTA 时系统会重新擦写备用分区。这就是双分区的价值所在升级失败不影响当前运行。但要注意如果备用分区之前有旧数据下载失败后可能残留半截固件。虽然 Bootloader 不会去启动它但会占用 Flash 空间。ESP-IDF 的 OTA 组件在开始写入前会自动擦除目标分区所以这个问题不用太担心。5.3 分区表改了之后原来的 OTA 数据还能用吗不能。分区表一变otadata里的偏移和分区信息就对不上了Bootloader 会认为 OTA 数据无效直接回退到出厂分区。如果你改了分区表必须重新烧录一次完整固件让系统重新初始化otadata。这个操作会清空 OTA 历史但不会影响 NVS 里的用户数据前提是 NVS 分区没动。5.4 常见问题速查表现象可能原因排查方向升级后反复重启未调用确认函数检查esp_ota_mark_app_valid_cancel_rollback回滚不生效回滚支持未开启检查 menuconfig 中Enable app rollback supportOTA 下载失败网络或服务器问题检查 URL、超时时间、服务器可达性分区表报错CSV 格式错误检查逗号、空格、偏移对齐确认后仍回滚确认后应用崩溃检查确认后的代码路径延后确认时机串口无输出启动分区错误用esptool.py read_flash读 otadata 分析5.5 几个我踩过的坑和独家技巧第一个坑是确认函数放在app_main开头。我一开始图省事app_main第一行就调用确认结果有次 WiFi 初始化失败导致系统卡死但固件已经被标记为 VALID回滚机制失效设备彻底失联。后来我把确认挪到所有关键初始化之后并且加了 10 秒延时再没出过问题。第二个坑是看门狗没开。有次固件里有个死循环设备不重启也不崩溃就是卡在那里。因为没开看门狗Bootloader 根本没机会计数回滚自然不触发。后来我养成了习惯所有正式固件必须开 Task Watchdog并且把超时时间设成 30 秒以内。第三个技巧是在 OTA 升级前先检查电池电量或供电状态。如果是电池供电的设备电量低于 30% 就别升级了万一升级到一半没电虽然不会变砖但备用分区会残留脏数据下次升级还得先擦除浪费时间。我现在会在 OTA 前读一次电压低于阈值直接拒绝升级并上报服务器。第四个技巧是保留一个“救砖”固件。我会在ota_0里烧一个功能极简但绝对稳定的固件只负责连 WiFi 和接收 OTA 指令。日常运行在ota_1升级也往ota_0写。这样即使ota_1彻底跑不起来回滚到ota_0也能保证设备在线还能远程重新升级。这个思路特别适合部署在难以物理接触的场景。6. 关于固件安全与长期维护的几点体会双分区加自动回滚解决的是“升级失败”的问题但它不解决“固件本身有 bug”的问题。如果你的新固件逻辑上就是错的比如连不上服务器、传感器读数全错回滚机制不会触发因为固件“能启动”。这时候需要的是应用层的健康检查。我的做法是在确认函数之前加一段自检读一次传感器、连一次服务器、发一条心跳全部通过才确认。任何一项失败就主动重启让 Bootloader 计数最终触发回滚。另外OTA 服务器的固件版本管理也很重要。我见过有人把测试固件传到生产服务器结果所有设备升级后都出了问题。建议在服务器端做版本校验设备上报当前版本服务器只下发比当前版本高的固件并且支持灰度发布。这些虽然超出了 ESP32 本身的范围但和回滚机制配合起来才能构成一个完整的远程升级方案。固件加密和安全启动是另一个维度的话题。如果你的设备涉及商业部署建议开启 Flash 加密防止别人直接读 Flash 提取固件。但记住加密和回滚不冲突只是多了一层签名校验。开启加密后OTA 固件必须是加密后的 bin 文件服务器下发时也要注意格式。最后说个实际数据。我这套双分区加回滚的方案在一个两百多台设备的部署里跑了半年经历了十几次 OTA 升级其中三次因为网络问题下载失败一次因为固件 bug 触发回滚没有一台设备需要人工干预。这个稳定性单分区方案是给不了的。如果你还在用单分区做 OTA真的建议花半天时间把双分区配上后面省下的维护成本远超这点投入。