news 2026/9/23 18:44:31

ESP32分区级应用平台:像手机一样切换固件的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32分区级应用平台:像手机一样切换固件的实战指南

上电之后,串口终端打印出一个菜单:1号槽 LED_Blink,2号槽 温湿度采集,3号槽 WebServer_Demo。你没看错,这不是 Linux,是那颗 ESP32。输入 1,回车,单片机重启,几秒后 LED 按新的逻辑闪烁;下次想换功能,再按一下复位,菜单又回来了。整个过程不插 USB、不重新烧录,芯片里像是装了好几个应用,想用哪个启动哪个。这就是这个小项目的核心:在 ESP32 上做一个“分区级小型应用平台”,让开发板像手机一样“安装应用”。

这个项目解决的是我自己折腾 ESP32 时最大的痛点:每次换功能都要连电脑、改代码、编译、烧录,调试一个多功能板子时尤其崩溃。做出来之后,我可以在现场用串口菜单或手机网页直接切换应用,甚至能通过浏览器往空槽里传一个新的固件包,这在别人看来就像在“装 App”,实际原理是 ESP-IDF 的 OTA 分区机制加一个常驻 Launcher。今天把完整设计和踩坑记录都写出来,适合有一定 Arduino/ESP32 基础、想进一步理解分区与 OTA 机制的开发爱好者,也适合所有嫌烧录太麻烦的 DIY 玩家。

1. 这个项目的动机与边界:给 ESP32 装上“App Store”

1.1 反复烧录的痛点,做过三五个项目的人都懂

ESP32 是很流行的开发板,自带 WiFi 和蓝牙,Flash 空间通常是 4MB 起步,性能也够跑不少 IoT 应用。但我过去很长一段时间都把它当“高级单片机”用,每次改功能都要重新编译、连接 USB、手动烧录。这在一个项目从零到完成的过程中还算能接受,可一旦你手上同时维护好几个小项目,比如一个做 LED 氛围灯、一个做温湿度传感器、一个做网页服务器,你会发现一个很尴尬的情况:功能都没有坏,但你想在同一个板子上切换它们,只能一遍遍重新烧录。

刚开始我的应对办法是多买几块板子,一个项目用一块。但板子多了以后,找起来麻烦,而且每块板子还得单独配电源、外壳,桌面全是线。后来我做了一个网络摄像头项目,发现 ESP32 本身就有 OTA 升级能力,可以通过网络更新固件,于是我开始想:既然网络都能更新固件了,那么能不能更“暴力”一点——把 Flash 里划分出好几个槽位,每个槽位放一个完整可启动的固件,再做一个引导菜单,开机让你选跑哪一个。

本质上,手机“安装 App”是把应用安装到外部存储器,然后由操作系统调度运行。ESP32 因为资源限制,做不到真正意义上的多进程并发,但我也不需要它那么复杂。我需要的只是“多个固件轮流跑,切换尽量不要插线”,这就完全可以用分区方案解决。

1.2 平台化设计的边界:这不是操作系统,但能解决 80% 的折腾

在动手之前,我先把期望拉回地面。手机上的 App 是可以同时运行、后台切换的,而我要在 ESP32 上做的,其实更接近“多系统引导”,或者用嵌入式行业的话说,是多镜像分区管理。每个“应用”本质上是一个独立的 ESP32 固件镜像,里面可以包含 Arduino 代码、ESP-IDF 应用、MicroPython 固件等等,只要它能被引导并启动,就可以作为“应用”被安装。

这个方案有几个明显的约束:

  • 一个时刻只能跑一个应用,切换必须重启,没有后台运行。
  • 每个应用的大小受分区大小限制,我这次示例里每个槽位给 512KB,大多数小项目都够用,但跑带完整 Web UI 的大工程会有点紧。
  • 应用没有沙箱概念,它可以直接读写全部外设、甚至直接通过 esp_partition 读写其他分区,安全性靠自觉。

