固件OTA升级全流程
从嵌入式工程师的角度讲清 MCU 固件 OTA 升级全流程:Flash 分区怎么划、固件包头放什么、版本检查、分片传输与写入、完整性校验和签名、切换标志、启动自检与回滚、每个阶段掉电会怎样,以及升级结果怎么上报。

本文目录
"OTA 有什么难的,把新固件发过去写进 Flash 不就行了?"
第一次做 OTA 的工程师常这么想,直到某次升级中途断电,设备变砖,得拆机用烧录器救。或者一批设备升级后启动不了,只能一台台返厂。
OTA 真正难的不是传输,而是保证任何一步出问题,设备都还能回到可用状态。这篇按升级的时间顺序,把设备侧和 App / 平台侧要做的事完整走一遍。平台侧的任务管理、灰度和日志,可以另看 设备OTA升级平台怎么做。
全流程一览
1. 打包 固件 + 包头(型号、版本、长度、CRC、签名)
2. 检查 App / 平台读取设备当前版本和型号,判断是否需要升级
3. 准备 设备进入升级模式,擦除下载区
4. 传输 分片发送,逐片或分批确认
5. 校验 整包 CRC / 哈希,必要时验签
6. 切换 写升级标志,重启
7. 启动 Bootloader 检查新固件,跳转运行
8. 确认 新固件自检通过,标记为有效;失败则回滚
9. 上报 设备上报新版本号,App / 平台以此判定成功Flash 分区:先保证有退路
最常见的三种布局:
| 布局 | 结构 | 优点 | 代价 |
|---|---|---|---|
| A/B 双区 | Bootloader + A 区 + B 区 | 升级失败直接回到旧固件 | Flash 要两倍应用空间 |
| 下载区 + 搬运 | Bootloader + 应用区 + 下载区 | 应用区可以大一些 | 搬运过程掉电要能恢复 |
| 单区 + Bootloader 接收 | Bootloader + 应用区 | 最省空间 | 升级中失败只能留在 Bootloader 重新接收 |
不管哪种,Bootloader 本身不参与升级,任何时候都能运行,这是最后一道保险。资源实在紧张、只能单区的,至少保证升级失败后停在 Bootloader 里等待重新升级,而不是跳进一个半截的固件。
固件包头:设备自己判断能不能装
不要只发裸的 bin 文件。在固件前面加一个包头,设备收到后先检查:
typedef struct {
uint32_t magic; // 固定标识,判断是不是合法的升级包
uint16_t hw_model; // 硬件型号,防止装错型号
uint16_t hw_rev_min; // 支持的最低硬件版本
uint32_t fw_version; // 新固件版本号
uint32_t fw_size; // 固件长度
uint32_t fw_crc32; // 固件 CRC32
uint8_t sign[64]; // 签名(可选,ECDSA 等)
} ota_header_t;型号不匹配直接拒绝。我们的 BMS 项目里电池、仪表和 4G 模组三类固件,升级地址和流程都不一样,包头里的型号信息就是防止"把仪表固件发给电池"的第一道检查。
版本检查
升级前 App 或平台先读设备的型号、硬件版本和当前固件版本:
- 新版本是否高于当前版本(是否允许降级要单独约定)
- 硬件版本是否在新固件支持范围内
- 设备电量、运行状态是否允许升级(比如电机运行中不允许升级)
这些条件不满足,就不要开始,提示用户原因。
传输:分片、确认、续传
固件按片发送,每片带序号或偏移地址。设备收到后写入 Flash 并回确认,确认里带上"下一片要哪个",这样丢片、重复都能处理。
- 片的大小取决于通道:蓝牙受 MTU 限制,我们的 BMS 蓝牙 OTA 用 64 字节;储能 BMS 走 Modbus 透传用 1KB
- 单片超时重试,累计失败超过上限就中止,并记录停在哪一片
- 设备记住已接收的偏移,断开后重新连接可以从断点继续
写 Flash 要注意:大多数 Flash 必须先按扇区擦除才能写入,擦除比写入慢得多。可以在准备阶段一次擦完下载区,也可以边收边擦,但要把擦除耗时算进确认超时里。
不同通道的具体做法,见 蓝牙OTA升级怎么实现 和 4G设备远程固件升级怎么做。
校验和签名
收完以后,对下载区整包算 CRC32 或 SHA-256,和包头对比:
- CRC / 哈希:保证数据完整,没有传错
- 签名:保证来源可信,防止有人伪造固件刷进设备。设备里只存公钥,私钥留在出包的服务器上
对外销售、能被用户接触到升级入口的产品,建议做签名校验。
切换和重启
校验通过后,写一个升级标志(放在单独的 Flash 扇区或者备份寄存器里),然后重启。Bootloader 读到标志:
- A/B 双区:把启动目标切到新区
- 下载区方案:把下载区搬到应用区,搬完清除标志
搬运过程是最危险的阶段。要记录搬运进度,掉电重启后 Bootloader 能从中断的位置继续搬,而不是跳进一个只搬了一半的应用。
启动确认和回滚
新固件能启动,不代表能正常工作。比较稳妥的做法是"试运行 + 确认":
- Bootloader 第一次启动新固件时,把它标记为"试运行",并记一次启动计数
- 新固件启动后做自检:外设初始化、关键参数读取、通讯模组联网
- 自检通过,新固件自己把状态改成"有效"
- 如果新固件反复重启、没能完成确认,Bootloader 发现启动计数超限,回滚到旧固件
开源的 MCUboot 就是类似的 test / confirm 机制,可以参考它的设计。
每个阶段掉电会怎样
设计完以后,按阶段过一遍掉电:
| 阶段 | 掉电后 | 要求 |
|---|---|---|
| 传输中 | 旧固件正常运行 | 下载区数据作废或断点续传 |
| 校验中 | 旧固件正常运行 | 重新校验或重新下载 |
| 写标志 | 标志写一半 | 标志要有校验,无效标志按"不升级"处理 |
| 搬运中 | 应用区不完整 | Bootloader 按进度续搬 |
| 新固件试运行 | 未确认 | 按启动计数回滚 |
上线前,每个阶段都要真机断电测试。
上报:以设备回读为准
升级完成的判定,不是"最后一片发送成功",而是设备重启后上报的版本号等于目标版本。我们的储能 BMS 远程升级平台,升级完成后会自动回读版本号,确认设备真的跑在新固件上。参考 储能 BMS 远程固件升级平台。
差分升级要不要做
差分升级只传新旧固件的差异,能大幅减少传输量,对 4G 流量和蓝牙升级时间都有帮助。代价是设备端要能还原(需要额外的 RAM 和 Flash),出包时也要针对每个旧版本生成差分包。固件不大、升级不频繁的项目,整包升级更简单可靠。
常见问题
OTA 设备侧和 App / 平台侧怎么分工?
一般是:Bootloader、分区、写入、校验、回滚由嵌入式工程师实现;App 和平台负责版本检查、分片发送、重试、进度展示和升级记录。双方要共同定一份升级协议。
没有外部 Flash,空间只够一个应用怎么办?
可以用"单区 + Bootloader 接收"的方案:升级时跳回 Bootloader,由 Bootloader 直接接收新固件写入应用区。失败了停在 Bootloader 等待重试,不会变砖,只是升级期间设备不能正常工作。
升级到一半用户关了 App 怎么办?
旧固件还在运行(双区或下载区方案),下次打开 App 可以从断点继续,或者重新开始。
你们能做 OTA 的 App 和平台部分吗?
可以,参考 OTA 升级平台开发。如果设备侧还没设计,也可以 联系我们 一起定升级协议。