☰
OTA 断电变砖——擦完旧区没备份就跳,加双备份 + 标记有效区再切
2026/10/11 19:44:26 网站建设 项目流程

OTA 断电变砖——擦完旧区没备份就跳,加双备份 + 标记有效区再切

文章标签:#嵌入式开发 #单片机 #Flash #OTA升级 #OTA #远程升级 #Bootloader #固件 #双备份

摘要:OTA 升级最怕中途断电——运行区只剩残包,设备直接变砖。本文从真实事故切入,剖析原地覆盖的"真空期"缺陷,给出 A/B 双备份 + 有效标记 + 原子切换的防砖方案:新固件先写备份区,整包 CRC32 + magic 校验通过后才切换启动槽,断电自动回退原区。附错误/正确代码、时序图与实测对比。

一、开篇:那个"升级到一半断电"的噩梦

做一款联网设备,OTA 流程是:收到新固件 → 直接擦掉运行区 → 写入新固件 → 跳转。一次现场升级时,用户恰好在"擦完旧固件、新固件才写 30%"时断电。再次上电,运行区是半截残包,Bootloader 跳过去直接 HardFault,设备变砖,只能返厂烧录。复现率虽低,但一旦发生就是 100% brick。

如果你做 OTA 还是"原地覆盖",这一讲能救你的售后。我会从双备份分区讲起,给你一套"断电可恢复"的 A/B 升级方案,并附实测对比。

全文导航:入门认知(为什么原地升级会砖)→ 进阶原理(A/B 分区与有效标记机制)→ 高级应用(错误流程复现 + 正确 Bootloader 代码 + 实测对比)。

二、入门认知:OTA 的本质是"换座位"

OTA(Over-The-Air)升级,核心是"把新固件安全地放到能跑的地方,再切过去"。原地覆盖最大的问题是:在"擦旧"和"写新"之间存在一个"无可用固件"的真空期,此时断电必砖。

类比:你给行驶中的汽车换轮胎,直接把旧胎拆了才发现新胎没气——车只能停路边。正确做法是备好另一条满气胎,确认装好再拆旧的。

如图 1 展示原地覆盖(危险)与 A/B 双备份(安全)的差异:

收到新固件

擦运行区

写新固件(中断电→变砖)

跳转

收到新固件

写备份区B(运行区A不动)

校验B完整

标记B有效,切启动槽

下次启动从B跑,A作备份

图 1:原地覆盖 vs A/B 双备份升级流程

适用边界:Flash 够大(能容两份 App)才用 A/B;极小芯片可用"差分+外部 SPI Flash 备份"折中。

三、进阶原理:双备份 + 有效标记如何防砖

关键三要素:

  1. 双区:Slot A、Slot B 各一份 App,当前跑 A 时新包写 B。
  2. 有效标记:每个 Slot 头部存magic + 版本 + CRC。Bootloader 只跳"magic 正确且 CRC 通过"的区。
  3. 原子切换:用独立的一小段"启动槽标志"(存另一页 Flash)记录"下次从哪启动",写好新包并校验通过后再更新该标志。

断电场景推演:写到 B 中途断电 → B 的 magic/CRC 不完整 → 下次启动 Bootloader 检测 B 无效,仍从 A 启动 → 设备照常工作,可重试升级。

易混淆点对比:

方案断电可恢复存储开销复杂度
原地覆盖否(变砖)1 份低
A/B 双备份是2 份中
外部 SPI+回滚是1.5 份高

[坑] 高频翻车:校验只算"长度对"不算 CRC,残包长度凑巧对就误判有效。必须整包 CRC32 校验 + magic 双保险。

断电场景推演(时序图):

FlashBootloader用户设备FlashBootloader用户设备Slot B 的 magic/CRC 不完整设备照常工作,可重试升级写入新固件到 Slot B(中途断电)重新上电启动读取启动槽标志校验 Slot B(失败)回退校验 Slot A(通过)从 Slot A 正常启动

