第 8 篇:OTA 远程升级——不用拆机更新固件

设备已经部署到客户家里了,Bug 修好了新功能写完了,怎么办——让他寄回来串口烧录?这就离谱了。OTA(Over-The-Air)远程升级是 IoT 产品的标配。


1. ESP32 OTA 的原理

Flash 布局(4MB):
┌──────────┬──────────┬──────────┬──────────┬────────┐
│ Bootloader│ OTA Data │  OTA_0   │  OTA_1   │  NVS   │
│  固定     │ 记录哪个  │  当前固件 │  新固件   │ 用户数据│
│           │ 分区启动  │ (2MB)    │ (2MB)    │        │
└──────────┴──────────┴──────────┴──────────┴────────┘

升级流程:
1. 当前运行在 OTA_0
2. 下载新固件 → 写入 OTA_1
3. 校验通过 → 修改 OTA Data → "下次从 OTA_1 启动"
4. 重启 → Bootloader 读 OTA Data → 从 OTA_1 启动
5. 如果 OTA_1 启动失败 → Bootloader 自动回滚到 OTA_0

关键设计:两个分区交替用,升级成功了才切换。新固件启动失败自动回滚,永远不会变砖。


2. 分区表配置

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,  0x1E0000
ota_1,    app,  ota_1,             , 0x1E0000
nvs_user, data, nvs,               , 0x4000

重要ota_1 不写 Offset——让工具自动计算,避免手写出错。

sdkconfig 里指定分区表路径,或在 menuconfig 中:

Partition Table → Partition Table → Custom partition table CSV
                → Custom partition CSV file → "partitions.csv"

3. HTTP OTA——完整实现

// ota_manager.c
#include "esp_https_ota.h"
#include "esp_ota_ops.h"

// MQTT 消息回调:收到 OTA 指令
// topic: device/ota/cmd
// payload: {"url":"http://ota.example.com/firmware_v1.2.bin","version":"1.2","md5":"abc123..."}

static void on_ota_cmd(const char *json_str) {
    cJSON *root = cJSON_Parse(json_str);
    const char *url = cJSON_GetObjectItem(root, "url")->valuestring;
    const char *md5 = cJSON_GetObjectItem(root, "md5")->valuestring;
    const char *ver = cJSON_GetObjectItem(root, "version")->valuestring;

    ESP_LOGI("OTA", "收到升级指令 → 版本 %s, URL=%s", ver, url);

    // 上报状态:正在下载
    mqtt_publish("device/ota/status", "{\"status\":\"downloading\"}");

    // 开始 OTA
    esp_http_client_config_t http_cfg = {
        .url = url,
        .timeout_ms = 30000,  // 大固件需要更长时间
        .keep_alive_enable = true,
    };

    esp_https_ota_config_t ota_cfg = {
        .http_config = &http_cfg,
    };

    esp_err_t ret = esp_https_ota(&ota_cfg);

    if (ret == ESP_OK) {
        ESP_LOGI("OTA", "固件下载完成,校验通过,3 秒后重启...");
        mqtt_publish("device/ota/status", "{\"status\":\"success\",\"version\":\"%s\"}", ver);
        vTaskDelay(pdMS_TO_TICKS(3000));
        esp_restart();
    } else {
        ESP_LOGE("OTA", "OTA 失败: %s", esp_err_to_name(ret));
        mqtt_publish("device/ota/status", "{\"status\":\"failed\",\"error\":\"%s\"}",
                     esp_err_to_name(ret));
    }
    cJSON_Delete(root);
}

4. OTA 安全——不能谁都能给你设备刷固件

方案 A:HTTPS + 服务器认证

esp_https_ota 默认验证服务器证书(如果 URL 是 https://)。确保服务端证书有效即可。

方案 B:固件签名校验

在 HTTP OTA 下载完成后,用 RSA/ECDSA 校验固件签名:

// 在 esp_https_ota 下载完成后的回调中
esp_ota_handle_t update_handle;
const esp_partition_t *update_partition = esp_ota_get_next_update_partition(NULL);
esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &update_handle);

// ... 写入固件 ...

// 校验签名
esp_image_metadata_t data;
esp_err_t err = esp_image_verify(ESP_IMAGE_VERIFY, &update_partition, &data);
if (err != ESP_OK) {
    ESP_LOGE("OTA", "固件签名校验失败!可能是恶意固件。");
    esp_ota_abort(update_handle);
    return;
}

方案 C:最简单的——私有 URL + 短有效期 Token

服务器端:生成带过期时间的签名 URL
固件 URL: http://ota.example.com/fw.bin?token=xxx&expire=1710000000
设备端:不校验(小产品够用了)

5. MQTT 触发 OTA 的完整流程

┌──────┐        ┌────────┐        ┌──────────┐
│ 管理  │        │  MQTT  │        │  ESP32   │
│ 后台  │        │ Broker │        │          │
└──┬───┘        └───┬────┘        └────┬─────┘
   │                │                  │
   │  1. 上传固件到 OSS/CDN             │
   │──────────────────────────────────→│
   │                │                  │
   │  2. 下发升级指令                   │
   │──────────────→│                  │
   │                │  3. device/ota/cmd│
   │                │───────────────→  │
   │                │                  │ 4. 下载固件
   │                │                  │ 5. 校验、切换分区
   │                │  6. 上报成功      │
   │                │←───────────────  │
   │                │                  │ 7. 重启

6. 常见 OTA 翻车现场

问题 原因 对策
下载到一半断电 没做断点续传 小固件重试即可,大固件用 Range 请求
新固件启动后不停重启 新固件有 Bug 自动回滚机制(ESP-IDF 自带)
OTA 后配置丢失 NVS 被覆盖 NVS 单独分区,不要和 app 混
固件太大 OTA 分区放不下 分区太小 检查 idf.py size-components,压缩或扩分区

最坑的坑:OTA 后 WiFi 凭据丢了。因为之前有人把 WiFi 凭据写在固件里(而不是 NVS 里)→ OTA 覆盖固件 → 凭据没了。永远把用户数据放 NVS,不要硬编码在固件里。


下一篇:低功耗设计——18650 电池如何撑 3 个月

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