4、向量表重映射机制:SCB->VTOR寄存器详解、双区向量表切换、实战中踩过的坑
好,咱们接着聊。Bootloader 的核心任务之一,就是跳转到应用程序。但跳转不是简单的「GOTO」语句。你想想看,CPU 遇到中断怎么办?它得知道去哪里找中断服务函数。这个「去哪里找」的信息,就存在向量表里。
默认情况下,向量表在 Flash 的起始地址,也就是 0x08000000。但你的应用程序可能放在 0x08020000,或者干脆在 RAM 里运行。这时候,向量表也得跟着搬家。怎么搬?靠的就是 SCB->VTOR 这个寄存器。
4.1 SCB->VTOR 寄存器详解
VTOR,全称 Vector Table Offset Register。说白了,它就是一个偏移量寄存器。告诉 CPU:「嘿,向量表不在老地方了,挪到新地址去了。」
在 STM32H7 上,这个寄存器是 32 位的。但要注意,不是所有位都能随便写。它的低 7 位(bit 6:0)是保留的,必须保持为 0。为什么?因为向量表要求 128 字节对齐。嗯,这里要注意,H7 的向量表每个条目是 8 字节(64 位),一共 16 个异常向量,正好 128 字节。
所以,你设置 VTOR 的时候,地址必须 128 字节对齐。否则,CPU 会忽略你设置的值,继续用默认的向量表。我在项目中遇到过这个问题,当时查了半天,最后发现是地址没对齐,尴尬得很。
代码写法很简单,就一行:
SCB->VTOR = (uint32_t)APPLICATION_ADDRESS;
但这里有个坑。H7 的 Flash 是 256 位宽度的,也就是 32 字节。如果你把应用程序放在 0x08020000,这个地址是 128 字节对齐的,没问题。但如果你放在 0x08020080,虽然也是 128 字节对齐,但和 Flash 的 32 字节对齐不一致。我个人习惯,直接按 Flash 扇区对齐,省心。
重要:VTOR 设置后,CPU 会立即使用新向量表。所以,在跳转到应用程序之前,必须确保新向量表已经就绪。否则,一旦发生中断,CPU 会跑到错误的地方去。
4.2 双区向量表切换
双区 Bootloader,说白了就是 Flash 里有两个区域存放应用程序。一个在运行,另一个在升级。升级完成后,切换过去。这种设计,可以保证系统永不掉线。
双区切换的核心,就是 VTOR 的切换。Bootloader 在启动时,根据某个标志位(比如 Flash 里的一个字节),决定加载哪个区的向量表。
我画个简单的流程图给你看:
启动 -> 读取标志位
|
+-- 标志位 = 区A -> 设置 VTOR = 区A地址 -> 跳转到区A
|
+-- 标志位 = 区B -> 设置 VTOR = 区B地址 -> 跳转到区B
代码实现大概是这样的:
void jump_to_application(uint32_t app_address)
{
// 1. 关闭全局中断
__disable_irq();
// 2. 设置新的向量表
SCB->VTOR = app_address;
// 3. 从新向量表取出栈顶指针和复位向量
uint32_t msp = *(uint32_t*)app_address;
uint32_t reset_vector = *(uint32_t*)(app_address + 4);
// 4. 设置主栈指针
__set_MSP(msp);
// 5. 跳转到应用程序
void (*app_entry)(void) = (void (*)(void))reset_vector;
app_entry();
// 这里永远不会回来
}
你可能会问:「为什么要关中断?」嗯,这个问题问得好。如果不关中断,在设置 VTOR 和跳转之间,万一来了个中断,CPU 可能看到的是新旧向量表混在一起的状态。我曾经因为这个原因,导致系统在跳转时随机死机。后来养成了习惯,跳转前必关中断。
提示:跳转到应用程序后,应用程序自己的启动代码会重新开启中断。所以,你不用担心中断一直关着。
4.3 实战中踩过的坑
做 Bootloader 这么多年,我在向量表这块踩过的坑,可以写个小本本了。挑几个典型的说说。
坑一:中断优先级分组不一致
Bootloader 和应用程序的中断优先级分组必须一致。否则,中断嵌套会出问题。我曾经在 Bootloader 里设置了 4 位抢占优先级,应用程序里用了 3 位。结果升级完成后,低优先级中断老是抢不过高优先级的中断,系统响应变得很奇怪。
解决办法:在跳转前,把中断优先级分组设置成和应用程序一致。或者,干脆在应用程序启动时重新设置。
坑二:外设中断未清理
Bootloader 阶段可能使能了一些外设中断,比如串口接收中断。跳转到应用程序前,如果没有清理这些中断的挂起位,应用程序一启动就会触发一个莫名其妙的中断。我遇到过最离谱的一次,是 DMA 中断没清,应用程序刚跑起来就卡死了。
我的建议:跳转前,把所有外设的时钟关掉,中断使能位清零,挂起位也清了。别偷懒。
坑三:向量表在 RAM 中
有些场景下,需要把向量表放到 RAM 里。比如,你要动态修改中断服务函数。这时候,VTOR 指向 RAM 地址。但要注意,RAM 的内容是易失的。系统复位后,RAM 里的向量表就没了。
所以,如果你用 RAM 向量表,必须在每次启动时重新复制。我一般这样处理:
// 在系统启动时,把 Flash 里的向量表复制到 RAM
memcpy((void*)RAM_VECTOR_TABLE_ADDR, (void*)FLASH_BASE, VECTOR_TABLE_SIZE);
// 然后设置 VTOR 指向 RAM
SCB->VTOR = RAM_VECTOR_TABLE_ADDR;
另外,RAM 向量表要求 RAM 区域有足够的空间。H7 的向量表有 16 个异常向量 + 最多 240 个外部中断向量,每个 8 字节,总共大约 2KB。别算错了。
警告:在设置 VTOR 指向 RAM 后,如果 RAM 内容被意外修改(比如栈溢出),系统会直接崩溃。所以,用 RAM 向量表时,一定要做好栈保护。
坑四:双区切换时的标志位处理
双区切换时,标志位的读写要小心。如果标志位在 Flash 里,写入前需要擦除扇区。但擦除 Flash 会耗时,而且如果此时掉电,标志位可能处于不一致状态。
我个人的做法:用两个标志位。一个表示「正在升级」,一个表示「升级完成」。这样,即使掉电,也能通过检查这两个标志位,恢复到上一个稳定状态。
| 标志位 | 地址 | 含义 |
|---|---|---|
| FLAG_UPDATE_PENDING | 0x0801FF00 | 0x55AA55AA 表示正在升级 |
| FLAG_UPDATE_DONE | 0x0801FF04 | 0xAA55AA55 表示升级完成 |
启动时,先检查 FLAG_UPDATE_DONE。如果有效,直接切换到新区。如果无效,再检查 FLAG_UPDATE_PENDING。如果有效,说明上次升级掉电了,回滚到旧区。这样,系统永远不会变砖。
嗯,关于向量表重映射,核心内容就这些。记住一句话:VTOR 是 Bootloader 的灵魂,设置错了,系统就废了。多测试,多验证,别怕麻烦。