ESP32-S3 OTA 空中升级:不拆机不拔线,通过网络给设备刷固件
NVS(存储)↓SPIFFS(文件系统)↓分区表(Flash 空间规划)↓SmartConfig(Wi-Fi 配网)↓HTTP/HTTPS(网络通信)↓MQTT(实时通信,上云)↓OTA(远程升级) ← 本篇到这里,一个物联网设备从开发到产品化需要的核心技术栈就基本齐了。
ESP32-S3 OTA 空中升级:不拆机不拔线,通过网络给设备刷固件
前言:每次改代码都要插线烧录,太痛苦了
公众号 “坏柠编程”
学到这里,我已经能让 ESP32-S3 配网、HTTP 通信、MQTT 上云了。但有个问题一直困扰着我——每次更新固件都要用数据线连电脑烧录。
开发阶段还好,设备就在手边。但如果设备已经装到了天花板上的智能灯里?装到了工厂的产线设备上?装到了客户家里?总不能每次改个 bug 都上门拆机吧?
这就是 OTA(Over-The-Air)空中升级 要解决的问题——让设备通过网络自动下载新固件并完成升级,全程不需要物理连接。
今天就来记录我从零实现 OTA 的完整过程,包括分区表设计、本地 HTTPS 服务器搭建,以及设备端代码实现。
一、OTA 升级的原理
在动手之前,我先搞明白了一个关键问题:设备正在运行的固件,怎么能"自己升级自己"?
答案是:不是替换自己,而是写到另一个分区,然后切换启动。
1.1 分区设计
OTA 需要一个特殊的分区表,至少包含四个关键分区:
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, , 0x4000,
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 1M,
ota_0, app, ota_0, , 1M,
ota_1, app, ota_1, , 1M,
| 分区 | 作用 |
|---|---|
| factory | 出厂固件,永远不会被覆盖,是最后的"保底" |
| ota_0 | OTA 升级用的第一个分区 |
| ota_1 | OTA 升级用的第二个分区 |
| otadata | 记录"当前应该从哪个分区启动" |
1.2 升级流程
第一次烧录
┌──────────┐ ┌──────────┐ ┌──────────┐
│ factory │ │ ota_0 │ │ ota_1 │
│ 出厂固件 │ │ (空) │ │ (空) │
│ ★ 运行中 │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘
第一次 OTA 升级
┌──────────┐ ┌──────────┐ ┌──────────┐
│ factory │ │ ota_0 │ │ ota_1 │
│ 出厂固件 │ │ 新固件 v2 │ │ (空) │
│ │ │ ★ 运行中 │ │ │
└──────────┘ └──────────┘ └──────────┘
第二次 OTA 升级
┌──────────┐ ┌──────────┐ ┌──────────┐
│ factory │ │ ota_0 │ │ ota_1 │
│ 出厂固件 │ │ 旧固件 v2 │ │ 新固件 v3 │
│ │ │ │ │ ★ 运行中 │
└──────────┘ └──────────┘ └──────────┘
第三次 OTA 升级
┌──────────┐ ┌──────────┐ ┌──────────┐
│ factory │ │ ota_0 │ │ ota_1 │
│ 出厂固件 │ │ 新固件 v4 │ │ 旧固件 v3 │
│ │ │ ★ 运行中 │ │ │
└──────────┘ └──────────┘ └──────────┘
核心思路:ota_0 和 ota_1 轮流写入,factory 永远不动。
这个设计非常巧妙:
- 升级过程中如果断电,原来的固件还在另一个分区,不会变砖
- 出厂固件永远保留,是最后的安全网
otadata分区记录了当前该从哪个分区启动
二、ESP-IDF 提供的两种 OTA 方式
| 方式 | 说明 | 复杂度 |
|---|---|---|
原生 API(app_update 组件) |
底层 API,灵活但代码量大 | 难 |
简化 API(esp_https_ota 组件) |
封装好的 HTTPS OTA,几行代码搞定 | 简单 |
我选了简化 API——esp_https_ota,因为它把 HTTP 下载 + 固件写入 + 分区切换全封装好了,只需要一个函数调用就能完成升级。
三、搭建本地 HTTPS 服务器
OTA 升级需要从某个服务器下载固件文件。用第三方服务器不太靠谱(随时可能关停),所以我先在本地搭一个。
3.1 生成 SSL 证书
因为用的是 HTTPS,所以需要一个 SSL 证书。设备在升级时会验证服务器证书是否合法。
openssl req -x509 -newkey rsa:2048 -keyout huaining_key.pem -out huaining_cert.pem -days 3650 -nodes
执行后会让你填一些信息(国家、省份、组织名等),随便填就行,不影响后续操作。
这条命令会生成两个文件:
huaining_key.pem— 私钥(服务器用)huaining_cert.pem— 证书(服务器和设备都要用)
3.2 搭建 HFS 文件服务器
我用的是开源工具 HFS(HTTP File Server),下载对应平台的版本,解压后双击打开。
配置步骤:
① 设置 HTTPS 端口
我设成了 8088,只要电脑上没被占用就行。记住这个端口号,后面代码要用。
② 上传证书和密钥
把刚才生成的 huaining_cert.pem 和 huaining_key.pem 上传到 HFS 的 SSL 配置中,上传完一定要点保存!
③ 查看电脑 IP
在 Windows 命令行输入 ipconfig,找到本机 IP,比如我的是 192.168.1.242。记住这个 IP。
④ 添加固件文件
把要升级的固件文件(比如 hello_world.bin)添加到服务器中。固件文件在工程编译后的 build 目录里:
hello_world/build/hello_world.bin

