☰
ESP32本地OTA升级实战:基于HTTP Server的固件更新实现
2026/10/16 8:24:49 网站建设 项目流程

简介:这是一份基于ESP-IDF的ESP32本地OTA升级例程,思路对标Arduino生态的OTAWebUpdater,面向需要实现局域网固件维护的嵌入式开发者,解决无公网服务器条件下的设备升级问题。例程完整演示了AP与STA两种WiFi模式的连接初始化:既可在AP模式下让设备自身开启热点,也支持STA模式接入现有路由器,两种方式都无需外部OTA服务器。设备上直接运行轻量HTTP服务器(监听89端口),提供固件上传页面、POST文件接收以及当前固件信息查询三类URI;上传页采用原生JavaScript编写,无需额外框架,即可展示上传进度、速度和错误信息,交互直观。固件诊断程序通过检测GPIO2引脚电平确认新固件是否正常启动,若失败会自动回滚到上一版本,同时附带BuildVer.sh脚本,在编译时依据时间自动生成版本号,便于追踪每次构建;代码模块划分清晰,网络初始化、HTTP服务、诊断回滚和版本号生成各自独立,方便按需裁剪或二次开发。zip压缩包内共1116个文件,以编译中间产物(obj/a)、CMake与Ninja构建配置、c/cpp/h源码为主,另含bin/elf固件、html页面、shell脚本及sdkconfig等,整包约35.37MB,目录按功能模块组织、结构清晰,导入ESP-IDF即可对照研究,适合初中级开发者快速上手。目前已有3814人学习下载,这套例程可帮助开发者节省从零搭建OTA流程的时间,无论产品原型验证还是产线批量维护,都提供了可落地的参考。

1. 从拆机刷固件到网页点一下就升级:ESP32 本地 OTA 的落地姿势

搞过 ESP32 量产项目的开发者,大概率经历过这种场景:设备已经装进外壳、接好线,却发现固件有个小 bug 要修。拆机、连 USB、重新编译烧录,一套下来十分钟起步,要是设备嵌在墙里或机柜中,折腾半小时也不奇怪。这个例程解决的就是这个痛点——把 ESP32 变成一个 HTTP Server,通过浏览器上传新的固件 bin 文件,设备自己完成分区擦写、固件写入、校验和重启,全程不需要拆机也不需要连调试器。

这个项目基于 ESP-IDF 实现,思路和 Arduino 生态里那个经典的 OTAWebUpdater 例程一致,但换成了 ESP-IDF 的组件体系和 esp_ota_ops 接口。适合两类人:一类是手头有 ESP-IDF 工程、想给设备加 OTA 能力的嵌入式开发者;另一类是刚接触 ESP-IDF、想知道 HTTP Server 怎么和 OTA 分区配合工作的新手。读完这篇文章,你能跑通一个完整的本地 OTA 流程,还能直接把它嵌进自己的工程里。

2. 技术选型和整体架构:为什么用 HTTP Server 模式做本地 OTA

2.1 本地 OTA 三种实现路径的取舍

ESP32 做 OTA 大体有三条路:一是走云端,用 esp_https_ota 组件从服务器拉固件;二是走蓝牙,通过 GATT 通道传固件;三就是本主题——设备自己开一个 HTTP Server,浏览器通过局域网访问上传固件。三条路的适用场景完全不同。

云端 OTA 适合产品化设备,但前期要有服务器、要有设备认证、要处理断点续传和大规模分发,对一个内网工具型设备来说太重了。蓝牙 OTA 适合没有 Wi-Fi 模块的场景,但需要配合手机 App,开发量也不小。HTTP Server 模式是三者里最轻量的:设备连上路由器,浏览器打开一个网页,选固件、点上传,完事。它不依赖外网,固件文件在本地传输,速度受限于 Wi-Fi 和 HTTP 缓冲区配置,但足够快。

这个例程的定位非常明确——面向开发调试和产线维护。开发者把设备放在办公桌上,浏览器直接连设备 IP 就能刷固件;产线工人不需要懂串口工具,打开网页点两下就能完成固件灌装。所以 HTTP Server 模式不是云端 OTA 的替代品,而是补上了「不想拆机、不想装驱动、不想配服务器」这个中间地带的需求。

2.2 ESP-IDF 方案和 Arduino OTAWebUpdater 的差异