这些约束决定了它的定位:适合做开发调试、现场演示、多教室项目的“口袋多合一”,不适合做严肃的产品级动态更新。我心里很清楚,这个方案就像给开发板装了一个“简易抽屉”,它的价值不是技术上的突破,而是把开发中的切换成本降到最低。

1.3 为什么选 ESP32,而不是 STM32 或树莓派 Pico

好多人在评论区会问:能不能用 STM32 实现?能,但要别扭得多。STM32 的 Flash 也可以分区,也可以用 IAP 方式跳转引导多个固件,但 STM32 没有自带 WiFi/BLE,你要想从网页或手机传一个新应用进去,还得外接网络模块或者自己写 USB 协议。ESP32 天生就带 WiFi 和蓝牙,让“应用分发”这件事变得非常顺手。

还有一个很重要的点是 ESP-IDF 已经提供了比较完整的 OTA 基础设施。它内置了 otadata 分区用来记录“下次从哪个槽启动”,也有 esp_ota_set_boot_partition、esp_image_verify 这些 API,我不用自己从零造轮子。这比我早年在 STM32 上自己写 Flash 擦写跳转函数省了太多功夫。STM32 的优势是实时性、丰富的外设和低功耗场景,但在做“小型应用平台”这个玩法上,ESP32 的开发体验确实更合适。

1.4 和真正的 App Store 差在哪里

如果把这个项目介绍给不懂硬件的朋友,最准确的类比其实是:不是 Android 的 App Store,更像电脑上的“双系统引导器”。你在开机时选择一个系统进入,只是这里每个“系统”都很轻量,一共 4 个槽位,加起来也就 2MB 左右。

和真正的应用商店相比,我还没有做在线商店页面、版本管理、签名机制。现在能做到的是:

  • 开机进入 Launcher 菜单。
  • 在菜单里选定某个应用启动。
  • 通过 Web 页面上传新的应用镜像到指定空槽位。
  • 校验镜像合法性,非法的镜像不会被启动。

这就已经解决了“减少折腾”的核心问题。后面如果你想做得更完整,可以继续加网络下载、远程更新,甚至做一个简单的云端签名校验,我在文章最后会补充一些扩展思路。

2. 底层机制拆解:分区表、启动链路与镜像校验

2.1 Flash 分区表是什么,为什么它决定整个方案

ESP32 的 Flash 不是一整块乱用的,而是通过一张“分区表”来划分区域。分区表里每行定义一个分区的名字、类型、子类型、起始偏移地址和大小。默认情况下,一套常见的 Arduino 分区表大概是这样的:

分区名类型子类型说明
nvsdatanvs存储密钥、校准数据、NVS 变量
otadatadataota记录当前/下次 OTA 启动槽
app0appfactory默认出厂应用,烧录时的默认入口
app1appota_0OTA 应用槽 0
spiffsdataspiffs文件系统存储

bootloader 上电后会读取 otadata 分区,根据里面的启动信息决定加载 factory 还是某个 ota 槽。这才是整个方案的“地基”:如果把应用槽命名为 ota_0、ota_1、ota_2、ota_3,那么标准 bootloader 就能直接识别并引导它们,不需要改 bootloader 源码。

这里补充一个概念:otadata 是 ESP-IDF OTA 机制里的“指针记录”,它只有 8KB,不存应用内容,只存“下次该启动谁”的索引。你可以通过esp_ota_mark_app_valid_cancel_rollback等函数控制回滚,也可以直接用esp_ota_set_boot_partition指定下次启动的分区。我做的 Launcher 大量依赖这些 API。

2.2 我给 4MB Flash 做的分配方案

为了照顾最常见的 4MB Flash 开发板,我把分区表设计成如下样子:

# Name, Type, SubType, Offset, Size nvs, data, nvs, 0x9000, 0x5000 otadata, data, ota, 0xE000, 0x2000 phy_init, data, phy, 0xF000, 0x1000 factory, app, factory, 0x10000, 0xC0000 ota_0, app, ota_0, 0xD0000, 0x80000 ota_1, app, ota_1, 0x150000, 0x80000 ota_2, app, ota_2, 0x1D0000, 0x80000 ota_3, app, ota_3, 0x250000, 0x80000 spiffs, data, spiffs, 0x2D0000, 0x130000

简单算一下空间:

  • factory 分区也就是 Launcher,占用 0x10000 到 0xD0000,大小 0xC0000,等于 768KB,足够放一个带 Web 上传和 BLE 传输的引导程序。
  • 四个 ota 槽位,每个 512KB,从 0xD0000 一直排到 0x2D0000,一共 2MB,正好可以装下四五个小型应用。
  • 最后从 0x2D0000 到 0x400000 留了约 1.2MB 给 spiffs 文件系统,应用运行时可以把配置、图标或日志写进去。

选择 512KB 一个槽位是我实际测试后的权衡:一个带 WiFi 的 Arduino 应用,编译产物通常在 200KB 到 400KB 之间,512KB 比较安全;如果扩大槽位,Launcher 空间就会变小,或者总槽位数减少。对于 4MB Flash 的开发板,这个比例是我试下来比较均衡的。

如果你手头是 8MB / 16MB Flash,可以把每个槽位扩大到 1MB 甚至 1.5MB,分区表结构完全不用变,只需调整偏移即可。

2.3 启动链路:上电之后到底发生了什么

很多初学者第一次看到分区表会疑惑:bootloader 怎么知道先跑哪个?实际流程比想象中简单:

  1. 芯片上电后,ROM 里固化的启动代码跳到 0x1000 地址的一级 bootloader。
  2. 一级 bootloader 读取分区表,并根据 otadata 里的信息决定加载哪个 app 分区。
  3. 如果 otadata 无效或为空,默认加载 factory 分区。
  4. factory 分区就是我们的 Launcher,它扫描所有 ota_0 到 ota_3 槽位,把状态和菜单打印出来。
  5. 用户选择某个应用后,Launcher 调用esp_ota_set_boot_partition把目标槽记录到 otadata,然后主动esp_restart
  6. 再次上电,bootloader 发现 otadata 指向 ota_1,于是直接启动 ota_1 里的应用。

这套链路唯一的“机关”就是 Launcher 在每次选择后都要写 otadata 并重启,因为 ESP32 的 bootloader 一般不支持运行时跨分区跳转。虽然能做到不重启直接用app_main加载,但会破坏应用的堆栈和初始化假设,非常容易遇到玄学问题。所以我的方案是“宁可重启几十毫秒,也不要在内存里硬跳”。

2.4 镜像校验:防止把半截文件写进 Flash

如果你曾经用手机下载安装包,一定碰到过“空间不足”或者“下载失败”的提示,系统不会让你安装一个残缺的 APK。在 ESP32 上,这个“检查完整性”的角色由esp_image_verify承担。它会在启动前检查分区头部、段表、App Descriptor,甚至计算哈希,确认这个镜像格式正确、内容完整。

我在 Web 上传功能里,每次写完整个镜像后都会调用esp_image_verify,只有校验通过才更新菜单里的状态。这样即使传输中途断网,或者写入时突然断电,也不会在重启时走进一个坏分区导致“砖机”。你可能会觉得多此一举,但我就因为测试时断电,把某个槽写成了半截,后来每次启动那个槽都会 crash,排查了很久才发现是镜像校验没做。

3. 代码实操:从零写出一个可用的 Launcher

3.1 开发环境准备与自定义分区表生效

我用的是 Arduino + 官方 esp32 支持包的组合,因为社区资料最多,坑相对少。开发环境可以选 Arduino IDE 也可以选 PlatformIO,关键步骤是让编译系统使用我们自定义的分区表。

在 Arduino IDE 里,只要把上面那段分区表存成一个partitions.csv文件,然后在Tools → Partition Scheme里选择Custom Partition Table,并确认Flash Size选的是4MB (Default)或对应你开发板的实际大小。如果你用 PlatformIO,直接在platformio.ini里加一行:

