STM32 BootLoader 实战八A/B 双分区升级、启动选择与失败回滚设计摘要前面几篇默认采用的是单 APP 分区BootLoader 区 APP 区 参数区 / 升级记录区单 APP 分区能满足很多场景但它有一个明显问题一旦开始擦除 APP旧程序就不完整了。如果升级中途断电、传输断开、Flash 写失败、CRC 校验失败设备就只能停留在 BootLoader等待重新升级。如果设备在现场无人值守或者升级失败后必须尽量自动恢复就要考虑 A/B 双分区。A/B 双分区的核心思路是当前运行 A 区 APP 升级时把新固件写到 B 区 B 区校验通过后下次启动切到 B 区 B 区运行成功后由 APP 确认 如果 B 区启动失败BootLoader 回滚到 A 区这篇重点讲 A/B 双分区的 Flash 划分、启动选择、升级记录、APP 确认、失败回滚以及在 STM32F103C8T6、STM32F407VET6、STM32G473VET6 上做这件事时要注意什么。目录1. 单 APP 分区的问题2. A/B 双分区的基本思路3. 哪些项目适合做 A/B 双分区4. Flash 空间够不够5. F103C8T6 的分区建议6. F407VET6 的分区建议7. G473VET6 的分区建议8. A/B 分区记录结构9. 固件头需要增加哪些字段10. BootLoader 上电如何选择启动分区11. 升级时为什么要写非活动分区12. 新 APP 第一次启动为什么不能马上确认成功13. APP 如何确认启动成功14. 回滚逻辑怎么设计15. APP 向量表和链接地址16. Flash 擦写注意事项17. A/B 分区状态机18. 常见问题19. 总结1. 单 APP 分区的问题单 APP 分区结构一般是这样0x08000000 -------------------- | BootLoader | -------------------- APP_BASE | APP | | | -------------------- PARAM_BASE | Upgrade Record | --------------------升级流程收到固件头 检查固件头 擦除 APP 区 接收 APP 数据 写入 APP 区 CRC 校验 标记 APP 有效 跳转 APP问题出在“擦除 APP 区”之后。只要擦除开始旧 APP 就不完整了。如果此时发生下面任意一种情况断电 串口线拔掉 网线断开 上位机崩溃 YMODEM 失败 Flash 写失败 CRC 校验失败BootLoader 都不能再跳旧 APP。它只能停在 BootLoader等待用户重新升级。这种设计在调试阶段、有人维护的设备、低成本项目里没问题。但如果设备分布在现场升级失败后没人能马上处理单 APP 分区就不够稳。A/B 双分区就是为了解决这个问题。2. A/B 双分区的基本思路A/B 双分区结构0x08000000 -------------------- | BootLoader | -------------------- APP_A_BASE | APP A | | | -------------------- APP_B_BASE | APP B | | | -------------------- PARAM_BASE | Boot Record | --------------------设备当前运行 A 区时升级新固件写到 B 区。设备当前运行 B 区时升级新固件写到 A 区。也就是活动分区当前正在运行的 APP 备用分区本次升级要写入的新 APP升级过程当前运行 APP A BootLoader 收到新固件 擦除 APP B 写入 APP B 校验 APP B 标记 APP B 为 PENDING 复位 BootLoader 启动 APP B APP B 运行正常后确认 BootLoader 标记 APP B 为 VALID如果 APP B 启动失败BootLoader 发现 B 区没有确认 回滚到 APP A这样做的价值是升级失败不影响旧 APP 新 APP 第一次运行失败可以回滚 断电恢复逻辑更可靠 现场维护压力小很多3. 哪些项目适合做 A/B 双分区A/B 双分区不是所有项目都要做。适合做 A/B 的项目设备长期运行在现场 升级失败后不能变砖 设备无人值守 网口远程升级 客户现场不方便接调试器 APP 体积没有占满 Flash 产品对可靠性要求高不一定适合做 A/B 的项目Flash 很小 APP 很大 升级总是有人现场操作 成本极限敏感 产品允许失败后手动重新刷机不要为了“看起来高级”硬做 A/B。A/B 双分区会增加Flash 空间占用 分区管理复杂度 启动选择逻辑 APP 链接地址处理 升级记录区设计 测试工作量如果 F103C8T6 只有 64KB Flash而 APP 已经 40KB再强行分 A/B就很痛苦。4. Flash 空间够不够做 A/B 前先算空间。公式很简单总 Flash BootLoader APP_A APP_B 参数区 预留空间如果 APP A 和 APP B 大小一样APP_SLOT_SIZE (FLASH_SIZE - BOOT_SIZE - PARAM_SIZE) / 2实际工程里还要留一点余量BootLoader 后续可能变大 APP 后续可能增加功能 参数区可能增加字段 Flash 擦除粒度可能不允许刚好切分不要把 Flash 刚好分满。建议至少留BootLoader 预留 4KB ~ 16KB APP 分区预留 10% 左右空间 参数区至少双记录不同 STM32 系列的 Flash 擦除粒度不同分区边界必须对齐擦除页或 Sector。5. F103C8T6 的分区建议STM32F103C8T6 常见 Flash 是 64KB页大小通常按 1KB 处理。它的空间很紧。如果做单 APP 分区常见划分0x08000000 -------------------- | BootLoader 16KB | 0x08004000 -------------------- | APP 46KB | 0x0800F800 -------------------- | Record 2KB | 0x08010000 --------------------如果做 A/B 双分区要非常克制0x08000000 -------------------- | BootLoader 12KB | 0x08003000 -------------------- | APP A 24KB | 0x08009000 -------------------- | APP B 24KB | 0x0800F000 -------------------- | Record 4KB | 0x08010000 --------------------这个方案的问题很明显BootLoader 只能 12KB 左右 每个 APP 只有 24KB 左右 YMODEM、CRC、Flash、日志都要压缩 APP 功能稍微复杂就不够所以 F103C8T6 不推荐强行做完整 A/B。更现实的选择方案 1单 APP 分区 断电恢复 方案 2换更大 Flash 型号比如 F103RCT6 / F103VET6 方案 3外部 SPI Flash 存新固件校验后再覆盖内部 APP 方案 4只保留最小 BootLoaderAPP 做得非常小再做 A/B如果产品要求远程升级可靠性F103C8T6 这种容量最好一开始就慎重。6. F407VET6 的分区建议STM32F407VET6 常见 Flash 是 512KB。F407 的内部 Flash 是 Sector 结构前面几个 Sector 小后面 Sector 大。常见 SectorSector 0: 0x08000000 ~ 0x08003FFF 16KB Sector 1: 0x08004000 ~ 0x08007FFF 16KB Sector 2: 0x08008000 ~ 0x0800BFFF 16KB Sector 3: 0x0800C000 ~ 0x0800FFFF 16KB Sector 4: 0x08010000 ~ 0x0801FFFF 64KB Sector 5: 0x08020000 ~ 0x0803FFFF 128KB Sector 6: 0x08040000 ~ 0x0805FFFF 128KB Sector 7: 0x08060000 ~ 0x0807FFFF 128KB一种比较稳的 A/B 划分0x08000000 ------------------------ | BootLoader 64KB | 0x08010000 ------------------------ | APP A 192KB | 0x08040000 ------------------------ | APP B 192KB | 0x08070000 ------------------------ | Record / Reserve 64KB | 0x08080000 ------------------------但要注意F407 Sector 7 是 128KB。如果你把最后 64KB 单独当记录区会遇到一个问题擦除记录区时会擦整个 Sector 7所以更符合 F407 Sector 的划分是0x08000000 ------------------------ | BootLoader 64KB | Sector 0~3 0x08010000 ------------------------ | Config / Record 64KB | Sector 4 0x08020000 ------------------------ | APP A 128KB | Sector 5 0x08040000 ------------------------ | APP B 128KB | Sector 6 0x08060000 ------------------------ | Reserve / Factory 128KB| Sector 7 0x08080000 ------------------------这个方案的优点每个分区按 Sector 对齐 记录区单独占 Sector 4 APP A/B 擦除互不影响 Sector 7 可以留作工厂固件、备份参数或后续扩展缺点每个 APP 只有 128KB 如果 APP 超过 128KB就要重新规划如果 APP 比较大也可以BootLoader Record 放前 128KB APP A 使用 192KB APP B 使用 192KB但一定要按照 Sector 擦除边界设计不要在 128KB Sector 中间硬切。7. G473VET6 的分区建议STM32G473VET6 常见 Flash 是 512KB内部 Flash 页大小通常按 2KB 处理。G4 的分区比 F407 好规划因为页大小更均匀。一种常见划分0x08000000 ------------------------ | BootLoader 64KB | 0x08010000 ------------------------ | APP A 192KB | 0x08040000 ------------------------ | APP B 192KB | 0x08070000 ------------------------ | Record 16KB | 0x08074000 ------------------------ | Reserve 48KB | 0x08080000 ------------------------也可以更均衡0x08000000 ------------------------ | BootLoader 48KB | 0x0800C000 ------------------------ | Record 16KB | 0x08010000 ------------------------ | APP A 224KB | 0x08048000 ------------------------ | APP B 224KB | 0x08080000 ------------------------G473 做 A/B 比 F103 现实很多。要注意APP A 和 APP B 起始地址都要按页对齐 擦除时按 page 计算 写入时按 G4 Flash 写入粒度处理 如果开启 TrustZone 或双 Bank要再结合实际工程配置本文先按普通单 Bank Flash 使用方式讲。8. A/B 分区记录结构A/B 分区最关键的是记录区。记录区要回答几个问题当前应该启动哪个分区 A 区固件是否有效 B 区固件是否有效 是否有一个新分区正在试运行 试运行失败后要回滚到哪个分区 每个分区的版本号、大小、CRC 是多少 上一次错误码是什么可以设计成这样#defineBOOT_RECORD_MAGIC0x424C5244U/* BLRD */#defineBOOT_RECORD_VERSION0x00010000Utypedefenum{SLOT_A0,SLOT_B1,}AppSlot_t;typedefenum{SLOT_STATE_EMPTY0xFFFFFFFFU,SLOT_STATE_VALID0xA5A55A5AU,SLOT_STATE_PENDING0x12345678U,SLOT_STATE_CONFIRMED0x5AA5A55AU,SLOT_STATE_BAD0xDEAD0001U,}SlotState_t;typedefstruct{uint32_tstate;uint32_tversion;uint32_tapp_base;uint32_tapp_size;uint32_tapp_crc32;uint32_tboot_count;uint32_treserved[2];}AppSlotInfo_t;typedefstruct{uint32_tmagic;uint32_trecord_version;uint32_tsequence;uint32_tactive_slot;uint32_trollback_slot;uint32_tpending_slot;uint32_tlast_error;uint32_tflags;AppSlotInfo_t slot[2];uint32_trecord_crc32;}BootRecord_t;字段说明active_slot 当前默认启动分区 rollback_slot 新 APP 试运行失败后回滚到哪个分区 pending_slot 当前正在试运行的分区 sequence 双记录时选择最新记录 boot_count PENDING 分区已经尝试启动几次 record_crc32 记录本身的 CRC这里有两个状态容易混淆。8.1 VALID 和 CONFIRMED可以把它们合成一个状态也可以分开。一种简单规则VALID: 分区 CRC 校验通过可以被 BootLoader 启动 PENDING: 新固件刚升级成功允许试运行但还没被 APP 确认 CONFIRMED: APP 已经运行成功并确认可以长期作为活动分区 BAD: 该分区启动失败或校验失败不允许启动如果不想分太细也可以只保留EMPTY VALID PENDING BADAPP 确认后把PENDING改成VALID。本文后面采用这个简化规则。8.2 为什么要双记录记录区本身写入时也可能断电。所以不要只存一份记录。建议Record A Record B每次写新记录时写到另一份记录里并增加sequence。上电读取时检查 magic 检查 record_crc32 选择 sequence 最大的一条如果最新记录写坏了还可以使用上一条完整记录。9. 固件头需要增加哪些字段第六篇已经设计过固件头。A/B 分区下建议增加或明确下面几个字段typedefstruct{uint32_tmagic;uint32_theader_size;uint32_theader_version;uint32_ttarget_mcu;uint32_ttarget_board;uint32_tapp_base;uint32_tapp_size;uint32_tapp_crc32;uint32_tapp_version;uint32_tslot_mode;uint32_tmin_bootloader_version;uint32_tflags;uint32_theader_crc32;uint8_treserved[256-13*4];}FirmwareHeader_t;其中slot_mode: 0 不指定分区由 BootLoader 自动选择非活动分区 1 指定写 A 区 2 指定写 B 区 min_bootloader_version: 固件要求的最低 BootLoader 版本 flags: 是否允许回滚 是否强制升级 是否允许写当前活动分区一般情况下不建议上位机强制指定 A/B 分区。更稳的方式是BootLoader 自己选择非活动分区 固件头只声明 APP 链接地址和大小 BootLoader 检查固件是否能放入目标分区如果 APP A 和 APP B 使用不同链接地址固件头里的app_base必须和目标分区一致。10. BootLoader 上电如何选择启动分区上电后不要简单地跳active_slot。要按规则判断。推荐流程1. 读取 BootRecord 2. 如果记录无效扫描 A/B 分区 3. 检查 pending_slot 4. 如果存在 PENDING 分区优先试运行它 5. 如果 PENDING 启动次数超过限制标记 BAD回滚 rollback_slot 6. 如果 active_slot 有效启动 active_slot 7. 如果 active_slot 无效尝试另一个有效分区 8. 两个分区都无效停留 BootLoader伪代码staticAppSlot_tBoot_SelectSlot(BootRecord_t*rec){if(Boot_RecordIsValid(rec)0){returnBoot_ScanAndSelectSlot(rec);}if(rec-pending_slotSLOT_B){AppSlot_t pending(AppSlot_t)rec-pending_slot;if(Boot_SlotIsBootable(rec,pending)){if(rec-slot[pending].boot_countBOOT_PENDING_MAX_TRY){rec-slot[pending].boot_count;Boot_RecordSave(rec);returnpending;}else{rec-slot[pending].stateSLOT_STATE_BAD;rec-last_errorBL_ERR_PENDING_BOOT_TIMEOUT;rec-pending_slot0xFFFFFFFFU;Boot_RecordSave(rec);}}}if(Boot_SlotIsBootable(rec,(AppSlot_t)rec-active_slot)){return(AppSlot_t)rec-active_slot;}if(Boot_SlotIsBootable(rec,SLOT_A)){returnSLOT_A;}if(Boot_SlotIsBootable(rec,SLOT_B)){returnSLOT_B;}returnSLOT_INVALID;}Boot_SlotIsBootable()至少检查分区状态是否 VALID 或 PENDING APP 栈顶是否在 SRAM Reset_Handler 是否在该分区范围内 APP CRC 是否正确不要只看状态标志。11. 升级时为什么要写非活动分区A/B 分区升级的核心就是写非活动分区。假设当前运行 Aactive_slot A target_slot B升级流程检查固件头 检查 B 区空间 擦 B 区 写 B 区 校验 B 区 设置 B 区 PENDING 设置 rollback_slot A 复位这样升级过程中即使失败A 区没有被擦 A 区还能启动失败处理固件头错误 - 不擦 B继续运行 A B 区擦除失败 - A 仍然可用 B 区写入失败 - A 仍然可用 B 区 CRC 错 - A 仍然可用这就是 A/B 的价值。如果升级时写当前活动分区就退化成单 APP 分区了。除非是特殊工厂模式否则不建议允许写当前活动分区。12. 新 APP 第一次启动为什么不能马上确认成功很多人第一次做 A/B 会这样处理B 区 CRC 通过 马上把 B 区标记为 VALID 以后都启动 B这还不够安全。因为 CRC 通过只能说明Flash 里的 B 区数据和固件包一致它不能说明B 区 APP 能正常初始化时钟 B 区 APP 能正常初始化外设 B 区 APP 没有启动后 HardFault B 区 APP 没有因为参数格式变化卡死 B 区 APP 能跑到主循环所以新 APP 第一次启动应该是PENDING状态。流程BootLoader 校验 B 区通过 标记 B 区 PENDING 启动 B 区 B 区运行起来后主动告诉 BootLoader我启动成功 BootLoader 才把 B 区改为 VALID如果 B 区启动失败没有确认BootLoader 下次上电看到 B 还是 PENDING 如果超过尝试次数就回滚到 A这叫“试运行确认”。13. APP 如何确认启动成功APP 确认启动成功有几种方式。13.1 APP 直接写 BootRecordAPP 运行稳定后直接写记录区voidApp_ConfirmRunning(void){BootRecord_t rec;if(Boot_RecordLoad(rec)!0){return;}if(rec.pending_slotAPP_CURRENT_SLOT){rec.slot[APP_CURRENT_SLOT].stateSLOT_STATE_VALID;rec.active_slotAPP_CURRENT_SLOT;rec.pending_slot0xFFFFFFFFU;rec.rollback_slot0xFFFFFFFFU;rec.last_error0;Boot_RecordSave(rec);}}这个方式简单但要注意APP 必须知道记录区地址 APP 和 BootLoader 要共用记录结构 APP 写 Flash 时不能影响自己的代码区 APP 写记录区时也要考虑断电13.2 APP 写确认标志复位后由 BootLoader 确认APP 不直接修改完整记录只写一个确认标志。例如APP_CONFIRM_MAGIC current_slot app_version confirm_crc然后软复位。BootLoader 上电后看到确认标志再更新 BootRecord。这种方式的好处记录区主要由 BootLoader 管理 APP 写入逻辑更少 减少 APP 和 BootLoader 对记录区结构的耦合缺点确认后需要复位一次 流程稍微多一步13.3 APP 通过命令请求 BootLoader 确认如果系统有内部通信机制也可以让 APP 通过命令请求 BootLoader。但大多数裸机 STM32 项目里BootLoader 和 APP 不会同时运行所以这种方式不常见。实际工程里我更推荐前两种。如果项目简单可以 APP 直接写 BootRecord。如果可靠性要求高可以 APP 写确认标志复位后由 BootLoader 统一更新。14. 回滚逻辑怎么设计回滚只处理一种情况新 APP 试运行失败不要把所有错误都叫回滚。例如固件头检查失败拒绝升级不叫回滚 写 B 区失败升级失败继续用 A不叫回滚 B 区 CRC 失败升级失败继续用 A不叫回滚 B 区启动后没有确认回滚到 A回滚判断关键是PENDING。伪代码staticvoidBoot_HandlePendingSlot(BootRecord_t*rec){AppSlot_t pending(AppSlot_t)rec-pending_slot;if(pendingSLOT_B){return;}if(rec-slot[pending].boot_countBOOT_PENDING_MAX_TRY){rec-slot[pending].boot_count;Boot_RecordSave(rec);Boot_JumpToSlot(pending);}rec-slot[pending].stateSLOT_STATE_BAD;rec-pending_slot0xFFFFFFFFU;rec-last_errorBL_ERR_PENDING_NOT_CONFIRMED;if(Boot_SlotIsBootable(rec,(AppSlot_t)rec-rollback_slot)){rec-active_slotrec-rollback_slot;Boot_RecordSave(rec);Boot_JumpToSlot((AppSlot_t)rec-rollback_slot);}Boot_RecordSave(rec);Boot_StayInBootloader();}BOOT_PENDING_MAX_TRY可以设置为1 次最严格新 APP 第一次没确认就回滚 2 次允许偶发复位 3 次给复杂系统更多启动机会不要无限尝试 PENDING 分区。否则新 APP 一直 HardFault设备会一直重启。15. APP 向量表和链接地址A/B 双分区最容易踩坑的是链接地址。15.1 A/B 使用不同链接地址如果 APP A 在APP_A_BASE 0x08010000APP B 在APP_B_BASE 0x08040000那么 APP A 和 APP B 的链接地址不同。你需要分别编译两个 binapp_a.bin 链接地址 0x08010000 app_b.bin 链接地址 0x08040000优点APP 运行时不需要重定位代码 BootLoader 直接跳对应地址缺点同一份源码要生成两个固件 上位机要知道发 A 包还是 B 包 打包流程更复杂15.2 A/B 使用相同链接地址另一种方式是让 APP 始终按同一个地址链接比如都按APP_RUN_BASE。但如果 B 区实际地址不同就涉及重映射或拷贝运行。在普通 STM32 内部 Flash 场景下这样做会复杂很多。所以多数项目更推荐A 区 APP 按 A 区地址链接 B 区 APP 按 B 区地址链接15.3 如何减少打包复杂度可以让打包工具根据目标 slot 生成固件包make app_a pack --slot A --base 0x08010000 --input app_a.bin --output firmware_a.pkg make app_b pack --slot B --base 0x08040000 --input app_b.bin --output firmware_b.pkg也可以在上位机连接设备后先读取当前活动分区设备当前运行 A 上位机选择 B 包发送 设备当前运行 B 上位机选择 A 包发送如果想让上位机只发一个固件包那就要设计更复杂的可重定位 APP 或运行时重映射。普通项目不建议一开始就这样做。15.4 VTOR 设置APP 启动后仍然要设置自己的向量表。APP ASCB-VTOR0x08010000;APP BSCB-VTOR0x08040000;如果同一份 APP 源码编译 A/B 两个版本可以用宏#defineAPP_BASE_ADDR0x08010000UvoidSystemInit_User(void){SCB-VTORAPP_BASE_ADDR;}编译 B 版本时改成#defineAPP_BASE_ADDR0x08040000U不要忘记中断向量表。A/B 分区能跳进去不代表中断一定正常。16. Flash 擦写注意事项16.1 擦除范围必须限制在目标 slot写 B 区时绝对不能擦到 A 区。擦除前检查staticintBoot_CheckSlotRange(AppSlot_t slot,uint32_taddr,uint32_tlen){uint32_tbaseBoot_GetSlotBase(slot);uint32_tsizeBoot_GetSlotSize(slot);if(addrbase){return-1;}if((addrlen)(basesize)){return-1;}return0;}每次写入也检查。不要只在开始时检查一次。16.2 F407 要特别注意 SectorF407 的 Sector 不均匀。如果 APP A/B 不是按 Sector 边界切分擦除一个 Sector 时可能把另一个分区擦掉。例如Sector 5: 0x08020000 ~ 0x0803FFFF Sector 6: 0x08040000 ~ 0x0805FFFFAPP A 放 Sector 5APP B 放 Sector 6就比较清楚。不要这样APP A: 0x08020000 ~ 0x08047FFF APP B: 0x08048000 ~ ...因为0x08040000 ~ 0x0805FFFF是一个完整 Sector擦 APP B 前半部分时会影响 APP A 尾部。16.3 记录区不要和 APP 共用擦除页记录区必须独立。不要让最后一页既放 APP又放记录。否则写记录时可能擦掉 APP 尾部。正确做法APP 分区结束地址对齐到 page/sector 记录区单独占用 page/sector16.4 写入完成后读回校验写 Flash 后可以分两级校验写入一小段后读回比较 整包写完后 CRC32 校验读回比较能更早发现 Flash 写入问题。整包 CRC32 是最终判断。17. A/B 分区状态机A/B 双分区下状态机比单 APP 多几个状态。typedefenum{BL_STATE_INIT0,BL_STATE_LOAD_RECORD,BL_STATE_SELECT_SLOT,BL_STATE_WAIT_TRIGGER,BL_STATE_RECV_HEADER,BL_STATE_CHECK_HEADER,BL_STATE_SELECT_TARGET_SLOT,BL_STATE_ERASE_TARGET_SLOT,BL_STATE_WRITE_TARGET_SLOT,BL_STATE_VERIFY_TARGET_SLOT,BL_STATE_MARK_PENDING,BL_STATE_JUMP_SLOT,BL_STATE_WAIT_CONFIRM,BL_STATE_ROLLBACK,BL_STATE_ERROR,}BootState_t;升级流程LOAD_RECORD SELECT_SLOT WAIT_TRIGGER RECV_HEADER CHECK_HEADER SELECT_TARGET_SLOT ERASE_TARGET_SLOT WRITE_TARGET_SLOT VERIFY_TARGET_SLOT MARK_PENDING JUMP_SLOT上电启动流程LOAD_RECORD SELECT_SLOT 如果有 PENDING 且未超过次数 - JUMP_SLOT 如果 PENDING 超过次数 - ROLLBACK 如果 active_slot 有效 - JUMP_SLOT 否则停留 BootLoaderAPP 确认流程APP 写确认标志 系统复位 BootLoader 读取确认标志 把 pending_slot 改为 active_slot 清除 pending_slot 启动该分区错误处理固件头检查失败 不修改 active_slot 不擦目标分区 目标分区写入失败 标记目标分区 BAD active_slot 不变 目标分区 CRC 失败 标记目标分区 BAD active_slot 不变 PENDING 启动失败 标记 pending 分区 BAD 回滚 rollback_slot可以看到A/B 分区下大部分升级失败都不影响当前可用 APP。这正是它的意义。18. 常见问题18.1 A/B 双分区是不是一定能防止变砖不是。它能降低升级失败导致不可运行的概率但不是万能。下面这些情况仍然危险BootLoader 自己被破坏 记录区两份都损坏 Flash 硬件故障 APP A 和 APP B 都无效 固件包写错目标芯片 APP 确认逻辑写错BootLoader 自己所在区域仍然要重点保护。所以 BootLoader 还是应该放在0x08000000前面固定区域并且 APP 升级时绝对不能擦写 BootLoader 区。18.2 F103C8T6 能不能做 A/B能做但不推荐做完整 A/B。64KB Flash 太紧。除非 APP 很小BootLoader 也很小否则很快就不够。更建议换大 Flash 型号 使用外部 SPI Flash 存新固件 继续使用单 APP 分区 完整异常恢复18.3 APP A 和 APP B 要不要完全一样源码可以一样但链接地址通常不同。也就是同一份源码 两套链接脚本 两个输出 bin 两个固件包不要把按 A 地址链接的固件写到 B 区。除非你做了可重定位设计。18.4 新 APP 多久确认成功合适不要一进main()就确认。建议至少等关键初始化完成时钟初始化完成 外设初始化完成 参数加载完成 通信任务启动完成 主循环或关键任务已运行如果项目有联网功能可以等网络服务启动后再确认。如果项目有电机、阀门、加热等危险输出确认点要放在安全状态建立之后。18.5 PENDING 尝试次数设置多少常见设置1 次最稳失败立即回滚 2 次允许一次偶发复位 3 次复杂系统可用但不要再多不要无限重试。18.6 回滚后要不要删除新 APP可以不删除。只要把新 APP 所在 slot 标记为 BADBootLoader 不再启动它即可。下次升级时目标 slot 可以重新擦除覆盖。保留 BAD 分区还有一个好处现场可以读取失败固件信息 分析版本号、CRC、错误码18.7 记录区写坏了怎么办用双记录。两条都坏时不要冒险跳 APP。可以扫描 A/B 分区检查 A 区向量表和 CRC 检查 B 区向量表和 CRC 选择版本较高或固定优先级的分区 重新生成记录区但这个恢复策略要谨慎。如果没有足够信息宁可停在 BootLoader。18.8 能不能做三分区可以。比如APP A APP B Factory APPFactory APP 是一个最小维护固件用来恢复网络、串口、参数和升级功能。但 Flash 空间会更紧逻辑也更复杂。先把 A/B 做稳再考虑 Factory 分区。19. 总结A/B 双分区解决的是单 APP 分区最大的痛点升级过程中不破坏当前可用 APP新 APP 启动失败后可以回滚。这篇的核心点单 APP 分区在擦除 APP 后无法保留旧程序A/B 分区升级时永远写非活动分区新固件写入并 CRC 通过后先标记为PENDING新 APP 运行成功后必须主动确认没有确认的新 APP 不能长期作为有效 APPPENDING 启动失败后回滚到旧分区BootRecord 要保存 active、pending、rollback、slot 状态、版本、CRC记录区建议做双记录防止写记录时断电F103C8T6 Flash 太小不推荐完整 A/BF407VET6 要按 Sector 边界规划G473VET6 做 A/B 相对更舒服但仍要注意页对齐A/B 的 APP 链接地址要处理清楚A 包和 B 包不能混写做到 A/B 双分区后BootLoader 就从“升级失败后等人救”变成了“升级失败后尽量自己恢复”。这对现场设备、远程升级、无人值守设备都很有价值。文章标签STM32 BootLoader IAP A/B分区 双分区升级 失败回滚 固件升级 Flash YMODEM W5500 嵌入式 单片机