第4章:中断向量表重定向——VTOR寄存器原理与实战
中断向量表,说白了就是一张“中断处理函数地址表”。CPU发生中断时,硬件会从这里取出对应的处理函数地址,跳过去执行。默认情况下,这张表放在Flash起始地址0x00000000。但Bootloader和APP各有一套中断处理,怎么办?
答案就是——中断向量表重定向。让CPU知道:“嘿,别去0x00000000找了,去我指定的新地址拿中断向量。”
4.1 VTOR寄存器:中断重定向的“方向盘”
ARM Cortex-M系列内核(M3/M4/M7/M33等)提供了一个叫VTOR(Vector Table Offset Register)的寄存器。它位于系统控制块(SCB)中,地址是0xE000ED08。
VTOR的作用很简单:告诉硬件中断向量表的基地址在哪。写入一个新地址后,所有中断都会从新地址处读取向量表。
VTOR寄存器位域说明(32位):
| 位域 | 名称 | 描述 |
|---|---|---|
| [31:7] | TBLBASE | 向量表基地址(高25位),低7位必须为0(128字节对齐) |
| [6:0] | 保留 | 必须写0 |
注意这个128字节对齐的要求。为什么是128字节?因为Cortex-M的向量表前16个是系统异常,每个4字节,共64字节;加上至少16个外部中断,总共128字节。所以VTOR的低7位硬件强制为0。
我在项目中遇到过有人把地址写成了0x08010001,结果中断全乱套了。嗯,对齐这事,真不能马虎。
4.2 运行时重定向:代码怎么写?
重定向中断向量表,其实就是往VTOR寄存器写一个地址。但这里有个坑——写之前要确保目标地址处已经有有效的向量表,否则一旦发生中断,CPU取到的地址是错的,直接HardFault。
来看一段标准的VTOR重定向代码:
/* 定义VTOR寄存器地址 */
#define SCB_VTOR (*(volatile uint32_t *)0xE000ED08)
/* 重定向中断向量表到APP起始地址 */
void vtor_redirect(uint32_t vector_table_addr)
{
/* 1. 检查地址是否128字节对齐 */
if (vector_table_addr & 0x7F) {
/* 对齐错误,直接死循环(实际项目可加错误处理) */
while(1);
}
/* 2. 关闭全局中断,防止重定向过程中发生中断 */
__disable_irq();
/* 3. 写入VTOR寄存器 */
SCB_VTOR = vector_table_addr;
/* 4. 读取验证(可选,但建议做) */
if (SCB_VTOR != vector_table_addr) {
while(1); /* 写入失败处理 */
}
/* 5. 重新使能全局中断 */
__enable_irq();
/* 6. 执行一次DSB指令,确保存储器访问完成 */
__DSB();
__ISB();
}
这里有个细节:为什么要先关中断?你想想看,如果在写入VTOR的瞬间,一个中断来了,硬件去新地址取向量——但新地址的向量表可能还没准备好(比如还在擦写Flash),那就完蛋了。
我曾经踩过的坑:在OTA升级后跳转到APP时,没有先关中断就写VTOR。结果一个SysTick中断恰好在写入瞬间触发,CPU从Flash擦写中的地址取向量,直接HardFault。从那以后,我写VTOR前必关中断,写完再加DSB/ISB指令。
4.3 双区Bootloader的中断处理策略
双区架构下,中断处理比单区复杂得多。为什么?因为Bootloader和APP可能共用中断源,比如串口中断、定时器中断。Bootloader阶段使能了某个外设中断,跳转到APP后,这个中断还在——但向量表已经切到APP了。
我总结了一套双区中断处理三原则:
| 原则 | 说明 | 实现方式 |
|---|---|---|
| 原则1:跳转前清理 | Bootloader退出前,关闭所有已使能的外设中断 | 遍历NVIC寄存器,逐个清除使能位 |
| 原则2:向量表切换 | 跳转到APP前,将VTOR指向APP向量表 | 写入APP起始地址(128字节对齐) |
| 原则3:栈指针更新 | APP的初始栈指针在向量表第一个字,需同步设置 | 读取APP向量表首字写入MSP |
来看一个完整的双区跳转函数:
/* 跳转到APP,带完整的中断处理 */
void jump_to_app(uint32_t app_addr)
{
uint32_t app_sp; /* APP栈指针 */
uint32_t app_entry; /* APP入口地址 */
/* 1. 从APP向量表读取栈指针(向量表首字) */
app_sp = *(volatile uint32_t *)app_addr;
/* 2. 从APP向量表读取复位向量(向量表第二个字) */
app_entry = *(volatile uint32_t *)(app_addr + 4);
/* 3. 检查APP向量表有效性(栈指针不能为0,且必须在RAM范围) */
if ((app_sp == 0) || (app_sp > 0x20000000 + RAM_SIZE)) {
return; /* 无效APP,留在Bootloader */
}
/* 4. 关闭全局中断 */
__disable_irq();
/* 5. 关闭所有外设中断(遍历NVIC) */
for (int i = 0; i < 8; i++) {
NVIC->ICER[i] = 0xFFFFFFFF; /* 清除所有中断使能 */
NVIC->ICPR[i] = 0xFFFFFFFF; /* 清除所有中断挂起 */
}
/* 6. 重定向中断向量表到APP */
SCB_VTOR = app_addr;
/* 7. 设置主栈指针为APP的栈指针 */
__set_MSP(app_sp);
/* 8. 执行DSB/ISB同步 */
__DSB();
__ISB();
/* 9. 跳转到APP入口(使用函数指针) */
void (*app_entry_func)(void) = (void (*)(void))app_entry;
app_entry_func();
/* 永远不会执行到这里 */
}
个人经验:第5步关闭NVIC时,我习惯把ICER和ICPR都清一遍。ICPR清除挂起位很重要——如果某个中断在跳转前已经触发了但还没处理,不清掉的话,跳到APP后这个挂起位还在,APP一开中断就会响应,但APP可能根本没初始化这个外设。
4.4 中断向量表重定向的SVG流程图
下面这张图展示了从Bootloader跳转到APP时,中断向量表重定向的完整流程:
4.5 双区中断处理的特殊场景
有些场景下,Bootloader需要在跳转前执行一些中断相关的操作,比如通过串口打印“升级完成”的日志。这时候中断不能全关,怎么办?
我的做法是分阶段处理:
- 第一阶段:保持Bootloader的中断向量表,正常处理中断,完成收尾操作(如打印日志、关闭文件系统)。
- 第二阶段:关闭所有外设中断(只保留SysTick等系统中断),清理NVIC。
- 第三阶段:关全局中断,切换VTOR,跳转。
这样既保证了收尾操作的完整性,又避免了中断冲突。
关键总结:
- VTOR寄存器是中断重定向的核心,写入地址必须128字节对齐
- 写VTOR前必须关全局中断,写完后执行DSB/ISB同步
- 双区跳转前要清理NVIC的使能位和挂起位
- APP向量表的首字(栈指针)和第二个字(入口地址)必须有效
- 分阶段处理中断,可以兼顾收尾操作和安全性
嗯,中断向量表重定向这块,说白了就是让CPU“换个地方找中断处理函数”。原理不复杂,但细节决定成败。我见过太多因为VTOR没对齐、中断没清理导致的HardFault了。记住一句话:跳转前,把中断收拾干净。