news 2026/10/28 9:07:39

ESP32 刷固件会变砖吗?双分区+自动回滚机制详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32 刷固件会变砖吗?双分区+自动回滚机制详解

1. 从一次刷机翻车说起:ESP32 到底会不会变砖

很多人第一次给 ESP32 刷固件,手都是抖的。尤其是用 USB-TTL 接上开发板、敲下esptool.py write_flash回车的那一刻,脑子里全是同一个问题:万一刷到一半断了,这板子是不是就废了?我当年也是这么想的,甚至专门多买了两块 ESP32 备着,生怕刷坏一块就没得用。

先说结论,让你把心放回肚子里:ESP32 在绝大多数情况下不会真正变砖。它内部固化了一段 ROM Bootloader,这段代码是出厂就烧死在芯片里的,你通过常规手段根本改不了它。只要这段 ROM 还在,芯片就永远能被重新拉起来,哪怕你的应用固件被刷成了一堆乱码。这跟很多老式单片机一旦烧错就彻底没救的情况完全不同,ESP32 从设计之初就考虑到了"刷坏"这件事。

那为什么网上还是经常看到有人喊"我的 ESP32 变砖了"?这里面其实混着好几种情况。一种是硬件层面的损坏,比如供电接反、IO 口灌了大电流、芯片本身烧了,这种是真砖,神仙难救。另一种是软件层面的"假砖",比如刷了不匹配的固件导致不断重启、串口一直打印乱码、或者进了深度睡眠再也醒不过来。这类问题 99% 都能通过重新烧录解决,只是很多人不知道正确的进入下载模式的方法,误以为板子死了。

我写这篇东西的目的,就是想把"ESP32 刷固件到底会不会变砖"这件事彻底讲清楚,并且给出一个真正能落地的方案:用双分区加自动回滚机制,让设备在 OTA 升级失败时自己退回上一个能用的固件。这套方案我在好几个量产项目里用过,实测下来非常稳,尤其适合那些装到现场之后你根本够不着、只能靠远程升级的设备。不管你是刚入门的新手,还是已经在做产品固件的老手,这套思路都值得你花时间吃透。

关键词里提到了 ESP32、固件、双分区、自动回滚、OTA,这几个词基本就是本文的主线。我会从分区表的原理讲起,一直讲到代码怎么写、怎么测、怎么避坑,尽量让你看完就能直接抄作业。

2. 搞懂 ESP32 的启动链路,才知道"砖"是怎么来的

2.1 从 ROM Bootloader 到你的 app_main 之间发生了什么

要理解为什么 ESP32 不容易变砖,得先搞清楚它上电之后到底走了哪几步。整个过程大致是这样的:芯片上电复位,首先运行的是固化在 ROM 里的Bootloader,这段代码由芯片厂商在流片时就写好,用户无法修改。ROM Bootloader 会去读eFuse里的配置和几个特定的 strapping 引脚电平,决定从哪个接口加载下一阶段代码——通常是 SPI Flash。

接着,ROM Bootloader 从 Flash 的固定偏移(默认 0x1000)加载二级 Bootloader,也就是我们平时说的 bootloader.bin。这个二级 Bootloader 是可以用idf.py flash或esptool烧写的,它才是真正负责"选哪个应用分区启动"的角色。二级 Bootloader 会读取分区表,找到标记为factory或某个ota_x的分区,校验它的镜像头,然后跳转过去执行。你的app_main就是在这之后才跑起来的。

所以你看,从 ROM 到二级 Bootloader 再到应用,中间有好几层。真正"改不了"的只有最底层的 ROM。只要 ROM 没坏、Flash 没物理损坏、strapping 引脚没被拉错,芯片就一定能被重新烧录。这就是 ESP32 抗造的根本原因。

2.2 那些被误判为"变砖"的典型现象

我在论坛和群里见过太多"求救"帖,总结下来,被误判成砖的情况主要有这么几类:

  • 串口一直刷乱码:多半是波特率不对,或者芯片进了某个异常复位循环。换个波特率、按住 BOOT 键再上电,基本都能救回来。
  • 烧录时报 "Failed to connect":这是最常见的"假砖"。原因通常是没进下载模式。ESP32 需要在上电或复位时把 GPIO0 拉低、GPIO2 保持高电平,才能进入串口下载模式。很多开发板有自动下载电路,但有些便宜板子没有,就得手动按住 BOOT 再点 EN。
  • 刷完固件后设备不断重启:这是应用层的问题,比如分区表不匹配、固件太大超出分区、或者代码里有 panic。这种不是砖,重新烧个正确的固件就好。
  • OTA 升级到一半断电,设备起不来:这才是真正需要"自动回滚"来兜底的场景,也是本文重点要解决的。

