4、Bootloader开发实战(上):启动流程、向量表重映射、跳转函数实现

Bootloader 这东西,说白了就是 MCU 上电后跑的第一段程序。它的任务很简单——决定要不要更新固件,不更新就跳去跑 App。但就是这“跳”的一下,坑特别多。我最早做 Bootloader 时觉得不就是改个 PC 指针嘛,结果板子直接飞了。后来才明白,启动流程、向量表、跳转时机,每一步都有讲究。

4.1 启动流程:从 Reset 到 main() 之前

MCU 上电后,硬件会做几件固定的事:

  1. 从 0x00000000 取栈顶指针(MSP),初始化堆栈。
  2. 从 0x00000004 取复位向量,跳转到 Reset_Handler。
  3. Reset_Handler 里做时钟配置、拷贝 .data、清零 .bss、初始化 C 运行时。
  4. 最后调用 main()。

嗯,这里要注意——Bootloader 和 App 是两个独立的工程。Bootloader 的 main() 里做完升级判断后,要跳转到 App 的 Reset_Handler,而不是直接跳 App 的 main()。为什么?因为 App 也需要重新初始化外设和全局变量。直接跳 main() 会导致外设状态混乱,我踩过这个坑。

核心原则: Bootloader 跳转的目标是 App 的 Reset_Handler,不是 main()。

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

MCU 默认从 0x00000000 找向量表。如果 App 放在 0x08010000,中断来了 CPU 还是去 0x00000000 查表,结果查到的全是 Bootloader 的中断服务函数。App 的中断就永远跑不到了。

解决方式有两种:

方式 原理 适用芯片 注意事项
SCB->VTOR 寄存器 修改向量表偏移地址 Cortex-M3/M4/M7/M33 地址必须对齐到 512 字节边界
内存映射重映射 通过 SYSCFG 或 MPU 重映射地址 部分 Cortex-M0/M0+ 需要查芯片参考手册

我个人习惯用 VTOR 方式,简单可靠。代码就一行:

/* 设置向量表偏移到 App 起始地址 */
SCB->VTOR = APP_BASE_ADDRESS & 0xFFFFFF80;  /* 低 7 位清零,对齐 128 字节 */

注意对齐要求。Cortex-M3/M4 要求 512 字节对齐,M0+ 可能要求 128 字节。我建议统一用 & 0xFFFFFF80 保证最低 128 字节对齐,兼容性最好。

避坑: 我曾经在 STM32F103 上把 VTOR 写成了 APP_BASE_ADDRESS 没做对齐,结果 PendSV 中断偶尔跑飞。查了两天才发现是 VTOR 低 9 位没清零。记住:对齐是硬约束。

4.3 跳转函数实现:干净地“交棒”

跳转函数的核心就三件事:

  1. 关闭全局中断,避免跳转过程中断乱入。
  2. 清零所有外设时钟,把外设恢复到复位状态。
  3. 从 App 向量表取出栈顶指针和复位向量,然后跳转。

我常用的跳转函数长这样:

typedef void (*pFunction)(void);

void JumpToApp(uint32_t app_addr)
{
    uint32_t msp_value;
    uint32_t reset_vector;
    pFunction app_entry;

    /* 1. 关全局中断 */
    __disable_irq();

    /* 2. 关闭所有外设时钟(以 STM32 为例) */
    RCC->AHBENR  = 0;
    RCC->APB1ENR = 0;
    RCC->APB2ENR = 0;

    /* 3. 从 App 向量表取栈顶指针和复位向量 */
    msp_value    = *(__IO uint32_t*)app_addr;
    reset_vector = *(__IO uint32_t*)(app_addr + 4);

    /* 4. 设置主栈指针 */
    __set_MSP(msp_value);

    /* 5. 跳转到 App 的 Reset_Handler */
    app_entry = (pFunction)reset_vector;
    app_entry();
}

这里有个细节——关外设时钟很多人会漏掉。如果不关,App 启动时外设可能处于半配置状态,再初始化时会产生冲突。比如串口已经在 Bootloader 里配好了波特率,App 又配一次,中间可能发出乱码。

小技巧: 跳转前把 SysTick 也关掉,并清零 SysTick 的计数值。否则 App 里重新配置 SysTick 时,可能因为残留的计数值导致第一次中断来得特别快。

4.4 整体流程:一张图说清楚

我把 Bootloader 启动到跳转的完整流程画成了 SVG,方便你对照理解:

Bootloader 启动 → 跳转 App 完整流程 MCU 上电复位 硬件取 MSP + Reset_Handler Bootloader 初始化(时钟/外设) 检查升级标志 有升级请求 无升级请求 执行固件升级流程 关中断 + 关外设时钟 取 App 向量表 + 设 MSP 跳转 App Reset_Handler ⚠ 关键检查点 跳转前必须关中断、关外设时钟

4.5 常见踩坑点汇总

我把这些年遇到的跳转问题整理了一下,你写代码时可以对照检查:

问题现象 根本原因 解决方案
跳转后跑飞(HardFault) MSP 设置错误或向量表地址不对齐 检查 App 起始地址的 4 字节是否为有效栈顶值
中断不响应 VTOR 未设置或设置错误 在 App 启动早期(Reset_Handler 开头)设置 VTOR
外设工作异常 Bootloader 配置的外设状态残留 跳转前关闭所有外设时钟,App 重新初始化
SysTick 中断提前触发 SysTick 计数值未清零 跳转前停掉 SysTick 并清零 VAL 和 LOAD 寄存器
全局变量值不对 App 的 .data 段未重新拷贝 必须跳 App 的 Reset_Handler,不能跳 main()
我曾经 在一个量产项目上,Bootloader 跳转后 App 的串口中断偶尔丢数据。查了三天,最后发现是 Bootloader 里开了 DMA,跳转前没关。App 初始化 DMA 时,残留的 DMA 请求还在跑。从那以后,我跳转前都会加一个 DeInitAllPeripherals() 函数,把所有外设都复位一遍。

4.6 跳转后的第一件事:App 的 Reset_Handler

App 收到控制权后,第一件事就是重新设置向量表。我建议在 Reset_Handler 的最开头就做:

__attribute__((naked)) void Reset_Handler(void)
{
    /* 立即设置向量表偏移 */
    SCB->VTOR = APP_BASE_ADDRESS & 0xFFFFFF80;

    /* 然后才是常规的 .data/.bss 初始化 */
    SystemInit();
    __main();
}

为什么这么急?因为 Reset_Handler 里可能调用了其他函数,这些函数里如果有中断使能,向量表还没改过来就会出问题。先设 VTOR,再干别的,这个顺序不能乱。

个人习惯: 我会在 App 的链接脚本里把 APP_BASE_ADDRESS 定义成全局符号,然后在代码里直接用。这样换 Flash 分区时只改链接脚本,不用改 C 代码。

好了,Bootloader 的启动流程、向量表重映射和跳转函数实现,核心就是这些。你写代码时记住三句话:对齐 VTOR、关中断再跳、跳 Reset_Handler 不跳 main。这三条做到了,跳转基本不会出大问题。