ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ESP32固件刷坏防变砖:双分区自动回滚原理与实战

ESP32固件刷坏防变砖:双分区自动回滚原理与实战 我一直觉得“ESP32 固件刷坏了会变砖吗”这种问题答案得分场合说。前两天群里有人把新固件烧进去板子串口一只循环打印乱码他急得喊“砖了砖了”。其实那只是应用分区里的程序起不来bootloader 还活着用双分区和自动回滚就能把这种“假砖”挡住。我的做法很朴素旧固件永远留在另一个分区新固件上线后先跑一个健康检查检查不过就自己把自己标记成无效重启后自动退回旧版。整个过程不需要拆壳、不需要短接远程设备也能自救。这篇文章就聊聊这套方案的原理、配置和我在实践里踩过的坑适合正在做远程 OTA 升级、又怕把设备刷成砖的朋友。1. 先给结论ESP32 的“砖”其实分四种双分区能救的是其中一大半先说最直接的结论普通应用固件刷坏了绝大多数情况下 ESP32 不会变成真正的砖。真正的砖是那种 USB 识别不到芯片、电脑上没有任何串口设备、只能换 Flash 芯片或者扔垃圾桶的物理级损坏。日常大家嘴上说的“刷砖了”九成是“系统起不来”的软错误。我把常见的“砖”按危害程度排了一下砖的类型具体现象损坏位置双分区是否救得了应用固件崩溃开机黑屏、循环重启、串口打印异常APP 分区能自动回滚就能处理应用固件被擦空升级中断分区里全是 0xFFAPP 分区部分能取决于 bootloader 回滚策略分区表损坏芯片反复引导失败无法进入正常逻辑0x8000 地址附近救不了需要重新烧分区表bootloader 被写坏完全无法启动只能进下载模式重写0x1000 地址附近救不了但可以手动重烧Flash 芯片物理损坏擦除失败、写入校验失败、寿命耗尽Flash 芯片本身救不了只能换芯片做远程 OTA 最怕的就是第一种和第二种。单分区方案里新固件覆盖老固件一旦新固件起不来设备就真的暂时“砖”了必须有人到现场用串口烧。双分区方案相当于给系统留了一条退路旧固件住在一个固定房间新固件住进隔壁房间启动时先试新的试不动就切回旧的。所以标题里的问题我的答案是只要你有两个可启动的分区并且正确配置了自动回滚固件刷坏的后果会从“变砖”降级成“重启失败”而且这个失败还能自己修复。这也是现在很多商业 IoT 设备普遍采用的 A/B 升级思路ESP32 的分区表天然支持这种玩法只是很多人不知道或者没用起来。1.1 官方术语里没有“双分区”这个说法它对应的是 A/B OTA在 ESP-IDF 的文档里官方不会说“双分区”而是说 Application OTA 升级时可以有factory、ota_0、ota_1这样多个app类型分区。简单理解factory是出厂固件ota_0和ota_1是后续 OTA 将要写入的槽位。所谓双分区就是你至少保留了两个可以引导的应用分区一个跑新固件一个跑旧固件。我习惯把这种设计叫“双保险启动”。一颗 4MB Flash 的普通 ESP32 开发板完全可以划出两个 1.8MB 左右的应用分区。现在的 ESP32 固件一般控制在 1MB 到 1.5MB 以内所以这种划分在绝大多数项目里都够用。如果你用的是 8MB、16MB Flash 的模组那就更宽裕了甚至可以保留 factory 加两个 OTA 槽做滚动升级。1.2 为什么“变砖”这个词被滥用得太厉害了很多人第一次接触 ESP32 刷机用的是 ArduinIDE 一键烧录烧录失败或者固件本身有 bug板子不干活了就认为自己把板子“烧废了”。实际上只要下载模式还在按住 BOOT 键能识别出一个COM口那就完全能救。双分区存在的意义就是让这种软故障连人工干预都不需要设备自己就能决定“刚才那个新版本不行我用旧版本继续干活”。2. 双分区能不能自动切换关键看分区表、otadata 和引导选择机制要理解双分区得先知道 ESP32 加电之后到底是怎么找到固件的。很多人只会在 IDE 里点上传很少看自己烧进去的东西包含几块。其实 ESP32 的 Flash 里不是一个完整的大固件而是好几个不同功能的区域。2.1 分区表是 Flash 的地基ESP32 至少要有一个分区表它记录了 Flash 上每个区域的名称、类型、子类型、偏移量和大小。这个表本身存在 Flash 的0x8000地址。你在 Arduino 或者 PlatformIO 的编译输出里看到partitions.bin就是这个地基。一个典型的分区表长这样# 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, 0x1F0000, ota_0, app, ota_0, 0x210000, 0x1E0000,这里面的关键点有三个nvs分区用来存各种键值对比如 WiFi 配置、设备参数。两个固件可以共享这个分区但共享也会带来兼容性问题后面第五部分再细说。otadata分区专门记录“当前该从哪个 app 分区启动”以及这个分区的回滚状态。没有它双分区根本不知道自己该引导谁。factory和ota_0是两个独立的应用槽。一个放稳定版一个放测试版。对于 4MB Flash 的板子两个 app 分区各给 1.8MB 左右已经比较均衡。如果你用 PlatformIO直接在platformio.ini里指定这个 CSV 文件路径就行[env:esp32dev] platform espressif32 board esp32dev framework arduino board_build.partitions partitions/ota.csv2.2 bootloader 是如何决定跑哪个固件的ESP32 上电后ROM 引导程序先运行它负责加载二级 bootloader。二级 bootloader 读取otadata分区看当前推荐的引导目标是谁然后跳转到对应的 app 分区执行代码。这套流程写死了你不需要自己改 bootloader只需要维护好otadata里的状态。这个流程跟电脑开机有点像。ROM 是主板 BIOS二级 bootloader 是引导程序otadata是启动项配置factory和ota_0就相当于 C 盘和 D 盘各装了一个操作系统。你觉得新版系统不好用可以靠引导配置切回 D 盘的老系统。ESP32 的双分区自动回滚本质上就是在开机时自动完成这个“切回 D 盘”的动作。2.3 手动烧录时的注意事项如果你第一次往一块板子上烧双分区方案不能只烧 app 固件。也要确保分区表、bootloader 和 otadata 都被正确写入。PlatformIO 的pio run -t upload默认会处理这些但如果你像我喜欢用 esptool 直接照着地址写至少要注意bootloader 写到0x1000分区表写到0x8000otadata 分区里面是空白数据也没关系bootloader 第一次会默认找factory应用固件写到对应的 app 偏移量如果手滑把空白数据写到了0x8000分区表就没了此时虽然能通过下载模式重刷但不会自动回滚。3. 自动回滚的底层原理不是检测到故障才回滚而是没收到“确认信号”就回滚我看到很多人在讨论自动回滚时第一反应是问“bootloader 怎么判断新固件坏了”。其实 bootloader 本身不跑你的业务代码它没法判断新固件逻辑上有没有问题。它的工作方式更像一个倒计时机制新固件启动后必须在规定时间内给系统发一个“我已经确认没问题”的信号否则就认为这版固件不可靠重新引导旧版本。这个设计非常关键。理解它之后很多奇怪的现象就解释得通了为什么某次新固件已经能正常跑业务了设备一重启又回到旧版多半就是新固件里没有调用确认函数导致倒计时一直没被取消。3.1 otadata 里的状态机ESP-IDF 把 app 分区的状态记录在otadata里每个可引导分区都有对应的标记位。大体可以理解成三种状态UNDEFINED刚烧录进去还没确定好坏VALID应用主动确认过表示“这版固件可靠继续用”INVALID应用主动标记或者倒计时到了表示“这版固件不行回滚”当你在一个空分区里写完新固件bootloader 引导它之后它并不立即是VALID。此时你的应用代码就该承担起“自我检查”和“自我确认”的责任。如果你在新固件里什么都不做系统会在多次重启后认为它始终未确认最终把它标记成INVALID并引导回旧分区。在 ESP-IDF 里开启这个功能需要配置CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLEy在menuconfig里可以勾选。我自己的经验是做远程 OTA 项目时千万别把这个开关关掉否则双分区就变成了手动切换分区失去了“自动”的意义。3.2 健康检查窗口确认动作不该放在 setup 第一行很多初学者写的自动回滚代码长这样启动后立刻调用esp_ota_mark_app_valid_cancel_rollback()把新固件标记成有效。这等于考试还没开始就先交卷任何隐藏问题都没有经过验证。正确做法是给新固件一个健康检查窗口在这段时间里检查设备的关键能力。比如你的设备是一块采集环境温湿度的传感器那健康检查就不能只看 MCU 能不能运行setup()而要确认真实业务链路是通的传感器 I2C 能不能正常读回数据读回的数据是否在合理范围内WiFi 能不能连接上能不能与服务器建立会话这些检查全部通过才执行确认动作。如果任何一个环节失败就主动把当前固件标记为无效并重启让系统回滚到上一版。整个过程可以用一句话总结先上岗再验收验收不过就换人。3.3 ESP-IDF 和 Arduino 的 API 对应关系Arduino 框架和 ESP-IDF 底层共用同一个 ESP-IDF 系统所以相关函数在 Arduino 里也能直接用。只需要引入头文件#include esp_ota_ops.h三个核心操作操作函数作用确认当前固件有效esp_ota_mark_app_valid_cancel_rollback()取消回滚倒计时固定使用当前分区标记当前固件无效并重启esp_ota_mark_app_invalid_rollback_and_reboot()立即回退到上一有效固件获取当前运行分区esp_ota_get_running_partition()判断当前是从 factory 还是 ota_0 启动在 PlatformIO 的 Arduino 框架工程里直接写这段代码就能调用。这些函数就在 ESP-IDF 组件里Arduino core 编译时会一起链接进去。4. 完整示例一个自带健康检查的自动回滚工程上面讲的都是原理这一节给出一套可以直接抄作业的工程结构。我以 PlatformIO 加 Arduino 框架为例因为这是目前最主流的开发组合。4.1 工程目录和分区文件工程结构很简单my_project/ ├── platformio.ini ├── partitions/ │ └── ota.csv └── src/ └── main.cppota.csv的内容用我前面给的那份分区表。platformio.ini里除了指定分区文件还可以把串口监视波特率固定下来[env:esp32dev] platform espressif32 board esp32dev framework arduino board_build.partitions partitions/ota.csv monitor_speed 1152004.2 健康检查与回滚代码main.cpp里我写了一个非常典型的健康检查流程。为了让演示更直观我故意把“健康检查通过条件”做成一个变量正常情况下你把它替换成真实的传感器读取、服务器连接检测就行。#include Arduino.h #include esp_ota_ops.h #define HEALTH_TIMEOUT_MS 30000UL // 模拟一项需要验证的外设检查 bool checkSensor() { // 实际项目里这里会读 I2C 或者 ADC并判断数值是否合理 // 这里直接模拟成功 return true; } bool checkNetwork() { // 实际项目里这里会检测 WiFi 是否连上、能否 ping 通服务器 // 这里直接模拟成功 return true; } bool healthCheckPassed() { if (!checkSensor()) return false; if (!checkNetwork()) return false; // 还可以加更多业务自检 return true; } void setup() { Serial.begin(115200); delay(300); Serial.printf(Booting from: %s\r\n, esp_ota_get_running_partition()-label); unsigned long start millis(); bool ok false; while (millis() - start HEALTH_TIMEOUT_MS) { if (healthCheckPassed()) { ok true; break; } delay(200); } if (ok) { esp_ota_mark_app_valid_cancel_rollback(); Serial.println(Health check passed, firmware confirmed.); } else { Serial.println(Health check failed, rolling back...); delay(100); esp_ota_mark_app_invalid_rollback_and_reboot(); } } void loop() { // 正常业务逻辑 }这段代码的精髓在while循环。它既给了新固件 30 秒的验证时间又不会让设备在故障状态下无限等下去。条件不通过就调用esp_ota_mark_app_invalid_rollback_and_reboot()系统会立刻重启并引导到上一个有效分区。顺带提醒一句正式项目里healthCheckPassed()里的检查一定要有真实业务意义不要只是return true。我见过有人把所有检查函数都写成空返回最后回滚机制一次都没触发过新固件把传感器接反的硬件问题带到了所有设备上。4.3 故意刷一个“坏固件”来验证自动回滚写完代码之后我强烈建议你先在本地做一次“自杀式测试”。这比在生产设备上直接踩雷舒服多了。做法分三步第一步把当前代码烧进factory分区让它作为稳定版本。第二步修改healthCheckPassed()让网络检查永远失败然后编译固件通过 OTA 方式烧到ota_0分区。第三步重启设备观察串口日志。正常情况下你会看到类似这样的过程新固件启动健康检查失败串口打印回滚提示系统重启随后 bootloader 引导回factory分区的旧固件。整个过程设备不需要连接电脑完全靠自己的状态判断完成了版本切换。如果你的板子没有预先烧录好factory固件只是通过 USB 线直接上传新固件这期间的逻辑也成立但建议把第一版确认过的固件先放进factory。这样即使后续 OTA 的ota_0分区被反复写废只要factory没动设备永远有一条保底路线。5. 回滚方案里最容易踩的坑空间、otadata 和 NVS 兼容性理论说再多不如实际摔两跤。这套方案我跑了几个月有些坑属于文档里不会写、但一踩一个准的类型。5.1 分区大小和编译产物不匹配我最早用 PlatformIO 默认的 1.2MB app 分区跑一个小型固件绰绰有余。后来固件引入了蓝牙、网页前端资源体积瞬间膨胀到 2MB 以上。编译是过了但烧写完启动就失败因为分区根本装不下完整的镜像。检查方式是编译完成后看firmware.bin的大小再用它和分区表 CSV 里的分区大小对比。别只看 CSV 的总空间更要看你是从什么地址开始写的。5.2 otadata 被擦掉或者写坏会导致启动选择混乱有一种情况非常隐蔽你在测试回滚时习惯性用esptool.py erase_flash把整个 Flash 清空然后只烧bootloader、partitions和app。此时otadata分区是空的bootloader 还能按默认逻辑去找factory但这不代表自动回滚机制还能正常工作。真正干净的首次烧录应该让 IDE 或 esptool 按照完整流程把分区表、bootloader、otadata 和 app 都写入而不是只烧一个 app。5.3 NVS 分区被新旧固件共用带来的脏数据这是我觉得最坑的一点。分段回滚保证了固件版本能退回但 NVS 里的数据不会跟着回滚。假设新固件在运行过程中写入了新版的数据结构把某个键值的格式改掉了然后它因为健康检查失败回滚到旧固件旧固件读取到新格式的数据可能会直接崩掉。这就出现了一个尴尬现象固件版本已经回滚了但设备依然处于不可用状态。我的解决思路是给 NVS 键值加版本前缀比如v2_calib_value和v1_calib_value。旧固件只读自己认识的键新固件写自己的键两边互不干扰。等新版本在线上稳定运行一段时间后再通过一次干净的升级把废弃键清掉。5.4 确认时机太早或太晚确认太早健康检查形同虚设确认太晚设备每次重启都要等完整个健康检查窗口生产环境会显得很迟钝。更好的做法是把确认时机放在业务真正就绪之后。比如你的设备每次启动后需要先联网、再同步一次服务器时间、最后打开某个执行器那么确认动作就应该放在“执行器打开”之后而不是setup()开头。5.5 注意死循环不一定会触发自动回滚很多人以为新固件一旦 “hang” 住bootloader 就会主动回滚。严谨地说这取决于你有没有开启看门狗以及系统的复位路径。如果新固件陷入一个死循环但一直没有喂狗也没有任何中断触发复位bootloader 的倒计时可能根本不会推进。所以你的健康检查里至少要有一个 loop 周期计时或看门狗刷新逻辑确保异常时能产生复位事件让 bootloader 有机会介入。6. 双分区和自动回滚救不了的砖你得知道怎么手动自救双分区不是万能药它保护的是应用分区这一层。下面这几种情况双分区插件也拦不住但通过合理的手动操作完全可以救回来。6.1 bootloader 和分区表被写坏怎么进下载模式不管你的应用分区里放着什么只要 bootloader 或分区表被写坏芯片都无法正常进入业务逻辑。好在 ESP32 的 ROM 引导程序是最底层的它不受分区表影响只要你把 IO0 拉低在供电状态下按住 BOOT 再短按 EN芯片就会进入串口下载模式。这个时候 Windows 或 Linux 下会识别出一个新的串口设备esptool 可以和它通信。重新从头烧录的命令大致是这样的esptool.py --port COMx erase_flash esptool.py --port COMx write_flash 0x1000 bootloader.bin 0x8000 partitions.bin 0xe000 boot_app0.bin 0x10000 firmware.bin不同工具链生成的文件名略有差异但思路一致。关键是不管当前 Flash 里有多乱下载模式永远给你留了最后一扇门。6.2 养成备份原始 bootloader 和分区表的习惯很多厂家的 ESP32 模组出厂时可能预置了特殊固件bootloader 的配置也可能和官方默认不同。玩刷机之前我建议先把整颗 Flash 的内容备份一份。最简单的备份命令是esptool.py --port COMx read_flash 0x00000 0x400000 flash_backup.bin这样即使后面把 bootloader 擦得干干净净也能从备份里把这些部分捞回来而不是满网找别人分享的镜像。6.3 回滚确认后的进阶思路自动回滚能解决“新固件起不来”的问题但它不能解决“新固件起来了但是业务逻辑有缺陷”的问题因为这种缺陷不稳定有可能健康检查通过了运行几个小时后才出错。对这种故障比较务实的做法是继续叠加远程控制命令设备每隔一段时间上报版本号服务器如果发现设备在重复上报同一个高版本号可以下发“强制切回旧分区”的指令。这一层属于运维策略不依赖 bootloader但对远程设备管理很有价值。归根结底我自己的体会是ESP32 本身就是一颗做了不少容错设计的芯片给它配上双分区和自动回滚相当于是给系统上了一道非常便宜但又非常牢靠的保险。设备的侥幸心理不能靠烧录前反复检查来弥补因为散落在真实环境里的设备不可能每次都亲自上手。把“最坏情况”写进系统设计里让设备自己具备恢复能力才是做 IoT 升级该有的觉悟。
返回列表