board_build.partitions = partitions.csv

很多第一次做这个项目的人都会忘记切换分区方案,结果烧进去以后发现 Flash 布局还是默认的,Launcher 里扫描不到任何 ota 槽位。这一点我在文章后面还会重点提。

Launcher 本身建议用普通 Arduino 工程编译,不需要魔改任何库。核心依赖是esp_partition.hesp_ota.h以及一个 Web Server 库。我推荐ESPAsyncWebServer,因为它支持异步接收请求体,配合我们的“边收边写 Flash”策略最方便。

3.2 实现应用列表与菜单选择

Launcher 最重要的任务是扫描分区并让用户选择启动哪个应用。下面是精简版的 Arduino 代码:

#include <esp_partition.h> #include <esp_ota.h> const char* slot_names[4] = { "ota_0", "ota_1", "ota_2", "ota_3" }; void printAppMenu() { Serial.println("\n========== ESP32 App Launcher =========="); for (int i = 0; i < 4; i++) { const esp_partition_t* part = esp_partition_find_first( ESP_PARTITION_TYPE_APP, static_cast<esp_partition_subtype_t>(ESP_PARTITION_SUBTYPE_APP_OTA_MIN + i), NULL); if (!part) { Serial.printf("%d: Invalid slot\n", i); continue; } // 简单判断分区里有没有合法镜像 bool is_valid = esp_image_verify(ESP_IMAGE_VERIFY, NULL, part) == ESP_OK; Serial.printf("%d: %-12s size=0x%X %s\n", i, slot_names[i], part->size, is_valid ? "[ok]" : "[empty]"); } Serial.println("Input slot number to boot, 'U' to upload via Web"); } esp_partition_subtype_t slotToSubtype(int slot) { return static_cast<esp_partition_subtype_t>(ESP_PARTITION_SUBTYPE_APP_OTA_MIN + slot); }

这里有两个地方需要解释。

第一,ESP_PARTITION_SUBTYPE_APP_OTA_MIN + i是枚举 ota_0、ota_1、ota_2、ota_3 的标准做法,它要求分区表里的子类型必须连续,并且从ota_0开始。我最初偷懒直接写死字符串"ota_1"然后用esp_partition_find_first查,也可以,但不如这种索引方式干净。

第二,esp_image_verify不仅用来校验上传后的文件,也可以用来判断一个槽位当前是否已经有可启动的镜像。如果分区还没写过,或者写坏了,这个函数会返回错误,菜单里就显示[empty],这样你就不会选定一个坏槽。

选择启动的代码更简单:

void bootSlot(int slot) { esp_partition_subtype_t subtype = slotToSubtype(slot); const esp_partition_t* target = esp_partition_find_first( ESP_PARTITION_TYPE_APP, subtype, NULL); if (!target) { Serial.printf("Slot %d not found\n", slot); return; } esp_err_t err = esp_ota_set_boot_partition(target); if (err != ESP_OK) { Serial.printf("Set boot partition failed: %s\n", esp_err_to_name(err)); return; } Serial.println("Rebooting into selected app..."); delay(200); esp_restart(); }

esp_ota_set_boot_partition是 ESP-IDF 提供的标准接口,它会写 otadata,让 bootloader 下一次启动时加载目标分区。

3.3 通过 Web 上传一个“应用安装包”

菜单只是第一步,真正让这个平台变得好玩的是能通过 WiFi 往空槽里装新应用。我在这里选了 ESPAsyncWebServer,因为普通 WebServer 在处理大文件上容易导致内存紧张,而异步库可以在回调里把数据分块写进 Flash。

核心逻辑是注册一个PUT请求,接收方根据 URL 参数slot判断目标槽位,然后把请求体里的二进制内容按 4KB 的粒度擦写分区。

