嵌入式 OTA 空中升级:从“为什么要升级“到 ESP32 实战

适用人群:刚学完 STM32/ESP32 基础、听说过"远程升级"但没真正跑通过的同学
读完你能得到:① 搞懂 OTA 到底在解决什么问题;② 看懂 ESP32 怎么几十行代码就跑通 OTA;③ 避开新手最容易把设备"刷成砖"的 5 个坑。


一、先别管技术,看个你一定会遇到的场景

假设你做了个智能温湿度传感器,卖了 1000 台给客户,装在人家家里、仓库里。

两周后你发现自己代码里有个 bug:某次浮点计算溢出,温度高于 40℃ 时读数变成负数。

现在问题来了——你怎么把这 1000 台设备修好?

  • ❌ 方案 A:挨家挨户上门,拆壳、接串口、用 ST-Link 重新烧录。1000 台,人先废了。
  • ❌ 方案 B:让客户自己拆壳烧录。客户不是工程师,大概率把设备搞坏还来找你退货。
  • ✅ 方案 C:OTA。设备本身连着 Wi-Fi,你在全国任何地方把修好的固件往服务器一传,设备半夜自己下载、自己刷进去、重启,bug 没了,客户毫无感知。

OTA = Over-The-Air(空中升级):设备通过网络把新固件下载到本地 Flash,自己完成擦写、校验、重启,全程不需要你人到现场、不需要拆机接线。

一句话记住它:传统升级是"人带着线去设备跟前",OTA 是"新固件自己飞到设备里"。


二、前置概念(先搞懂这 6 个词,否则后面代码看天书)

名词白话解释
固件 Firmware跑在 MCU 上的程序,就是你 idf.py build / keil 编出来的那个 .bin 文件
BootloaderMCU 上电后第一个跑的一小段程序。它决定"接下来跑哪份 app"“要不要升级”。ESP32 的 bootloader 出厂就带,STM32 要自己写
分区表 Partition Table整颗 Flash 被切成好几块,每块规定干啥(放 app、放 Wi-Fi 配置、放空的备用区)。相当于 Flash 的"土地使用规划"
A/B 双分区(双 Bank)Flash 里放两份 app:一份正在跑(A),一份接新固件(B)。下载完校验 OK,下次启动切到 B
回滚 Rollback新固件跑起来发现起不来 / 有 bug?自动或手动切回 A,设备不死机、不报废
校验 / 签名防止"下载到一半断电"或"被人篡改"的固件被刷进去。常用 CRC、SHA256、RSA 签名

💡 为什么要用 A/B 双分区?因为设备正在跑的 app 不能自己擦自己(你不能在脚底下抽砖)。所以新固件必须写到"另一块空地",写好了再切换过去。


三、OTA 的原理(一张图说清全流程)

Flash 里简化布局长这样:

┌─────────────────────────────────────────┐  ← Flash 顶端
│ bootloader        (出厂引导,不可轻易动) │
├─────────────────────────────────────────┤
│ partition table   (记录下面每块是啥)     │
├─────────────────────────────────────────┤
│ ota_0  (app)      ← 当前正在跑的固件      │
├─────────────────────────────────────────┤
│ ota_1  (app)      ← 备用区,新固件写这里  │
├─────────────────────────────────────────┤
│ ota_data          ← 记着"下次启动跑 ota_0 │
│                    还是 ota_1"            │
└─────────────────────────────────────────┘  ← Flash 底端

标准 OTA 流程(建议背下来,面试常问):

1. 设备联网 → 问服务器:"我有 v1.0,有没有新版?"
2. 服务器:"有 v1.1,地址在这"
3. 设备下载 v1.1 的 .bin(一般走 HTTP/HTTPS,或 MQTT 透传)
4. 设备把 .bin 写进 ota_1(当前没在跑的那块)
5. 写完做校验(长度对不对、SHA256/签名对不对)
6. 校验通过 → 改 ota_data,标记"下次启动跑 ota_1"
7. 设备重启 → bootloader 读 ota_data → 跑 ota_1(v1.1)
8. 新固件自检 OK → 提交,从此 ota_1 成为正式版本
        │
        失败/崩溃重启
        ↓
   bootloader 发现没提交 → 自动回滚跑 ota_0(v1.0)

第 8 步的"提交/回滚"是 OTA 的命门,下文实战会重点讲。


四、ESP32 + ESP-IDF 实战(学生最省事的上手路径)