四、高级应用:复现与根治

错误流程演示(伪代码)

voidota_bad(uint8_t*pkg,uint32_tlen){flash_erase(APP_ADDR,len);// 擦旧固件flash_write(APP_ADDR,pkg,len);// 这里断电 → 残包,变砖jump_to_app(APP_ADDR);}

正确完整代码(双备份 Bootloader 核心)

#defineSLOT_A0x08004000#defineSLOT_B0x08020000#defineFLAG_ADDR0x08002000// 启动槽标志页typedefstruct{uint32_tmagic;uint32_tver;uint32_tcrc;}hdr_t;#defineGOOD_MAGIC0x544F5441// "TOFA"/* 校验某 slot 头部 + 整包 CRC */intslot_valid(uint32_taddr,uint32_tlen){hdr_t*h=(hdr_t*)addr;if(h->magic!=GOOD_MAGIC)return0;if(crc32((uint8_t*)(addr+sizeof(hdr_t)),len-sizeof(hdr_t))!=h->crc)return0;return1;}/* 升级:写另一个 slot,校验通过后切标志 */voidota_good(uint8_t*pkg,uint32_tlen,uint32_tcur_slot){uint32_tdst=(cur_slot==SLOT_A)?SLOT_B:SLOT_A;flash_erase(dst,len);flash_write(dst,pkg,len);// 写备份区,当前区照常跑if(!slot_valid(dst,len)){report_fail();return;}// 校验不过不切flash_write_word(FLAG_ADDR,dst);// 原子切启动槽}/* Bootloader 启动:只跳有效 slot */voidboot(void){uint32_tslot=*(uint32_t*)FLAG_ADDR;if(slot!=SLOT_A&&slot!=SLOT_B)slot=SLOT_A;if(!slot_valid(slot,APP_LEN))slot=(slot==SLOT_A)?SLOT_B:SLOT_A;// 无效回退if(slot_valid(slot,APP_LEN))jump_to_app(slot);elsehard_fault_trap();// 双区都坏,进恢复模式}

Bootloader 启动决策流程:

否

是

是

否

是

否

上电启动

读取启动槽标志

标志指向 A 或 B?

默认指向 Slot A

校验目标 Slot

magic + CRC 通过?

跳转该 Slot 运行

回退校验另一 Slot

另一 Slot 有效?

进入恢复模式(DFU)

改前 vs 改后实测对比

指标原地覆盖A/B 双备份
升级中途断电100% 变砖0(回退原区)
售后返厂率高趋近于 0
存储占用1 份2 份

[注意] 切换标志本身也要防断电:可写两副本 + 各自 CRC,读取时多数表决;或先把标志写入再擦旧区,保证"任何时刻至少一个可用固件"。

五、收尾:要点复盘与最佳实践

核心要点:① 原地覆盖有"真空期",断电必砖;② A/B 双备份 + 有效标记 + 原子切换;③ 校验必须整包 CRC,不只看长度。

最佳实践清单(可收藏):

  • OTA 绝不原地覆盖,用 A/B 或外部备份。
  • 每个固件包带 magic + 版本 + CRC,Bootloader 只跳校验通过者。
  • 切启动槽是最后一步,且写前确保新包已完整校验。
  • 双区都坏要进"恢复模式"(如 UART/USB DFU)而非直接崩。
  • 升级成功建议软复位一次确认可启动(见第74讲)。

进阶延伸:可加"滚动版本号 + 签名验签"防降级攻击与固件篡改;A/B 还可配合"健康心跳"——新固件跑满 N 秒才确认提交,否则回滚。

完整 OTA 升级流程总览:

否

是

是

否

收到新固件

写入备份区(当前区不动)

整包 CRC32 + magic 校验

校验通过?

报告失败,保留原区

原子切换启动槽标志

软复位,从新区启动

健康心跳确认?

提交新固件,旧区作备份

回滚到旧区

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询