7.3 OTA固件升级


文档摘要

7.3 OTA 固件升级 设备一旦出厂,拆壳烧录就成了奢望:几百台传感器节点装在仓库高处、上千盏灯挂在天花板上。OTA(Over-The-Air,空中固件升级)让设备经网络自己完成换血——下载新固件、校验、切换、失败回滚。这一节讲双分区方案的完整性设计,并以 ESP32 的原生 OTA 接口走一遍完整代码。它是全书知识的会师大 Sprint:Bootloader(7.1)、任务与队列(第 6 章)、看门狗(4.1)、CRC 校验(2.2 的查表法)在此拧成一股绳。 OTA 的完整性设计:假设每一步都会被打断 OTA 与烧录的本质区别在于环境失控:网络可能中途断、电源可能被拔、新固件可能根本跑不起来。所以方案设计的第一原则是任何一步失败,设备必须还能回到能用的状态。

7.3 OTA 固件升级

设备一旦出厂,拆壳烧录就成了奢望:几百台传感器节点装在仓库高处、上千盏灯挂在天花板上。OTA(Over-The-Air,空中固件升级)让设备经网络自己完成换血——下载新固件、校验、切换、失败回滚。这一节讲双分区方案的完整性设计,并以 ESP32 的原生 OTA 接口走一遍完整代码。它是全书知识的会师大 Sprint:Bootloader(7.1)、任务与队列(第 6 章)、看门狗(4.1)、CRC 校验(2.2 的查表法)在此拧成一股绳。

OTA 的完整性设计:假设每一步都会被打断

OTA 与烧录的本质区别在于环境失控:网络可能中途断、电源可能被拔、新固件可能根本跑不起来。所以方案设计的第一原则是任何一步失败,设备必须还能回到能用的状态。双分区(A/B)方案由此而来:

  1. 下载阶段:当前应用(跑在分区 A)从服务器拉取新固件,写入备用分区 B——绝不原地覆盖自己,写到一半断电也还有 A 可跑。
  2. 校验阶段:B 区写完后核对完整性(长度 + CRC32 或签名)。校验不过,B 区作废,继续跑 A,相当于无事发生。
  3. 切换阶段:写一个切换标志(bootloader 读它决定下次起跑哪个分区),然后软复位。Bootloader 起跑后读标志:B 区有效则跳 B。
  4. 确认与回滚:新固件 B 启动后必须"自我汇报"(跑起来了、连上网了、自检过了),应用主动确认;若到超时仍无人确认,Bootloader 判定新固件起不来,自动切回 A。回滚这一步是产品级 OTA 与玩具级 OTA 的分水岭

各阶段的故障与兜底对应关系:

故障时刻 现场 兜底机制
下载中断电 B 区半截固件 A 区未动,正常起跑
下载后发现文件错 B 区坏数据 CRC/签名校验拦截
切换后新固件崩溃 B 区起不来 启动计数超限,回滚 A
升级中看门狗复位 升级被腰斩 回到 A 区重来即可

ESP32 实战:三步走完 OTA

ESP-IDF 把双分区机制全部制度化:分区表管理 A/B 区、esp_ota API 管下载写入与切换、bootloader 管启动计数与回滚。应用代码只需要"取固件、喂数据、收尾确认"三步。下面的任务从固件服务器拉取并写入:

#include "esp_ota_ops.h" #include "esp_http_client.h" #include "esp_system.h" static void ota_task(void *arg) { /* 1. 开始 OTA:API 自动选中非当前的备用分区,并写入切换标志 */ const esp_partition_t *part = esp_ota_get_next_update_partition(NULL); esp_ota_handle_t ota; ESP_ERROR_CHECK(esp_ota_begin(part, OTA_SIZE_UNKNOWN, &ota)); /* 2. 分块下载、分块写入:固件可能有几 MB,绝不能整包进内存 */ esp_http_client_config_t cfg = { .url = "http://192.168.1.20:8070/app.bin", .timeout_ms = 8000 }; esp_http_client_handle_t cli = esp_http_client_init(&cfg); esp_http_client_open(cli, 0); esp_http_client_fetch_headers(cli); char buf[4096]; int n, total = 0; while ((n = esp_http_client_read(cli, buf, sizeof(buf))) > 0) { ESP_ERROR_CHECK(esp_ota_write(ota, buf, n)); /* 写入备用分区 */ total += n; vTaskDelay(1); /* 喂一口系统:让 WDT 与网络任务喘气 */ } /* 3. 收尾:esp_ota_end 内部做完整性校验(镜像头与哈希)*/ esp_err_t err = esp_ota_end(ota); if (err == ESP_OK && esp_ota_set_boot_partition(part) == ESP_OK) { printf("OTA ok, %d bytes, rebooting\n", total); esp_restart(); /* 软复位,bootloader 起跑新分区 */ } printf("OTA failed: %s, keep running\n", esp_err_to_name(err)); esp_http_client_cleanup(cli); vTaskDelete(NULL); /* 升级失败:留在旧固件继续服务 */ }

