4、Bootloader核心流程:启动流程、跳转逻辑、向量表重映射

好,咱们今天聊点硬核的。Bootloader 说到底就干三件事:启动、跳转、重映射。这三件事要是没捋清楚,后面双 Bank 切换就是空中楼阁。我当年第一次做 Bootloader 时,就栽在向量表重映射上,芯片死活不进应用程序,后来发现是向量表地址写错了。嗯,这种坑,咱们今天一次性填平。

4.1 启动流程:从复位到 Main

芯片上电后,CPU 第一件事是去固定地址取向量表。以 Cortex-M 系列为例,它从 0x00000000 取栈顶指针,从 0x00000004 取复位向量。说白了,这就是芯片的「出厂设定」,谁也别想改。

但问题来了——我们的 Bootloader 放在 Flash 的起始地址,应用程序放在另一个 Bank。那 CPU 怎么知道该跑哪个?

答案很简单:Bootloader 先跑,它来决定下一步。启动流程大致如下:

  1. CPU 复位,从 0x00000000 加载栈顶指针
  2. 0x00000004 跳转到 Reset_Handler
  3. Reset_Handler 初始化系统时钟、配置 Flash 等待周期
  4. 调用 main() 函数,进入 Bootloader 主逻辑

关键点:Bootloader 的向量表必须放在 Flash 起始地址。这是硬件强制要求的,没得商量。

我在项目中遇到过一种情况:客户要求 Bootloader 只占 16KB,但向量表加上中断服务函数就占了 4KB。怎么办?我建议把不用的中断全部指向一个空函数,这样既省空间,又不会跑飞。

4.2 跳转逻辑:从 Bootloader 到 App

Bootloader 检查完固件合法性后,就要跳转到应用程序了。跳转不是简单的 goto,你得把「接力棒」交接清楚。

跳转的核心步骤:

  1. 关闭全局中断:跳转前必须关中断,否则跳转过程中一个中断进来,PC 指针就乱了。
  2. 禁用外设时钟:我习惯把用到的外设都复位一遍,避免 App 那边初始化时出问题。
  3. 设置主栈指针:从 App 向量表的起始地址取出栈顶指针,写入 MSP 寄存器。
  4. 跳转到复位向量:取出 App 的复位向量地址,强制转换函数指针,然后调用。

代码长这样:

typedef void (*pFunction)(void);

void jump_to_app(uint32_t app_addr)
{
    uint32_t msp_value;
    pFunction reset_handler;

    // 1. 关中断
    __disable_irq();

    // 2. 从 App 向量表取栈顶指针
    msp_value = *(volatile uint32_t *)app_addr;
    __set_MSP(msp_value);

    // 3. 取复位向量地址
    reset_handler = (pFunction)(*(volatile uint32_t *)(app_addr + 4));

    // 4. 跳转
    reset_handler();
}

注意:跳转后,Bootloader 的代码就「死」了。所有 RAM 数据都会被 App 重新初始化。所以,别想着跳转后还能回来——除非你做了特殊设计。

你想想看,如果跳转前忘了关中断,会发生什么?嗯,我见过一次:跳转瞬间,一个定时器中断触发,CPU 跑去执行 Bootloader 的中断服务函数,但此时 PC 已经指向 App 区域了,结果就是 HardFault。所以,关中断这步,打死也不能省。

4.3 向量表重映射:让中断找到新家

App 跑起来了,但中断来了怎么办?CPU 还是去 0x00000000 找向量表,可那里是 Bootloader 的地盘。App 的中断向量表在另一个 Bank 里,CPU 找不到。

解决办法就是向量表重映射。Cortex-M3/M4 提供了一个寄存器叫 SCB->VTOR,你往里面写 App 向量表的基地址,CPU 就会去新地址找中断向量。

重映射代码:

#define SCB_VTOR_ADDR  0xE000ED08

void remap_vector_table(uint32_t app_base_addr)
{
    // 写入 App 向量表基地址
    SCB->VTOR = app_base_addr;

    // 读回确认
    __DSB();
    __ISB();
}

这里有个细节:App 向量表必须 512 字节对齐。为什么?因为 VTOR 寄存器的低 9 位是保留位,硬件直接忽略。所以,App 的起始地址必须是 0x200 的整数倍。我刚开始做时没注意这个,App 地址设成了 0x08010080,结果 VTOR 写进去后,低 9 位被截断了,中断全乱套。

芯片系列 VTOR 地址 对齐要求
Cortex-M0 0xE000ED08 128 字节
Cortex-M3/M4 0xE000ED08 512 字节
Cortex-M7 0xE000ED08 1024 字节

小技巧:在链接脚本里,把 App 的起始地址声明为 . = ALIGN(0x200);,这样编译器会自动对齐,省得你手动算。

4.4 避坑指南:我踩过的三个坑

做 Bootloader 这么多年,有些坑是共通的。我挑三个最典型的说说:

  • 坑一:跳转前没清理外设状态。我曾经在跳转前没关 DMA,结果 App 初始化时 DMA 还在传输,直接覆盖了 App 的 RAM 数据。从那以后,我跳转前必调 HAL_RCC_DeInit()
  • 坑二:向量表重映射后没加内存屏障。Cortex-M 有流水线,你写完 VTOR 后,CPU 可能还在用旧值。加 __DSB()__ISB() 能强制刷新流水线,确保新配置生效。
  • 坑三:App 的链接脚本没对齐。这个前面说过了,App 起始地址不对齐,VTOR 写进去也是白搭。检查链接脚本里的 FLASH_APP 起始地址,确保它是 0x200 的倍数。

嗯,这三个坑,你只要避开一个,就能省下至少半天调试时间。我当年可是三个全踩了一遍,那滋味,啧啧。

4.5 总结

Bootloader 的核心流程,说白了就是:启动时自己先跑,检查完固件后跳转,跳转前把中断向量表指向 App。这三步环环相扣,哪一步出问题,App 都跑不起来。

我个人习惯在跳转前打印一条日志,记录跳转的目标地址和时间。这样万一出问题,至少知道 Bootloader 有没有执行到跳转这一步。调试时,这条日志能帮你快速定位是 Bootloader 的问题,还是 App 的问题。

下一章,咱们聊聊双 Bank 切换的具体实现。到时候你会看到,今天讲的这些流程,在双 Bank 场景下会怎么变。准备好了吗?