适用人群:刚学完 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 文件 |
| Bootloader | MCU 上电后第一个跑的一小段程序。它决定"接下来跑哪份 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_0、ota_1和一个ota_data分区,并非自动生成。大小 0x2000 足够。
然后在 menuconfig 里:
Partition Table → Partition Table选你这个 csvExample 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 个坑
| # | 坑 | 后果 | 正确做法 |
|---|---|---|---|
| 1 | ota_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 加密,防止别人把你设备刷成别家的。
七、动手练一练(照着做一遍才真的会)
- 拿一块 ESP32,打开 ESP-IDF 例程
system/ota,用idf.py menuconfig配好 Wi-Fi 和分区表,先build通过。 - 在电脑起一个本地 HTTP 服务器:
python -m http.server 8080,把一个改过版本号的.bin放进去。 - 烧录旧固件,观察日志:设备联网 → 下载 → 重启 → 跑新固件,打印出新版本号 ✅。
- 故意搞破坏:放一个损坏/截断的
.bin,看设备会不会因为校验失败而拒绝升级、老固件照常跑。 - 验证回滚:在新固件
main()里故意vTaskDelay(10000)之后再mark valid,期间手动复位,观察会不会回滚到旧版本。
这 5 步全跑通,你对 OTA 的理解就已经超过 80% 的初学者了。
小结
OTA 不是什么高深黑科技,本质就三件事:下载到备用区 → 校验 → 切换启动。ESP32 帮你把底层全包了,重点是把"校验"和"回滚"这两道安全锁理解透。先把例程跑通,再回头看本文的 5 个坑,你会发现自己已经能接住量产级的升级需求了。
下一篇预告:《STM32 自己写 Bootloader,实现串口升级》

7万+

被折叠的 条评论
为什么被折叠?