⑤ 验证
在浏览器中访问 https://192.168.1.242:8088/,如果能看到文件列表,说明配置成功。
浏览器可能会提示"不安全",因为是自签名证书,忽略就好。
如果打不开页面,排查清单:
- 端口是否被占用?
- IP 是否正确?
- HFS 配置是否点了保存?
四、设备端代码实现
4.1 准备工作
放置证书文件: 把 huaining_cert.pem 拷贝到工程的 main 目录下。
修改 CMakeLists.txt:
idf_component_register(
SRCS "main.c"
INCLUDE_DIRS "."
EMBED_TXTFILES huaining_cert.pem
)
4.2 配置分区表
打开 idf.py menuconfig:
选择 OTA 分区表:
Partition Table → Factory app, two OTA definitions

修改 Flash 大小: 因为 OTA 分区表需要 3MB 以上(factory 1M + ota_0 1M + ota_1 1M + 数据分区),默认的 2MB 不够用:
Serial flasher config → Flash size → 4 MB

配置好后可以查看分区表:
idf.py partition-table

# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs,data,nvs,0x9000,16K,
otadata,data,ota,0xd000,8K,
phy_init,data,phy,0xf000,4K,
factory,app,factory,0x10000,1M,
ota_0,app,ota_0,0x110000,1M,
ota_1,app,ota_1,0x210000,1M,
4.3 OTA 核心代码
OTA 的核心代码出奇地简单,就一个函数调用:
extern const uint8_t server_cert_pem_start[] asm("_binary_huaining_cert_pem_start");
extern const uint8_t server_cert_pem_end[] asm("_binary_huaining_cert_pem_end");
void simple_ota_run()
{
ESP_LOGI(TAG, "Starting OTA...");
// ① 配置 HTTP 客户端参数
esp_http_client_config_t config = {
.url = "https://192.168.1.242:8088/hello_world.bin", // ★ 改成你的 IP 和文件名
.cert_pem = (char *)server_cert_pem_start, // 证书验证
.event_handler = _http_event_handler,
.skip_cert_common_name_check = true, // 自签名证书没有域名信息,跳过检查
};
// ② 配置 OTA 参数
esp_https_ota_config_t ota_config = {
.http_config = &config,
};
// ③ 一行代码完成 OTA!
ESP_LOGI(TAG, "Downloading from %s", config.url);
esp_err_t ret = esp_https_ota(&ota_config);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "OTA Succeed, Rebooting...");
esp_restart(); // 升级成功,重启加载新固件
} else {
ESP_LOGE(TAG, "Firmware upgrade failed");
}
}
esp_https_ota() 这一个函数就干了所有事情:
- 通过 HTTPS 连接服务器
- 下载固件数据
- 写入到空闲的 OTA 分区(ota_0 或 ota_1)
- 更新 otadata 分区,标记新分区为启动分区
- 返回结果
成功后调用 esp_restart() 重启,系统就会自动从新写入的分区启动了。
4.4 完整代码
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_netif.h"
#include "esp_ota_ops.h"
#include "esp_http_client.h"
#include "esp_https_ota.h"
static const char *TAG = "simple_ota_example";
static EventGroupHandle_t s_wifi_event_group;
static const int CONNECTED_BIT = BIT0;
static bool is_connect_wifi = false;
#define WIFI_SSID "hyx123"
#define WIFI_PASSWORD "123456789"
extern const uint8_t server_cert_pem_start[] asm("_binary_huaining_cert_pem_start");
extern const uint8_t server_cert_pem_end[] asm("_binary_huaining_cert_pem_end");
// ==================== HTTP 事件回调 ====================
esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
switch (evt->event_id) {
case HTTP_EVENT_ERROR:
ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
break;
case HTTP_EVENT_ON_CONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED");
break;
case HTTP_EVENT_ON_DATA:
ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
break;
case HTTP_EVENT_ON_FINISH:
ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH");
break;
case HTTP_EVENT_DISCONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED");
break;
default:
break;
}
return ESP_OK;
}
// ==================== OTA 核心逻辑 ====================
void simple_ota_run()
{
ESP_LOGI(TAG, "Starting OTA example task");
esp_http_client_config_t config = {
.url = "https://192.168.137.1:8088/hello_world.bin", // 改成你的 IP
.cert_pem = (char *)server_cert_pem_start,
.event_handler = _http_event_handler,
.skip_cert_common_name_check = true,
};
esp_https_ota_config_t ota_config = {
.http_config = &config,
};
ESP_LOGI(TAG, "Attempting to download update from %s", config.url);
esp_err_t ret = esp_https_ota(&ota_config);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "OTA Succeed, Rebooting...");
esp_restart();
} else {
ESP_LOGE(TAG, "Firmware upgrade failed");
}
while (1) {
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
// ==================== OTA 任务 ====================
static void ota_task(void *pvParameters)
{
while (1) {
if (is_connect_wifi) {
simple_ota_run();
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
// ==================== Wi-Fi 事件处理 ====================
static void event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
wifi_config_t wifi_config = {0};
memcpy(wifi_config.sta.ssid, WIFI_SSID, strlen(WIFI_SSID) + 1);
memcpy(wifi_config.sta.password, WIFI_PASSWORD, strlen(WIFI_PASSWORD) + 1);
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_connect());
ESP_LOGI(TAG, "Connecting to WiFi %s...", WIFI_SSID);
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
is_connect_wifi = false;
esp_wifi_connect();
xEventGroupClearBits(s_wifi_event_group, CONNECTED_BIT);
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ESP_LOGI(TAG, "WiFi Connected");
xEventGroupSetBits(s_wifi_event_group, CONNECTED_BIT);
is_connect_wifi = true;
}
}
// ==================== 主函数 ====================
void app_main(void)
{
ESP_LOGI(TAG, "OTA example app_main start");
// NVS 初始化(OTA 分区表的 NVS 分区可能比非 OTA 的小,需要处理兼容问题)
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK(err);
ESP_ERROR_CHECK(esp_netif_init());
s_wifi_event_group = xEventGroupCreate();
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
assert(sta_netif);
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
xTaskCreate(&ota_task, "ota_task", 8192, NULL, 5, NULL);
}
五、运行效果
编译烧录后,设备先配网(如果之前配过就直接连接),连上 Wi-Fi 后立刻开始 OTA:
I (1730) simple_ota_example: WiFi Connected to ap
I (2570) simple_ota_example: Starting OTA example task
I (2570) simple_ota_example: Attempting to download update from https://192.168.3.24:8088/hello_world.bin
I (3220) esp_https_ota: Starting OTA...
I (3220) esp_https_ota: Writing to <ota_0> partition at offset 0x110000
I (5690) esp_image: segment 0: paddr=00110020 vaddr=3c020020 size=0a328h ( 41768) map
...
I (5820) simple_ota_example: OTA Succeed, Rebooting...

重启后运行的就是新下载的 hello_world 固件了:
Hello world!
This is esp32s3 chip with 2 CPU core(s), WiFi/BLE, silicon revision v0.1, 4MB external flash
Minimum free heap size: 390340 bytes
Restarting in 10 seconds...
从下载到重启,整个过程只花了几秒钟。
六、OTA 失败排查清单
| 问题 | 排查方向 |
|---|---|
| 连接服务器超时 | 设备和电脑是否在同一网段?(如都是 192.168.3.x) |
| 证书验证失败 | xiaozhi_cert.pem 是否正确放到 main 目录并在 CMakeLists 中声明? |
| 下载失败 | 先用浏览器访问 https://你的IP:端口/文件名 测试能否下载 |
| 端口连不上 | 检查 HFS 端口配置,检查防火墙是否放行 |
| 分区不够大 | 确认 Flash 大小设置 ≥ 4MB,确认选了 OTA 分区表 |
| 固件格式错误 | 确认上传的是 .bin 文件(build 目录下的),不是 .elf |
七、总结
知识链路已经完整了
NVS(存储)
↓
SPIFFS(文件系统)
↓
分区表(Flash 空间规划)
↓
SmartConfig(Wi-Fi 配网)
↓
HTTP/HTTPS(网络通信)
↓
MQTT(实时通信,上云)
↓
OTA(远程升级) ← 本篇
到这里,一个物联网设备从开发到产品化需要的核心技术栈就基本齐了。
关键收获
- OTA 的本质是"乾坤大挪移":不是覆盖正在运行的固件,而是写到另一个分区再切换
esp_https_ota()一个函数搞定一切:下载、写入、切换分区,全自动- 分区表设计是 OTA 的基础:没有 ota_0、ota_1 和 otadata,OTA 无从谈起
- HTTPS + 证书验证是生产必备:确保固件来源可信,防止被篡改
- factory 分区是最后的保底:无论怎么升级,出厂固件永远在
一点感悟
回顾整个学习过程,从最开始的 GPIO 点灯,到现在能做到通过网络远程升级固件,ESP32-S3 的能力远超我最初的想象。它不仅仅是一个"单片机",更像是一个完整的物联网解决方案平台。
最让我感慨的是 ESP-IDF 的 API 设计——从 NVS 到 SPIFFS,从 HTTP 到 MQTT,再到 OTA,所有模块的使用套路都是一致的:配置结构体 → 初始化 → 注册回调 → 启动。掌握了这个模式,学新功能的速度会越来越快。
更多推荐




所有评论(0)