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足够,除非你要支持加密校验或多协议。
分区时还要注意对齐。很多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();
}
这个方案的优点是灵活,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分区到中断向量表放置的完整链路:
3.5 避坑指南
最后,分享几个我踩过的坑:
- VTOR对齐问题:有些MCU要求VTOR的值必须对齐到512字节或1KB边界。你设了个0x08010001,结果中断全乱套。记得用宏定义强制对齐。
- 中断使能时机:在VTOR切换完成之前,千万不要开全局中断。我见过有人在SystemInit里就开了中断,结果VTOR还没改,中断一来直接跑飞。
- Bootloader自身中断:如果你的Bootloader也用中断(比如串口接收),那切换VTOR后Bootloader的中断就失效了。解决办法是在跳转到App前关掉所有外设中断,或者用RAM映射保留Bootloader的中断向量。
- 编译器链接脚本:记得在链接脚本里把App的起始地址和向量表偏移对齐。我见过有人代码里改了VTOR,但链接脚本没改,结果编译出来的向量表还在0x08000000。
好了,这一章的内容就到这里。分区策略和向量表放置方案是Bootloader的基石,搞懂了这些,后面的升级流程才能稳稳落地。