资讯详情

ESP32固件刷坏会变砖吗?双分区与自动回滚实战指南

📅 2026/10/30 1:09:33 | 华诺云谱 👁 阅读
ESP32固件刷坏会变砖吗?双分区与自动回滚实战指南
1. 从一次“刷废了”的深夜抢救说起凌晨两点我盯着串口助手里那一行不断重复的invalid header: 0xffffffff心里凉了半截。手里这块 ESP32 开发板刚被我刷进一个自己改过的固件结果启动直接卡在 bootloader连串口打印都只有乱码。那一刻我脑子里冒出的第一个念头就是这玩意儿是不是彻底变砖了如果你也玩 ESP32大概率听过“刷坏固件会变砖”这种说法。尤其是新手阶段每次点下 Arduino IDE 或者 esptool 的烧录按钮手都会抖一下生怕哪一步搞错板子就成了一块只能当镇纸的小电路板。但实际情况是ESP32 这颗芯片在设计之初就考虑到了固件损坏的场景它内置的 bootloader 和分区机制让“变砖”这件事的门槛比很多人想象的高得多。真正会让人束手无策的往往是那些把 bootloader 本身或者 eFuse 加密位搞坏的操作而不是普通应用固件刷错。这篇文章我想聊的就是标题里那个问题ESP32 固件刷坏了到底会不会变砖以及我是怎么用双分区加自动回滚这套组合拳把“刷错固件”这件事从“灾难”降级成“重启一下就好”的。核心关键词会围绕 ESP32、固件、双分区、自动回滚、OTA 这几个点展开但不会只停留在概念层面我会把分区表怎么配、回滚逻辑怎么写、OTA 升级时怎么保证安全这些实操细节都拆开讲。不管你是刚入手 ESP32 的新手还是已经在做量产项目的开发者这套思路都能直接抄作业。先说结论免得你心里一直悬着只要 bootloader 没被破坏、eFuse 没被不可逆地写错ESP32 的应用固件刷坏了基本都能救回来。而双分区加自动回滚就是让这个“救回来”的过程变成自动化的关键。下面我从整体设计思路开始一步步把这件事讲透。2. 双分区与自动回滚的整体设计思路2.1 为什么单分区方案让人睡不踏实传统单片机开发里很多人的习惯是整块 Flash 只放一份固件从固定地址启动。这种单分区方案在 STM32 或者一些 8 位机上很常见简单直接烧录地址固定程序入口清晰。但放到 ESP32 上尤其是带 OTA 需求的场景单分区就有点不够看了。问题出在 OTA 升级的过程。假设你设备正在运行固件 A现在要升级到固件 B。如果是单分区你得先把 A 擦掉再把 B 写进去。这中间有个致命的空窗期万一写入过程中断电了、网络断了、或者新固件本身有 bug 启动不了那设备里既没有完整的 A也没有完整的 B直接卡死。这时候除非你手边有串口线能重新烧录否则设备就真的“失联”了。对于装在墙上、装在设备箱里、甚至发到客户手里的产品来说这种风险是不可接受的。我早期做过一个带 OTA 的温湿度采集节点用的就是单分区结果有一次升级到一半路由器重启了设备直接变砖最后只能拆下来接串口线救。那次之后我就下定决心必须上双分区。2.2 双分区到底解决了什么问题双分区的核心思路很朴素Flash 里留出两块同样大小的区域分别叫ota_0和ota_1再加上一个很小的otadata区域来记录当前该从哪块启动。设备正常运行时比如从ota_0启动要升级时把新固件写到ota_1写完之后在otadata里标记“下次从 ota_1 启动”然后重启。重启后 bootloader 读取otadata发现标记指向ota_1就从新固件启动。这个过程中旧的ota_0里的固件始终没有被破坏。如果新固件启动失败或者你主动触发回滚只要把otadata改回指向ota_0重启后就能回到旧固件。这就是双分区最大的价值任何时刻设备里至少有一份完整可用的固件。你可以把它理解成电脑装系统时的“双系统”思路或者手机 OTA 时的 A/B 分区。安卓手机这些年推的 A/B 无缝更新底层逻辑和这个几乎一模一样。ESP32 的 ESP-IDF 把这套机制做进了 bootloader我们只需要在分区表和代码里配合一下就行。2.3 自动回滚是怎么判断“新固件不行”的光有双分区还不够因为如果新固件能启动但功能有问题比如连不上 WiFi、跑飞了、看门狗不断复位设备自己是不知道要回滚的。这时候就需要“自动回滚”机制来兜底。ESP-IDF 提供了一套叫esp_ota_mark_app_valid_cancel_rollback的机制。原理是这样的新固件启动后先处于“待验证”状态。你的代码需要在确认自己运行正常之后主动调用一个函数告诉系统“我没问题别回滚了”。如果新固件启动后一直没调用这个函数或者在这之前就崩溃重启了bootloader 在下次启动时会发现“上次启动的固件没被标记为有效”于是自动切回上一个已知可用的分区。这个“确认正常”的时机很关键。我一般会放在设备成功连上 WiFi、并且完成一次心跳上报之后。因为连上网络说明基本的外设和协议栈都跑通了心跳上报成功说明业务逻辑也走通了。这时候再标记有效比较稳妥。如果放得太早比如 main 函数一进来就标记那新固件即使后面会崩溃系统也认为它是好的回滚就失效了。2.4 方案选型背后的取舍有人可能会问为什么不直接用 ESP-IDF 默认的factory加ota_0这种方案其实 factory 分区是给“出厂固件”用的它不能被 OTA 覆盖只能作为最后的保底。如果你的产品需要保留一个绝对不能被改动的出厂版本那 factory 加 ota_0 是合理的。但对于大多数需要频繁 OTA 的场景两个 ota 分区来回切换更灵活Flash 利用率也更高。另一个取舍是分区大小。双分区意味着每个分区只能占 Flash 的一半左右。比如一块 4MB 的 ESP32去掉 bootloader、分区表、nvs、otadata 这些两个 ota 分区各能分到大概 1.8MB 左右。如果你的固件本身就接近 1.5MB那剩余空间就很紧张了。这时候要么换 8MB 或 16MB 的模组要么就得精简固件。我一般建议在项目选型阶段就把 Flash 容量留够别等到固件膨胀了才发现分区不够。3. 分区表配置与核心参数实操3.1 分区表怎么写才不容易踩坑ESP32 的分区表是一个 CSV 文件通常叫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, 0x1C0000, ota_1, app, ota_1, 0x1E0000,0x1C0000,这里有几个点需要特别注意。otadata的大小是0x2000也就是 8KB这是 ESP-IDF 规定的不能随便改。它里面存了两份 ota 选择记录互为备份所以需要两个扇区。ota_0和ota_1的 Offset 必须对齐到 0x10000也就是 64KB 边界这是 Flash 擦除扇区的要求。Size 我这里是0x1C0000约 1.75MB两块加起来 3.5MB加上前面的区域刚好塞进 4MB Flash。如果你用的是 8MB 或 16MB 的模组可以把 ota 分区放大比如各给 2MB 甚至 4MB。但要注意ota_0和ota_1的大小必须完全一致否则 OTA 写入时会因为目标分区放不下而失败。提示分区表改完之后一定要执行一次idf.py erase-flash或者用 esptool 擦除整片 Flash再重新烧录。因为旧的 otadata 里可能还残留着指向旧分区的记录不擦干净会导致启动行为异常。3.2 otadata 里到底存了什么otadata这个区域很多人不太关注但它其实是双分区切换的“大脑”。它里面存的是两个esp_ota_select_entry_t结构体每个结构体包含一个ota_seq序号和一个crc校验值。bootloader 启动时会读取这两个 entry选序号更大、校验通过的那个然后根据序号奇偶决定从ota_0还是ota_1启动。具体来说序号是 0 或 1 时对应ota_02 或 3 时对应ota_1以此类推。每次 OTA 成功写入新固件后系统会把序号加一并写入另一个 entry。这样即使写入过程中断电至少还有一个旧的 entry 是完整的bootloader 能回退到旧记录。我遇到过一种情况设备反复重启串口打印ota data invalid。后来查出来是 otadata 区域被意外擦除了可能是分区表偏移写错导致 otadata 和 phy_init 重叠。所以分区表的 Offset 一定要仔细核对别让区域之间打架。3.3 编译配置里必须打开的选项光有分区表还不够ESP-IDF 的 menuconfig 里还有几个关键选项要打开。进入Component config-ESP32-specific找到Support for external, SPI-connected RAM不用管重点是下面这些Bootloader config-Bootloader log verbosity可以设成Info方便调试时看启动日志。Partition Table-Partition Table选择Custom partition table CSV并填上你的 CSV 文件名。Application Level Tracing不用开。更关键的是在Component config-ESP System Settings里确认Channel for console output是Default: UART0这样串口能看到启动信息。另外Bootloader config里的Enable app rollback support必须勾上这是自动回滚的总开关。如果这个没开后面代码里调用标记有效的函数也不会生效。还有一个容易忽略的点Compiler options里的Optimization Level建议用-Og或-Os别用-O0否则固件体积会大很多双分区可能放不下。3.4 烧录方式对分区的影响ESP32 的烧录方式主要有串口烧录和 OTA 烧录两种。串口烧录时esptool 会按照分区表把 bootloader、分区表、应用固件分别写到对应地址。如果你只烧了应用固件没烧分区表那设备启动时读到的还是旧分区表可能根本找不到 ota_0 和 ota_1。我一般用idf.py flash一键烧录它会自动处理 bootloader、分区表和应用。如果是用 Arduino IDE那就要注意了Arduino 默认的分区方案可能不带双 ota 分区。你需要在Tools-Partition Scheme里选Minimal SPIFFS (1.9MB APP with OTA)或者自定义分区表。选错分区方案OTA 功能直接不可用。注意每次修改分区表后必须重新烧录 bootloader 和分区表只烧应用固件是不够的。我见过有人改了 CSV 但只idf.py app-flash结果设备还是按旧分区启动白白折腾半天。4. 自动回滚的代码实现与验证4.1 新固件启动后的“自我确认”流程自动回滚的核心代码其实就几行但放的位置很讲究。下面是我常用的一个流程放在app_main里或者在一个独立的任务里执行#include esp_ota_ops.h #include esp_system.h void app_main(void) { // 先做基础初始化比如 nvs_flash_init、外设初始化 // ... // 连接 WiFi等待获取 IP // ... // 上报一次心跳确认业务逻辑正常 // ... // 以上都成功了标记当前固件为有效取消回滚 esp_err_t err esp_ota_mark_app_valid_cancel_rollback(); if (err ! ESP_OK) { ESP_LOGE(OTA, Failed to mark app valid: %s, esp_err_to_name(err)); } else { ESP_LOGI(OTA, App marked as valid, rollback cancelled); } }这段代码的关键在于它必须在新固件“证明自己能用”之后才执行。如果新固件连 WiFi 都连不上或者心跳上报失败那就不应该调用这个函数。这样下次重启时bootloader 会发现当前分区没有被标记为有效自动回滚到上一个分区。4.2 怎么判断当前是不是在“待验证”状态有时候我们需要在代码里知道当前固件是不是刚 OTA 上来、还没被确认的。可以用esp_ota_get_state_partition来查询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_LOGW(OTA, Running in pending verify state); // 这里可以做一些额外的自检比如检查关键外设 } }这个状态在调试时很有用。比如你发现设备每次 OTA 后都会回滚那就在 pending verify 状态里多打点日志看看是哪一步没走通导致没调用标记有效的函数。4.3 手动触发回滚的两种方式除了自动回滚有时候我们还需要手动回滚。比如新固件虽然能启动但发现某个功能有严重 bug想立刻切回旧版本。这时候有两种方式第一种是在代码里调用esp_ota_mark_app_invalid_rollback_and_reboot()它会直接把当前分区标记为无效并重启bootloader 会切到另一个分区。这个函数适合在检测到严重错误时调用。第二种是通过串口或者网络发送一个命令触发回滚。我一般会在设备上留一个调试接口收到特定指令后执行回滚。但要注意回滚后旧固件里的逻辑也要能正确处理这个状态别又自动标记有效然后切回来。4.4 实测故意刷一个会崩溃的固件为了验证自动回滚真的有效我做过一个实验写一个固件在app_main里延时 5 秒后主动触发abort()模拟崩溃。然后通过 OTA 把这个固件推上去。设备重启后新固件运行 5 秒崩溃重启bootloader 发现它没被标记有效自动回滚到旧固件。串口日志里能清楚看到Rollback to ota_0之类的信息。这个实验建议每个做 OTA 的开发者都做一遍亲自确认回滚链路是通的。别等到量产了才发现回滚没生效那时候代价就大了。提示测试自动回滚时确保旧固件本身是能正常启动并且已经标记有效的。如果旧固件也是 pending verify 状态回滚过去之后可能又触发一次回滚导致设备在两个分区之间反复横跳。5. OTA 升级流程中的安全细节5.1 固件校验别让半截固件害了你OTA 升级时固件是通过网络传输的中间可能因为信号不好、服务器异常等原因导致数据不完整。如果直接把不完整的固件写进分区设备重启后大概率启动失败。所以写入完成后必须做校验。ESP-IDF 的esp_ota_end函数会自动校验固件的 SHA256 和镜像格式。如果校验失败它会返回错误这时候就不要切换分区继续用旧固件。我一般会在 OTA 任务里这样写esp_ota_handle_t update_handle; const esp_partition_t *update_partition esp_ota_get_next_update_partition(NULL); esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, update_handle); while (还有数据) { esp_ota_write(update_handle, data, len); } esp_err_t err esp_ota_end(update_handle); if (err ! ESP_OK) { ESP_LOGE(OTA, OTA end failed: %s, esp_err_to_name(err)); // 不切换分区继续用旧固件 return; } esp_ota_set_boot_partition(update_partition); esp_restart();注意esp_ota_end之后才能调用esp_ota_set_boot_partition顺序不能反。如果先切换分区再校验校验失败时已经切过去了就麻烦了。5.2 断点续传与超时处理OTA 升级最怕的就是网络中断。如果每次中断都要从头开始传那大固件在弱网环境下几乎传不完。ESP-IDF 本身不提供断点续传但我们可以自己在应用层实现。思路是记录已经写入的偏移量下次连接时从该偏移继续写。不过esp_ota_write是顺序写入的不能跳着写所以断点续传需要服务器端配合把固件分片每片带偏移信息。更简单的做法是加超时和重试。比如设置一个 30 秒的接收超时超时后放弃本次 OTA保留旧固件等网络好了再试。我一般会在 OTA 任务里加一个看门狗如果 60 秒内没有收到任何数据就主动终止。5.3 固件加密与安全启动的取舍ESP32 支持 Flash 加密和安全启动这两个功能能防止固件被读取和篡改但也会带来一些限制。开启 Flash 加密后OTA 写入的固件必须是加密后的否则启动会失败。安全启动则要求固件有正确的签名否则 bootloader 拒绝启动。如果你的项目对安全要求高建议开启这两个功能。但要注意开启后 eFuse 会被写入这是不可逆的。一旦开启就不能再关闭而且烧录方式会变复杂。我一般建议在量产前就确定好安全策略别等到产品发出去了再改。注意开启 Flash 加密后如果 otadata 区域被擦除设备可能无法启动因为 bootloader 无法解密分区表。所以加密设备的 otadata 区域要格外小心别随便擦。5.4 OTA 升级标志位的设计在实际项目中我习惯在 nvs 里存一个 OTA 标志位记录当前升级的状态比如idle、downloading、verifying、pending_reboot。这样设备重启后能知道上次升级到哪一步了避免重复下载或者状态混乱。这个标志位在自动回滚场景下也很有用。比如新固件启动后先读这个标志位如果是pending_reboot说明是刚 OTA 上来的那就进入待验证流程如果是idle说明是正常启动直接标记有效。6. 常见问题与排查技巧实录6.1 设备反复重启串口打印 invalid header这是最典型的“固件刷坏”症状。invalid header: 0xffffffff通常意味着 bootloader 在目标分区读到的不是有效的应用镜像可能是分区里是空的0xFF或者镜像头被破坏。排查步骤先确认分区表是否正确烧录用esptool.py read_flash把 ota_0 区域读出来看看开头是不是0xE9ESP32 应用镜像的魔术字节。如果不是说明固件没写进去或者写错地址了。这时候重新烧录一次完整固件通常能解决。如果重新烧录后还是不行检查分区表的 Offset 是否和实际烧录地址一致。我遇到过有人手动用 esptool 烧录地址填错把应用固件写到了 nvs 区域结果 bootloader 找不到应用。6.2 OTA 后设备不回滚一直卡在新固件这种情况通常是新固件虽然有问题但没有触发重启或者触发了重启但 bootloader 认为它还是有效的。原因可能是代码里过早调用了esp_ota_mark_app_valid_cancel_rollback或者根本没开启回滚支持。先检查 menuconfig 里的Enable app rollback support是否勾选。然后检查代码确认标记有效的函数是在所有自检通过之后才调用的。如果新固件是死循环导致看门狗复位那 bootloader 应该能检测到并回滚。但如果新固件只是功能异常没有复位那就不会自动回滚需要手动触发。6.3 分区表改了但设备没变化这是很常见的新手坑。改了partitions.csv之后只执行了idf.py app-flash没有重新烧录分区表和 bootloader。设备启动时读到的还是旧分区表自然没变化。解决办法是执行idf.py erase-flash擦除整片 Flash然后idf.py flash完整烧录。如果不想擦除 nvs 里的数据可以只擦除分区表所在区域但操作起来麻烦不如直接全擦。6.4 OTA 写入速度慢得离谱OTA 速度受网络和 Flash 写入速度双重影响。如果 WiFi 信号弱传输时间会很长。另外esp_ota_write每次写入都会擦除 Flash 扇区如果写入块太小擦除次数会很多速度就慢。优化方法是增大每次写入的块大小比如 4KB 对齐。另外可以在 OTA 期间提高 CPU 频率或者把 WiFi 的省电模式关掉。我实测下来把写入块从 1KB 改成 4KB速度能提升差不多一倍。6.5 常见问题速查表现象可能原因排查方法解决方式串口打印 invalid header应用分区无有效固件读 Flash 看魔术字节重新烧录完整固件OTA 后不回滚回滚支持未开启或过早标记有效检查 menuconfig 和代码开启回滚调整标记时机分区表改了没效果未重新烧录分区表检查烧录日志erase-flash 后完整烧录OTA 速度慢写入块太小或 WiFi 信号弱查看日志中的写入耗时增大写入块优化网络设备反复重启固件崩溃或看门狗复位看串口复位原因修复固件或触发回滚otadata invalidotadata 区域被擦除或重叠检查分区表 Offset重新烧录分区表6.6 几个我踩过的坑第一个坑是分区表里ota_0和ota_1大小不一致。当时我觉得 ota_1 用不到那么大就设小了一点。结果 OTA 写入时新固件比 ota_1 大esp_ota_begin直接返回错误。后来改成一样大就好了。第二个坑是忘记在 OTA 完成后调用esp_ota_set_boot_partition。固件写进去了但 otadata 没更新重启后还是从旧分区启动白忙一场。第三个坑是在中断里调用 OTA 相关函数。esp_ota_write不是线程安全的如果在中断里调用可能导致 Flash 操作冲突设备直接死机。OTA 一定要放在独立任务里别在中断里搞。7. 关于“变砖”这件事的个人体会回到标题那个问题ESP32 固件刷坏了会变砖吗我的答案是普通应用固件刷坏基本不会。只要 bootloader 还在你总能通过串口重新烧录。真正会变砖的情况是把 bootloader 本身擦掉了或者 eFuse 写错了导致芯片无法正常启动。而双分区加自动回滚就是把“应用固件刷坏”这个最常见的风险降到了几乎为零。我在实际项目里凡是带 OTA 功能的设备都会强制要求双分区加自动回滚。这套机制的成本很低无非是多占一点 Flash 空间多写几行代码但换来的是设备在升级失败时能自己恢复不用派人去现场拆机。对于部署在偏远地区或者客户手里的设备这个价值太大了。最后分享一个小技巧在设备第一次出厂时把ota_0和ota_1都烧上同一个可用的固件并且在 otadata 里把两个 entry 都写成有效。这样即使后续 OTA 过程中出现极端情况两个分区里至少有一个是能启动的。这个操作在量产烧录时加进去能省掉很多售后麻烦。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