3. Bootloader架构设计:内存分区与中断向量表放置

好,咱们进入第三章。这一章很关键,说白了就是决定你的Bootloader和App怎么在Flash里「分家」,以及中断来了之后CPU该找谁。

我刚开始做Bootloader时,觉得分区不就是划个地址范围嘛,简单。结果第一次做OTA升级,App跑起来后一按按键就死机——中断向量表没处理好。嗯,从那以后我再也不敢轻视这个环节了。

3.1 Bootloader与App的内存分区策略

先聊聊分区。Flash空间就那么大,怎么分才合理?我一般按功能把Flash切成三块:

分区名称 起始地址(示例) 大小 内容
Bootloader区 0x08000000 32KB ~ 64KB 启动代码、升级协议、Flash驱动
App区 0x08010000 剩余空间 用户应用程序
参数/备份区 0x080F0000 16KB ~ 64KB 升级标志、备份App、配置参数

这里有个经验:Bootloader区不要贪大。我见过有人给Bootloader分了128KB,结果里面就放了个串口下载程序,太浪费了。一般32KB足够,除非你要支持加密校验或多协议。

我的习惯:Bootloader区末尾留4KB作为「升级状态标志区」。这样App升级失败时,Bootloader能通过标志位判断是继续重试还是回滚。

分区时还要注意对齐。很多MCU的Flash有扇区大小限制,比如STM32的扇区可能是2KB、16KB或128KB。你分区时一定要按扇区边界对齐,否则擦除操作会出问题。我曾经因为没对齐,导致升级时把Bootloader自己的代码给擦掉了……那场面,真叫一个「砖头」。

3.2 中断向量表的两种放置方案

分区搞定了,接下来是重头戏——中断向量表怎么放?

CPU上电后默认从Flash起始地址(0x08000000)取向量表。但App跑在偏移后的地址,比如0x08010000。这时候中断来了,CPU还是去0x08000000找中断服务函数,结果找到的是Bootloader的向量表,程序就乱套了。

解决这个问题有两种主流方案:RAM映射和Flash重定向。我分别讲讲。

3.2.1 方案一:RAM映射

这个方案的核心思路是:把中断向量表拷贝到RAM里,然后修改VTOR(向量表偏移寄存器)指向RAM地址。

为什么这么做?因为RAM可以随时改写。App启动时,把App区的向量表拷贝到RAM,然后告诉CPU「以后中断去RAM里找」。这样既不影响Bootloader的向量表,又能让App正常响应中断。

代码实现大概是这样:

// 定义RAM中的向量表地址(需对齐到256字节边界)
#define VECTOR_TABLE_RAM_BASE  0x20000000

void vector_table_remap_to_ram(void)
{
    uint32_t i;
    uint32_t *pSrc = (uint32_t *)APP_VECTOR_TABLE_BASE;
    uint32_t *pDst = (uint32_t *)VECTOR_TABLE_RAM_BASE;

    // 拷贝前32个中断向量(具体数量看MCU型号)
    for (i = 0; i < 32; i++)
    {
        pDst[i] = pSrc[i];
    }

    // 设置VTOR寄存器指向RAM
    SCB->VTOR = VECTOR_TABLE_RAM_BASE;

    __DSB();
    __ISB();
}
注意:RAM映射方案要求RAM空间足够大,且向量表地址必须对齐到2的整数次幂(通常是256字节或512字节)。另外,拷贝完成后一定要加数据同步屏障指令(DSB/ISB),否则CPU可能还在读旧的VTOR值。

这个方案的优点是灵活,App可以动态修改中断向量(比如做动态中断注册)。缺点嘛,就是占用了宝贵的RAM空间,而且启动时多了一次拷贝开销。

3.2.2 方案二:Flash重定向

这个方案更直接——修改VTOR寄存器,让它直接指向App在Flash中的向量表基地址。

说白了就是:CPU,你别去0x08000000找中断了,去0x08010000找。App的向量表本来就在Flash里,直接告诉CPU新地址就行。

// App向量表在Flash中的偏移地址
#define APP_VECTOR_TABLE_BASE  0x08010000

void vector_table_remap_to_flash(void)
{
    // 直接设置VTOR
    SCB->VTOR = APP_VECTOR_TABLE_BASE;

    __DSB();
    __ISB();
}

你看,代码比RAM映射简单多了。不需要拷贝,不占RAM,启动速度也快。

但有个坑:有些MCU的VTOR寄存器是只读的,或者只能在特定模式下修改。我遇到过一颗芯片,VTOR在User模式写不进去,必须在Privilege模式下才能改。当时排查了好久才发现是权限问题。

3.3 两种方案的对比与选型建议

我把两种方案的核心差异整理成了一张表,方便你对比:

对比维度 RAM映射 Flash重定向
RAM占用 占用约128~512字节 不占用
启动速度 慢(需拷贝) 快(仅写寄存器)
灵活性 高(可动态修改) 低(固定Flash地址)
实现复杂度 中等 简单
适用场景 需要动态中断、RAM充足 资源紧张、追求启动速度

我个人更推荐Flash重定向方案,除非你的项目有特殊需求。为什么?因为简单可靠。你想想看,嵌入式系统最怕什么?最怕不确定性。RAM映射多了一次拷贝,多了一次内存访问,万一拷贝过程中被更高优先级的中断打断……嗯,你懂的。

我的选型原则:
  • 量产产品 → 优先Flash重定向(稳定第一)
  • 原型验证/研究项目 → 可以用RAM映射(灵活调试)
  • RAM小于64KB的MCU → 别用RAM映射,省着点用

3.4 知识结构总览

下面这张图把本章的核心逻辑串起来了。你可以看到从Flash分区到中断向量表放置的完整链路:

Bootloader架构设计核心逻辑 Flash 分区策略 Bootloader区 App区 参数/备份区 中断向量表偏移 → CPU找不到App的中断服务函数 方案一:RAM映射 方案二:Flash重定向 拷贝向量表到RAM VTOR指向RAM地址 占用RAM,启动慢 VTOR直接指向Flash 不占RAM,启动快 实现简单,稳定可靠 选型建议:量产优先Flash重定向,原型验证可用RAM映射

3.5 避坑指南

最后,分享几个我踩过的坑:

  • VTOR对齐问题:有些MCU要求VTOR的值必须对齐到512字节或1KB边界。你设了个0x08010001,结果中断全乱套。记得用宏定义强制对齐。
  • 中断使能时机:在VTOR切换完成之前,千万不要开全局中断。我见过有人在SystemInit里就开了中断,结果VTOR还没改,中断一来直接跑飞。
  • Bootloader自身中断:如果你的Bootloader也用中断(比如串口接收),那切换VTOR后Bootloader的中断就失效了。解决办法是在跳转到App前关掉所有外设中断,或者用RAM映射保留Bootloader的中断向量。
  • 编译器链接脚本:记得在链接脚本里把App的起始地址和向量表偏移对齐。我见过有人代码里改了VTOR,但链接脚本没改,结果编译出来的向量表还在0x08000000。
一个小技巧:调试时可以在VTOR切换前后各读一次SCB->VTOR的值,打印出来确认是否写成功。很多问题都是VTOR没写进去导致的。

好了,这一章的内容就到这里。分区策略和向量表放置方案是Bootloader的基石,搞懂了这些,后面的升级流程才能稳稳落地。