用过 Arduino 的开发者对 OTAWebUpdater 应该不陌生:一个 ESP8266/ESP32 的示例,直接编译烧进去,配好 Wi-Fi,浏览器访问设备 IP 就能上传固件。它本质是一个 WebServer + Update 库的简单组合,几个回调函数就搞定了。

ESP-IDF 版实现思路一致,但组件划分更细。HTTP 部分用的是 esp_http_server 组件,而不是 Arduino 的 WebServer 库;OTA 部分用的是 esp_ota_ops 和 esp_ota_ops.h 里的接口,而不是 Update 库。这两者的差异决定了代码结构的不同。esp_http_server 需要注册 URI 处理函数,请求进来后由框架分发到对应的 handler;esp_ota_ops 则要求开发者自己管理 OTA 分区的 begin、write、end 三个阶段,不像 Update 库那样封装得严实。

但反过来,ESP-IDF 的灵活性也更高。你可以自定义上传页面的样式,可以在固件写入前做自定义校验,可以在写入过程中实时回调进度给前端,甚至可以加入多分区回退逻辑。Arduino 的 Update 库实现的是「能用」,ESP-IDF 的 esp_ota_ops 实现的是「可控」。

2.3 整体架构:一个请求从进入到固件生效的完整链路

这个例程的架构可以用一条链路来理解。设备上电后,Wi-Fi 连接路由器获取 IP,同时启动 HTTP Server,监听 80 端口。浏览器访问设备 IP,HTTP Server 返回一个带文件选择框的上传页面。用户选中固件 bin 文件并点击上传,浏览器以 multipart/form-data 格式发起 POST 请求。

HTTP Server 的固件上传 handler 开始接收请求体,数据到达后在回调里做两件事:一是解析 HTTP 头,确认文件名和内容长度,二是把收到的数据逐个字节块写入 OTA 分区。写入过程中要注意对齐——esp_ota_write 要求每次写入的 buffer 大小按 4 字节对齐,这和 SPI Flash 的扇区擦写特性有关。

数据全部写完后,调用 esp_ota_end 结束这次 OTA 会话,它会做固件校验,确认整个镜像没有被截断或篡改。校验通过后,调用 esp_ota_set_boot_partition 把启动分区切换为新固件所在的 OTA 分区,然后调用 esp_restart 重启。设备第二次启动时,bootloader 读取分区表,从新的 OTA 分区启动,新固件生效。

3. 核心模块拆解:HTTP 上传与 OTA 写入的完整代码实现

3.1 分区表配置:OTA 能用的前提

在写任何 OTA 代码之前,先要把分区表配好。ESP32 的出厂固件一般用的是默认的单 App 分区表,只有一个 factory 分区和一个 app 分区,这种配置跑普通应用没问题,但做 OTA 是跑不通的——因为没有第二个用于存放新固件的 OTA 分区。

这个例程用的是带 OTA 支持的分区表,典型配置如下:

# 分区表文件 partitions_ota.csv # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, phy_init, data, phy, 0x10000, 0x1000, factory, app, factory, 0x20000, 0x180000, ota_0, app, ota_0, 0x1A0000,0x180000, ota_1, app, ota_1, 0x320000,0x180000,

这个分区表里最关键的是三部分:factory 分区存放出厂固件,ota_0 和 ota_1 是两个交替使用的 OTA 分区,otadata 分区专门记录当前应该从哪个 OTA 分区启动。

ota_0 和 ota_1 各占 1.5MB,如果项目编译出来的固件比较大,需要把 Size 调大。esp-idf 默认的 4MB Flash 下,factory 加两个 ota 分区各 1.5MB 是能放下的,但如果你的项目启用了大量组件导致固件超过 1.5MB,就要压缩分区比例或者换用 8MB/16MB Flash 的模组。

注意 proteus 这个分区表是通过 menuconfig 里配置的。在工程目录下执行idf.py menuconfig,找到 "Partition Table" 选项,选择 "Custom partition table CSV" 并指定 csv 文件名。这里有一个常见的坑:分区表文件名填错或者路径不对,编译不会报错,但烧录后启动会直接 panic,提示 partition 无效。

3.2 HTTP Server 注册:接收固件上传的端点

这套例程的 HTTP 部分用 esp_http_server 实现,核心是注册两个 URI 处理函数:

