3. 存储器与启动流程

这一章,我们正式进入Bootloader的“骨架”部分。说白了,就是回答三个问题:代码放哪?怎么跳?跳过去怎么让App正常工作?

我在做第一个IAP项目时,就踩过中断向量表没重定向的坑——App跑起来了,但一按按键就死机。后来才发现,CPU中断了还往Bootloader的向量表跑,能不崩吗?

3.1 分区规划:Flash怎么分才合理?

先看一张我常用的分区结构图,这几乎是所有双区备份方案的起点。

典型双区备份Flash分区结构 Bootloader 区域 (0x08000000 ~ 0x0800FFFF, 64KB) App A 区域 (0x08010000 ~ 0x0803FFFF, 192KB) App B 区域 (0x08040000 ~ 0x0806FFFF, 192KB) 参数/标志存储区 (0x08070000 ~ 0x0807FFFF, 64KB) 说明:以STM32F103为例,Flash总容量512KB,Bootloader占64KB,双App各192KB,参数区64KB

分区不是拍脑袋定的,得看芯片Flash大小和你的升级策略。我一般遵循几个原则:

  • Bootloader区:建议16KB~64KB。够放串口驱动、Flash擦写、跳转逻辑就行。别贪大,留给App更多空间。
  • App A区:当前运行的主固件。启动后默认从这里执行。
  • App B区:备份区。新固件先下载到这里,校验通过后再复制到A区,或者直接标记为下次启动的入口。
  • 参数区:存升级标志、版本号、校验值。掉电不丢失。
我的习惯:参数区单独放一页(Page),不要和代码混在一起。我曾经遇到过擦除参数区时把App代码也擦了的惨案——因为它们在同一个扇区。

3.2 跳转条件判断:什么时候该跳?

Bootloader启动后,不能无脑跳App。得先问自己几个问题:

  1. 有没有升级请求?——比如检测到某个GPIO电平、串口收到特定命令、或者参数区有“待升级”标志。
  2. App区有没有有效固件?——检查App起始地址的前4字节(栈顶指针)是否在RAM范围内,后4字节(复位向量)是否在Flash范围内。
  3. 版本对不对?——如果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。