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升级;

相关推荐
攸尔的笔记本~2 天前
ESP32-C3的WIFI信号飘忽不定、连接不了
esp32·射频·硬件设计
youcans_3 天前
【嵌入式软件AI编程】12. Claude Code的基本操作
stm32·单片机·ai编程·嵌入式软件·claude code
youcans_4 天前
【嵌入式软件AI编程】09. 使用VS Code调试STM32程序
stm32·mcu·ai编程·嵌入式软件·claude code
星源~4 天前
MCU-OTA升级的flash分散加载应用
单片机·嵌入式·ota·flash分散加载
乐鑫科技 Espressif4 天前
案例研究:基于 ESP32-C5 与 ESP RainMaker 构建符合 EN 18031 标准的 IoT 解决方案
esp32·乐鑫科技·rainmaker
jianqiang.xue6 天前
ESP-IDF保姆级入门41|产品级故障排查与稳定性优化全解:死机复位排查/内存泄漏定位/性能瓶颈分析/长期稳定性测试,掌握量产运维问题定位方法论
单片机·mcu·物联网·esp32
youcans_6 天前
【嵌入式软件AI编程】08. 第一个STM32工程
stm32·单片机·ai编程·嵌入式软件·claude code
jianqiang.xue7 天前
ESP-IDF保姆级入门40|量产烧录与产线测试全解:批量烧录方案/校准流程/功能测试/不良分拣/产线追溯,掌握工业级量产交付全流程
单片机·物联网·esp32
jianqiang.xue7 天前
ESP-IDF保姆级入门39|固件安全与安全启动全解:签名校验/Flash加密/防回滚/OTP密钥/知识产权保护,掌握工业级固件安全防护体系
单片机·物联网·esp32
jianqiang.xue8 天前
ESP-IDF保姆级入门38|电源管理与锂电池充电全解:充电拓扑/电源路径控制/充放电保护/电量检测/低功耗策略,掌握嵌入式电池供电系统设计
单片机·嵌入式·esp32