新固件起跑后的"确认"同样关键——它向 bootloader 声明"我活得好好的,别回滚":

void app_main(void) { /* 自检:网络连上、传感器应答正常之后才确认; 若迟迟不确认,bootloader 数完启动次数会切回旧分区。 */ if (selftest_all_ok()) { esp_ota_mark_app_valid_cancel_rollback(); /* 确认:取消回滚 */ } /* 之后照常初始化应用、创建任务…… */ }

配合的机制在背后全自动运转:bootloader 为待确认固件记启动次数(CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE 打开),连续几次起跑都没人确认就回滚旧分区;版本号与防降级(防回刷旧版漏洞固件)由镜像头字段携带。三段代码加上分区表,就是一套产品级的 OTA 骨架。

STM32 自研 OTA:把机制自己拼一遍

没有现成全家桶的 STM32 平台,OTA 由你用前文积木自搭:分区(7.1 的 Bootloader + 双应用区)、下载(任一通信口,串口带 Ymodem 或网络带 HTTP/MQTT 都行)、写入(Flash 按页擦写)、校验(CRC32 查表)、切换与回滚(Flash 里的标志位 + Bootloader 计数)。写入一段固件数据的核心函数长这样:

#include "stm32f10x.h" #define DL_BANK_ADDR 0x08010000UL /* 下载区起点(Bootloader 约定)*/ int flash_write_chunk(uint32_t offset, const uint16_t *data, uint32_t halfwords) { if (offset % (1024UL) == 0) { /* 跨页前先擦除(1KB/页)*/ FLASH->KEYR = 0x45670123; FLASH->KEYR = 0xCDEF89AB; /* 解锁 */ while (FLASH->SR & FLASH_SR_BSY) { } FLASH->CR |= FLASH_CR_PER; FLASH->AR = DL_BANK_ADDR + offset; FLASH->CR |= FLASH_CR_STRT; while (FLASH->SR & FLASH_SR_BSY) { } FLASH->CR &= ~FLASH_CR_PER; } FLASH->CR |= FLASH_CR_PG; /* 半字编程模式 */ for (uint32_t i = 0; i < halfwords; i++) { *(volatile uint16_t *)(DL_BANK_ADDR + offset + i * 2) = data[i]; while (FLASH->SR & FLASH_SR_BSY) { } if (FLASH->SR & FLASH_SR_EOP) FLASH->SR = FLASH_SR_EOP; /* 清完成标志 */ if (FLASH->SR & FLASH_SR_PGERR) return -1; /* 写 1 为 0 的失败 */ } FLASH->CR &= ~FLASH_CR_PG; FLASH->CR |= FLASH_CR_LOCK; /* 重新上锁 */ return 0; }

这段代码提醒我们两件 STM32 OTA 的独有坑。其一,擦写 Flash 时 CPU 停摆:F1 读 Flash 与写 Flash 共用总线,写页期间取指被阻塞,中断响应延迟可达毫秒级——升级期间必须与看门狗协调(4.1 的"已知耗时操作前喂狗"在此兑现),实时任务也要避开这个窗口。其二,下载与写入必须分区隔离:如果程序"边下载边原地擦写自己",写第一页就把正在运行的代码擦了——下载区与运行区分离(或先落外部存储/RAM 再搬运)是铁律。

安全与进阶:值得知道的边界

产品级 OTA 还有三道进阶题,这里点到边界。固件签名:防"固件包被中途调包",服务器用私钥签名,设备用内置公钥验签,验不过不切换——这也是安全启动的地基。加密传输:明文 HTTP 只适合内网验证,公网设备应走 HTTPS(ESP32 的 esp-tls 组件支持),证书烧在设备端。差分升级:几 MB 固件经蜂窝网传输成本高,只下发新旧版本的差分包、设备端重组,能省八成流量——代价是重组算法与更强的校验要求。

至此全书闭环:第 3 章点亮的那颗 LED,如今运行在一台能多任务并行、能自我升级的设备里。回头看这条路径——寄存器、时钟、中断、定时器、总线、RTOS、启动与 OTA——每一层都踩着下一层的地基,而起点不过是往一个地址写进几位配置。把这册的代码全部亲手跑过一遍,你就拥有了嵌入式开发最硬核的那部分直觉;剩下的,是拿一块新板子、一份新手册,把这套方法再走一遍。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U