#include "esp_http_server.h" static httpd_handle_t server = NULL; /* 处理 GET / :返回上传页面 */ static esp_err_t index_handler(httpd_req_t *req) { const char* resp_str = "<!DOCTYPE html>" "<html><body>" "<h2>ESP32 OTA Updater</h2>" "<form method=\"POST\" action=\"/ota\" enctype=\"multipart/form-data\">" "<input type=\"file\" name=\"firmware\" accept=\".bin\">" "<input type=\"submit\" value=\"Upload\">" "</form></body></html>"; httpd_resp_set_type(req, "text/html"); httpd_resp_send(req, resp_str, strlen(resp_str)); return ESP_OK; } /* 启动 HTTP Server */ void start_ota_server(void) { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = 80; config.recv_wait_timeout = 15; config.max_uri_handlers = 8; if (httpd_start(&server, &config) == ESP_OK) { httpd_uri_t index_uri = { .uri = "/", .method = HTTP_GET, .handler = index_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &index_uri); httpd_uri_t ota_uri = { .uri = "/ota", .method = HTTP_POST, .handler = ota_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &ota_uri); } }

index_handler 返回的是一个最简单的 HTML 表单,表单的 action 指向 /ota,method 是 POST,enctype 是 multipart/form-data——这个是文件上传的标准编码方式。

start_ota_server 里两个关键参数要注意:recv_wait_timeout 控制 HTTP 接收数据的超时时间,固件文件比较大时如果这个值设得太小,比如默认的 5 秒,传个大文件很容易因为接收超时被断开;max_uri_handlers 决定最多能注册多少个 URI handler,这个例程用了两个,但如果后续要加进度查询接口、加版本检查接口,就需要调大。

HTTPD_DEFAULT_CONFIG() 宏会给 config 结构体填入一组默认值,server_port 默认是 80,但注意如果你已经在工程其他地方占用了 80 端口(比如配了别的 HTTP 服务),启动会失败,log 里会报 web server bind 端口失败。

3.3 OTA 写入:核心缓冲区对齐与逐块写入

接下来是这个例程的主菜——OTA handler。这段代码负责接收 POST 请求里的固件数据,写入 OTA 分区:

#include "esp_ota_ops.h" #include "esp_partition.h" #define BUFFER_SIZE 2048 static esp_err_t ota_handler(httpd_req_t *req) { esp_ota_handle_t ota_handle; const esp_partition_t *update_partition = NULL; char ota_buff[BUFFER_SIZE] = {0}; int content_len = req->content_len; int remaining = content_len; int received = 0; int total_written = 0; /* 找到下一个可用的 OTA 分区 */ update_partition = esp_ota_get_next_update_partition(NULL); if (update_partition == NULL) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No OTA partition"); return ESP_FAIL; } /* 开始 OTA 会话,擦除目标分区并准备写入 */ esp_err_t err = esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &ota_handle); if (err != ESP_OK) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed"); return ESP_FAIL; } /* 循环读取 HTTP 请求体,写入 OTA 分区 */ while (remaining > 0) { int read_len = httpd_req_recv(req, ota_buff, MIN(remaining, BUFFER_SIZE)); if (read_len < 0) { if (read_len == HTTPD_SOCK_ERR_TIMEOUT) { /* 超时,继续读取 */ continue; } esp_ota_abort(ota_handle); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Receive timeout"); return ESP_FAIL; } if (read_len > 0) { /* 写入 OTA 分区,注意对齐 */ err = esp_ota_write(ota_handle, ota_buff, read_len); if (err != ESP_OK) { esp_ota_abort(ota_handle); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA write failed"); return ESP_FAIL; } total_written += read_len; remaining -= read_len; /* 打印进度,方便串口观察 */ ESP_LOGI(TAG, "Progress: %d/%d bytes", total_written, content_len); } } /* 结束 OTA 会话,内部会做固件完整性校验 */ err = esp_ota_end(ota_handle); if (err != ESP_OK) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA end failed"); return ESP_FAIL; } /* 设置新固件为启动分区 */ err = esp_ota_set_boot_partition(update_partition); if (err != ESP_OK) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Set boot partition failed"); return ESP_FAIL; } httpd_resp_sendstr(req, "OTA success, rebooting..."); vTaskDelay(pdMS_TO_TICKS(1000)); esp_restart(); return ESP_OK; }

