平台:ESP32S3R8N8 VSCODE+PIO
cpp
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
最近研究了一下ESP32的OTA升级思路,主要是联网上传BIN文件,通过服务器匹配获取新文件,蓝牙上传更新;
作为个人开发者而言,感觉第一种方案属于最实用的,也翻了翻开源的LIB库,最后发现好像没那么麻烦,Arduino自带的库就可以满足需求;
在开始之前,需要研究下ESP32S3 8MB的默认分区表,是自带OTA区域的
cpp
TEXT:=== 分区表信息 ===
Flash 总大小: 8 MB (8388608 bytes)
分区: nvs 类型: 0x01 子类型: 0x02 地址: 0x009000 大小: 0x005000 (20 KB)
分区: otadata 类型: 0x01 子类型: 0x00 地址: 0x00e000 大小: 0x002000 (8 KB)
分区: app0 类型: 0x00 子类型: 0x10 地址: 0x010000 大小: 0x330000 (3264 KB)
分区: app1 类型: 0x00 子类型: 0x11 地址: 0x340000 大小: 0x330000 (3264 KB)
分区: spiffs 类型: 0x01 子类型: 0x82 地址: 0x670000 大小: 0x180000 (1536 KB)
分区: coredump 类型: 0x01 子类型: 0x03 地址: 0x7f0000 大小: 0x010000 (64 KB)
| 分区 | 类型 | 用途 |
|---|---|---|
| nvs | 数据 | 非易失性存储 (Key-Value) WiFi密码、配置参数、Preferences 库数据 |
| otadata | 数据 | OTA 状态数据 记录当前启动的是 app0 还是 app1 |
| app0 | 程序 | 应用程序分区 0 主程序存储区 (3.2MB) |
| app1 | 程序 | 应用程序分区 1 OTA 升级时的新程序缓存区 |
| spiffs | 数据 | 文件系统 网页文件、图片、日志等 |
| coredump | 数据 | 崩溃转储 程序崩溃时保存调试信息 |
这里以一个模糊的物联网情景距离,出厂程序是LED慢闪,升级之后变成快闪,而在OTA的过程中需要保留之前用户保存的WIFI名称密码;
程序一:慢闪
cpp
#include <WiFi.h>
#include <WebServer.h>
#include <Update.h>
#include <Preferences.h>
#define LED 48
#define VERSION "v1.0-SLOW"
#define BLINK_INTERVAL 1000
#define WIFI_SSID "YOUR_SSID"
#define WIFI_PASS "YOUR_PASSWORD"
WebServer server(80);
Preferences prefs;
// 前置声明函数
void handleRoot();
void handleUpdatePost();
void handleUpdateUpload();
// HTML上传页面
const char* uploadPage = R"(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ESP32 OTA</title>
<style>
body{font-family:Arial;text-align:center;margin-top:50px}
h2{color:#333}
.version{color:#666;margin:20px}
form{margin:30px}
input[type=file]{margin:10px}
input[type=submit]{padding:10px 20px;background:#007bff;color:white;border:none;cursor:pointer}
input[type=submit]:hover{background:#0056b3}
.status{margin:20px;padding:10px;background:#f0f0f0;border-radius:5px}
</style>
</head>
<body>
<h2>ESP32 OTA 更新</h2>
<div class="version">当前版本: %s</div>
<div class="status">LED状态: 慢闪(1秒)</div>
<form method="POST" action="/update" enctype="multipart/form-data">
<input type="file" name="firmware" accept=".bin" required><br>
<input type="submit" value="上传并更新">
</form>
</body>
</html>
)";
void setup() {
Serial.begin(9600); //PIO默认是9600
pinMode(LED, OUTPUT);
Serial.println("\n===================");
Serial.print("版本: ");
Serial.println(VERSION);
Serial.println("===================");
prefs.begin("wifi", false);
String ssid = prefs.getString("ssid", "");
String pass = prefs.getString("pass", "");
if (ssid == "") {
Serial.println("首次运行,保存WiFi信息...");
prefs.putString("ssid", WIFI_SSID);
prefs.putString("pass", WIFI_PASS);
ssid = WIFI_SSID;
pass = WIFI_PASS;
} else {
Serial.println("读取已保存的WiFi信息");
}
prefs.end();
Serial.print("连接WiFi: ");
Serial.println(ssid);
WiFi.begin(ssid.c_str(), pass.c_str());
while (WiFi.status() != WL_CONNECTED) {
delay(100);
digitalWrite(LED, !digitalRead(LED));
Serial.print(".");
}
Serial.println();
Serial.print("IP地址: ");
Serial.println(WiFi.localIP());
server.on("/", HTTP_GET, handleRoot);
server.on("/update", HTTP_POST, handleUpdatePost, handleUpdateUpload);
server.begin();
Serial.println("Web服务器已启动");
Serial.print("OTA页面: http://");
Serial.print(WiFi.localIP());
Serial.println("/");
}
void loop() {
server.handleClient();
static unsigned long last = 0;
if (millis() - last >= BLINK_INTERVAL) {
last = millis();
digitalWrite(LED, !digitalRead(LED));
}
}
// ========== 函数实现 ==========
void handleRoot() {
char html[1024];
snprintf(html, sizeof(html), uploadPage, VERSION);
server.send(200, "text/html", html);
}
void handleUpdatePost() {
server.sendHeader("Connection", "close");
if (Update.hasError()) {
server.send(500, "text/plain", "更新失败!");
} else {
server.send(200, "text/html", "<h2>更新成功!设备重启中...</h2>");
delay(500);
ESP.restart();
}
}
void handleUpdateUpload() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("接收文件: %s\n", upload.filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {
Serial.println("OTA初始化失败");
}
}
else if (upload.status == UPLOAD_FILE_WRITE) {
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Serial.println("写入失败");
}
static bool toggle = false;
toggle = !toggle;
digitalWrite(LED, toggle);
}
else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) {
Serial.printf("更新完成: %u字节\n", upload.totalSize);
} else {
Serial.println("更新结束失败");
}
}
}
点击串口里面提示的网页