为什么用 ESP32 讲?因为乐鑫把 OTA 整套都封装好了:bootloader 自带、分区表改个配置就行、esp_ota_ops.h 把擦写校验全包了。你几乎不用碰底层寄存器,适合先跑通建立信心。

4.1 改分区表:让 Flash 支持双 app

ESP-IDF 工程里找到 partitions.csv(或 partitions_two_ota.csv),改成下面这样:

# Name,   Type, SubType, Offset,  Size,   Flags
nvs,      data, nvs,     0x9000,  0x6000,
phy_init, data, phy,     0xf000,  0x1000,
factory,  app,  factory, 0x10000, 1M,
ota_0,    app,  ota_0,   ,        1M,
ota_1,    app,  ota_1,   ,        1M,
otadata,  data, ota,     ,        0x2000,

逐列解释:

  • Name/Type/SubType:名字、类型(app 还是 data)、子类型。
  • Offset 留空:让烧录工具自动往后排,不容易算错地址。
  • Size 1M:每块给 1MB。⚠️ 必须 ≥ 你编译出的 app 实际大小,建议留 10%~20% 余量,否则 OTA 写入直接失败。
  • otadata 分区(Type=data, SubType=ota)必须显式声明:ESP-IDF 的 OTA 流程依赖它记录"下次从哪个 ota 分区启动"。官方文档明确要求分区表至少包含 ota_0ota_1一个 ota_data 分区,并非自动生成。大小 0x2000 足够。

然后在 menuconfig 里:

  • Partition Table → Partition Table 选你这个 csv
  • Example Configuration 里把 OTA 用的 Wi-Fi SSID/密码填好

实操部分以 ESP-IDF 编程指南的 OTA 章节为准,里面明确写了分区表必须包含 ota_data 分区。

4.2 核心代码:下载新固件并写入

下面这段代码,就是"把 HTTP 下载到的字节流,边收边写到 ota_1"的关键逻辑(基于 ESP-IDF 官方例程 system/ota 精简):

#include "esp_ota_ops.h"
#include "esp_http_client.h"
#include "esp_log.h"

static const char *TAG = "OTA";
static const char *url = "http://192.168.1.100:8080/firmware.bin";

esp_err_t do_ota_update(void)
{
    /* 1. 建一个 HTTP 客户端,指向固件地址 */
    esp_http_client_config_t config = {
        .url = url,
        .timeout_ms = 5000,
    };
    esp_http_client_handle_t client = esp_http_client_init(&config);
    if (esp_http_client_open(client, 0) != ESP_OK) {
        esp_http_client_cleanup(client);
        return ESP_FAIL;
    }

    /* 2. 拿到"当前没在跑"的那个分区,新固件写这里 */
    const esp_partition_t *update_part = esp_ota_get_next_update_partition(NULL);

    /* 3. 开写:ota_handle 是这次升级的句柄 */
    esp_ota_handle_t ota_handle;
    if (esp_ota_begin(update_part, OTA_SIZE_UNKNOWN, &ota_handle) != ESP_OK) {
        return ESP_FAIL;
    }

    /* 4. 边下载边写:每次读 1KB,立刻 esp_ota_write 进 Flash */
    int content_len = esp_http_client_fetch_headers(client);
    char *buf = malloc(1024);
    int total = 0, read_len;
    while ((read_len = esp_http_client_read(client, buf, 1024)) > 0) {
        esp_ota_write(ota_handle, buf, read_len);
        total += read_len;
    }
    free(buf);

    /* 5. 关键校验:下载字节数 != 服务器说的长度 → 半截固件,放弃! */
    if (total != content_len) {
        ESP_LOGE(TAG, "下载不完整 %d/%d", total, content_len);
        esp_ota_abort(ota_handle);
        esp_http_client_cleanup(client);
        return ESP_FAIL;
    }

    /* 6. 收尾校验(内部会算 SHA256) */
    if (esp_ota_end(ota_handle) != ESP_OK) {
        esp_http_client_cleanup(client);
        return ESP_FAIL;
    }

    /* 7. 切换启动分区:下次重启就跑新固件 */
    esp_err_t err = esp_ota_set_boot_partition(update_part);
    esp_http_client_cleanup(client);
    return err;   // 返回 ESP_OK 后,调用处执行 esp_restart() 即可
}