整个写入流程分四个阶段,对应 esp_ota_ops 提供的四个核心接口。

esp_ota_get_next_update_partition 的作用是查找当前没在运行的那个 OTA 分区。因为 ESP32 的 OTA 机制是交替使用 ota_0 和 ota_1,当前从 ota_0 启动,它就返回 ota_1;当前从 ota_1 启动,它就返回 ota_0。还有一个隐含的逻辑:如果设备第一次从 factory 启动,没有 OTA 历史,它默认返回 ota_0。

esp_ota_begin 会擦除整个目标分区,然后建立一个 OTA 句柄。这里有个参数 OTA_WITH_SEQUENTIAL_WRITES,意思是顺序写入,写入的偏移量由系统管理。如果传 0,则需要自己管理写入偏移,复杂度高很多,不建议新手碰。

esp_ota_write 就是逐块写入。写入 buffer 的大小选择有讲究:太小了,比如 256 字节,循环次数多,HTTP 解析开销大;太大了,比如 8192,内存占用高。ESP32 的 RAM 有限,HTTP Server 本身还要占一部分堆空间。这个例程选了 2048,均衡了速度和内存占用。如果你要传大固件,可以试试 4096,但如果系统内存吃紧,优先保稳定性。

esp_ota_end 做的事情是收尾——把最后不足一个扇区的数据补齐,然后读取整个镜像头和校验值,验证固件完整性。这一步如果校验失败,固件不会被标记为可启动,下次重启仍然走旧固件。

最后 esp_ota_set_boot_partition 只是改 otadata 分区里的标记位,不搬移任何数据,然后 esp_restart 重启。bootloader 启动时会查 otadata,决定跳转到哪个分区执行。

3.4 循环里的边界处理

ota_handler 里最容易写翻车的就是 while 循环。httpd_req_recv 的返回值有三类:正数表示实际读到的字节数;HTTPD_SOCK_ERR_TIMEOUT 表示超时,但连接还在;HTTPD_SOCK_ERR_FAIL 或其他负值表示连接已断开。

超时的处理是很多人容易搞错的地方。continue不是无限空转,因为 recv_wait_timeout 的计时是从上一次收到数据开始算的,只要一段时间内收不到数据就返回超时。如果持续超时,说明对端不再发数据,这时应该做超时计数,超过一定次数就主动断开。但例程为了保持简单,直接 continue,风险是如果浏览器那边暂停了上传,这个循环会一直空转,直到整个 HTTP 请求被框架层清理。

还有一个边界:如果固件文件大小和 content_len 不一致——比如浏览器端截断了连接——循环会因为 remaining 一直是正数而卡住。不过 esp_ota_end 会检查写入的字节数是否和分区内记录的镜像长度一致,不一致就返回错误,所以最终会走到 esp_ota_abort 的路径上。

4. 工程搭建与运行:从拉取代码到浏览器上传固件

4.1 工程目录结构和关键配置

拿到这个例程后,整个工程的结构是这样的:

esp32_ota_http/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt │ ├── app_main.c # 入口:Wi-Fi 连接 + 启动 HTTP Server │ └── ota_handlers.c # HTTP handler 和 OTA 写入逻辑 ├── partitions_ota.csv # 带 OTA 分区的分区表 ├── sdkconfig # 编译配置,由 menuconfig 生成 └── README.md

main 目录下的 CMakeLists.txt 关键内容:

idf_component_register( SRCS "app_main.c" "ota_handlers.c" INCLUDE_DIRS "." REQUIRES esp_http_server esp_ota_ops nvs_flash )

REQUIRES 这一行列出的是组件依赖,esp_http_server 提供 HTTP 服务,esp_ota_ops 提供 OTA 接口,nvs_flash 是因为 OTA 过程需要 nvs 存储一些系统变量。如果 REQURIES 漏掉了任何一个,编译时会有未定义的函数引用错误。

app_main.c 里的入口逻辑比较简单:先初始化 NVS,然后连接 Wi-Fi。Wi-Fi 连接在例程里通常有两种做法:一种是硬编码 SSID 和密码在代码里,另一种是从 NVS 读取。这个例程为了方便演示,直接在代码里写死,但做成产品时要改成配网或从 NVS 读取的方式。

4.2 编译烧录和运行验证

有了工程代码,跑起来的步骤如下。

