tags: 嵌入式,固件升级,Flash,双镜像,OTA,IAP,安全启动 category: 嵌入式 type: original固件升级断电了怎么办双镜像 块交换升级失败也不变砖一句话: 固件升级最怕中途断电——Flash 写一半代码不完整下次上电直接变砖。双镜像方案把 Flash 分成 Bank 0 和 Bank 1当前固件跑在 Bank 0新固件写到 Bank 1CRC 校验通过后交换两块——升级失败永远有老固件兜底。单镜像的致命问题最简单的固件升级方式收到新固件 → 擦除 Flash → 写入新固件 → 重启。升级中如果断电 │ ▼ Flash 里写了一半新固件老固件已经没了 │ ▼ 芯片上电读到不完整的代码 → 起不来 │ ▼ 设备变砖这就是单镜像方案的致命缺陷——升级过程中只有一个固件副本出任何问题都没法回滚。双镜像方案永远有一个好的把 Flash 分成两块Bank┌─────────────┐ 低地址 │ Bank 0 │ ← 当前运行的固件 │ (老版本) │ ├─────────────┤ │ Bank 1 │ ← 空闲等着装新固件 │ (空/旧版本) │ └─────────────┘ 高地址升级流程变成1. 芯片正常运行跑 Bank 0 的固件 │ 2. 收到新固件 → 写入 Bank 1 │ 3. 对 Bank 1 做 CRC 校验 │ ┌─── 通过 → 交换 Bank 0 和 Bank 1 → 重启 → 跑新固件 │ └─── 失败 → 报错Bank 0 不受影响老固件继续跑任何时候都有一个完整、校验过的固件副本。断电没事Bank 0 还是好的。块交换不用搬数据换一个寄存器就行交换 Bank 0 和 Bank 1——听起来要搬几百 KB 的数据实际上不用。很多 MCU 内置了一个块交换Bank Swap机制Flash 控制器里有一个寄存器写入这个寄存器之后硬件自动把两块 Flash 的地址映射对调。交换前: CPU 看到的 0x00000000 → 物理 Bank 0 CPU 看到的 0x00010000 → 物理 Bank 1 写入交换寄存器: 交换后: CPU 看到的 0x00000000 → 物理 Bank 1 ← 对调了 CPU 看到的 0x00010000 → 物理 Bank 0不需要搬数据不需要擦除改一个寄存器两块位置就对调了。通常只需要几个时钟周期。完整升级流程// 固件升级状态机 typedef enum { FW_STATE_IDLE, // 空闲 FW_STATE_RECEIVING, // 正在接收新固件 FW_STATE_VERIFYING, // 校验 CRC FW_STATE_COMMITTING, // 交换块 FW_STATE_ERROR // 升级失败 } E_FW_STATE;步骤 1接收新固件新固件通过 I2C、串口、CAN 等接口分包发送。每次收到一包写入 Bank 1 的对应位置void FW_WriteBlock(uint16_t u16Offset, const uint8_t *pu8Data, uint16_t u16Len) { // 关中断Flash 写期间不能被打断 uint32_t u32Primask __get_PRIMASK(); __disable_irq(); // 擦除只在扇区边界做 if (u16Offset 0) { Flash_EraseSector(BANK1_BASE_ADDR); } // 写入 Flash_WriteWords(BANK1_BASE_ADDR u16Offset, pu8Data, u16Len); // 恢复中断 if (u32Primask 0) { __enable_irq(); } }步骤 2CRC 校验全部写入完成后对 Bank 1 做一次完整的 CRC 校验uint8_t FW_Verify(void) { // 新固件头里的 CRC 值发送方计算的 uint32_t u32ExpectedCRC FW_GetHeaderCRC(); // 对 Bank 1 实际计算 CRC uint32_t u32ActualCRC CRC32_Calculate( (uint8_t *)BANK1_BASE_ADDR, FW_GetImageSize() ); if (u32ActualCRC ! u32ExpectedCRC) { // 校验失败 → 可能传输出错或写 Flash 出错 return FW_ERR_CRC_FAIL; } return FW_OK; }CRC 校验能检测出传输出错、Flash 写入出错、Flash 存储位翻转。三者通吃。步骤 3交换块CRC 通过后执行最关键的步骤void FW_Commit(void) { // 1. 先写一个魔术数到备份区标记正在交换 Flash_WriteWord(FLAG_ADDR, FLAG_SWAPPING); // 2. 写块交换寄存器 Flash_BankSwap(); // 3. 写魔术数标记交换完成 Flash_WriteWord(FLAG_ADDR, FLAG_SWAP_DONE); // 4. 软件复位 NVIC_SystemReset(); }上电时判断跑哪个版本每次上电固件先检查我在哪个 Bank 上运行然后看交换标志void FW_BootCheck(void) { uint32_t u32Flag *(uint32_t *)FLAG_ADDR; if (u32Flag FLAG_SWAPPING) { // 上次升级在交换过程中断电了 // → 块可能处于不确定状态 → 再交换一次恢复 Flash_BankSwap(); Flash_WriteWord(FLAG_ADDR, FLAG_SWAP_DONE); NVIC_SystemReset(); } else if (u32Flag FLAG_SWAP_DONE) { // 升级成功正常启动 Flash_WriteWord(FLAG_ADDR, FLAG_IDLE); // 清除标志 } // else: FLAG_IDLE老固件什么都不做 }为什么比单镜像安全维度单镜像双镜像升级断电变砖Bank 0 完好上电继续跑Flash 写入出错同上CRC 校验不过不执行交换新固件本身有 bug跑不起来紧急回滚到 Bank 0升级时间短略长要写完整的一个 BankFlash 占用少多两个 Bank 各占一半代价是 Flash 占用翻倍收益是永远不会变砖。对于已经部署到现场的产品这个代价完全是值得的——固件升级失败导致返厂的代价远远大于多配一块 Flash。总结步骤做什么1. 接收新固件写到 Bank 12. 校验CRC 校验 Bank 1 的完整性3. 交换写块交换寄存器Bank 0 ↔ Bank 14. 重启软复位从新 Bank 0 启动5. 兜底上电检查交换标志异常回滚一句话新固件永远先写到空闲的 Bank验证通过才切换。切换是用硬件寄存器换地址映射不是搬数据。这套方案在光模块、汽车电子、工业控制等不允许变砖的场景里是标配。