Skip to content

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 file

Flash 要够大

两个分区各 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 步:做一个新版本 ​

改两样东西:

  1. 版本号加一:#define FIRMWARE_VERSION "1.0.1"
  2. 改点看得见的,比如开机画面的文字

版本号必须改

不改的话平台认为设备已是最新,根本不会下发升级任务 —— 你会以为 OTA 坏了。

然后只编译、不烧录:

产物在 build/你的工程名.bin。

第 6 步:上传到 OneNET 并推送 ​

  1. 在 OneNET 后台进入 固件升级 / OTA
  2. 上传刚才那个 .bin,版本填 1.0.1
  3. 创建升级任务,选中你的设备

第 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 都在赌。

动手试试 ​

  1. 改一下分区表加上双 app 分区,重新编译,看分区表有没有生效。
  2. 在 OneNET 后台上传一个新版本固件,观察串口能否收到 /ota/inform 通知。
  3. 做一次完整的 OTA,屏幕上显示下载百分比。
  4. 故意上传一个开机就崩溃的固件,验证回滚能把设备救回来。
  5. 最终考验:让设备连续跑 24 小时,每小时记录一次剩余内存,确认没有泄漏、没有重启。