#include <ESPAsyncWebServer.h> #include <esp_partition.h> AsyncWebServer server(80); void setupWebUpload() { server.onRequestBody([](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) { if (!request->hasParam("slot")) { request->send(400, "text/plain", "Missing slot"); return; } int slot = request->getParam("slot")->value().toInt(); if (slot < 0 || slot > 3) { request->send(400, "text/plain", "Bad slot"); return; } esp_partition_subtype_t subtype = slotToSubtype(slot); const esp_partition_t* part = esp_partition_find_first( ESP_PARTITION_TYPE_APP, subtype, NULL); if (!part) { request->send(400, "text/plain", "Partition not found"); return; } // 每次第一个分片到达时,先整体擦除目标分区 if (index == 0) { esp_partition_erase_range(part, 0, part->size); Serial.printf("Erasing slot %d, size=0x%X\n", slot, part->size); } // 按 4KB 扇区边界擦除,然后写入 if (index % 0x1000 == 0) { esp_partition_erase_range(part, index, 0x1000); } esp_partition_write(part, index, data, len); Serial.printf("Written %u bytes at offset %u / %u\n", len, index, total); // 最后一个分片到达,做整体校验 if (index + len == total) { bool ok = esp_image_verify(ESP_IMAGE_VERIFY, NULL, part) == ESP_OK; if (ok) { request->send(200, "text/plain", "OK"); } else { request->send(400, "text/plain", "Invalid image"); } } }); server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(200, "text/html", "<h2>ESP32 App Upload</h2>" "<form id='f'>" "Slot: <select name='slot'>" "<option value='0'>0</option><option value='1'>1</option>" "<option value='2'>2</option><option value='3'>3</option>" "</select><br>" "<input type='file' name='bin' id='bin'><br>" "<button type='button' onclick='upload()'>Install</button>" "</form>" "<script>" "function upload(){" "let f=document.getElementById('bin').files[0];" "let s=document.querySelector('[name=slot]').value;" "fetch('/upload?slot='+s,{method:'PUT',body:f}).then(r=>r.text().then(t=>alert(t))).catch(e=>alert('ERR '+e));" "}" "</script>"); }); server.begin(); }

这套流程的实际体验是:电脑连上 ESP32 发射的热点,打开192.168.4.1,选一个槽,选择一个app.bin,点上传,几秒钟后提示OK。然后回到串口菜单选择那个槽,应用就“装”上了。

需要提醒的是,ESP32 的 Flash 擦写是有磨损寿命的,我为了测试反复擦写了同一个槽几百次,目前还没有出问题,但量产场景必须考虑寿命问题。在做实验时,建议尽量把常用应用放固定槽,减少频繁整片擦写。

3.4 BLE 上传的思路

除了 Web,还可以通过蓝牙 BLE 上传,因为 ESP32 自带经典蓝牙和低功耗蓝牙。我在早期版本里用 BLE 做过一次:把 BLE 建立一个 Write characteristic,手机端用 nRF Connect 这类工具连接后,选择固件文件,通过 Write Long 分块写入。

BLE 的好处是不需要电脑也没有路由器,手机就能操作,适合现场演示。坏处是 MTU 通常很小,速度比 WiFi 慢不少,而且要处理长包分段、粘包等问题,代码量会多不少。如果你的应用场景主要是“电池供电的野外设备”,可以考虑 BLE;如果只是想省事,Web 比 BLE 实用得多。

3.5 让应用能“返回桌面”

手机 App 一般都有返回按键,但是 ESP32 应用自己跑起来之后,如果不做一个通行的返回机制,你只能复位物理按键——而复位后 bootloader 还是会根据 otadata 启动上次选中的应用,回不到 Launcher。

解决办法是给每个应用加一个“返回 Launcher”函数。下面是标准代码,直接放到应用的setup()或按键回调里:

#include <esp_partition.h> #include <esp_ota.h> void backToLauncher() { Serial.println("Returning to Launcher..."); const esp_partition_t* launcher = esp_partition_find_first( ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, NULL); if (launcher) { esp_ota_set_boot_partition(launcher); } delay(200); esp_restart(); }