先用 menuconfig 确认分区表。进入idf.py menuconfig,在 "Partition Table" 里选择 custom partition CSV,填上 partitions_ota.csv。如果不做这一步,默认的 factory-only 分区表会在 esp_ota_get_next_update_partition 时返回 NULL,OTA 流程根本走不下去。

接着设置 Wi-Fi 凭据。在 app_main.c 里找到类似:

#define WIFI_SSID "your_wifi" #define WIFI_PASSWORD "your_password"

改成你自己的路由器信息。然后编译烧录:

idf.py set-target esp32 idf.py build idf.py -p /dev/ttyUSB0 flash monitor

上电后串口会打印连接 Wi-Fi 的过程,拿到 IP 后会有类似Starting HTTP server on port 80的日志。然后在同一局域网内的电脑浏览器里打开http://设备IP,就能看到上传页面。

此时先不着急传新固件,先做一次基本的验证:用idf.py build编译出来的 bin 文件路径,一般是build/esp32_ota_http.bin。但要注意,如果你原地编译不改任何代码,生成的新固件和旧固件是一模一样的,上传后一样能刷,因为 OTA 校验只验证镜像的哈希和签名,不验证内容是否重复。这样跑通一次,确认 OTA 链路本身没问题,再改代码验证真正的升级效果。

一个更稳妥的验证方式:把代码里随便改一个 log 字符串,比如把开机打印改成"OTA demo v2",重新编译再上传。上传完成后设备自动重启,串口里如果能刷出新字符串,说明新固件已经跑起来了。

4.3 常见失败时看什么

这一节讲怎么定位问题,比跑通本身更重要。

如果浏览器上传后显示根本没有 OTA 响应,先去看串口日志。最常见的问题是分区表没配好,日志里会报No OTA partition found或ota_begin failed。这时回到 menuconfig 检查 partition table 配置,确认 csv 文件名没写错、文件在工程根目录。

如果上传到一半网页卡住,最后提示 timeout,先检查 recv_wait_timeout 参数。固件文件 500KB 传到一半停住,大多是超时时间设了 5 秒,浏览器端一停顿就断了。把 recv_wait_timeout 调到 15 到 30 秒,基本能解决。

如果写入过程中报OTA write failed,一种可能是 Flash 分区空间不足,确认固件 bin 大小没有超过分区表里 ota_0/ota_1 的大小。另一种可能是写入字节数没对齐,但 esp_ota_write 内部会处理对齐,所以大多数情况不会走到这——如果走到,优先往前看前面接收的数据是否存在 buffer 溢出,比如 BUFFER_SIZE 开太大,在数据拷贝时把栈给搞崩了。

最后是固件跑飞。如果上传成功、重启后直接 boot loop,很可能是固件本身有问题。这时候 ESP-IDF 的 OTA 机制会发挥后手作用:因为 otadata 记录的是上次成功的 OTA 状态,新固件校验不通过时 bootloader 会回滚到旧分区启动。你可以通过串口日志确认是从哪个分区启动的,这也能反过来验证 esp_ota_set_boot_partition 是否写成功了。

5. 避坑指南:OTA 开发里最容易踩的五个坑

做 OTA 半年下来踩过的坑,集中在分区、对齐、超时这三个维度上。每一条都附上排错思路,遇上了不至于抓瞎。

坑一:上传固件时进度到一半就断,串口日志显示 receive timeout

现象:浏览器上传稍大的固件(超过 300KB),传一会儿进度条就卡住,最后提示超时,设备自动放弃写入。

原因:HTTP Server 的 recv_wait_timeout 默认值只有 5 秒。现代浏览器上传大文件时不是匀速的,中间会有停顿,超过 5 秒没数据来就超时断开。

解决:把 start_ota_server 里的 recv_wait_timeout 从 5 改到 15 或 30。改完重新编译烧录一遍,再试大文件上传。

坑二:设备怎么刷都起不来,日志提示 partition 无效

现象:第一次编译烧录 OTA 例程,烧完启动直接 panic,提示类似partition invalid的错误。

原因:分区表没有生效。有的开发者在 menuconfig 里改了分区表,但忘了保存退出;有的改了 csv 文件名,但文件没放在工程根目录。

解决:用idf.py menuconfig进入分区表配置,保存后退出;确认 csv 文件就在工程目录下,路径写的是文件名而不是完整路径。如果都不行,用idf.py erase-flash擦除整个 Flash 重来。

