3. Bootloader 架构设计:分区策略、启动流程与中断向量表重映射
好,咱们进入第三章。这一章是 Bootloader 的骨架,也是很多新手容易踩坑的地方。说白了,就是回答三个问题:代码放哪?怎么启动?中断怎么找?
我见过不少项目,功能逻辑写得飞起,结果分区没规划好,后期升级一次就变砖。嗯,分区策略这东西,一开始就得想清楚。
3.1 分区策略:Boot 区、App 区、备份区
嵌入式 Flash 就那么大,怎么切?我个人的习惯是,至少分三个区:Boot 区、App 区、备份区。别嫌多,这是保命的。
| 分区名称 | 存储内容 | 大小建议 | 写保护 |
|---|---|---|---|
| Boot 区 | Bootloader 固件 | 32KB ~ 64KB | 建议硬件写保护 |
| App 区 | 主应用程序 | 剩余 Flash 的 60% | 可读写 |
| 备份区 | App 的备份/升级临时镜像 | 剩余 Flash 的 40% | 可读写 |
为什么要有备份区?你想想看,升级过程中突然断电了怎么办?如果没有备份区,App 区写到一半,下次上电 Boot 发现 App 校验失败,直接进不了主程序。有了备份区,我们可以先擦除备份区,把新固件完整写入备份区并校验通过后,再一次性把备份区的内容复制到 App 区,或者直接交换指针。这样,即使升级中断,App 区还是旧的、可用的固件。
还有一种更激进的做法:双 App 区。A 区运行,B 区升级;下次启动时如果 B 区有效,就切到 B 区运行,同时 A 区变成待升级区。这种方案适合 OTA 频繁的场景,但 Flash 占用也翻倍。
3.2 启动流程与跳转逻辑
芯片上电后,第一件事就是执行 Bootloader。流程其实不复杂,但每一步都有坑。
- 硬件初始化:关看门狗、初始化时钟、配置堆栈指针。这一步要快,别拖沓。
- 检查升级请求:比如检测某个 GPIO 电平、CAN 报文、或者 Flash 里的标志位。
- 校验 App 区:计算 CRC 或 SHA256,跟预存的值比对。
- 跳转决策:如果有升级请求,进入刷写模式;如果 App 有效,跳转到 App;如果 App 无效,等待升级。
- 跳转执行:修改栈指针,跳转到 App 的复位向量地址。
跳转逻辑的代码,我直接贴一段我常用的模板。注意看注释,每一行都有讲究。
/* 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);
}
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 地址修改。
下面这张图,把整个流程串起来,你一看就明白了。
看到没?整个流程就是一个决策树。Bootloader 的角色就是那个「交通警察」,指挥车流往哪走。而 VTOR 就是那个「路牌」,告诉中断向量该去哪找对应的处理函数。
- 分区策略:Boot 区要写保护,App 区和备份区要留够空间。
- 启动流程:先初始化,再决策,最后跳转。每一步都要考虑异常情况。
- 中断重映射:VTOR 是首选方案,设置时机要在任何中断使能之前。