我在所有测试应用里,习惯用一个按钮接在某个 GPIO 上,长按 2 秒调用这个函数。这样 Launcher → 应用 → Launcher 的循环就跑通了,整个平台的操作逻辑和手机桌面几乎一致。

4. 实测记录:装两个 App 并用菜单切换

4.1 准备测试应用

为了验证平台,我准备了两个小应用。

第一个是传统的 LED 闪烁,没什么特殊之处,但结构上能说明问题。我加了backToLauncher,把 GPIO2 接一个按键,按下去就返回 Launcher。编译后生成的二进制文件叫LED_Blink.ino.bin,大小约 180KB。

第二个是我在项目热词里经常提到的 WebServer Demo:启动后连上局域网,提供一些 JSON 接口并控制一个 GPIO 点灯。这个应用编译出来接近 280KB,放在 512KB 槽位里很宽裕。

4.2 烧录 Launcher 并上传应用

第一步,把自定义partitions.csv和 Launcher 工程一起编译、烧录。注意烧录后首次启动,四个应用槽都是空的,菜单会显示[empty]

第二步,启动 Web 上传服务。Launcher 上电后启动 WiFi AP,热点名是ESP32-AppLauncher,电脑连接后访问192.168.4.1,选择 slot 0 和LED_Blink.ino.bin,点击安装。上传过程中串口会输出分片写入日志,几秒后页面提示OK

第三步,把第二个应用装到 slot 1。这里有一个小细节:同一台电脑如果之前连的是路由器,需要断开再连回 AP,或者让 Launcher 同时连接已有 WiFi 并且你与开发板在同一局域网内。为了简单我直接开了 AP 模式。

4.3 启动切换实测

上传完成后复位 Launcher,串口输出大概是:

========== ESP32 App Launcher ========== 0: ota_0 size=0x80000 [ok] 1: ota_1 size=0x80000 [ok] 2: ota_2 size=0x80000 [empty] 3: ota_3 size=0x80000 [empty] Input slot number to boot, 'U' to upload via Web

输入0,开发板重启,LED 开始闪烁。进入应用后,我按下 GPIO2 键,串口打印 “Returning to Launcher…” 并重启,菜单再次出现;输入1再重启,进入 WebServer Demo,手机浏览器访问 ESP32 的 IP 就能看到测试页面。

这次实测让我最满意的一点是:从“决定切换功能”到“进入另一个应用”,整个过程只需要一个串口输入或一次按键,不碰 USB,不碰 IDE,也不重新编译。

4.4 实测过程中的资源占用

Launcher 本身包含菜单、OTA、Web Server、分区扫描,编译后大约 260KB,放在 768KB 的 factory 分区里绰绰有余。运行内存方面,Launcher 启动后空闲堆大约 170KB 到 190KB,处理 4KB 一包的上传不会出现内存吃紧。

实际上,如果你愿意把 Web Server 换成极简铁板烧式的原生 TCP 服务器,Launcher 还能再瘦身。但考虑到开发效率和可维护性,我都尽量使用成熟库,这比无谓地压空间更重要。

5. 避坑指南:这个方案最容易翻车的几个地方

5.1 自定义分区表没有生效

这是新手最容易踩的坑。你在 Arduino IDE 里存了自定义 CSV,也填好了partitions.csv路径,但烧录后菜单里依然扫描不到任何 ota 槽,或者启动地址错误直接无法开机。

解决办法有两步。第一步,检查 Tools 菜单里的Partition Scheme是否选中了Custom Partition Table,如果没有,Arduino 会继续用默认分区表。第二步,区分“Bootloader + Partition Table + App”三种 bin,烧录时不要只烧 App,开机会跑飞。最稳妥的方法是先用 Arduino IDE 的Sketch → Export Compiled Binary导出 bin,再用esptool.py或者支持“合并烧录”的 GUI 工具,把 bootloader、partition table、Launcher 一起写入指定地址。如果你对烧录方式不熟,建议看一遍官方的烧录地址表,避免分区表地址写错。