把这几种情况分清楚,你就明白:真正意义上的"变砖"在 ESP32 上极其罕见,绝大多数是操作问题或配置问题。而 OTA 升级失败导致设备失联,才是实际项目里最要命的痛点。

2.3 为什么 OTA 才是真正的高风险环节

有线烧录的时候,你人就在设备旁边,刷坏了拔下来重刷就行。但 OTA 不一样,设备可能装在几十米高的塔上、埋在地下的管道里、或者用户家里的墙角。一旦升级失败又没法回滚,你就只能派人去现场,成本高得离谱。

OTA 的风险点在于:新固件下载到 Flash 的过程中可能断电、可能网络中断、可能新固件本身有 bug 一启动就崩。如果没有回滚机制,设备就会卡在一个永远起不来的状态。而 ESP-IDF 提供的双分区 + 自动回滚机制,正是为了解决这个问题而生的。它的核心思想很简单:永远保留一个已知能用的旧固件,新固件只有在确认自己能正常跑起来之后,才被"转正"。

3. 双分区方案的分区表怎么设计才不踩坑

3.1 分区表的基本结构和字段含义

ESP32 的分区表是一张放在 Flash 固定位置(默认 0x8000)的小表,每行描述一个分区。一个典型的分区表 CSV 长这样:

# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x180000, ota_0, app, ota_0, 0x190000,0x180000, ota_1, app, ota_1, 0x310000,0x180000,

几个关键字段得说清楚。Type和SubType决定这个分区是干嘛的,app类型下面又分factory、ota_0到ota_15。Offset是分区在 Flash 里的起始地址,Size是大小。这里有个硬性要求:每个 app 分区的大小必须是 64KB 的整数倍,因为 Flash 擦除的最小单位是 64KB 的扇区。你要是写个 0x170000 这种不是 64KB 倍数的值,编译时就会报错。

另外,分区表里所有分区的 Offset 必须连续且不重叠,从 0x9000 开始(0x8000 是分区表本身占的位置)。我第一次自己写分区表的时候,就因为算错了偏移,导致两个分区重叠,烧进去之后设备疯狂重启,排查了半天才发现是分区表的问题。

3.2 双 OTA 分区与 factory 分区的取舍

上面那张表里同时有factory和ota_0、ota_1。这是最经典的三分区布局。factory是出厂固件,ota_0和ota_1轮流作为 OTA 的目标分区。设备第一次启动跑 factory,之后每次 OTA 就在 ota_0 和 ota_1 之间来回切。

但这里有个很多人不知道的细节:一旦你从 factory 切到了 ota_0,就再也回不到 factory 了(除非手动擦除 otadata)。因为二级 Bootloader 是根据 otadata 分区里的记录来决定启动哪个分区的,一旦 otadata 被写成指向 ota_0,factory 就被"遗忘"了。

所以如果你想要更省 Flash 空间的方案,可以干脆不要 factory,只用 ota_0 和 ota_1 两个分区。第一次烧录时直接把固件烧到 ota_0,otadata 留空,Bootloader 在没有有效 otadata 时会默认启动 ota_0。这样两个分区各占一半空间,能塞下更大的固件。我在实际项目里更倾向这种两分区方案,简单、省空间、逻辑清晰。

注意:如果你用的是两分区方案,第一次量产烧录时一定要确保 otadata 是空的,否则设备可能启动到一个不存在的分区上。

3.3 分区大小怎么算才够用

分区大小直接决定了你能塞多大的固件。ESP-IDF 编译出来的 app 镜像,除了代码本身,还包含各种常量、初始化数据、以及可能的文件系统。一个带 WiFi、蓝牙、HTTP 服务的固件,轻松就能到 1MB 以上。如果你还用了 LVGL 做界面、或者集成了 TensorFlow Lite Micro,2MB 都打不住。

我的经验是:先编译一次,看生成的 .bin 文件多大,然后分区大小至少留出 30% 的余量。因为后续加功能、加日志、加 OTA 校验逻辑,固件只会越来越大。比如你的固件现在是 1.2MB,那分区至少给到 1.6MB,也就是 0x1A0000 往上取到 64KB 的倍数,实际写 0x1C0000 比较稳妥。