逐行要点(新手必看):

  • esp_ota_get_next_update_partition(NULL):自动挑"当前没在跑"的分区,不用你手动判断 A 还是 B。
  • esp_ota_begin / write / end这三件套是原子操作——没 end 成功之前,bootloader 不会切过去。
  • total != content_len 这步是提前中止优化:网络抖一下、固件只下了 80%,先拦下来避免后面无谓写入。⚠️ 注意:若服务器用 chunked 分块传输esp_http_client_fetch_headers 可能返回 -1,此时长度比对会误判失败——所以这一行只是优化,真正的完整性校验在下一步
  • esp_ota_end 内部会做完整性校验(校验镜像头里的 SHA256),这才是兜底:长度对但数据被篡改照样拦下。二者配合最稳,不要只依赖长度。

4.3 回滚:新固件起不来怎么办?

ESP-IDF 用的是"待提交"机制——这是 OTA 最关键的安全设计:

  • esp_ota_set_boot_partition() 之后,新固件处于"未提交"状态。
  • 新固件正常跑起来、确认自己没问题,调用下面这行提交
// 新固件 main() 里,确认一切正常后再调用
esp_ota_mark_app_valid_cancel_rollback();
  • 如果新固件有 bug、起不来、反复崩溃重启:bootloader 发现它"一直没提交",自动回滚到旧版本。设备照样能跑,不会成砖。

⚠️ 血泪经验:一定要在"确认能干活"之后再 mark valid。比如在"连上服务器 + 传感器初始化成功 + 业务线程起来了"之后。千万别 app_main 第一句就 mark——万一后面崩了,已经提交,就回不去了。


五、新手最容易把设备刷成砖的 5 个坑

#后果正确做法
1ota_0/ota_1 分区比 app 小写入失败 / 烧录报错分区 Size ≥ app 实际大小,留 20% 余量
2忘了 ota_data 分区bootloader 不知道启动哪个,永远跑 factory用 ESP-IDF 默认分区模板,ota_data 自动生成
3不校验就切换半截固件被当真刷进去 → 变砖必须 esp_ota_end + 长度比对
4一启动就 mark valid新固件其实有 bug,但已提交,回滚不了 → 变砖业务自检 OK 后再 mark
5用 HTTP 明文传固件被中间人篡改刷进恶意固件量产上 HTTPS + 固件签名校验

记住一个总原则:OTA 的底线不是"升得上",是"升坏了也不死"。A/B + 未提交回滚,就是为了这条底线。


六、进阶方向(知道有这些,面试能接话)

  • STM32 自己写 bootloader:用 Ymodem/串口或自定义协议收固件,写内部 Flash,CRC 校验后跳转到 app。难点在"中断向量表偏移 __set_MSP + 地址重映射",后面可以单独写一篇。
  • Nordic nRF + MCUboot:Zephyr 生态的成熟 OTA 方案,原生支持签名、加密、A/B。
  • 差分升级(Delta OTA):只传"新旧固件的差异包",流量能省 90%,适合 4G 流量贵、NB-IoT 窄带的场景。
  • 安全启动(Secure Boot):硬件级防刷非法固件,配合 Flash 加密,防止别人把你设备刷成别家的。

七、动手练一练(照着做一遍才真的会)

  1. 拿一块 ESP32,打开 ESP-IDF 例程 system/ota,用 idf.py menuconfig 配好 Wi-Fi 和分区表,先 build 通过。
  2. 在电脑起一个本地 HTTP 服务器:python -m http.server 8080,把一个改过版本号.bin 放进去。
  3. 烧录旧固件,观察日志:设备联网 → 下载 → 重启 → 跑新固件,打印出新版本号 ✅。
  4. 故意搞破坏:放一个损坏/截断的 .bin,看设备会不会因为校验失败而拒绝升级、老固件照常跑。
  5. 验证回滚:在新固件 main() 里故意 vTaskDelay(10000) 之后再 mark valid,期间手动复位,观察会不会回滚到旧版本。

这 5 步全跑通,你对 OTA 的理解就已经超过 80% 的初学者了。


小结

OTA 不是什么高深黑科技,本质就三件事:下载到备用区 → 校验 → 切换启动。ESP32 帮你把底层全包了,重点是把"校验"和"回滚"这两道安全锁理解透。先把例程跑通,再回头看本文的 5 个坑,你会发现自己已经能接住量产级的升级需求了。


下一篇预告:《STM32 自己写 Bootloader,实现串口升级》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值