将下方的程序二编译,BIN文件一般路径是.PIO/build/espXX/firmware.bin,上传后变快闪
cpp
#include <WiFi.h>
#include <WebServer.h>
#include <Update.h>
#include <Preferences.h>
#define LED 48
#define VERSION "v2.0-FAST"
#define BLINK_INTERVAL 200
WebServer server(80);
Preferences prefs;
// 前置声明函数
void handleRoot();
void handleUpdatePost();
void handleUpdateUpload();
// HTML上传页面
const char* uploadPage = R"(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ESP32 OTA</title>
<style>
body{font-family:Arial;text-align:center;margin-top:50px;background:#f5f5f5}
h2{color:#d9534f}
.version{color:#5cb85c;font-weight:bold;margin:20px}
form{margin:30px}
input[type=file]{margin:10px}
input[type=submit]{padding:10px 20px;background:#d9534f;color:white;border:none;cursor:pointer}
input[type=submit]:hover{background:#c9302c}
.status{margin:20px;padding:10px;background:#dff0d8;color:#3c763d;border-radius:5px}
</style>
</head>
<body>
<h2>ESP32 OTA 更新</h2>
<div class="version">当前版本: %s ✅</div>
<div class="status">LED状态: 快闪(200ms) - OTA升级成功!</div>
<form method="POST" action="/update" enctype="multipart/form-data">
<input type="file" name="firmware" accept=".bin" required><br>
<input type="submit" value="上传并更新">
</form>
</body>
</html>
)";
void setup() {
Serial.begin(9600);
pinMode(LED, OUTPUT);
Serial.println("\n===================");
Serial.print("版本: ");
Serial.println(VERSION);
Serial.println("===================");
prefs.begin("wifi", true);
String ssid = prefs.getString("ssid", "");
String pass = prefs.getString("pass", "");
prefs.end();
if (ssid == "") {
Serial.println("错误:未找到WiFi信息!请先烧录v1.0");
while (1) {
digitalWrite(LED, HIGH);
delay(50);
digitalWrite(LED, LOW);
delay(50);
}
}
Serial.print("读取WiFi: ");
Serial.println(ssid);
WiFi.begin(ssid.c_str(), pass.c_str());
while (WiFi.status() != WL_CONNECTED) {
delay(50);
digitalWrite(LED, !digitalRead(LED));
Serial.print(".");
}
Serial.println();
Serial.print("IP地址: ");
Serial.println(WiFi.localIP());
server.on("/", HTTP_GET, handleRoot);
server.on("/update", HTTP_POST, handleUpdatePost, handleUpdateUpload);
server.begin();
Serial.println("Web服务器已启动");
Serial.print("OTA页面: http://");
Serial.print(WiFi.localIP());
Serial.println("/");
Serial.println(">>> OTA升级成功!当前为快闪模式 <<<");
}
void loop() {
server.handleClient();
static unsigned long last = 0;
if (millis() - last >= BLINK_INTERVAL) {
last = millis();
digitalWrite(LED, !digitalRead(LED));
}
}
// ========== 函数实现 ==========
void handleRoot() {
char html[1024];
snprintf(html, sizeof(html), uploadPage, VERSION);
server.send(200, "text/html", html);
}
void handleUpdatePost() {
server.sendHeader("Connection", "close");
if (Update.hasError()) {
server.send(500, "text/plain", "更新失败!");
} else {
server.send(200, "text/html", "<h2>更新成功!设备重启中...</h2>");
delay(500);
ESP.restart();
}
}
void handleUpdateUpload() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("接收文件: %s\n", upload.filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {
Serial.println("OTA初始化失败");
}
}
else if (upload.status == UPLOAD_FILE_WRITE) {
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Serial.println("写入失败");
}
static bool toggle = false;
toggle = !toggle;
digitalWrite(LED, toggle);
}
else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) {
Serial.printf("更新完成: %u字节\n", upload.totalSize);
} else {
Serial.println("更新结束失败");
}
}
}
===================
版本: v1.0-SLOW
===================
读取已保存的WiFi信息
连接WiFi: ........
IP地址: 192.168.101.36
Web服务器已启动
OTA页面: http://192.168.101.36/
[ 13050][E][WebServer.cpp:638] _handleRequest(): request handler not found
接收文件: firmware.bin
更新完成: 743632字节
===================
版本: v2.0-FAST
===================
读取WiFi:........
IP地址: 192.168.101.36
Web服务器已启动
OTA页面: http://192.168.101.36/
>>> OTA升级成功!当前为快闪模式 <<<
当然还有很多问题没有考虑到,比如说OTA一般突然断电,回滚机制等等,这里只是实现了最基本的OTA升级;