4、Bootloader核心流程:启动流程、跳转逻辑、向量表重映射
好,咱们今天聊点硬核的。Bootloader 说到底就干三件事:启动、跳转、重映射。这三件事要是没捋清楚,后面双 Bank 切换就是空中楼阁。我当年第一次做 Bootloader 时,就栽在向量表重映射上,芯片死活不进应用程序,后来发现是向量表地址写错了。嗯,这种坑,咱们今天一次性填平。
4.1 启动流程:从复位到 Main
芯片上电后,CPU 第一件事是去固定地址取向量表。以 Cortex-M 系列为例,它从 0x00000000 取栈顶指针,从 0x00000004 取复位向量。说白了,这就是芯片的「出厂设定」,谁也别想改。
但问题来了——我们的 Bootloader 放在 Flash 的起始地址,应用程序放在另一个 Bank。那 CPU 怎么知道该跑哪个?
答案很简单:Bootloader 先跑,它来决定下一步。启动流程大致如下:
- CPU 复位,从
0x00000000加载栈顶指针 - 从
0x00000004跳转到 Reset_Handler - Reset_Handler 初始化系统时钟、配置 Flash 等待周期
- 调用
main()函数,进入 Bootloader 主逻辑
关键点:Bootloader 的向量表必须放在 Flash 起始地址。这是硬件强制要求的,没得商量。
我在项目中遇到过一种情况:客户要求 Bootloader 只占 16KB,但向量表加上中断服务函数就占了 4KB。怎么办?我建议把不用的中断全部指向一个空函数,这样既省空间,又不会跑飞。
4.2 跳转逻辑:从 Bootloader 到 App
Bootloader 检查完固件合法性后,就要跳转到应用程序了。跳转不是简单的 goto,你得把「接力棒」交接清楚。
跳转的核心步骤:
- 关闭全局中断:跳转前必须关中断,否则跳转过程中一个中断进来,PC 指针就乱了。
- 禁用外设时钟:我习惯把用到的外设都复位一遍,避免 App 那边初始化时出问题。
- 设置主栈指针:从 App 向量表的起始地址取出栈顶指针,写入 MSP 寄存器。
- 跳转到复位向量:取出 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 场景下会怎么变。准备好了吗?