
1. 从“变砖焦虑”说起ESP32 固件升级到底怕什么玩 ESP32 的朋友十个里有八个问过同一个问题固件刷坏了会不会变砖我刚开始接触 ESP32 那会儿每次点下烧录按钮手心都冒汗生怕一块几十块钱的开发板就此报废。后来折腾多了才明白ESP32 这颗芯片在设计之初就考虑到了固件损坏的场景它内置的 ROM Bootloader 和分区机制让“真变砖”这件事变得非常困难。真正会让人抓狂的不是芯片彻底没救而是设备已经装在现场、装在墙上、装在天花板里你没法把它拆下来重新插 USB 线烧录。这就是OTAOver-The-Air升级和双分区自动回滚要解决的核心痛点。简单说双分区就是在 Flash 里划出两块存放应用程序的区域一块跑着当前固件另一块用来接收新固件。新固件写进去之后如果启动失败或者运行异常系统自动切回旧分区设备继续正常工作。这套机制配合 ESP-IDF 提供的回滚 API能把“刷坏固件导致设备失联”的风险压到极低。这篇文章适合谁看如果你正在用 ESP32 做联网设备、远程控制、数据采集或者你已经被 OTA 升级搞崩过一两次那接下来的内容应该能帮你少走不少弯路。我会从分区表设计、OTA 流程、回滚触发条件、常见坑位几个角度把这件事讲透。代码以 ESP-IDF 为主Arduino 框架下也有对应思路我会在关键位置说明差异。2. 双分区机制到底怎么工作把 Flash 想象成两间房2.1 分区表是整件事的地基ESP32 的 Flash 默认从 0x10000 开始存放应用程序但具体怎么划分完全由分区表决定。你可以把 Flash 想象成一栋楼分区表就是楼层平面图。没有平面图Bootloader 不知道该去哪里找固件。一个支持 OTA 的典型分区表长这样# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x4000, otadata, data, ota, 0xd000, 0x2000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x140000, ota_0, app, ota_0, 0x150000, 0x140000, ota_1, app, ota_1, 0x290000, 0x140000,这里有几个关键点需要解释。factory是出厂固件分区ota_0和ota_1是两个 OTA 应用分区。otadata分区只有 8KB但它极其重要它记录着当前应该从哪个分区启动、下一个要启动哪个分区、以及启动是否成功。注意如果你用的是 4MB Flash 的模组上面这个分区表已经接近极限了。每个 app 分区 0x140000 就是 1.25MB三个 app 分区加起来接近 3.75MB再加上其他数据分区基本塞满。实际项目中我通常会把 factory 分区去掉只保留 ota_0 和 ota_1这样每个分区可以给到 1.5MB 以上。2.2 启动流程Bootloader 如何决定跑哪块固件上电之后流程是这样的ROM Bootloader 读取 Flash 起始位置的二级 Bootloader。二级 Bootloader 读取otadata分区判断当前有效的是哪个 OTA 槽位。如果otadata为空或者无效就回退到factory分区。如果factory也不存在就尝试ota_0。找到目标分区后校验镜像头部、计算 SHA256、检查芯片型号匹配全部通过才跳转执行。这个流程里最妙的一步是Bootloader 在跳转之前会把目标分区标记为“待验证”状态。也就是说新固件第一次启动时系统并不认为它是可靠的。只有应用程序自己调用esp_ota_mark_app_valid_cancel_rollback()才会把这个分区标记为“有效”。如果一直不标记下一次重启就会自动回滚到上一个有效分区。2.3 自动回滚的触发条件自动回滚不是万能的它只在特定条件下生效。我整理了一张表方便你对照排查场景是否触发回滚说明新固件启动后崩溃重启是只要未标记有效重启即回滚新固件启动后卡死但没重启否需要看门狗或主动重启新固件写入过程中断电是otadata 未更新仍指向旧分区新固件校验失败是Bootloader 直接拒绝跳转新固件正常运行但功能异常视情况需应用层主动判断并调用回滚旧分区也被擦除否无有效分区可回退可能真变砖从表里可以看出自动回滚的核心前提是旧分区必须完好且 otadata 没有被破坏。这也是为什么我反复强调OTA 升级过程中最危险的操作就是同时擦除两个 app 分区。3. 手把手实现 OTA 与自动回滚从分区表到代码3.1 分区表配置与编译选项在 ESP-IDF 项目里分区表通常放在partitions.csv文件中然后在menuconfig里指定Component config → Partition Table → Custom partition table CSV同时要确保以下选项开启Component config → ESP32-specific → Support for external, SPI-connected RAM (按需) Component config → Application Level Tracing (按需) Component config → Bootloader config → Bootloader log verbosity (调试时可调高)还有一个容易被忽略的选项Component config → Partition Table → Offset of partition table默认是 0x8000一般不用改。但如果你用的是自定义 Flash 布局这里必须和烧录配置一致。3.2 OTA 升级的核心代码流程下面这段代码是我在实际项目中反复打磨过的 OTA 升级骨架基于 ESP-IDF 的esp_https_ota组件简化而来#include esp_ota_ops.h #include esp_http_client.h #include esp_https_ota.h void ota_task(void *pvParameter) { esp_http_client_config_t config { .url http://your-server/firmware.bin, .timeout_ms 5000, .keep_alive_enable true, }; esp_https_ota_config_t ota_config { .http_config config, }; esp_https_ota_handle_t https_ota_handle NULL; esp_err_t err esp_https_ota_begin(ota_config, https_ota_handle); if (err ! ESP_OK) { ESP_LOGE(TAG, OTA begin failed); vTaskDelete(NULL); return; } while (1) { err esp_https_ota_perform(https_ota_handle); if (err ! ESP_ERR_HTTPS_OTA_IN_PROGRESS) { break; } // 可以在这里更新进度条 } if (esp_https_ota_is_complete_data_received(https_ota_handle) ! true) { ESP_LOGE(TAG, OTA data incomplete); esp_https_ota_abort(https_ota_handle); vTaskDelete(NULL); return; } err esp_https_ota_finish(https_ota_handle); if (err ESP_OK) { ESP_LOGI(TAG, OTA success, restarting...); esp_restart(); } else { ESP_LOGE(TAG, OTA finish failed); } vTaskDelete(NULL); }这段代码跑完之后设备会重启Bootloader 会尝试从新分区启动。但此时新固件还处于“待验证”状态必须在应用初始化完成后调用esp_ota_mark_app_valid_cancel_rollback();我通常把这行代码放在网络连接成功、关键外设初始化完成之后。这样如果新固件连 WiFi 都连不上它永远没机会标记有效重启几次后自动回滚。3.3 主动回滚应用层如何判断“新固件不行”有些故障不是崩溃而是功能异常。比如新固件能启动、能联网但传感器读数全是零或者控制指令没响应。这种情况下自动回滚不会触发需要应用层自己判断。我的做法是维护一个“健康检查”计数器。设备启动后在 NVS 里记录启动次数和关键功能状态。如果连续三次启动后关键功能都异常就主动调用const esp_partition_t *running esp_ota_get_running_partition(); const esp_partition_t *last_valid esp_ota_get_last_invalid_partition(); if (last_valid ! NULL) { esp_ota_set_boot_partition(last_valid); esp_restart(); }提示esp_ota_get_last_invalid_partition()返回的是上一次被标记为无效的分区。如果当前运行的分区本身就是无效的这个函数可能返回 NULL所以实际使用时要结合esp_ota_get_next_update_partition()一起判断。3.4 参数计算分区大小怎么定很多人卡在分区大小计算上。假设你用 4MB Flash去掉 Bootloader约 0x8000、分区表0x1000、NVS0x4000、otadata0x2000、phy_init0x1000剩余大约 0x3F0000 给应用分区。如果保留 factory ota_0 ota_1每个分区约 0x1400001.25MB。如果你的固件编译出来超过 1.25MB就必须调整。我一般建议简单 WiFi 传感器项目每个分区 1MB 足够带 HTTPS JSON 解析 OTA建议 1.25MB 以上带音频、图像处理至少 1.5MB考虑换 8MB Flash 模组编译后可以用idf.py size查看固件实际大小留 10% 余量比较稳妥。4. 那些年我踩过的坑OTA 回滚常见问题速查4.1 回滚不生效的几种典型情况我遇到过最诡异的一次是新固件明明崩溃了但设备重启后还是跑的新固件没有回滚。排查了半天发现是otadata分区被意外擦除了。原因是我在代码里调用了nvs_flash_erase()而那个项目的分区表里 otadata 和 nvs 偏移量计算错误导致擦除范围重叠。这件事给我的教训是每次修改分区表后必须用esptool.py read_flash把整个 Flash 读出来确认各分区边界没有重叠。具体命令esptool.py --port /dev/ttyUSB0 read_flash 0x0 0x400000 full_dump.bin然后用十六进制工具查看 0x8000 附近的分区表内容确认偏移量和大小。另一个常见问题是新固件启动后立刻调用了esp_ota_mark_app_valid_cancel_rollback()导致即使后续崩溃也不会回滚。我的建议是这行代码至少放在以下条件满足之后WiFi 连接成功关键外设初始化完成至少稳定运行 30 秒4.2 OTA 升级过程中断电怎么办这是用户最担心的场景。好消息是ESP32 的 OTA 设计对断电是相对友好的。因为新固件是写入到“下一个更新分区”而不是覆盖当前运行分区。只要 otadata 没有更新重启后仍然从旧分区启动。但有一种情况会出问题如果断电发生在esp_https_ota_finish()内部otadata 可能处于半写入状态。ESP-IDF 对 otadata 的写入做了 CRC 校验如果校验失败Bootloader 会认为 otadata 无效回退到 factory 分区。所以只要 factory 分区存在且完好设备仍然能启动。注意如果你的项目去掉了 factory 分区只保留 ota_0 和 ota_1那么 otadata 损坏时 Bootloader 会尝试 ota_0。如果 ota_0 正好是那个写了一半的新固件就可能启动失败。这种情况下建议保留 factory 分区作为最后防线。4.3 常见问题速查表现象可能原因排查方法解决思路OTA 后设备失联新固件 WiFi 配置错误串口日志看是否连上 AP回滚后修复配置回滚后仍跑新固件otadata 未更新读 otadata 分区内容检查esp_ota_set_boot_partition调用编译提示分区不够固件超过分区大小idf.py size调整分区表或优化固件OTA 下载到一半失败网络不稳定看 HTTP 状态码增加重试和断点续传回滚后旧固件也异常旧分区被擦读 Flash 确认避免擦除运行分区串口无输出Bootloader 校验失败看 ROM 日志检查镜像头和 Flash 模式4.4 独家避坑技巧第一个技巧在 OTA 升级前先把当前固件的分区信息打印出来。我习惯在设备启动时输出const esp_partition_t *running esp_ota_get_running_partition(); ESP_LOGI(TAG, Running partition: %s, address: 0x%x, size: 0x%x, running-label, running-address, running-size);这样一旦出问题串口日志里就有完整现场。第二个技巧用 otadata 的原始数据判断启动状态。otadata 分区里有两个 4KB 的扇区每个扇区开头有序列号和状态标记。你可以用esptool.py read_flash 0xd000 0x2000 otadata.bin读出来然后用十六进制查看。状态 0xFFFFFFFF 表示无效0xFFFFFFFE 表示待验证0xFFFFFFFD 表示有效。这个技巧在排查“为什么没回滚”时特别有用。第三个技巧给 OTA 升级加一个物理按键确认。对于现场设备我通常会在新固件启动后点亮 LED 或者蜂鸣器要求维护人员在 60 秒内按下按键确认。如果不按自动回滚。这样能避免“新固件能跑但功能不对”的情况长期存在。5. 进阶话题让 OTA 更稳的几个工程习惯5.1 固件签名与安全启动如果你的设备部署在不可控环境OTA 升级必须考虑固件来源合法性。ESP32 支持 Secure Boot 和 Flash Encryption开启后只有签名正确的固件才能启动。配置路径Security features → Enable Secure Boot Security features → Enable Flash Encryption开启 Secure Boot 后OTA 升级的固件必须用对应的私钥签名否则 Bootloader 拒绝加载。这能有效防止恶意固件刷入但也意味着你一旦丢失私钥设备就再也无法升级。我的建议是开发阶段先不开量产前再开并且把私钥备份到至少两个离线位置。5.2 OTA 版本管理与灰度发布双分区机制只解决“能不能回滚”不解决“该不该升级”。实际项目中我通常会在固件里嵌入版本号OTA 服务器根据设备当前版本决定推送哪个固件。简单做法是用esp_app_get_description()获取版本信息const esp_app_desc_t *app_desc esp_app_get_description(); ESP_LOGI(TAG, Current version: %s, app_desc-version);版本号在menuconfig里设置Application manager → Project version。服务器端可以维护一个版本映射表只对特定版本范围的设备推送新固件实现灰度发布。5.3 离线包与本地升级有些场景没有网络比如工厂产线烧录或者现场维护。这时候可以用 SD 卡或者串口做本地 OTA。ESP-IDF 提供了esp_ota_write()接口你可以从任意数据源读取固件并写入 OTA 分区。我做过一个项目设备上插一张 SD 卡固件放在根目录上电时检测到特定文件名就自动升级。核心逻辑和网络 OTA 一样只是数据来源从 HTTP 换成了文件系统。5.4 监控与日志回滚后要知道发生了什么自动回滚虽然能救设备但如果不记录原因下次还会踩同样的坑。我的做法是在 NVS 里保存最近三次启动的分区标签和回滚次数。设备联网后把这些信息上报到服务器。这样即使现场设备回滚了你也能在后台看到“某台设备在升级到 v1.2.3 后回滚到 v1.2.2”然后针对性排查。具体实现可以用一个简单的结构体typedef struct { char partition_label[16]; uint32_t boot_count; uint32_t rollback_count; esp_reset_reason_t reset_reason; } boot_record_t;每次启动时更新这个结构体并通过 MQTT 或 HTTP 上报。这个习惯让我在多个项目中提前发现了固件兼容性问题避免了批量设备回滚。6. 关于“变砖”的最终结论回到标题那个问题ESP32 固件刷坏了会变砖吗我的答案是只要分区表设计合理、otadata 完好、至少有一个有效 app 分区就不会真变砖。双分区加自动回滚机制本质上是在 Flash 里留了一条退路。真正会导致设备彻底失联的往往是人为操作失误比如擦除了 Bootloader、破坏了分区表、或者两个 app 分区同时损坏。我在实际项目中总结出一条经验任何 OTA 升级方案都必须假设新固件会失败。基于这个假设去设计回滚策略、健康检查、日志上报设备才能在无人值守的环境里长期稳定运行。ESP32 提供的这套机制已经足够强大关键在于你有没有把它用对。最后分享一个我常用的调试方法在开发阶段故意在新固件里加入一个“启动后 10 秒必崩溃”的代码然后观察设备是否能自动回滚。这个测试能帮你验证整个回滚链路是否真的生效。我每次做新项目都会跑一遍这个测试比看文档管用得多。