3. Bootloader 架构设计:分区策略、启动流程与中断向量表重映射

好,咱们进入第三章。这一章是 Bootloader 的骨架,也是很多新手容易踩坑的地方。说白了,就是回答三个问题:代码放哪?怎么启动?中断怎么找?

我见过不少项目,功能逻辑写得飞起,结果分区没规划好,后期升级一次就变砖。嗯,分区策略这东西,一开始就得想清楚。

3.1 分区策略:Boot 区、App 区、备份区

嵌入式 Flash 就那么大,怎么切?我个人的习惯是,至少分三个区:Boot 区、App 区、备份区。别嫌多,这是保命的。

分区名称 存储内容 大小建议 写保护
Boot 区 Bootloader 固件 32KB ~ 64KB 建议硬件写保护
App 区 主应用程序 剩余 Flash 的 60% 可读写
备份区 App 的备份/升级临时镜像 剩余 Flash 的 40% 可读写
注意:Boot 区一定要加硬件写保护。我曾经在一个量产项目中,因为 Boot 区没锁,升级时误写了 Boot 区地址,结果整个 ECU 变砖,只能拆壳用编程器救。那叫一个惨。

为什么要有备份区?你想想看,升级过程中突然断电了怎么办?如果没有备份区,App 区写到一半,下次上电 Boot 发现 App 校验失败,直接进不了主程序。有了备份区,我们可以先擦除备份区,把新固件完整写入备份区并校验通过后,再一次性把备份区的内容复制到 App 区,或者直接交换指针。这样,即使升级中断,App 区还是旧的、可用的固件。

还有一种更激进的做法:双 App 区。A 区运行,B 区升级;下次启动时如果 B 区有效,就切到 B 区运行,同时 A 区变成待升级区。这种方案适合 OTA 频繁的场景,但 Flash 占用也翻倍。

3.2 启动流程与跳转逻辑

芯片上电后,第一件事就是执行 Bootloader。流程其实不复杂,但每一步都有坑。

  1. 硬件初始化:关看门狗、初始化时钟、配置堆栈指针。这一步要快,别拖沓。
  2. 检查升级请求:比如检测某个 GPIO 电平、CAN 报文、或者 Flash 里的标志位。
  3. 校验 App 区:计算 CRC 或 SHA256,跟预存的值比对。
  4. 跳转决策:如果有升级请求,进入刷写模式;如果 App 有效,跳转到 App;如果 App 无效,等待升级。
  5. 跳转执行:修改栈指针,跳转到 App 的复位向量地址。
核心原则:Bootloader 要足够小、足够稳定。它只做一件事——保证你能安全地更新 App。别把复杂逻辑塞进 Boot 区。

跳转逻辑的代码,我直接贴一段我常用的模板。注意看注释,每一行都有讲究。

/* Bootloader 跳转到 App 的示例代码 */
typedef void (*app_entry_t)(void);

void jump_to_app(uint32_t app_base_addr)
{
    uint32_t stack_ptr;
    app_entry_t app_entry;

    /* 1. 关闭全局中断,防止跳转过程中断响应异常 */
    __disable_irq();

    /* 2. 从 App 起始地址读取栈指针 */
    stack_ptr = *(volatile uint32_t *)app_base_addr;

    /* 3. 从 App 起始地址 + 4 读取复位向量(即 App 入口地址) */
    app_entry = (app_entry_t)(*(volatile uint32_t *)(app_base_addr + 4));

    /* 4. 检查栈指针和入口地址是否合法(防止跳转到空地址) */
    if ((stack_ptr == 0xFFFFFFFF) || (app_entry == NULL)) {
        /* 非法地址,留在 Bootloader */
        return;
    }

    /* 5. 设置主栈指针 */
    __set_MSP(stack_ptr);

    /* 6. 跳转前再次确认中断已关闭 */
    __disable_irq();

    /* 7. 跳转到 App 入口 */
    app_entry();

    /* 8. 正常情况下不会执行到这里 */
    while(1);
}
经验之谈:跳转前一定要关闭所有外设中断,尤其是定时器和 DMA。我遇到过跳转后 App 莫名其妙进 HardFault,查了两天才发现是跳转前 UART 中断没关,App 的中断向量表还没准备好,中断一来直接飞了。

3.3 中断向量表重映射

这是 Bootloader 里最容易让人懵的地方。为什么需要重映射?

芯片上电后,默认的中断向量表在 Flash 起始地址(0x08000000)。Bootloader 就在那里。当 Bootloader 跳转到 App 后,App 的代码在另一个地址(比如 0x08010000)。如果中断来了,CPU 还是去 0x08000000 找中断向量,找到的是 Bootloader 的中断处理函数,App 的中断就永远响应不了。

解决办法有两个:

  • 使用 SCB->VTOR 寄存器:Cortex-M 内核支持向量表偏移寄存器。在 App 启动时,把 VTOR 设置为 App 的中断向量表基地址。
  • 使用 Bootloader 转发中断:Bootloader 里写一个中断转发函数,把中断转发到 App 的对应处理函数。这种方法不推荐,因为每增加一个中断,Bootloader 就要改一次。

我强烈建议用 VTOR 方案。代码就一行:

/* 在 App 的启动文件或 main 函数最开头执行 */
SCB->VTOR = APP_BASE_ADDR;

但要注意时机。VTOR 必须在任何中断使能之前设置。我习惯把它放在 App 的 Reset_Handler 里,第一条指令就执行。如果你用的是 STM32,标准库的 SystemInit 函数里其实已经做了这件事,但默认值是 0x08000000,你需要根据你的 App 地址修改。

又一个坑:有些芯片(比如某些 NXP 的 S32K 系列)不支持 VTOR,或者 VTOR 的地址对齐要求很严格(比如必须 512 字节对齐)。做项目前一定要查芯片手册,别想当然。

下面这张图,把整个流程串起来,你一看就明白了。

Bootloader 启动与跳转流程 上电复位 Bootloader 初始化 检查升级请求 有请求 无请求 进入刷写模式 校验 App 有效性 等待 CAN 刷写 跳转到 App 设置 VTOR 重映射

看到没?整个流程就是一个决策树。Bootloader 的角色就是那个「交通警察」,指挥车流往哪走。而 VTOR 就是那个「路牌」,告诉中断向量该去哪找对应的处理函数。

总结一下本章核心:
  • 分区策略:Boot 区要写保护,App 区和备份区要留够空间。
  • 启动流程:先初始化,再决策,最后跳转。每一步都要考虑异常情况。
  • 中断重映射:VTOR 是首选方案,设置时机要在任何中断使能之前。