资讯动态

ESP32双分区与自动回滚机制:OTA升级防变砖实战指南

发布时间:2026/10/28 1:06:55 来源:尧图企业网站定制
1. 从一次“刷砖”事故说起为什么ESP32需要双分区和自动回滚很多刚接触ESP32的朋友第一次听到“刷固件”这三个字心里都会咯噔一下。尤其是从Arduino UNO、STM32这类没有内置OTA能力的平台转过来的人脑子里往往还留着“刷错一次就变砖、得拿镊子短接引脚、上编程器救砖”的阴影。我当年第一次给ESP32刷固件的时候手都是抖的生怕一个断电就把板子送走。但实际用下来你会发现ESP32在固件升级这件事上的设计思路和传统单片机完全不是一个量级——它从芯片层面就为“刷坏了怎么办”这个问题准备了答案而这个答案的核心就是双分区Dual Partition和自动回滚Rollback。先把结论摆在前面ESP32正常刷固件几乎不会变砖。真正会变砖的场景是你在刷写过程中破坏了引导加载程序Bootloader或者分区表本身比如刷到一半断电、烧录了错误的分区表、或者用错误的烧录地址覆盖了关键区域。而只要你走的是标准的OTA升级路径哪怕新固件一启动就崩溃、哪怕升级过程中断电ESP32都能靠双分区机制和回滚逻辑把自己救回来。这篇文章我就把这件事从头到尾讲清楚双分区到底怎么划分、OTA升级时数据怎么流转、回滚标志位是怎么工作的、以及在实际项目里怎么配置才能让这套机制真正生效。这套机制适合谁看如果你正在用ESP32做联网设备、需要远程升级固件、或者单纯想搞清楚“为什么别人刷固件不怕砖”那这篇内容就是给你准备的。我会尽量用生活化的类比把原理讲透同时给出可以直接抄的配置和代码不管你是刚上手Arduino IDE的新手还是已经在用ESP-IDF做量产固件的老手都能从中拿到能落地的东西。2. ESP32的Flash分区布局双分区到底分的是什么2.1 从一张分区表看懂ESP32的存储结构要理解双分区得先知道ESP32的Flash是怎么被切分的。ESP32外挂的SPI Flash常见4MB、8MB、16MB并不是一整块随便用的空间而是被**分区表Partition Table**划分成若干个功能区域。你可以把它想象成一栋楼分区表就是楼层平面图每个房间有明确的用途和门牌号。一个典型的ESP32分区表包含这些区域分区名称类型作用典型大小bootloaderbootloader引导加载程序负责启动流程0x8000偏移约28KBpartition-tabledata分区表本身0x8000偏移后约3KBnvsdata非易失存储存WiFi配置、回滚标志等0x9000偏移24KBotadatadataOTA状态数据记录当前启动哪个分区0x20008KBapp0app应用程序分区01MB~2MBapp1app应用程序分区11MB~2MBspiffs/littlefsdata文件系统剩余空间关键就在app0和app1这两个分区。它们大小相同、功能对等各自可以存放一份完整的应用程序固件。这就是“双分区”的字面含义——芯片里同时存在两个可以运行App的槽位。注意很多人误以为双分区是“一个主分区一个备份分区”其实不对。app0和app1是平等的谁在运行取决于otadata里的记录而不是固定谁主谁备。2.2 为什么必须是两个对等的App分区这里要解释一个核心问题为什么ESP32不采用“一个App分区一个备份区”的设计而是搞两个对等分区原因在于OTA升级的原子性需求。假设只有一个App分区升级时你得先把旧固件擦掉再写新的这中间如果断电旧固件没了、新固件没写完设备直接变砖。而双分区的做法是当前运行在app0升级时把新固件写到app1写完之后修改otadata指向app1然后重启。整个过程中app0始终保持完整哪怕app1写坏了重启后依然可以回到app0。这就是“A/B升级”或者叫“乒乓升级”的思路。用生活类比你家有两把钥匙一把在用一把放抽屉。要换锁芯的时候先把新锁芯装到备用门上测试确认没问题再把常用门切换过去。备用门装坏了常用门照样能用不至于进不了家。这个设计带来的直接好处是升级过程可以随时中断而不损坏设备。断电、网络断开、写入失败最坏情况就是新固件没生效设备继续跑旧固件用户甚至感知不到。2.3 分区表怎么配手把手写一个支持双分区的CSV在ESP-IDF里分区表是一个CSV文件通常叫partitions.csv。下面是一个支持双OTA分区的4MB Flash配置示例# 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,这里有几个细节值得说otadata分区必须存在大小固定0x20008KB它存的是两个OTA槽位的状态记录。没有它回滚机制无从谈起。factory分区是可选的。如果你不需要“出厂固件”这个概念可以去掉factory直接让ota_0和ota_1两个分区。但保留factory有个好处当两个OTA分区都出问题时还能回到factory这个保底固件。每个app分区大小要一致且要能装下你的固件。0x140000是1.25MB对于大多数带WiFi和蓝牙的ESP32应用够用但如果你的固件包含大量资源、AI模型或者文件系统就得相应放大同时Flash总容量要够。偏移地址要连续且不重叠这是硬性要求。写分区表时建议用工具校验ESP-IDF编译时会自动检查重叠了会直接报错。在Arduino IDE里分区方案是通过菜单选择的比如“Default 4MB with spiffs”或者“Minimal SPIFFS”。如果你想自定义双分区需要在boards.txt或者用PlatformIO的board_build.partitions指定自定义CSV。这一点后面实操部分会详细讲。3. OTA升级的完整流程新固件是怎么“上位”的3.1 OTA升级的五个阶段拆解理解了分区布局再来看OTA升级的完整流程就顺理成章了。一次标准的ESP32 OTA升级可以拆成五个阶段下载新固件设备通过WiFi从服务器下载固件二进制文件通常分块写入边下边写。写入备用分区把下载到的数据写入当前未运行的那个OTA分区比如当前跑ota_0就写到ota_1。校验固件写入完成后校验固件的完整性包括长度、校验和、以及ESP32特有的镜像头信息。更新otadata把otadata里的启动目标切换到新分区并标记这个分区为“待验证pending verify”状态。重启并验证设备重启Bootloader读取otadata启动新分区。新固件启动后需要主动调用esp_ota_mark_app_valid_cancel_rollback()确认自己没问题否则会被判定为启动失败。这五个阶段里第4和第5步是回滚机制的关键。很多人OTA升级失败问题就出在第5步——新固件启动后忘了“报到”结果被系统当成坏固件回滚了自己还一脸懵。3.2 otadata里到底存了什么otadata分区虽然只有8KB但它承载了整个OTA状态机的核心数据。它内部维护两个esp_ota_select_entry_t结构每个结构记录一个OTA槽位的状态主要字段包括ota_seq序列号用来判断哪个分区更新。ota_state状态标志取值有ESP_OTA_IMG_NEW、ESP_OTA_IMG_PENDING_VERIFY、ESP_OTA_IMG_VALID、ESP_OTA_IMG_INVALID、ESP_OTA_IMG_ABORTED等。crc校验值保证这条记录本身没损坏。当一次OTA写入完成系统会把新分区的状态设为ESP_OTA_IMG_PENDING_VERIFY序列号加一。Bootloader启动时会选序列号更大且状态不是INVALID/ABORTED的分区来启动。如果新分区是PENDING_VERIFY状态启动后固件必须在一定时间内调用确认接口否则下次重启就会回滚到旧分区。提示这个“一定时间”不是时间概念而是“重启次数”概念。如果PENDING_VERIFY状态的固件连续启动失败比如崩溃重启Bootloader会累计失败次数达到阈值就回滚。3.3 自动回滚的触发条件与判定逻辑自动回滚不是“新固件一崩溃就立刻回滚”而是有一套判定逻辑。具体来说触发回滚的典型场景包括新固件启动后在调用确认接口之前发生了panic重启。新固件启动后主动调用了esp_ota_mark_app_invalid_rollback_and_reboot()。新固件连续多次启动失败Bootloader判定其不可用。而不会触发回滚的情况是新固件正常启动、正常调用确认接口之后哪怕运行中崩溃也不会回滚——因为此时它已经被标记为VALID系统认为它是“正式版本”了。这里有个容易踩的坑有些开发者把确认接口放在了一个“永远执行不到”的分支里比如放在WiFi连接成功之后。结果设备在没网的环境下启动永远不确认跑几次就被回滚了。正确的做法是在固件启动早期、业务逻辑之前就确认或者至少设置一个超时兜底。4. 实操在Arduino IDE和PlatformIO里配置双分区与回滚4.1 Arduino IDE下的分区方案选择与自定义如果你用Arduino IDE开发ESP32分区方案是在“工具”菜单里选的。默认的“Default 4MB with spiffs”方案其实已经包含了OTA能力它内部就是双分区结构。但如果你想精确控制分区大小或者用更大的Flash就需要自定义。自定义的方法是在Arduino的ESP32硬件包目录下找到tools/partitions/文件夹里面有一堆预置的CSV。你可以复制一份改名比如my_ota.csv然后修改内容。接着在boards.txt里对应开发板的配置中添加一行esp32.menu.PartitionScheme.my_otaMy OTA Layout esp32.menu.PartitionScheme.my_ota.build.partitionsmy_ota这样重新打开IDE菜单里就能看到你的自定义方案了。实测下来Arduino IDE对分区表的校验比较宽松但偏移重叠依然会报错所以改的时候要仔细算地址。4.2 PlatformIO下的分区配置与board选择PlatformIO是很多进阶玩家的选择配置更灵活。在platformio.ini里你可以这样指定分区表[env:esp32dev] platform espressif32 board esp32dev framework arduino board_build.partitions partitions.csv然后把自定义的partitions.csv放在项目根目录。PlatformIO会在编译时自动使用它。关于热词里提到的“esp32 s3核心板板载1-n16r8在platformio软件中怎么选择开发板”这里顺带说一句N16R8表示16MB Flash 8MB PSRAM。在PlatformIO里你可以选esp32-s3-devkitc-1作为基础板然后在platformio.ini里覆盖Flash和PSRAM配置board esp32-s3-devkitc-1 board_build.flash_size 16MB board_build.psram enable分区表也要相应放大因为16MB Flash可以给每个app分区分到4MB甚至更多做边缘AI、跑模型都够用。4.3 一段可直接用的OTA升级与回滚确认代码下面这段代码展示了OTA升级的核心逻辑以及启动后如何确认固件有效。基于Arduino框架#include WiFi.h #include HTTPClient.h #include Update.h #include esp_ota_ops.h void performOTA(const char* firmwareUrl) { HTTPClient http; http.begin(firmwareUrl); int code http.GET(); if (code ! HTTP_CODE_OK) { Serial.printf(下载失败HTTP码%d\n, code); return; } int len http.getSize(); WiFiClient* stream http.getStreamPtr(); if (!Update.begin(len)) { Serial.println(Update.begin失败); return; } size_t written Update.writeStream(*stream); if (written ! len) { Serial.println(写入不完整); return; } if (!Update.end()) { Serial.printf(Update.end失败%s\n, Update.errorString()); return; } Serial.println(OTA写入完成准备重启); ESP.restart(); } void confirmFirmware() { const esp_partition_t* running esp_ota_get_running_partition(); esp_ota_img_states_t state; if (esp_ota_get_state_partition(running, state) ESP_OK) { if (state ESP_OTA_IMG_PENDING_VERIFY) { // 这里可以做一些自检比如检查关键外设 esp_ota_mark_app_valid_cancel_rollback(); Serial.println(固件已确认有效取消回滚); } } } void setup() { Serial.begin(115200); confirmFirmware(); // 尽早确认避免被回滚 // ... 其他初始化 }这段代码的关键点在于confirmFirmware()要在setup()早期调用。我见过有人把它放在loop()里等某个条件满足才调用结果设备一重启就被回滚排查半天才发现是确认时机太晚。5. 常见问题与排查那些年我踩过的回滚坑5.1 固件升级后设备反复重启并回到旧版本这是最典型的回滚现象。排查思路按顺序来检查新固件是否调用了确认接口。用串口日志看有没有打印“固件已确认有效”。如果没有说明确认逻辑没执行到。检查确认接口的调用时机。如果放在WiFi连接之后而设备在无网环境启动就会一直不确认。检查新固件是否在启动早期就崩溃。如果新固件有bug导致panic根本走不到确认那一步自然被回滚。查看otadata状态。可以用esptool.py read_flash把otadata读出来分析或者用esp_ota_get_state_partition在代码里打印状态。5.2 OTA写入到一半失败设备还能用吗能用。这正是双分区的价值。写入失败只会导致备用分区数据不完整otadata不会切换重启后依然跑旧分区。你唯一需要做的是清理掉那个写坏的分区下次升级时重新写入即可。ESP-IDF在下次OTA时会自动擦除目标分区不用手动处理。5.3 分区表改大了app分区结果编译报错空间不足这是分区表偏移计算错误导致的。改分区大小时要保证所有分区的偏移地址连续且总大小不超过Flash容量。建议用在线分区表计算器或者直接用ESP-IDF的idf.py partition-table命令生成并校验。Arduino IDE下如果报“Sketch too big”说明app分区给小了需要调大。5.4 常见问题速查表现象可能原因解决方向升级后回滚到旧版未调用确认接口在setup早期调用mark_app_valid升级后设备不断重启新固件启动即崩溃检查新固件初始化逻辑OTA下载失败网络不稳或URL错误加重试机制校验HTTP码编译提示分区重叠偏移地址算错用工具重新计算分区表回滚后旧固件也起不来factory分区也损坏需串口重新烧录确认接口调用了仍回滚确认前已panic把确认逻辑提前到最前面实操心得我在做量产固件时会在确认接口之前加一个“最小自检”比如检查关键GPIO、检查传感器I2C通信。只有自检通过才确认这样能避免“固件能启动但功能全废”的情况被标记为有效。这个自检要足够轻量不能引入新的崩溃风险。6. 进阶让回滚机制更可靠的几个工程实践6.1 双分区之外的第三道保险factory分区前面提到factory分区是可选的但在量产设备里我强烈建议保留。它的作用是当ota_0和ota_1都因为某种原因不可用时Bootloader还能回到factory这个“出厂固件”。虽然这种情况极少发生但一旦发生factory就是最后的救命稻草。代价只是多占一份固件大小的Flash空间对于16MB的模组来说完全可以接受。6.2 回滚标志位的持久化与NVS的关系otadata和NVS是两个不同的分区各管各的。otadata管启动选择NVS管应用数据。有些开发者想用NVS里的标志位来控制回滚这是行不通的——Bootloader只认otadata。所以不要试图在NVS里写“我要回滚”然后指望系统照做正确做法是调用官方的OTA状态接口。6.3 固件加密与安全启动对回滚的影响如果启用了Flash加密或安全启动OTA流程会多一层签名校验。新固件必须用正确的密钥签名否则写入后校验不过直接判定为INVALID不会进入PENDING_VERIFY。这其实增强了安全性——攻击者无法通过OTA植入未签名固件。但要注意启用这些功能后分区表和安全相关的eFuse配置一旦烧录就不可逆测试阶段要谨慎。6.4 边缘AI场景下的分区规划建议热词里提到“esp32 边缘ai”这类应用固件通常很大包含模型权重分区规划要提前算好。假设模型占2MB代码占1MB那每个app分区至少要3MB双分区就是6MB加上文件系统和其他分区Flash至少得8MB起步推荐16MB。这种情况下factory分区可能就放不下了可以只保留双OTA分区靠回滚机制兜底。7. 我个人的几条实战建议折腾ESP32的OTA和回滚机制这几年有几个体会是文档里不会写的。第一确认接口一定要放在最前面最好在setup()的第一行任何可能失败的操作之前。我吃过亏把确认放在传感器初始化之后结果某个批次的传感器I2C地址不对初始化卡死固件永远不确认升级一次回滚一次。第二测试回滚机制要主动制造失败比如故意在新固件里加一个abort()看设备能不能正确回到旧版。不测的话真出事的时候你都不知道机制有没有生效。第三串口日志是排查OTA问题的命根子Bootloader阶段的日志会明确打印“回滚”“启动分区选择”等信息遇到问题先看日志比瞎猜快得多。这套双分区加自动回滚的机制本质上是用一点Flash空间换来了升级过程的安全感。对于联网设备来说这笔买卖太划算了。你把机制配好、确认逻辑写对剩下的就是放心大胆地推OTA哪怕推错了版本设备自己也能爬回来。

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

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

免费获取报价 →
↑