OTA 断电变砖——擦完旧区没备份就跳加双备份 标记有效区再切文章标签#嵌入式开发 #单片机 #Flash #OTA升级 #OTA #远程升级 #Bootloader #固件 #双备份摘要OTA 升级最怕中途断电——运行区只剩残包设备直接变砖。本文从真实事故切入剖析原地覆盖的真空期缺陷给出 A/B 双备份 有效标记 原子切换的防砖方案新固件先写备份区整包 CRC32 magic 校验通过后才切换启动槽断电自动回退原区。附错误/正确代码、时序图与实测对比。一、开篇那个升级到一半断电的噩梦做一款联网设备OTA 流程是收到新固件 → 直接擦掉运行区 → 写入新固件 → 跳转。一次现场升级时用户恰好在擦完旧固件、新固件才写 30%时断电。再次上电运行区是半截残包Bootloader 跳过去直接 HardFault设备变砖只能返厂烧录。复现率虽低但一旦发生就是 100% brick。如果你做 OTA 还是原地覆盖,这一讲能救你的售后。我会从双备份分区讲起给你一套断电可恢复的 A/B 升级方案并附实测对比。全文导航入门认知为什么原地升级会砖→ 进阶原理A/B 分区与有效标记机制→ 高级应用错误流程复现 正确 Bootloader 代码 实测对比。二、入门认知OTA 的本质是换座位OTAOver-The-Air升级核心是把新固件安全地放到能跑的地方再切过去。原地覆盖最大的问题是在擦旧和写新之间存在一个无可用固件的真空期此时断电必砖。类比你给行驶中的汽车换轮胎直接把旧胎拆了才发现新胎没气——车只能停路边。正确做法是备好另一条满气胎确认装好再拆旧的。如图 1 展示原地覆盖危险与 A/B 双备份安全的差异收到新固件擦运行区写新固件(中断电→变砖)跳转收到新固件写备份区B(运行区A不动)校验B完整标记B有效,切启动槽下次启动从B跑,A作备份图 1原地覆盖 vs A/B 双备份升级流程适用边界Flash 够大能容两份 App才用 A/B极小芯片可用差分外部 SPI Flash 备份折中。三、进阶原理双备份 有效标记如何防砖关键三要素双区Slot A、Slot B 各一份 App当前跑 A 时新包写 B。有效标记每个 Slot 头部存magic 版本 CRC。Bootloader 只跳magic 正确且 CRC 通过的区。原子切换用独立的一小段启动槽标志存另一页 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*)(addrsizeof(hdr_t)),len-sizeof(hdr_t))!h-crc)return0;return1;}/* 升级:写另一个 slot,校验通过后切标志 */voidota_good(uint8_t*pkg,uint32_tlen,uint32_tcur_slot){uint32_tdst(cur_slotSLOT_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_Aslot!SLOT_B)slotSLOT_A;if(!slot_valid(slot,APP_LEN))slot(slotSLOT_A)?SLOT_B:SLOT_A;// 无效回退if(slot_valid(slot,APP_LEN))jump_to_app(slot);elsehard_fault_trap();// 双区都坏,进恢复模式}Bootloader 启动决策流程否是是否是否上电启动读取启动槽标志标志指向 A 或 B?默认指向 Slot A校验目标 Slotmagic CRC 通过?跳转该 Slot 运行回退校验另一 Slot另一 Slot 有效?进入恢复模式(DFU)改前 vs 改后实测对比指标原地覆盖A/B 双备份升级中途断电100% 变砖0(回退原区)售后返厂率高趋近于 0存储占用1 份2 份[注意] 切换标志本身也要防断电可写两副本 各自 CRC读取时多数表决或先把标志写入再擦旧区保证任何时刻至少一个可用固件。五、收尾要点复盘与最佳实践核心要点① 原地覆盖有真空期断电必砖② A/B 双备份 有效标记 原子切换③ 校验必须整包 CRC不只看长度。最佳实践清单可收藏OTA 绝不原地覆盖用 A/B 或外部备份。每个固件包带 magic 版本 CRCBootloader 只跳校验通过者。切启动槽是最后一步且写前确保新包已完整校验。双区都坏要进恢复模式如 UART/USB DFU而非直接崩。升级成功建议软复位一次确认可启动见第74讲。进阶延伸可加滚动版本号 签名验签防降级攻击与固件篡改A/B 还可配合健康心跳——新固件跑满 N 秒才确认提交否则回滚。完整 OTA 升级流程总览否是是否收到新固件写入备份区(当前区不动)整包 CRC32 magic 校验校验通过?报告失败,保留原区原子切换启动槽标志软复位,从新区启动健康心跳确认?提交新固件,旧区作备份回滚到旧区