外观
3.12 OTA 远程升级
这节要搞懂:设备怎么自己从网上下载新固件并完成升级。 需要基础:3.11
一、要解决什么问题
设备已经装好放在桌上了,改个 bug 还要拆下来插 USB —— 一台还好,十台呢?送出去的呢?
OTA(Over-The-Air,空中升级)指的是:设备通过网络下载新固件并自动完成更新,无需物理连接。
工程里走的是 OneNET 平台的 OTA(app_onenet_ota.c),和 MQTT 用同一套云端账号。
二、双分区:为什么断电不会变砖
OTA 的核心设计是准备两个"房间":
Flash
├── otadata 记录"现在该从哪个房间启动"
├── ota_0 ← 正在运行的旧固件(不动它)
└── ota_1 ← 新固件写到这里升级过程:
① 下载新固件 → 写进空着的那个分区(ota_1)
② 写完 → 校验完整性
③ 校验通过 → 改 otadata,指向 ota_1
④ 重启 → 从 ota_1 启动 ✅为什么这样就安全
整个过程中,旧固件一直原封不动躺在 ota_0 里。
- 下载到一半断电 → otadata 还没改,重启还是从 ota_0 启动,照常工作
- 校验失败 → 直接放弃,otadata 不动
- 只有全部成功了才切换
所以 OTA 中途断电不会变砖 —— 这是双分区最大的价值。
分区表要先配好
# Name, Type, SubType, Offset, Size
nvs, data, nvs, 0x9000, 0x6000
otadata, data, ota, 0xf000, 0x2000
phy_init, data, phy, 0x11000, 0x1000
ota_0, app, ota_0, 0x20000, 1600K
ota_1, app, ota_1, , 1600K两个分区都要放得下固件
两个 app 分区各自都要能装下完整固件,所以 Flash 至少要有 固件大小 × 2 的空间。
固件 1.5MB 的话,4MB Flash 会很紧张,建议用 8MB 或 16MB。
编译完成后,VS Code 的终端里会打印固件大小;也可以按 F1 用 ESP-IDF: Size Analysis 查看。
三、本工程走的是 OneNET OTA 流程 ⭐
不是"给个 URL 就下载"
网上大多数 OTA 教程是这样的:代码里写死一个固件 URL,调 esp_https_ota() 下载完事。
本工程不是。 它接的是 OneNET 平台的 OTA,整个流程由平台驱动:设备先上报自己的版本号,平台判断要不要升级,再把任务下发下来。
好处:在平台后台点几下就能给一批设备推送升级,不用改代码、不用自己搭服务器。
完整流程
① 设备启动 → 通过 OneNET HTTP 接口【上报当前固件版本】
↓
② 平台判断:这个版本需要升级吗?
↓
③ 需要 → 平台通过 MQTT 主题 /ota/inform 【通知设备】
↓
④ 设备收到通知 → 调 OneNET HTTP 接口【查询升级任务详情】
↓
⑤ 拿到固件下载地址和版本信息
↓
⑥ 下载固件 → 写进另一个分区 → 校验
↓
⑦ 上报升级进度 / 结果给平台
↓
⑧ 成功 → 切换启动分区 → 重启进新固件 ✅三个关键环节
| 环节 | 走什么通道 | 干什么 |
|---|---|---|
| 版本上报 | OneNET HTTP 接口 | 告诉平台"我现在是 v1.0.2" |
| 升级通知 | MQTT 主题 /ota/inform | 平台推一条"有新版本" |
| 任务查询 + 下载 | OneNET HTTP 接口 | 拿到固件地址并下载 |
为什么通知走 MQTT、下载走 HTTP
各用各的长处(3.11 讲过):
- MQTT 是长连接,平台随时能主动推消息给设备 —— 适合"通知"
- HTTP 适合传大文件,一个请求下载 1.5MB 固件,下完就断 —— 适合"下载"
用 MQTT 传固件? 它是为小消息设计的,传大文件要自己分包重组,费力不讨好。
代码大致长这样
c
// 1. 启动时上报版本
static void ota_report_version(void)
{
char url[256];
snprintf(url, sizeof(url),
"https://iot-api.heclouds.com/fuse-ota/%s/%s/version",
ONENET_PRODUCT_ID, ONENET_DEVICE_NAME);
// 带上 token 做鉴权,body 里放当前版本号
http_post_with_token(url, "{\"version\":\"" FIRMWARE_VERSION "\"}");
}
// 2. MQTT 收到 /ota/inform 通知时被调用
static void on_ota_inform(const char *payload)
{
ESP_LOGI(TAG, "ota notify: %s", payload);
// 3. 去查任务详情,拿固件下载地址
char download_url[256];
if (ota_query_task(download_url, sizeof(download_url)) != ESP_OK) {
return;
}
// 4. 下载并升级
do_ota_update(download_url);
}真正下载升级的那一步,还是用 ESP-IDF 封装好的函数:
c
#include "esp_https_ota.h"
esp_err_t do_ota_update(const char *url)
{
esp_http_client_config_t http_config = {
.url = url,
.crt_bundle_attach = esp_crt_bundle_attach, // HTTPS 证书校验
.timeout_ms = 30000,
.keep_alive_enable = true,
};
esp_https_ota_config_t ota_config = {
.http_config = &http_config,
};
esp_err_t err = esp_https_ota(&ota_config); // 下载 + 写入 + 校验
if (err == ESP_OK) {
ESP_LOGI(TAG, "ota success, rebooting");
esp_restart();
} else {
ESP_LOGE(TAG, "ota failed: %s", esp_err_to_name(err));
}
return err;
}版本号要记得改
每次编译新固件前,必须把 FIRMWARE_VERSION 加一。
版本号不变的话,平台会认为设备已经是最新版,不会下发升级任务 —— 你会以为 OTA 坏了,其实是平台觉得没必要升。
token 也用在 OTA 上
OneNET 的 HTTP 接口和 MQTT 用同一套 token 鉴权(3.11 讲过)。
token 过期的话,MQTT 和 OTA 会一起失效 —— 排查时注意这一点。
四、回滚:新固件启动不了怎么办
万一新固件有致命 bug、开机就崩,设备就废了 —— 回滚机制就是为了防这个。
先在 menuconfig 里打开:
Bootloader config → [*] Enable app rollback support打开之后的行为:
新固件第一次启动
↓
标记为"试用中"(pending verify)
↓
┌──────────────────────────────┐
│ 固件自检:能连网?能显示? │
└──────────────────────────────┘
↓ 通过 ↓ 崩溃 / 没确认
调用 mark_app_valid 重启后自动回滚到旧固件 ✅
↓
正式启用代码里要主动确认:
c
#include "esp_ota_ops.h"
void app_confirm_running_ok(void)
{
const esp_partition_t *running = esp_ota_get_running_partition();
esp_ota_img_states_t state;
if (esp_ota_get_state_partition(running, &state) != ESP_OK) {
return;
}
if (state == ESP_OTA_IMG_PENDING_VERIFY) {
// 跑到这里说明启动成功了,确认这个固件可用
esp_ota_mark_app_valid_cancel_rollback();
ESP_LOGI(TAG, "new firmware confirmed");
}
}这个确认放在哪里很讲究
别一开机就确认 —— 那样等于没做校验。
应该放在关键功能都跑起来之后,比如:LCD 显示正常、WiFi 连上了、主界面进去了 —— 这时候再确认。
放太早:有 bug 的固件也会被确认为"好的",回滚失效。 放太晚 / 忘了放:好固件也会被回滚,陷入"升级—回滚—升级"的循环。
五、三条纪律
① 栈要给足
HTTPS 下载 + TLS 握手非常吃栈,OTA 任务从 8192 起步,给少了必崩(1.10 讲过)。
② 升级时把资源让出来
下载固件要占带宽和内存。升级期间应该:
- 暂停天气请求
- 停止音频播放
- 降低界面刷新频率
否则容易内存不够导致下载失败。
③ 别在弱网环境下升级
下载中途断网会失败重来,浪费流量也浪费时间。
判断一下信号强度(esp_wifi_sta_get_ap_info() 拿 RSSI),太弱就先别升。
六、升级过程要给用户看
下载 1.5MB 可能要几十秒,屏幕上一定要有反馈,否则用户以为死机了:
c
// esp_https_ota 的分步用法,可以拿到进度
esp_https_ota_handle_t handle = NULL;
esp_https_ota_begin(&ota_config, &handle);
int total = esp_https_ota_get_image_size(handle);
while (esp_https_ota_perform(handle) == ESP_ERR_HTTPS_OTA_IN_PROGRESS) {
int done = esp_https_ota_get_image_len_read(handle);
ui_show_ota_progress(done * 100 / total); // 屏幕上显示百分比
}
esp_https_ota_finish(handle);七、结课:你学到了什么
走完这三章,你完整经历了一个嵌入式产品的开发流程:
想清楚要做什么 → 选硬件、分配引脚 → 设计代码结构
↓
拆分任务 → 写驱动 → 实现业务 → 接入云端 → 调稳定 → 远程运维这套流程换个项目一样能用 —— 智能门锁、环境监测仪、语音助手,无非是换外设、换业务逻辑,骨架是同一套。
本节要点
- OTA = 通过网络下载新固件自动升级,不用插线
- ⭐ 双分区:新固件写另一个分区,成功了才切换 —— 断电不会变砖
- 两个 app 分区各自都要装得下固件,Flash 建议 8MB 以上
- ⭐ 走 OneNET OTA 流程:HTTP 上报版本 → MQTT 收
/ota/inform通知 → HTTP 查任务拿地址 → 下载升级 - 通知走 MQTT(长连接能推),下载走 HTTP(适合传大文件)
- 每次编译前记得改版本号,否则平台不会下发任务
- ⭐ 必须开回滚,并在关键功能跑通之后才
mark_app_valid - OTA 任务栈 8192 起步,升级时把别的资源让出来
- 下载过程要在屏幕上显示进度
操作过程
目标:不插 USB,从云端把新固件推给设备并升级成功。
先决条件
必须先跑通 MQTT(3.11)—— OTA 的升级通知是通过 MQTT 下来的。
第 1 步:改分区表
在工程根目录建 partitions.csv:
# Name, Type, SubType, Offset, Size
nvs, data, nvs, 0x9000, 0x6000
otadata, data, ota, 0xf000, 0x2000
phy_init, data, phy, 0x11000, 0x1000
ota_0, app, ota_0, 0x20000, 1600K
ota_1, app, ota_1, , 1600K然后在 menuconfig 里指定:
Partition Table → Custom partition table CSV
(partitions.csv) Custom partition CSV fileFlash 要够大
两个分区各 1600K,加上其他分区至少要 4MB。建议用 8MB 或 16MB 的板子。
编译完成后看一眼打印出来的固件大小,必须小于 1600K。
第 2 步:打开回滚
Bootloader config → [*] Enable app rollback support第 3 步:设一个版本号
c
#define FIRMWARE_VERSION "1.0.0"第 4 步:烧录第一版(这次要插线)
串口应该出现版本上报:
I (9000) ota: report version 1.0.0第 5 步:做一个新版本
改两样东西:
- 版本号加一:
#define FIRMWARE_VERSION "1.0.1" - 改点看得见的,比如开机画面的文字
版本号必须改
不改的话平台认为设备已是最新,根本不会下发升级任务 —— 你会以为 OTA 坏了。
然后只编译、不烧录:
产物在 build/你的工程名.bin。
第 6 步:上传到 OneNET 并推送
- 在 OneNET 后台进入 固件升级 / OTA
- 上传刚才那个
.bin,版本填1.0.1 - 创建升级任务,选中你的设备
第 7 步:应该看到什么
I (120000) mqtt: ota inform received ← ① 收到通知
I (120100) ota: query task ok, url=https://...← ② 查到下载地址
I (120200) ota: downloading... 10%
I (125000) ota: downloading... 100%
I (125500) ota: verify ok, rebooting ← ③ 校验通过
--- 设备重启 ---
I (1000) main: firmware version 1.0.1 ← ④ 新版本起来了
I (9000) ota: mark app valid ← ⑤ 确认可用,不回滚屏幕上应该能看到你第 5 步改的那个文字。
第 8 步:验证回滚(重要)
故意做一个会崩溃的固件:
c
void app_main(void)
{
int *p = NULL;
*p = 1; // 故意触发崩溃
}版本号改成 1.0.2,上传推送。
应该看到:设备下载升级 → 重启 → 崩溃 → 再重启后自动回到 1.0.1。
这一步最值得做
它验证的是最坏情况下设备还能不能救回来。
没验证过回滚就往外发设备,等于每次 OTA 都在赌。
动手试试
- 改一下分区表加上双 app 分区,重新编译,看分区表有没有生效。
- 在 OneNET 后台上传一个新版本固件,观察串口能否收到
/ota/inform通知。 - 做一次完整的 OTA,屏幕上显示下载百分比。
- 故意上传一个开机就崩溃的固件,验证回滚能把设备救回来。
- 最终考验:让设备连续跑 24 小时,每小时记录一次剩余内存,确认没有泄漏、没有重启。