坑三:上传成功也重启了,但跑的还是旧固件

现象:网页提示 OTA success,设备也重启了,但串口打印显示还是旧固件的启动日志。

原因:esp_ota_set_boot_partition 设置的是下次启动的分区,但esp_restart是软重启,没有触发 bootloader 重新读取分区。正常情况下 esp_restart 会走 bootloader 流程,但如果你的工程里实现了自定义的深度睡眠唤醒或者 watchdog 重启路径,可能跳过了 bootloader。

解决:确认调用顺序是 esp_ota_set_boot_partition 之后紧跟着 esp_restart,中间不要插入其他系统调用。如果还是不生效,检查 bootloader 有没有被自己修改过。

坑四:新固件烧进去了,但是 NVS 里的配置全丢了

现象:OTA 升级成功后,设备连不上 Wi-Fi 了,串口报 NVS 读取失败。

原因:这台设备之前烧录用的分区表是 factory 单分区,升级用的分区表把 NVS 分区的 offset 改了。NVS 数据按地址存储,分区表一变,NVS 内容就读不出来了。

解决:如果设备要支持 OTA,从第一版开始就要用带 OTA 的分区表,不要先烧一个不带 OTA 的版本再升级分区表。如果已经发生了,只能接受配置丢失的现状,重新配置一遍。

坑五:固件校验失败,esp_ota_end 返回错误

现象:传输过程没有报错,但写完调用 esp_ota_end 时报 OTA end failed,固件不生效。

原因:最常见的场景是固件文件本身就不完整——比如从网页下载固件时下载到一半停了,文件只有一半大小。esp_ota_end 会拿镜像头里的长度信息和实际写入的字节数对比,不匹配就拒绝。

解决:上传前先确认 bin 文件的大小,和build/目录下对应的.map或size信息对比。还有一个通用习惯:在上传页面加一个文件哈希校验,MD5 匹配了才允许上传,能挡住八成这类问题。

6. 二次开发技巧:给这个 OTA 例程加版本校验和断点保护

到这个阶段,例程能跑通,接下来考虑怎么把它集成进真实项目。这里给出三个改造方向,按实施难度从低到高排列,你可以直接照着做。

第一步:增加固件版本校验

现在这个例程是来什么固件刷什么,不检查新旧版本。真实项目里应该加一个保护:新固件的版本号不低于当前运行中的版本号,否则拒绝写入。实现方式是在 app_main 里先读取当前固件的版本:

#include "esp_app_desc.h" const esp_app_desc_t *app_desc = esp_app_get_description(); ESP_LOGI(TAG, "Current firmware version: %s", app_desc->version);

然后在 ota_handler 里,从 HTTP 请求的 header 里取出新固件的版本号,和当前版本做比较,不满足条件就直接拒绝写入。前端上传时在 form 里加一个隐藏字段,或者在文件名里携带版本号,都能实现。

第二步:加入固件 MD5 校验

流程改为:前端在上传前先计算 bin 文件的 MD5,放在 HTTP 请求 header 里,比如X-Firmware-MD5;后端在 esp_ota_end 之前,计算已写入固件的 MD5,和 header 里的值比对。ESP-IDF 的 mbedtls 组件自带 MD5 计算接口,不需要额外引库。这个改造成本低,能挡住大部分传输损坏问题。

第三步:实现失败自动回滚

真正写生产环境代码,一定要做失败回滚。ESP32 的硬件本来就支持——ota_0 和 ota_1 两个分区互为备份,esp_ota_end 校验失败时 bootloader 会自动走回退路径。但在应用层你还可以多加一层:在新固件里写一个启动自检标志,启动后先做关键外设初始化检查,检查通过才清除标志;检查失败就调用 esp_ota_set_boot_partition 切回旧分区,再重启。这能解决「固件校验过了但运行时跑飞」的情况。

改造完这些,你手里的已经不再是一个 Demo,而是一个带版本保护、传输校验和失败回退的完整 OTA 模块。打个小补丁的经验是:每次调试 OTA 都强制走一遍完整流程——改代码、编译、上传、重启、看日志——而不是改完只烧串口。因为 OTA 的逻辑有一半发生在 bootloader 和分区表层面,只在串口烧录时永远看不见。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询