ESP32(PIO+Arduino框架)联网OTA升级思路

平台: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升级;

相关推荐
码农小韩1 天前
QT学习记录(三)——C++学习基础(三)
开发语言·c++·qt·学习·算法·嵌入式软件
NQBJT3 天前
双轮足机器人 5 连杆逆运动学:从几何模型到嵌入式实现
esp32·逆运动学·轮足机器人
wanghanjiett4 天前
笔记:ESP32驱动SimpleFOC成功(基于Espressif-IDE)
笔记·esp32·foc
NQBJT5 天前
双轮足导盲机器人:多传感融合与全局-局部分层导航系统设计
c++·esp32·openmv·避障·导盲·轮足
net3m336 天前
mic声音怎么才不容易卡顿 : 环形队列缓存要足够大
esp32·i2s
net3m336 天前
不要用esp_websocket_client_send_bin直接发送前导音频,会卡,导致mic声音卡顿,要用环形队列
esp32
net3m337 天前
24位INMP441的相关配置,原本是16位mic数据,麦克风音质不高
esp32·i2s
SmartRadio9 天前
ESP32-S3 双模式切换实现:兼顾手机_路由器连接与WiFi长距离通信
开发语言·网络·智能手机·esp32·长距离wifi
π同学9 天前
ESP-IDF+vscode开发ESP32第十讲——I2S工程2
vscode·esp32·sd·音频播放
SmartRadio9 天前
ESP32-S3 双模式切换实现:兼顾手机_路由器连接与WiFi长距离通信 (采用Arduino代码框架)
开发语言·智能手机·esp32·长距离wifi