Bootloader 从外挂 Flash 选择固件并引导启动

这个 Bootloader 的核心思路是把外挂 W25Q Flash 做成固件镜像池和启动决策区。Bootloader 复位后先读取外部 Flash 中的启动选项,再选择目标固件、完成校验、提交到内部 APP 运行区,最后跳转到 APP 向量表启动。

这样做的直接收益是:OTA 链路可以分包、断点续写或分多次会话完成;内部 Flash 只在固件头、固件本体、启动选项都通过校验后才被擦写。Bootloader 不需要在接收过程中反复动内部 Flash,APP 区也不会因为半包数据变成不可启动状态。

升级目标

这个项目解决的是小容量 MCU 做 OTA 升级时的一个基本矛盾:内部 Flash 空间有限,升级链路又不适合把数据直接写进运行区。

内部 Flash 被划成两块:

外挂 Flash 承担三类状态:

内部 Flash 因此只保留“当前可运行版本”。外部 Flash 保留“候选版本”和“启动意图”。

外挂 Flash 保存可启动固件

升级包先进入外挂 Flash。Bootloader 重启后先把这份数据视为候选固件,再按固定顺序判断它能否成为本次启动目标:

这套流程把“固件已写入外部 Flash”和“本次启动采用该固件”分开。外部 Flash 中可以长期保存多份固件镜像,Bootloader 每次复位后根据启动选项选择其中一份,再用完整性校验决定能否引导启动。

这里的启动路径是:外部 Flash 保存固件镜像和启动意图,Bootloader 在复位后选择目标镜像,校验后写入内部 APP 运行区,再交给 APP 向量表启动。外挂 Flash 提供可重读、可校验的固件来源,真正的控制权交接发生在内部 APP 运行区。

启动选项做成主备记录

外挂 Flash 里不只保存固件,也保存启动选项。项目里有主记录和备份记录,两份记录都有 CRC32。

Bootloader 启动后先读两份启动选项:

启动选项里最重要的是启动标志。它把外部 Flash 中的固件槽位分成几个启动目标:

每次启动选项被修改后,Bootloader 会重新计算 CRC32,再写回主备两份记录。这个动作让启动意图本身也具备掉电后的可恢复能力。

固件槽位保留回退余地

外挂 Flash 的价值不只在“比内部 Flash 大”。它让 Bootloader 可以把不同生命周期的可启动固件分开放。

用户固件是日常运行目标,DFU 固件是升级能力本身,工厂固件提供恢复入口,升级固件作为待提交版本,备份固件用于保留旧版本,临时固件用于不改变默认启动策略的试运行。

这些槽位让 Bootloader 的职责从“把一个 bin 写进 APP 区”扩展成“根据启动选项选择一个外部 Flash 固件槽位,再决定是否引导启动”。提交成功后,启动选项可以回到默认用户启动模式;需要保持工厂模式时,也可以让工厂启动标志继续保留。

这里的重点是责任分层:

这种分层减少了在线升级时最危险的耦合:业务 APP 不需要在自身运行期间覆盖自身所在的内部 Flash。

写内部 Flash 前先收口风险

内部 Flash 擦写是这个流程里风险最高的动作,所以它被放在最后。

项目中的写入路径有几个保护点:

最后一个细节很重要。Bootloader 跳到 APP 属于固件控制权交接。跳转前如果外设、中断、SysTick 状态没有收干净,APP 会带着 Bootloader 的运行现场启动,后续问题会很难定位。

适用边界

这类外挂 Flash Bootloader 适合几种场景:

它也有明确代价:

这个项目的核心设计可以概括成一句话:外挂 Flash 保存可校验的固件镜像和启动选项,Bootloader 在启动阶段选择目标镜像并引导启动。内部 Flash 只承接最终运行结果,外挂 Flash 承接 OTA 传输过程、版本选择和恢复路径。