1. 从一次OTA翻车说起ESP32到底会不会变砖很多人第一次给ESP32做OTA升级的时候心里都会有一个挥之不去的疑问万一固件刷坏了板子是不是就废了我当初也是这么想的甚至第一次做远程升级的时候特意开车到现场守着生怕刷完设备就再也连不上得拆机接串口重新烧录。先说结论ESP32在合理的分区设计和引导配置下几乎不会真正变砖。注意这里的限定条件——“合理的分区设计和引导配置”。如果你用的是默认的单应用分区OTA写坏了就是写坏了设备启动时找不到有效固件就会一直重启或者卡在bootloader这时候确实只能靠串口救回来。但如果用上双分区dual OTA partition加自动回滚rollback情况就完全不一样了新固件启动失败芯片会自动切回上一个能正常运行的版本设备照样在线你还有机会远程重新推一次。这套机制的核心依赖三个东西ESP-IDF的分区表设计、OTA数据分区里记录的状态标志位、以及bootloader在启动时的校验逻辑。三者配合起来才构成了“刷不死”的底气。很多人只知道“ESP32支持OTA”但不知道OTA背后还有一套状态机在管着启动决策所以一旦出问题就手足无措。这篇文章我会从分区表怎么划、回滚标志位怎么工作、代码里怎么配置、实测中会遇到哪些坑这几个角度把双分区加自动回滚这套方案讲透。适合已经会用Arduino或ESP-IDF烧录、准备做远程升级、或者已经被OTA坑过一次的开发者。看完你至少能做到知道自己的分区表够不够安全、知道怎么开启回滚、知道回滚没生效时该往哪里查。2. 分区表不是随便划的双分区方案的底层逻辑2.1 为什么单分区OTA本质上是在赌命先看默认情况。Arduino IDE给ESP32的默认分区方案通常是“Default 4MB with spiffs”里面只有一个app分区大小大概1.2MB到1.5MB。这种配置下做OTA流程是这样的新固件下载到一块临时区域然后擦除原来的app分区把新固件写进去。问题就出在“擦除再写入”这个动作上——如果写入过程中断电、网络中断导致数据不完整、或者新固件本身有bug起不来原来的固件已经被擦掉了设备就失去了可运行的代码。这就像你只有一把钥匙换锁的时候把旧锁拆了新锁还没装好门就打不开了。单分区OTA的本质是“覆盖式更新”没有退路。2.2 双分区OTA的分区表长什么样双分区方案的核心思路是永远保留至少一个已知可用的app分区。典型的分区表大概是这样# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, phy_init, data, phy, 0x10000, 0x1000, factory, app, factory, 0x11000, 0x140000, ota_0, app, ota_0, 0x151000, 0x140000, ota_1, app, ota_1, 0x291000, 0x140000,这里有几个关键点。factory分区是出厂固件ota_0和ota_1是两个OTA槽位。设备正常运行时可能跑在factory也可能跑在ota_0或ota_1。每次OTA升级新固件会被写入“当前没在运行的那个OTA分区”。比如现在跑在ota_0新固件就写到ota_1写完之后更新otadata告诉bootloader下次从ota_1启动。如果ota_1启动失败bootloader根据otadata里的状态判断自动切回ota_0。otadata分区只有0x2000大小但它极其关键。它里面存的是两个ota槽位的状态记录每个记录包含序列号、状态标志新固件、待验证、有效、无效等。bootloader启动时会读这个分区决定加载哪个app。2.3 分区大小怎么算才够用上面例子给每个app分区0x140000也就是1.25MB。这个大小够不够取决于你的固件编译出来多大。我一般建议app分区大小至少是实际固件体积的1.5倍留出未来功能增长的空间。如果你用ESP-IDF编译编译结束会打印固件大小比如“Project build complete. Binary size: 0xE5A30”换算下来大概918KB那1.25MB的分区是够的但余量不算特别充裕。如果固件超过分区大小编译阶段就会报错不会等到OTA才发现。但有一种情况容易忽略Arduino IDE里选了分区方案但代码里用了大量库编译出来的bin比预期大很多这时候要么换更大的分区方案要么精简代码。我见过有人用4MB flash划了两个1.25MB的app分区结果固件编译出来1.3MB直接卡在编译阶段。另外4MB flash做双分区其实是比较紧张的。factory ota_0 ota_1三个app分区加起来就3.75MB了加上nvs、otadata、phy_init基本把4MB吃满。如果你不需要factory分区可以去掉它只保留ota_0和ota_1这样每个分区能分到接近1.9MB宽裕很多。去掉factory的代价是首次烧录必须通过串口烧到ota_0不能靠factory兜底。但对于量产设备来说产线本来就要串口烧录所以问题不大。3. 自动回滚是怎么判断“这次启动失败了”的3.1 回滚不是魔法靠的是状态标志位很多人以为自动回滚是bootloader自己“感觉”固件有问题就切回去其实不是。回滚的触发条件非常明确新固件启动后必须在规定时间内主动调用esp_ota_mark_app_valid_cancel_rollback()否则bootloader就认为它有问题下次重启时回滚。这个设计很巧妙。新固件启动后你可以在初始化WiFi、连接服务器、自检外设都成功之后再调用这个函数“确认自己没问题”。如果固件有严重bug比如一启动就崩溃、卡在某个循环里出不来、或者WiFi连不上导致无法远程管理那就没机会调用确认函数bootloader在下次启动时就会回滚到旧版本。3.2 回滚的完整状态流转整个状态流转大概分几步OTA写入新固件到ota_1otadata里ota_1的状态标记为ESP_OTA_IMG_NEW。设备重启bootloader看到ota_1是NEW状态把它改成ESP_OTA_IMG_PENDING_VERIFY然后启动ota_1。新固件运行如果一切正常调用esp_ota_mark_app_valid_cancel_rollback()状态变成ESP_OTA_IMG_VALID回滚取消。如果新固件没调用确认函数就重启了比如崩溃重启、看门狗复位bootloader再次启动时看到ota_1还是PENDING_VERIFY状态就判定启动失败把ota_1标记为ESP_OTA_IMG_INVALID或ESP_OTA_IMG_ABORTED然后切回ota_0。这里有个细节PENDING_VERIFY状态下的重启不一定是崩溃。如果你在新固件里调用了esp_restart()但没先调用确认函数也会触发回滚。所以确认函数的调用时机很重要一般放在所有关键初始化完成之后、进入主循环之前。3.3 确认函数该放在哪里我一般的做法是在app_main()里WiFi连接成功、MQTT连上、传感器自检通过之后再调用确认。这样能最大程度保证“只有真正能用的固件才会被确认”。但也不能太晚如果确认之前设备因为其他原因重启了就会误回滚。有一种情况需要特别注意如果新固件的功能就是“修复WiFi连接问题”而旧固件的WiFi是坏的那新固件启动后可能连不上WiFi也就没法远程确认。这时候如果没调用确认函数设备会回滚到WiFi坏的旧固件陷入死循环。解决办法是在这种场景下把确认条件放宽比如只要系统启动没崩溃就确认WiFi问题通过其他方式排查。4. 在ESP-IDF里把双分区和回滚跑通4.1 分区表配置和menuconfig关键项在ESP-IDF项目里分区表放在partitions.csv然后在menuconfig里指定自定义分区表Partition Table --- Partition Table (Custom partition table CSV) --- (partitions.csv) Custom partition CSV file同时要确认这几个配置Component config --- ESP System Settings --- [*] Enable app rollback support [*] Enable app anti-rollback support (可选用于防止降级到旧版本)Enable app rollback support必须打开否则bootloader不会处理回滚逻辑。这个选项在ESP-IDF v4.x之后默认可能是开的但最好确认一下。4.2 OTA写入的核心代码下面是一段典型的OTA写入逻辑基于ESP-IDF的esp_ota_ops组件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 (data_remaining) { esp_ota_write(ota_handle, data_chunk, chunk_len); } esp_ota_end(ota_handle); esp_ota_set_boot_partition(update_partition); esp_restart();esp_ota_get_next_update_partition(NULL)会自动返回“当前没在运行的那个OTA分区”不需要你手动判断。esp_ota_set_boot_partition()会更新otadata告诉bootloader下次从新分区启动。写入过程中如果出错比如esp_ota_write返回非ESP_OK要调用esp_ota_abort(ota_handle)清理否则otadata可能处于不一致状态。4.3 确认函数的调用和超时处理新固件启动后在合适的位置调用esp_err_t err esp_ota_mark_app_valid_cancel_rollback(); if (err ! ESP_OK) { ESP_LOGE(TAG, Failed to mark app valid: %s, esp_err_to_name(err)); }如果这个函数返回错误说明当前运行的分区不是PENDING_VERIFY状态可能是已经确认过的也可能是状态异常。一般不影响运行但值得打日志观察。另外ESP-IDF还提供了esp_ota_get_state_partition()可以查询分区状态调试阶段可以打印出来看esp_ota_img_states_t state; esp_ota_get_state_partition(esp_ota_get_running_partition(), state); ESP_LOGI(TAG, Current partition state: %d, state);5. 实测中回滚没生效这几个坑我替你踩过了5.1 回滚没触发设备一直重启这是最常见的问题。现象是新固件启动后崩溃设备反复重启但就是不回滚。排查下来通常是两个原因。第一个原因是rollback support没开。在menuconfig里确认Enable app rollback support是勾选的。如果没勾bootloader根本不看otadata里的PENDING_VERIFY状态只会一直尝试启动新分区。第二个原因是新固件崩溃得太早bootloader还没来得及记录状态。bootloader在启动新固件之前会把状态从NEW改成PENDING_VERIFY但如果崩溃发生在bootloader完成状态更新之前比如flash读写异常状态可能还是NEW。这种情况下下次启动bootloader会再次尝试启动新分区形成循环。解决办法是确保flash操作正常或者在新固件里尽早调用确认函数。5.2 确认函数调用了但还是回滚了这种情况一般是确认函数调用之后、设备又重启了而且重启时otadata的写入还没完成。esp_ota_mark_app_valid_cancel_rollback()内部会写flash如果写完立刻断电可能写入不完整。建议调用确认函数之后至少等几百毫秒再执行其他可能触发重启的操作。还有一种可能是调用了确认函数但当前运行的分区不是OTA分区比如跑在factory分区这时候函数会返回错误状态不会改变。如果你从factory启动然后OTA到ota_0确认逻辑要针对ota_0来写。5.3 OTA写入过程中断电设备起不来双分区方案下OTA写入的是“非当前运行分区”所以写入过程中断电当前运行分区不受影响设备重启后还是跑旧固件。但otadata可能处于中间状态比如新分区被标记为NEW但没写完。bootloader看到NEW状态会尝试启动但固件不完整启动失败然后回滚。所以理论上不会变砖。但有一种极端情况如果otadata本身在写入时损坏bootloader读不到有效的启动分区信息可能会卡住。otadata分区有冗余设计两个记录互为备份一般不会同时损坏。如果真遇到只能串口擦除otadata重新烧录。5.4 Arduino IDE下的回滚支持Arduino IDE的ESP32核心从2.0版本开始支持OTA回滚但配置方式和ESP-IDF不同。在Arduino里你需要选择带OTA的分区方案比如“Minimal SPIFFS (1.9MB APP with OTA)”。在代码里调用Update.begin()、Update.write()、Update.end()完成OTA。新固件启动后调用esp_ota_mark_app_valid_cancel_rollback()确认。Arduino环境下这个函数可以直接调用因为底层还是ESP-IDF。但要注意Arduino的Update库在写入失败时的错误处理不如ESP-IDF细致建议在Update.end()之后检查返回值失败时不要调用esp_restart()。6. 让回滚更可靠几个进阶配置和习惯6.1 看门狗和回滚的配合ESP32有任务看门狗TWDT和中断看门狗IWDT。如果新固件卡在某个循环里看门狗会触发重启。重启后bootloader看到PENDING_VERIFY状态就会回滚。所以看门狗其实是回滚机制的好搭档——它能把“卡死”这种软件问题转化成“重启”进而触发回滚。但要注意如果新固件在启动早期就喂狗失败可能还没走到确认函数就重启了这正好是回滚想要的效果。所以不要为了避免重启而盲目关闭看门狗尤其是在OTA后的首次启动阶段。6.2 版本号和防降级Enable app anti-rollback support可以防止设备降级到旧版本固件。它通过在固件里嵌入版本号bootloader比较版本号来决定是否允许启动。这个功能在安全要求高的场景有用但调试阶段建议关掉否则回滚到旧版本会被阻止反而麻烦。版本号通过CONFIG_APP_PROJECT_VER或esp_app_desc_t里的版本字段设置。如果开了防降级每次OTA的版本号必须递增否则新固件会被拒绝启动。6.3 串口救砖的兜底方案即使有双分区和回滚我还是建议保留串口烧录的兜底能力。具体做法是在PCB上留出UART0的TX、RX、GND和EN、BOOT按键或者至少留出测试点。一旦OTA和回滚都失效比如otadata损坏、分区表被误擦可以通过串口重新烧录。ESP32的串口烧录需要拉低BOOT引脚再复位进入下载模式。如果设备已经装在外壳里操作起来很麻烦所以量产设备最好把BOOT和EN引到外部可操作的位置或者用自动下载电路。6.4 我的个人习惯OTA前先备份每次OTA之前我会先把当前固件的bin文件备份到服务器同时记录当前运行的OTA分区和otadata状态。这样即使回滚失败也能通过串口烧回备份固件。备份命令很简单esptool.py --port /dev/ttyUSB0 read_flash 0x10000 0x140000 backup.bin0x10000是app分区的起始地址0x140000是大小根据你的分区表调整。读出来的bin可以直接用esptool.py write_flash烧回去。这套双分区加自动回滚的方案我在好几个现场设备上跑了一年多经历过网络中断、固件bug、意外断电设备都自己恢复了。唯一一次需要现场救砖是因为分区表改错了把otadata的offset写重叠了那是人为失误不是机制问题。所以只要你分区表划对、rollback support打开、确认函数放对位置ESP32真的刷不死。
阅读完成 · 觉得有帮助?