第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时,中断向量表重定向的完整流程:

双区Bootloader中断向量表重定向流程 Bootloader运行中 步骤1:检查APP向量表有效性 步骤2:关闭全局中断 步骤3:清除NVIC使能/挂起位 步骤4:VTOR写入APP地址 步骤5:设置MSP为APP栈指针 跳转到APP运行 注意:步骤2-5不可被中断打断

4.5 双区中断处理的特殊场景

有些场景下,Bootloader需要在跳转前执行一些中断相关的操作,比如通过串口打印“升级完成”的日志。这时候中断不能全关,怎么办?

我的做法是分阶段处理:

  1. 第一阶段:保持Bootloader的中断向量表,正常处理中断,完成收尾操作(如打印日志、关闭文件系统)。
  2. 第二阶段:关闭所有外设中断(只保留SysTick等系统中断),清理NVIC。
  3. 第三阶段:关全局中断,切换VTOR,跳转。

这样既保证了收尾操作的完整性,又避免了中断冲突。

关键总结:

  • VTOR寄存器是中断重定向的核心,写入地址必须128字节对齐
  • 写VTOR前必须关全局中断,写完后执行DSB/ISB同步
  • 双区跳转前要清理NVIC的使能位和挂起位
  • APP向量表的首字(栈指针)和第二个字(入口地址)必须有效
  • 分阶段处理中断,可以兼顾收尾操作和安全性

嗯,中断向量表重定向这块,说白了就是让CPU“换个地方找中断处理函数”。原理不复杂,但细节决定成败。我见过太多因为VTOR没对齐、中断没清理导致的HardFault了。记住一句话:跳转前,把中断收拾干净。