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 存储启动标志、版本号等

升级流程是这样的:

  1. 系统在分区 A 正常运行。
  2. 下载新固件到分区 B。
  3. 校验分区 B 的完整性(CRC/SHA256)。
  4. 设置参数区,标记「下次从 B 启动」。
  5. 重启系统。
  6. Bootloader 读取参数,跳转到分区 B。
  7. 如果分区 B 启动成功,标记为「稳定运行」,下次继续从 B 启动。
  8. 如果分区 B 启动失败,回滚到分区 A。

这里有个关键点:「稳定运行」标记。我见过不少方案是启动成功就立刻标记,这其实有风险。万一启动后几分钟才崩溃呢?

我的习惯:应用层启动后,运行 30 秒无异常,才回写「稳定运行」标志。这 30 秒足够暴露大部分启动阶段的隐性故障。

启动流程与跳转逻辑

Bootloader 的启动流程,我一般分成四个阶段:

  1. 硬件初始化:关中断、设置时钟、初始化串口/看门狗。
  2. 分区决策:读取参数区,决定从 A 还是 B 启动。
  3. 固件校验:对目标分区做完整性检查。
  4. 跳转执行:设置向量表,跳转到应用入口。

跳转逻辑是 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 启动决策流程 双分区 Bootloader 启动决策流程 上电复位 Bootloader 初始化(关中断/时钟/看门狗) 读取参数区(启动标志) 从哪个 分区启动? 分区 A 校验分区 A 分区 B 校验分区 B 校验 通过? 校验 通过? 通过 通过 跳转到应用固件 应用固件运行 校验失败时: 回滚到另一分区 或进入升级等待模式 应用启动后 30 秒内 回写「稳定运行」标志 否则下次回滚

这张图把整个决策过程画得很清楚。你注意看那两个「校验通过?」的菱形节点——如果校验失败,系统不会硬着头皮跳转,而是回滚到另一个分区,或者进入升级等待模式。这就是双分区容错的精髓。

实战中的几个坑

最后,我分享几个我在 Bootloader 实战中踩过的坑,希望能帮你省点 debug 时间:

  • 向量表偏移没对齐:有些 MCU 要求向量表地址按 512 字节对齐。我吃过一次亏,偏移设错了,中断全跑飞。
  • 跳转前没关外设:比如 DMA 还在传输,UART 还在收发。跳转后外设状态不确定,应用层初始化时可能出问题。
  • 看门狗在擦写时复位:Flash 擦写时间跟芯片型号、温度都有关系。别按手册上的典型值设超时,留够余量。
  • 参数区没做备份:参数区如果只存一份,写一半断电就丢了。我习惯存两份,一份主参数,一份备份,启动时对比校验。

最重要的一个建议:Bootloader 代码越简单越好。别在 Bootloader 里塞复杂功能,比如文件系统、网络协议栈。Bootloader 的唯一使命是「加载并跳转」,做得越少,出错的概率越低。

嗯,Bootloader 这部分内容就聊到这儿。双分区机制、跳转逻辑、看门狗处理,这三块是 Bootloader 的基石。你把这些吃透了,固件升级的容错能力就能上一个台阶。