ESP32 实战教程:做一个带校验和回退思路的网页 OTA 升级
本文由 AI 协助整理,示例适合可信局域网学习。HTTP 上传页没有自动变成公网安全服务,正式部署还需要 HTTPS、强认证、固件签名、权限控制、分区回滚和审计。第一次测试请使用身边的备用板,不要把唯一一块重要设备当实验品。

OTA 解决的是维护问题,不是烧录问题
开发阶段插 USB 烧录很方便,设备装进盒子、挂到高处或部署到客户现场以后,重新接线就不再轻松。OTA 让电脑通过网络把固件写进设备,但它也把“升级失败后怎么办”带到了现场。
一个可用的网页 OTA 至少要回答:谁可以上传?上传多大的文件?当前固件是多少?上传过程中掉线怎么办?写完以后如何验证?新固件启动失败能不能回到旧版本?
本文先做一个局域网网页上传示例,再说明双分区和回退的工程边界。Arduino IDE 可以从“导出已编译二进制”得到 .bin 文件,浏览器把它提交给 ESP32。
分区表是回退的基础
最简单的单分区方案可能覆盖正在运行的固件,升级中断后设备直接失去启动能力。支持 OTA 的分区表通常要有两个应用分区:当前运行的分区和备用分区。
在 Arduino IDE 的工具菜单中选择带 OTA 分区的 Partition Scheme,具体名称因核心版本和 Flash 容量而异。确认方案后再编译和导出固件,不能拿一个超出分区大小的二进制硬塞进去。
分区只是提供空间,不自动替你完成安全回退。正式项目还应在新固件启动后自检,确认 WiFi、传感器、关键任务正常,再标记当前镜像为有效。
网页上传页面
#include <WiFi.h>
#include <WebServer.h>
#include <Update.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_PASSWORD";
const char* OTA_USER = "admin";
const char* OTA_PASSWORD = "CHANGE_THIS_PASSWORD";
WebServer server(80);
const char* FIRMWARE_VERSION = "1.0.0";
网页提供文件选择和上传按钮:
const char OTA_PAGE[] PROGMEM = R"rawliteral(
<!doctype html><html lang="zh-CN"><meta charset="utf-8">
<title>ESP32 OTA</title>
<style>body{font-family:system-ui;max-width:620px;margin:40px auto;padding:20px}button{padding:10px 18px}</style>
<h1>ESP32 固件升级</h1>
<p>当前版本:%VERSION%</p>
<form method="POST" action="/update" enctype="multipart/form-data">
<input type="file" name="firmware" accept=".bin" required>
<button type="submit">上传并升级</button>
</form>
)rawliteral";
生产环境不要只靠隐藏的网页地址。这里的页面只是一个学习入口,真正的认证在服务器处理函数中完成。
登录保护和上传处理
void setupOtaRoutes() {
server.on("/", HTTP_GET, []() {
if (!server.authenticate(OTA_USER, OTA_PASSWORD)) {
return server.requestAuthentication();
}
String page = OTA_PAGE;
page.replace("%VERSION%", FIRMWARE_VERSION);
server.send(200, "text/html; charset=utf-8", page);
});
server.on("/update", HTTP_POST,
[]() {
if (!server.authenticate(OTA_USER, OTA_PASSWORD)) {
return server.requestAuthentication();
}
bool ok = !Update.hasError();
server.send(200, "text/plain; charset=utf-8",
ok ? "Update complete, rebooting" : "Update failed");
delay(500);
if (ok) ESP.restart();
},
[]() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
if (!server.authenticate(OTA_USER, OTA_PASSWORD)) return;
Serial.printf("OTA start: %s\n", upload.filename.c_str());
if (!upload.filename.endsWith(".bin")) {
Serial.println("Only .bin files are accepted");
return;
}
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_WRITE) {
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) {
Serial.printf("OTA size=%u\n", upload.totalSize);
} else {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_ABORTED) {
Update.abort();
Serial.println("OTA aborted");
}
yield();
});
}
真实项目应在开始上传时限制文件大小,并在服务端确认目标分区能够容纳固件。上面使用 UPDATE_SIZE_UNKNOWN 是为了展示 API,部署代码应结合分区容量和上传回调实际限制。
让程序运行起来
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
unsigned long started = millis();
while (WiFi.status() != WL_CONNECTED && millis() - started < 15000) {
delay(200);
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi failed");
return;
}
setupOtaRoutes();
server.begin();
Serial.print("OTA URL: http://");
Serial.println(WiFi.localIP());
}
void loop() {
server.handleClient();
delay(2);
}
浏览器打开串口显示的 IP,输入账号密码,选择新导出的 .bin 文件。上传期间不要断电、不要让程序执行长时间阻塞操作,也不要关闭网络。网页只显示成功不代表新固件一定能正常运行,重启后的自检才是第二道关。
版本号和固件身份
版本号至少包含在网页、串口日志和设备状态里。可以用编译宏传入:
#define FW_VERSION "1.0.1"
String firmwareInfo() {
return String(FW_VERSION) + " build=" + __DATE__ + " " + __TIME__;
}
生产升级系统还应有设备型号、硬件版本、分区信息和构建提交号。避免把 ESP32-S3 的固件上传给普通 ESP32;文件名相同并不代表硬件兼容。
固件完整性与真实性
文件扩展名 .bin、文件大小和 HTTP 认证都不能证明固件来自可信构建。高风险设备需要固件签名,设备只接受用受信任私钥签名的镜像;传输过程使用 TLS,避免网络中间人替换文件。
即使是学习项目,也可以先计算 SHA-256 并记录构建产物哈希:
固件文件:env-node-1.0.1.bin
版本:1.0.1
目标:ESP32-WROOM-32
SHA-256:由构建流程生成并保存
设备端校验哈希与签名需要配合分区和启动验证机制,不是把一行字符串显示在网页上就算完成。文章里的网页认证只适合可信局域网实验,不能作为公网安全方案。
启动自检与回退思路
新固件启动后可以检查:
- NVS 配置是否能读取。
- WiFi 是否在限定时间内连接。
- 关键传感器是否响应。
- 主任务是否正常运行。
- 看门狗和错误计数是否清零。
如果自检通过,再把当前分区标记为有效;如果持续重启或关键功能失败,应让启动流程回退到上一版本。具体 API 依赖 ESP-IDF/Arduino 核心版本和分区配置,正式项目应参考当前官方 OTA 与回滚文档,不要凭一篇旧教程复制函数名。
在没有实现完整回退前,保留串口烧录通道和物理恢复入口。OTA 的第一条工程原则是:远程升级失败后,仍然要有办法把设备救回来。
升级前后的兼容策略
固件升级可能同时改变 NVS 配置、MQTT 主题和网页接口。升级前先考虑:
- 新固件能否读取旧配置。
- 配置版本是否需要迁移。
- MQTT 消息是否向后兼容。
- 升级失败时旧固件是否仍能使用旧字段。
- 是否支持分批设备和灰度升级。
数据库迁移的思路同样适用于设备配置:读取旧版本,转换成新结构,写入新版本号;不要看到字段少了就清空所有配置。
分阶段验证
- 用串口首次烧录带 OTA 页面的小固件。
- 在同一局域网访问页面,确认登录保护。
- 导出一个只改版本字符串的
.bin。 - 上传并观察进度、串口和重启。
- 验证新版本功能与配置仍在。
- 上传错误文件名,确认不会开始写入。
- 测试中途断网和断电,确认有物理恢复路径。
- 使用不同分区方案验证文件大小限制。
常见问题
| 现象 | 优先检查 |
|---|---|
| 页面打不开 | WiFi、IP、HTTP 服务和局域网隔离 |
| 上传立即失败 | 分区方案、文件格式、容量和 Update 初始化 |
| 上传成功但无法启动 | 固件目标板不匹配、分区或依赖问题 |
| 认证形同虚设 | 只保护首页,未保护上传接口 |
| 上传中设备重启 | 供电、网络、看门狗和阻塞任务 |
| 重启后仍是旧版本 | 实际没有写入备用分区,或启动分区未切换 |
| 设备变砖 | 没有回退、串口恢复或可靠电源 |
总结
网页 OTA 的代码并不长,真正的难点是升级流程:身份验证、固件大小、版本与型号、完整性、断电、启动自检和恢复路径。
先在局域网和备用板上做最小上传,再逐步增加签名、TLS、回滚和灰度发布。能远程升级是一种能力,升级失败后还能找到设备、恢复它,才是工程上的完整能力。
参考资料
打赏
如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏支持:王庆岩
浙公网安备 33010602011771号