Bootloader 设计原理:双分区机制与启动跳转
Bootloader 这东西,说白了就是固件世界的「看门大爷」。它不负责干活,只负责一件事:确保系统能正确启动,并且在升级失败时还能救回来。
我早年做一款工业网关时,就因为 Bootloader 设计太简陋,吃了大亏。那次远程升级到一半断电了,设备直接变砖,客户差点把设备寄回来砸我脸上。从那以后,我对 Bootloader 的敬畏心就刻进骨子里了。
今天咱们就聊聊 Bootloader 最核心的三个话题:双分区机制、启动跳转逻辑、以及看门狗与超时处理。这些是实战中绕不开的硬骨头。
双分区机制(A/B 分区)
双分区,也叫 A/B 分区、冗余分区。它的思路很简单:在 Flash 里放两份固件,一份运行,一份备用。
你想想看,如果只有一份固件,升级时写坏了怎么办?断电了怎么办?校验失败怎么办?那就真成砖了。双分区就是为了解决这个「升级容错」的问题。
核心思想:任何时候都至少有一个可启动的固件分区。
我习惯把 Flash 布局设计成这样:
| 区域 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64 KB | 启动加载器,不可升级 |
| 分区 A(Active) | 0x08010000 | 512 KB | 当前运行固件 |
| 分区 B(Standby) | 0x08090000 | 512 KB | 备用固件/升级目标 |
| 参数区 | 0x08110000 | 16 KB | 存储启动标志、版本号等 |
升级流程是这样的:
- 系统在分区 A 正常运行。
- 下载新固件到分区 B。
- 校验分区 B 的完整性(CRC/SHA256)。
- 设置参数区,标记「下次从 B 启动」。
- 重启系统。
- Bootloader 读取参数,跳转到分区 B。
- 如果分区 B 启动成功,标记为「稳定运行」,下次继续从 B 启动。
- 如果分区 B 启动失败,回滚到分区 A。
这里有个关键点:「稳定运行」标记。我见过不少方案是启动成功就立刻标记,这其实有风险。万一启动后几分钟才崩溃呢?
我的习惯:应用层启动后,运行 30 秒无异常,才回写「稳定运行」标志。这 30 秒足够暴露大部分启动阶段的隐性故障。
启动流程与跳转逻辑
Bootloader 的启动流程,我一般分成四个阶段:
- 硬件初始化:关中断、设置时钟、初始化串口/看门狗。
- 分区决策:读取参数区,决定从 A 还是 B 启动。
- 固件校验:对目标分区做完整性检查。
- 跳转执行:设置向量表,跳转到应用入口。
跳转逻辑是 Bootloader 最敏感的部分。写错一个指针,系统就飞了。我贴一段我常用的跳转代码,注释都写清楚了:
/* Bootloader 跳转到应用固件 */
typedef void (*app_entry_t)(void);
void jump_to_app(uint32_t app_addr)
{
uint32_t stack_ptr; /* 栈顶指针 */
app_entry_t entry; /* 入口函数指针 */
/* 1. 检查应用分区起始地址是否合法 */
if (app_addr < FLASH_BASE || app_addr >= FLASH_END) {
return; /* 地址非法,留在 Bootloader */
}
/* 2. 从应用分区头部读取栈顶指针 */
stack_ptr = *(volatile uint32_t *)app_addr;
/* 3. 从应用分区头部读取入口地址(复位向量) */
entry = (app_entry_t)(*(volatile uint32_t *)(app_addr + 4));
/* 4. 检查栈顶指针是否在 RAM 范围内 */
if (stack_ptr < SRAM_BASE || stack_ptr >= SRAM_END) {
return; /* 栈指针异常,不跳转 */
}
/* 5. 关闭全局中断 */
__disable_irq();
/* 6. 设置系统中断向量表偏移 */
SCB->VTOR = app_addr;
/* 7. 设置主栈指针 */
__set_MSP(stack_ptr);
/* 8. 跳转到应用入口 */
entry();
/* 正常情况下不会执行到这里 */
}
这段代码里,我特别强调第 4 步的栈指针检查。为什么?
我曾经遇到过一批芯片,Flash 某些地址读出来全是 0xFF。如果直接拿 0xFFFFFFFF 当栈指针,系统一启动就进 HardFault。加上这个检查,至少能兜个底。
注意:跳转前一定要关中断。否则跳转后应用还没初始化完,中断就来了,向量表还没切过去,直接跑飞。
看门狗与超时处理
看门狗(Watchdog)是嵌入式系统的最后一道防线。但在 Bootloader 里,看门狗用不好反而会添乱。
我见过一个案例:Bootloader 里开了看门狗,但升级过程需要擦写 Flash,耗时较长。结果看门狗超时复位了,升级写到一半被中断,分区 B 写坏了,分区 A 还是旧版本。设备反复重启,就是起不来。
所以我的原则是:Bootloader 里看门狗要「分段喂」。
具体做法:
- 在 Bootloader 入口启动看门狗,超时时间设为 5 秒。
- 在关键操作(擦除、写入、校验)之前,预估最大耗时。
- 如果操作可能超过看门狗超时,就在操作过程中分段喂狗。
- 如果操作失败,不喂狗,让看门狗复位系统,触发回滚。
看门狗超时处理逻辑,我一般这样设计:
| 场景 | 超时时间 | 超时后的行为 |
|---|---|---|
| 等待升级命令 | 10 秒 | 跳转到主分区启动 |
| 擦除 Flash 扇区 | 每扇区 2 秒 | 喂狗后继续 |
| 写入 Flash 页 | 每页 500 ms | 喂狗后继续 |
| 校验固件完整性 | 总大小 / 校验速度 | 喂狗后继续 |
| 应用启动确认 | 30 秒 | 回滚到另一分区 |
这里有个小技巧:用硬件定时器做辅助超时。看门狗负责「死锁保护」,硬件定时器负责「业务超时」。两者分工明确,互不干扰。
避坑指南:我曾经在 Bootloader 里用了一个 1 秒超时的看门狗,结果调试时单步执行,每走一步就复位一次。后来我改成:调试模式下关闭看门狗,发布版本再打开。这个开关用宏控制,别手误。
双分区启动决策流程图
下面这张图,是我做 Bootloader 时必画的一张决策图。它把整个启动流程串起来了:
这张图把整个决策过程画得很清楚。你注意看那两个「校验通过?」的菱形节点——如果校验失败,系统不会硬着头皮跳转,而是回滚到另一个分区,或者进入升级等待模式。这就是双分区容错的精髓。
实战中的几个坑
最后,我分享几个我在 Bootloader 实战中踩过的坑,希望能帮你省点 debug 时间:
- 向量表偏移没对齐:有些 MCU 要求向量表地址按 512 字节对齐。我吃过一次亏,偏移设错了,中断全跑飞。
- 跳转前没关外设:比如 DMA 还在传输,UART 还在收发。跳转后外设状态不确定,应用层初始化时可能出问题。
- 看门狗在擦写时复位:Flash 擦写时间跟芯片型号、温度都有关系。别按手册上的典型值设超时,留够余量。
- 参数区没做备份:参数区如果只存一份,写一半断电就丢了。我习惯存两份,一份主参数,一份备份,启动时对比校验。
最重要的一个建议:Bootloader 代码越简单越好。别在 Bootloader 里塞复杂功能,比如文件系统、网络协议栈。Bootloader 的唯一使命是「加载并跳转」,做得越少,出错的概率越低。
嗯,Bootloader 这部分内容就聊到这儿。双分区机制、跳转逻辑、看门狗处理,这三块是 Bootloader 的基石。你把这些吃透了,固件升级的容错能力就能上一个台阶。