3. 存储器与启动流程
这一章,我们正式进入Bootloader的“骨架”部分。说白了,就是回答三个问题:代码放哪?怎么跳?跳过去怎么让App正常工作?
我在做第一个IAP项目时,就踩过中断向量表没重定向的坑——App跑起来了,但一按按键就死机。后来才发现,CPU中断了还往Bootloader的向量表跑,能不崩吗?
3.1 分区规划:Flash怎么分才合理?
先看一张我常用的分区结构图,这几乎是所有双区备份方案的起点。
分区不是拍脑袋定的,得看芯片Flash大小和你的升级策略。我一般遵循几个原则:
- Bootloader区:建议16KB~64KB。够放串口驱动、Flash擦写、跳转逻辑就行。别贪大,留给App更多空间。
- App A区:当前运行的主固件。启动后默认从这里执行。
- App B区:备份区。新固件先下载到这里,校验通过后再复制到A区,或者直接标记为下次启动的入口。
- 参数区:存升级标志、版本号、校验值。掉电不丢失。
我的习惯:参数区单独放一页(Page),不要和代码混在一起。我曾经遇到过擦除参数区时把App代码也擦了的惨案——因为它们在同一个扇区。
3.2 跳转条件判断:什么时候该跳?
Bootloader启动后,不能无脑跳App。得先问自己几个问题:
- 有没有升级请求?——比如检测到某个GPIO电平、串口收到特定命令、或者参数区有“待升级”标志。
- App区有没有有效固件?——检查App起始地址的前4字节(栈顶指针)是否在RAM范围内,后4字节(复位向量)是否在Flash范围内。
- 版本对不对?——如果App版本和Bootloader不兼容,跳了也白跳。
下面是我常用的判断流程:
/* 伪代码:跳转条件判断 */
void check_and_jump(void)
{
uint32_t app_stack_top = *(volatile uint32_t*)APP_A_ADDR;
uint32_t app_reset_vec = *(volatile uint32_t*)(APP_A_ADDR + 4);
/* 条件1:没有升级请求 */
if (get_upgrade_flag() == 0)
{
/* 条件2:栈顶指针在RAM范围 */
if (app_stack_top >= RAM_BASE &&
app_stack_top < (RAM_BASE + RAM_SIZE))
{
/* 条件3:复位向量在Flash范围 */
if (app_reset_vec >= APP_A_ADDR &&
app_reset_vec < (APP_A_ADDR + APP_SIZE))
{
jump_to_app(APP_A_ADDR);
}
}
}
/* 否则:停留在Bootloader,等待升级 */
}
我曾经踩过的坑:只检查了栈顶指针,没检查复位向量。结果App区全是0xFF,栈顶指针恰好落在RAM范围内,Bootloader就跳过去了——然后直接跑飞。所以两个条件必须同时满足。
3.3 中断向量表重定向:让CPU找到App的中断
这是新手最容易忽略的一步。你想想看,CPU上电后默认从0x08000000取中断向量表。Bootloader在这里,没问题。但跳转到App后,CPU仍然盯着0x08000000找中断入口。App的中断向量表在0x08010000,CPU当然找不到了。
解决方式有两种:
| 方式 | 原理 | 适用芯片 | 我的评价 |
|---|---|---|---|
| 修改VTOR寄存器 | 直接告诉CPU:中断向量表挪到新地址 | Cortex-M3/M4/M7等 | 推荐,简单可靠 |
| 使用SCB->VTOR | 标准库/HAL库封装好的接口 | 所有支持VTOR的ARM核 | 最常用,代码可移植 |
| Bootloader转发中断 | Bootloader捕获中断,再转发给App | 不支持VTOR的老芯片 | 不推荐,性能差还容易出bug |
对于STM32这类Cortex-M内核芯片,我建议直接在App的启动代码最前面设置VTOR:
/* 在App的SystemInit()或main()最开头 */
#define APP_BASE_ADDR 0x08010000
void SystemInit(void)
{
/* 重定向中断向量表到App起始地址 */
SCB->VTOR = APP_BASE_ADDR;
/* 其他初始化... */
}
注意:VTOR必须在App使能任何中断之前设置。否则一旦中断来了,CPU还是去老地方找向量表,直接HardFault。我习惯把它放在SystemInit()的第一行,比任何外设初始化都早。
3.4 跳转函数实现:最后一步怎么走?
跳转函数的核心就两件事:关中断、改PC指针。别小看这两步,顺序错了也会出问题。
来看一个标准的跳转函数:
/* 跳转到App */
typedef void (*app_func_t)(void);
void jump_to_app(uint32_t app_addr)
{
uint32_t reset_vec;
app_func_t app_entry;
/* 1. 关闭全局中断 */
__disable_irq();
/* 2. 关闭外设中断(可选,但建议做) */
/* 比如:SysTick、USART、TIM等 */
SysTick->CTRL = 0;
/* 根据实际外设逐个关闭... */
/* 3. 从App起始地址取出复位向量 */
reset_vec = *(volatile uint32_t*)(app_addr + 4);
app_entry = (app_func_t)reset_vec;
/* 4. 设置主栈指针(MSP) */
__set_MSP(*(volatile uint32_t*)app_addr);
/* 5. 跳转! */
app_entry();
/* 永远不会回到这里 */
}
这里有几个细节,我吃了不少亏才记住:
- 先关中断,再关外设:如果先关外设,中断还在,外设关了但中断挂起,跳过去后App可能收到莫名其妙的中断。
- 清掉挂起的中断:有些芯片需要手动清除NVIC的挂起位,否则跳过去后CPU立即响应一个旧中断。
- 设置MSP:App的栈顶指针在它的向量表第一个字。如果不重新设置,App用的还是Bootloader的栈,可能溢出。
一个小技巧:跳转前把SysTick关了,因为App可能用不同的SysTick配置。我遇到过跳过去后SysTick中断频率不对,导致延时全乱套的情况。
3.5 本章小结
这一章的内容,说白了就是Bootloader的“三件套”:
- 分区规划:Bootloader、App A、App B、参数区,各占多少,怎么对齐Flash扇区。
- 跳转条件:检查升级标志、校验App有效性,缺一不可。
- 中断向量表重定向 + 跳转函数:让App能正常响应中断,并且平稳地从Bootloader过渡到App。