5.2 Web 上传中断导致镜像损坏

我在最初版本里没有做esp_image_verify,结果测试时中途拔掉了网线,或者网页上传到一半浏览器自动断开,槽位就写了一半。之后启动到那个槽位,bootloader 会提示“invalid app image”,然后继续回退到 Launcher,也就是说不会直接变砖,但它会反复启动失败,看起来像卡死。

解决办法就是文章里写的:上传完成后必须调用esp_image_verify。同时我还养成了一个习惯:上传前在上位机端先记录原始二进制的 MD5,上传后再从 Flash 读出来算一次 MD5,两边对比。虽然esp_image_verify已经能证明“可以启动”,但 MD5 能更直观地确认字节完全一致。在关键应用上,我强烈建议两者都做。

5.3 Flash 擦写顺序与寿命问题

ESP32 的 Flash 擦写不是按字节擦的,而是按扇区(通常 4KB)擦。我在上传代码里是先从 offset 0 开始,按 4KB 粒度一边擦一边写。这里有个细节:esp_partition_erase_range的起始地址和长度必须是 4KB 对齐,否则会返回错误。如果你传的 bin 长度不是 4KB 的整数倍,最后几个字节也是安全的,因为写入不要求按扇区对齐,只要分区边界对齐就行。

寿命问题也要多说两句。Flash 的擦除次数通常是 1 万到 10 万次,具体取决于芯片。我为了测试分区表、上传功能,两个星期里擦了同一个槽可能有三四百次,目前还能跑,但这个方向只能用于开发调试。如果做成产品,我建议把常用功能做成只读,把动态更新的槽位控制在少数几次更新以内,或者改用 8MB / 16MB 的 Flash,并在上层做版本限制。

5.4 常见问题速查表

症状可能原因排查与修复
启动后只跑 Launcher,应用菜单全是 empty分区表没换成自定义方案确认 Tools → Partition Scheme = Custom Partition Table
上传后提示 Invalid image文件不是完整 app bin,或传输被中断重新导出 bin;换一条稳定的 WiFi;增加 MD5 校验
选择了某个 slot,启动后反复重启该槽镜像损坏或分区地址越界esp_image_verify检查;确认 bin 大小不超过分区容量
Web 上传大文件时 OOM把整个文件读进了内存改成分片写入,不要用普通 WebServer 一次性读取 body
应用跑起来后没有返回菜单的方法应用里没加返回 Launcher 的逻辑在应用里调用esp_ota_set_boot_partition(factory)后重启
WiFi 上传速度太慢AP 模式信道干扰或者 MTU 设置不合适切换到 STA 模式并和电脑在同一个局域网;或者用以太网扩展模块 LAN8720,把上传服务架在有线网络上

5.5 可以继续扩展的方向

这个平台做完以后,我自己又往里加了几样东西:一个是局域网 OTA,Launcher 主动连接家里的路由器,不是开 AP,这样手机和电脑都不用切换网络,体验更顺;另一个是把 Launcher 的串口菜单做成 Web 页面,既能看状态也能在网页上直接点选启动哪个应用。

如果你研究网络拓扑,还可以把 WiFi Mesh 的思路加进来:多个 ESP32 节点都刷同一套 Launcher,通过 Mesh 网络分发应用镜像,这样就连每台设备单独连接路由器都不用了。对于分布在多个房间的设备,这套玩法特别省事。另外,如果有项目必须用有线网络,LAN8720 以太网模块搭配 ESP32 也很常见,Launcher 里可以加一个有线优先的判定逻辑,局域网内做远程批量更新会稳定很多。

我的几点体会

