☰
ESP32双分区与自动回滚机制:OTA升级防变砖实战指南
2026/10/28 1:06:53 网站建设 项目流程

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偏移,约28KB
partition-tabledata分区表本身0x8000偏移后,约3KB
nvsdata非易失存储,存WiFi配置、回滚标志等0x9000偏移,24KB
otadatadataOTA状态数据,记录当前启动哪个分区0x2000,8KB
app0app应用程序分区01MB~2MB
app1app应用程序分区11MB~2MB
spiffs/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分区必须存在,大小固定0x2000(8KB),它存的是两个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升级,可以拆成五个阶段:

  1. 下载新固件:设备通过WiFi从服务器下载固件二进制文件,通常分块写入,边下边写。
  2. 写入备用分区:把下载到的数据写入当前未运行的那个OTA分区(比如当前跑ota_0,就写到ota_1)。
  3. 校验固件:写入完成后,校验固件的完整性,包括长度、校验和、以及ESP32特有的镜像头信息。
  4. 更新otadata:把otadata里的启动目标切换到新分区,并标记这个分区为“待验证(pending verify)”状态。
  5. 重启并验证:设备重启,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_ota=My OTA Layout esp32.menu.PartitionScheme.my_ota.build.partitions=my_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 固件升级后设备反复重启并回到旧版本

这是最典型的回滚现象。排查思路按顺序来:

  1. 检查新固件是否调用了确认接口。用串口日志看有没有打印“固件已确认有效”。如果没有,说明确认逻辑没执行到。
  2. 检查确认接口的调用时机。如果放在WiFi连接之后,而设备在无网环境启动,就会一直不确认。
  3. 检查新固件是否在启动早期就崩溃。如果新固件有bug导致panic,根本走不到确认那一步,自然被回滚。
  4. 查看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,哪怕推错了版本,设备自己也能爬回来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询