Flash 总容量也得算清楚。常见的 ESP32 模块有 4MB、8MB、16MB 几种。4MB 的话,两个 1.6MB 的 app 分区加上 nvs、phy、otadata,基本就满了,没多少空间放文件系统。所以如果你要存音频、图片、日志,8MB 起步比较舒服。ESP32-S3 有些型号板载 16MB Flash 加 8MB PSRAM,那种就非常宽裕了。

4. 自动回滚的底层机制:otadata 和 rollback 是怎么配合的

4.1 otadata 分区里到底存了什么

otadata是一个很小的 data 类型分区,通常 0x2000 大小,里面存着两个esp_ota_select_entry_t结构体,每个 32 字节。这个结构体记录了当前应该启动哪个 OTA 分区、这个分区的状态(新固件、有效、无效、待验证等),以及一个序列号用于判断哪个 entry 更新。

Bootloader 启动时会读这两个 entry,选序列号更大、状态有效的那个,然后跳转到对应的 ota 分区。如果两个 entry 都无效,就回退到 factory 分区(如果有的话)。这套机制的好处是写入 otadata 是原子性的,即使写一半断电,另一个 entry 还是完好的,Bootloader 总能选出一个可用的。

理解这一点很重要,因为它解释了为什么自动回滚是可靠的:回滚的本质就是在 otadata 里把状态标记回退,而不是去动固件本身。固件一直躺在 Flash 里没变,变的只是"启动哪个"这个指针。

4.2 新固件的"待验证"状态与确认流程

自动回滚的核心在于一个叫ESP_OTA_IMG_PENDING_VERIFY的状态。当你通过 OTA 把新固件写进 ota_1 并调用esp_ota_set_boot_partition()之后,这个分区的状态就被标记为"待验证"。下次重启,Bootloader 会启动这个新固件,但此时它处于"试用期"。

新固件启动后,你的代码必须主动调用esp_ota_mark_app_valid_cancel_rollback()来"转正"自己。如果你没调用,或者调用之前设备就崩了、重启了,那么 Bootloader 下次启动时会发现这个分区还是"待验证"状态,于是自动回滚到上一个有效的分区。

这个设计非常巧妙:它把"确认固件能用"的责任交给了固件自己。只有新固件真正跑到了能调用确认函数的那一步,才说明它是健康的。如果它在初始化阶段就 panic 了,根本没机会调用确认函数,回滚就会自动发生。

4.3 回滚触发的完整时序

把整个流程串起来看是这样的:

  1. 设备当前跑在 ota_0,状态为 valid。
  2. 收到 OTA 任务,把新固件写入 ota_1,写入完成后调用esp_ota_set_boot_partition(ota_1)。
  3. 此时 otadata 里 ota_1 的状态是PENDING_VERIFY,序列号比 ota_0 大。
  4. 设备重启,Bootloader 读到 ota_1 序列号更大且状态是待验证,于是启动 ota_1。
  5. 情况 A:ota_1 固件正常启动,跑到某处调用了esp_ota_mark_app_valid_cancel_rollback(),状态变为 valid,回滚取消,升级成功。
  6. 情况 B:ota_1 固件启动过程中崩溃、看门狗复位、或者压根没调用确认函数就重启了。Bootloader 再次启动时发现 ota_1 还是待验证,于是回滚,启动 ota_0。

整个过程不需要人工干预,也不需要额外的硬件看门狗,纯靠软件状态机完成。这就是为什么我说这套机制"实测很稳"。

5. 手把手写代码:从 OTA 下载到确认转正

5.1 环境准备和分区表配置

先确保你的 ESP-IDF 环境是 4.x 或 5.x,这两个大版本在 OTA API 上基本一致。项目根目录下建一个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,

然后在menuconfig里把分区表设为自定义:Partition Table -> Custom partition table CSV,路径填partitions.csv。同时确认Bootloader config里的Enable app rollback support是打开的,这个选项默认就是开的,但值得检查一遍。

5.2 写一个带校验的 OTA 下载任务

下面这段代码是我在实际项目里用的简化版,去掉了业务逻辑,保留了核心流程:

#include "esp_ota_ops.h" #include "esp_http_client.h" #include "esp_log.h" static const char *TAG = "OTA"; esp_err_t do_ota_update(const char *url) { esp_http_client_config_t config = { .url = url, .timeout_ms = 10000, }; esp_http_client_handle_t client = esp_http_client_init(&config); esp_err_t err = esp_http_client_open(client, 0); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); return err; } int content_length = esp_http_client_fetch_headers(client); ESP_LOGI(TAG, "Firmware size: %d bytes", content_length); const esp_partition_t *update_partition = esp_ota_get_next_update_partition(NULL); ESP_LOGI(TAG, "Writing to partition subtype %d at offset 0x%lx", update_partition->subtype, update_partition->address); esp_ota_handle_t update_handle = 0; err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ota_begin failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); return err; } char buf[1024]; int total_read = 0; while (1) { int read_len = esp_http_client_read(client, buf, sizeof(buf)); if (read_len < 0) { ESP_LOGE(TAG, "HTTP read error"); esp_ota_abort(update_handle); esp_http_client_cleanup(client); return ESP_FAIL; } else if (read_len == 0) { break; } err = esp_ota_write(update_handle, buf, read_len); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ota_write failed: %s", esp_err_to_name(err)); esp_ota_abort(update_handle); esp_http_client_cleanup(client); return err; } total_read += read_len; } err = esp_ota_end(update_handle); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ota_end failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); return err; } err = esp_ota_set_boot_partition(update_partition); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); return err; } ESP_LOGI(TAG, "OTA update finished, total %d bytes. Rebooting...", total_read); esp_http_client_cleanup(client); esp_restart(); return ESP_OK; }

几个关键点解释一下。esp_ota_get_next_update_partition(NULL)会自动帮你找到"下一个"可用的 OTA 分区,也就是当前没在跑的那个。esp_ota_begin会擦除目标分区并准备写入。esp_ota_write可以多次调用,每次写一块。esp_ota_end会做镜像校验,如果下载的固件损坏,这一步会返回错误,此时不要调用set_boot_partition,设备继续跑旧固件,安全。

5.3 在新固件里正确调用确认函数

新固件启动后,你需要在合适的时机调用确认函数。这个时机很关键:不能太早,也不能太晚。太早的话,如果后面初始化失败,你已经把回滚取消了,设备就卡住了;太晚的话,如果设备在确认之前因为别的原因重启,会白白触发一次回滚。

我的做法是在所有关键外设初始化完成、网络连上、并且成功上报了一次心跳之后,再调用确认。这样能最大程度保证"能确认的固件确实是健康的"。代码大概长这样:

void app_main(void) { // ... 各种初始化 ... // 检查当前是不是待验证状态 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) { // 做一些自检,比如连 WiFi、读传感器 if (self_test_passed()) { ESP_LOGI(TAG, "Self test passed, mark app valid"); esp_ota_mark_app_valid_cancel_rollback(); } else { ESP_LOGE(TAG, "Self test failed, triggering rollback"); esp_ota_mark_app_invalid_rollback_and_reboot(); } } } // ... 主循环 ... }

注意esp_ota_mark_app_invalid_rollback_and_reboot()这个函数,它会主动标记当前固件无效并重启,触发回滚。这在你自检失败时非常有用,比等着看门狗复位更干净。

6. 实测中那些文档没写的坑

6.1 回滚后旧固件的状态问题

有个细节我第一次遇到时懵了很久:设备回滚到旧固件之后,旧固件的状态是什么?答案是valid。因为旧固件之前已经确认过了,它的状态一直是 valid,回滚只是把启动指针指回它,不会改变它的状态。所以回滚之后设备是正常运行的,不会陷入"回滚再回滚"的死循环。

但如果你连续 OTA 失败两次,情况就有点微妙了。第一次失败回滚到 ota_0,第二次又尝试升级 ota_1 又失败,还是回滚到 ota_0。只要 ota_0 一直是 valid,就永远有退路。这也是为什么我强烈建议至少保留一个已知能用的分区,不要两个分区都处于待验证状态。

6.2 看门狗和回滚的相互影响

ESP32 默认开了任务看门狗(TWDT)和中断看门狗(IWDT)。如果你的新固件在初始化时卡在某个循环里,看门狗会复位设备。这个复位会触发回滚,看起来是好事。但有个坑:如果看门狗复位发生在你调用确认函数之后,那回滚就不会发生了,设备会一直卡在崩溃循环里。

所以确认函数的调用时机一定要在"确定不会再卡死"之后。我一般会在确认之前把所有可能阻塞的操作都做完,并且给关键任务喂狗。另外,CONFIG_ESP_TASK_WDT_TIMEOUT_S这个超时时间可以适当调大,给新固件更多启动时间,避免误判。

6.3 Flash 擦写寿命与 OTA 频率

Flash 的擦写寿命是有限的,NOR Flash 一般标称 10 万次擦写。每次 OTA 都要擦除一整个 app 分区,如果你频繁 OTA,比如一天升级好几次,几年下来也可能把 Flash 写坏。当然正常产品不会这么频繁升级,但如果你在做开发调试,反复烧录同一个分区,还是要注意。

我的建议是:开发阶段尽量用有线烧录,量产设备才用 OTA。另外,OTA 下载的固件最好做完整性校验,比如在 HTTP 头里带上 MD5 或 SHA256,下载完对比一下,避免把损坏的固件写进去浪费一次擦写。

6.4 分区表改了之后旧设备怎么办

这是量产项目里最头疼的问题之一。如果你第一版固件用的分区表和第二版不一样,OTA 升级时新固件的分区布局和旧的不匹配,会直接导致升级失败甚至设备起不来。因为分区表是在烧录时写进 Flash 固定位置的,OTA 只更新 app 分区,不会更新分区表。

解决办法有两个:一是从第一版就把分区表设计好,后续不再改;二是如果非要改,得通过一个特殊的"过渡固件"来更新分区表,这个过渡固件要能同时兼容新旧布局,复杂度很高。所以我的经验是,分区表在项目启动阶段就要反复推敲,留足余量,一旦量产就别再动。

7. 怎么验证回滚真的生效了

7.1 故意做一个会崩溃的固件

验证回滚最直接的办法,就是故意写一个启动就崩的固件,然后 OTA 上去,看设备会不会自己退回来。你可以在新固件的app_main开头加一句assert(0)或者esp_restart()死循环,让它永远到不了确认函数那一步。

具体操作:先烧一个正常固件到 ota_0,确认它能跑。然后编译一个带assert(0)的固件,通过 OTA 推到 ota_1。设备重启后会跑 ota_1,立刻崩溃复位。反复几次之后,Bootloader 会发现 ota_1 一直是待验证状态,于是回滚到 ota_0。你会在串口看到类似Rollback to previously saved partition的日志。

7.2 用串口日志确认状态流转

ESP-IDF 的 Bootloader 日志会打印当前启动的是哪个分区、状态是什么。把CONFIG_BOOTLOADER_LOG_LEVEL调到 Info 或 Debug,你能看到非常详细的状态信息。重点关注这几行:

  • boot: Loaded app from partition at offset 0x...:当前启动的分区。
  • boot: Set actual ota_seq=...:otadata 里的序列号。
  • boot: Rollback to previously saved partition:回滚发生了。

如果你看到回滚日志,并且设备最终跑回了旧固件,说明整套机制工作正常。

7.3 用 otadata 工具直接读状态

除了看日志,你还可以用esptool.py read_flash把 otadata 分区读出来,用十六进制工具看里面的内容。otadata 在 0xf000,大小 0x2000,读出来之后你能看到两个 entry 的序列号和状态字节。这个方法在设备没法输出日志的时候特别有用,比如串口被占用了或者设备装得太深够不着。

我一般会在产测工装上集成这个读取步骤,出厂前确认 otadata 是干净的,避免把调试时的状态带到客户手里。

8. 几个容易被忽略的进阶细节

8.1 固件加密和签名对回滚的影响

如果你的产品开了Secure Boot和Flash Encryption,OTA 的流程会多一层校验。新固件必须用正确的密钥签名,否则 Bootloader 会拒绝启动它,直接回滚。这其实是好事,等于多了一道保险。但要注意,开启 Secure Boot 之后,回滚的目标分区也必须是签名有效的,否则一样起不来。所以密钥管理一定要做好,别把签名密钥弄丢了,否则连回滚都救不了你。

8.2 双分区之外,还能怎么玩

除了经典的双 OTA 分区,ESP-IDF 还支持最多 16 个 OTA 分区(ota_0 到 ota_15)。理论上你可以做多版本共存、A/B 测试之类的玩法。但分区越多,每个分区能分到的空间越小,实际项目里两个分区基本够用。如果你真的需要多版本,可以考虑把固件存在外部 Flash 或 SD 卡上,运行时再加载,不过那就复杂多了。

8.3 OTA 升级标志位的持久化

有时候你需要在 OTA 过程中记录一些状态,比如"正在升级""升级失败次数"之类的。这些信息不能存在 otadata 里,因为那是 Bootloader 管的。我一般会存在 NVS 里,用一个专门的 namespace。注意 NVS 的写入也有寿命限制,别在循环里频繁写。升级失败次数可以用来做退避策略,比如连续失败三次就停止自动重试,等人工介入。

9. 我在实际项目里踩过的几个真实坑

第一个坑是分区表偏移算错。当时我手动改了一个分区的 offset,结果和后面的分区重叠了 0x1000 字节。编译没报错,烧进去之后设备启动就崩,串口打印一堆看不懂的地址。排查了大半天才用esptool.py read_flash把分区表读出来对比,发现重叠。从那以后我每次改分区表都会用idf.py partition-table生成一份人类可读的版本核对一遍。

第二个坑是确认函数调用太早。有个项目我在app_main第一行就调了esp_ota_mark_app_valid_cancel_rollback(),结果新固件在初始化 WiFi 时因为配置错误崩了,但回滚已经被取消,设备就卡在崩溃循环里,只能现场重刷。后来我把确认挪到了网络连接成功并上报心跳之后,再也没出过这个问题。

第三个坑是HTTP 下载没有超时。早期代码里esp_http_client没设 timeout,结果服务器卡住的时候,OTA 任务一直挂着,看门狗把设备复位了,触发了回滚。虽然回滚救回来了,但用户体验很差。后来我加了 10 秒的连接超时和读取超时,并且把 OTA 任务放在独立的任务里跑,不阻塞主逻辑。

第四个坑是Flash 空间不够导致 OTA 写入失败。有个 4MB 的模块,我分区给大了,两个 app 分区加起来 3.5MB,加上其他分区超了。编译时没报错,但 OTA 写到一半就失败。后来换成 8MB 模块才解决。所以选型阶段一定要把 Flash 容量算够。

10. 关于"变砖"这件事,我的最终看法

回到最开始的问题:ESP32 刷固件会变砖吗?我的答案是,只要芯片没物理损坏,就几乎不会。ROM Bootloader 是最后的保命符,它保证了你永远有机会重新烧录。真正需要担心的是 OTA 升级失败导致设备失联,而这恰恰是双分区加自动回滚要解决的问题。

这套机制我在多个量产项目里用了两三年,经历过现场断电、网络抖动、固件 bug 各种情况,回滚每次都稳稳地把设备救回来。它的价值不在于技术多高深,而在于把"升级失败"这个必然会发生的事情,变成了一个可以自动恢复的常规事件。对于任何需要远程维护的 ESP32 设备来说,这都是值得从一开始就设计进去的能力。

如果你现在正在做第一个带 OTA 的项目,我的建议是:先把分区表设计好,把回滚机制跑通,再去做业务功能。因为等到设备装到现场再想加回滚,代价就大得多了。分区表这东西,改一次伤筋动骨,不如一开始就想清楚。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 9:06:48

蓝牙MCU集成方案与低功耗设计:CH592选型及调试实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/28 9:06:10

零基础大模型考点全梳理:从原理到部署的完整学习路线

每次遇到刚转行做AI应用或者准备大模型相关考试的朋友&#xff0c;我发现大家最焦虑的往往不是“学不会”&#xff0c;而是“不知道从哪里下手”。大模型技术这几年几乎包揽了所有热点词条&#xff1a;从微调、部署到RAG、Agent&#xff0c;再到多模态和本地私有化&#xff0c;…

作者头像 李华
网站建设 2026/10/28 9:04:57

PostIn插件实现API文档实时同步的技术解析

1. 为什么我们需要实时同步的API文档&#xff1f;在前后端分离的开发模式下&#xff0c;API文档是团队协作的生命线。我经历过太多因为文档不同步导致的"车祸现场"&#xff1a;前端按旧文档开发完才发现接口字段全变了&#xff0c;测试同学拿着过时的文档用例执行无效…

作者头像 李华
网站建设 2026/10/28 9:04:29

深度学习训练三要素:batch/lr/epoch关系与调参实践

1. 深度学习训练中的三驾马车&#xff1a;batch/lr/epoch关系全解析在深度学习模型训练过程中&#xff0c;batch size&#xff08;批大小&#xff09;、learning rate&#xff08;学习率&#xff09;和epoch&#xff08;训练轮数&#xff09;这三个参数的组合直接影响着模型的收…

作者头像 李华
网站建设 2026/10/28 9:03:46

YOLOv10在安全锥检测中的优化与应用实践

1. 项目概述&#xff1a;当YOLOv10遇上安全锥检测去年夏天参与高速公路巡检项目时&#xff0c;我亲眼目睹了因安全锥摆放不规范引发的连环追尾事故。传统人工巡检方式在夜间或恶劣天气下存在明显盲区&#xff0c;这促使我开始探索基于YOLOv10的智能检测方案。这个开源项目整合了…

作者头像 李华