做完这个项目,再回头看最初的问题“ESP32 能不能像手机一样安装应用”,我的答案是:能,但要让它的“应用”跑在独立的 Flash 分区里,而不是像手机那样动态加载到内存。分区级方案换来了稳定和隔离:某个应用崩溃了,拉闸复位,Launcher 还在;上传写坏了某个 slot,只影响那个槽,不影响其他应用。代价是切换必须重启,而且不能用系统调用去共享外设,但对于绝大多数单机 IoT 项目,这已经够了。

我目前把常玩的几个小项目都烧进了同一块板子,现场演示的时候不用带笔记本,只带一块板子和手机。你需要什么功能,就开机选对应的“应用”。这个思路放在开发调试阶段尤其好用,你甚至可以把它当成一个简易的“固件备份管理工具”,把好的版本留在某个槽里,随时切回去。最后想提醒一句:折腾归折腾,量产产品别这么玩,Flash 寿命和引导安全性都需要更严肃的设计。如果你也做了类似的 ESP32 应用平台,欢迎告诉我你踩过的最离谱的坑。

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

3分钟搞懂战地2mod,面试必问底层逻辑不丢分

3分钟搞懂战地2mod,面试必问底层逻辑不丢分 面试被问原理答不上来,是应届生最尴尬的时刻。很多兄弟觉得战地2mod这种老游戏跟后端开发没关系,但面试官考察的其实是你对底层状态机、内存管理以及多线程同步的理解。这不仅是游戏Mod的问题,更是考察你是否具备拆解复杂系统能力的试金石。在 面试必问…

作者头像 李华
网站建设 2026/9/23 18:44:18

3个坑搞定十渠源码解析:告别报错堆栈

3个坑搞定十渠源码解析:告别报错堆栈 报错信息像天书?StackTrace 一长串让你头大?别慌,今天咱们不背八股文,直接钻进 十渠 的 源码解析 里,看看到底是哪里断了线。 刚接触水利工程信息化或者相关后端开发的朋友,经常遇到一个尴尬局面:业务逻辑明明写对了,一跑起来就是 NullPointer…

作者头像 李华
网站建设 2026/9/23 18:44:10

3步搞定buildingblocks.dotx源码速查手册

3步搞定buildingblocks.dotx源码速查手册 版本升级后 API 全变了,文档还是老的,代码直接报错。这种抓心挠肝的时刻,谁不想有一本 buildingblocks.dotx 速查手册?别急,咱们不背文档,直接拆解核心逻辑,把底层原理吃透。 入口定位与痛点直击 很多开发者一上来就找…

作者头像 李华
网站建设 2026/9/23 18:44:08

面积转换避坑指南:3个优化让百万级数据快10倍

面积转换避坑指南:3个优化让百万级数据快10倍 别再说“概念都懂,一写代码就崩”了。我见过太多人,背熟了平方米转公顷的进率,但真到了处理几十万条土地测绘数据时,程序直接卡死,或者算出来的结果差出几毛钱,最后还得返工重跑。…

作者头像 李华
网站建设 2026/9/23 18:44:03

麦克风有电流怎么消除一文搞懂:3行代码解决采样噪声痛点

麦克风有电流怎么消除一文搞懂:3行代码解决采样噪声痛点 面试被问“音频采集为什么总有滋滋声”,你只能回答“加个滤波”?面试官皱眉,心里给你打上了“不懂底层”的标签。别慌,这不是你的错,90%的开发者都卡在“现象”层面,没摸到“数据流”的骨头。今天这篇长文,咱们不聊玄学,直接扒开麦克风驱动和音频处理库…

作者头像 李华
网站建设 2026/9/23 18:44:01

系统测试包括哪些内容保姆级教程:从跑不通到稳定交付

系统测试包括哪些内容保姆级教程:从跑不通到稳定交付 刚把同事发来的测试脚本复制进项目,运行报错一堆?或者看着满屏的“Error”完全不知道从哪下手调?别慌,这就是很多开发者接手新项目时的噩梦。很多教程只讲“怎么跑”,不讲“为什么跑不通”,导致你只能盲目改代码。这篇保姆级教程,直接带你拆解系统测试的核…

作者头像 李华