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

相关推荐
whik119416 小时前
ESP32-C3-DevKitM-1开发板深度上手评测
wifi·嵌入式·esp32·arduino·蓝牙·开发板·乐鑫
星野云联AIoT技术洞察1 天前
ESP32 Edge AI 架构设计:固件、OTA 与端侧推理的完整实践
深度学习·esp32·模型部署·aiot·esp-idf·ota升级·固件开发
小灰灰搞电子2 天前
ESP32 使用ESP-IDF 连接WiFi并使用TCP客户端通信源码分享
网络协议·tcp/ip·esp32
花开花落的个人博客2 天前
ESP32 IO扩展测试
esp32
非鱼䲆鱻䲜3 天前
esp32基于中断+FIFO+事件队列的uart
单片机·嵌入式硬件·esp32·uart
小程同学>o<3 天前
Linux 应用层开发入门(十九)| 输入系统框架及调试
linux·学习·嵌入式软件·输入系统·嵌入式应用层·应用层开发·linux应用层开发
飞睿科技6 天前
乐鑫智能开关方案解析:基于ESP32-C系列的低功耗、高集成设计
嵌入式硬件·物联网·esp32·智能家居·乐鑫科技
Lester_11016 天前
STM32 高级定时器PWM互补输出模式--如果没有死区,突然关闭PWM有产生瞬间导通的可能吗
stm32·单片机·嵌入式硬件·嵌入式软件
星野云联AIoT技术洞察7 天前
ESP32 系列芯片适合做什么:主流型号、应用场景与物联网边缘智能定位
物联网·esp32·嵌入式系统·aiot·esp32-s3·esp32-c3·低功耗wi-fi