4、向量表与中断管理:向量表重定向原理、中断服务函数注册、双Bank启动中的中断处理

中断管理,说白了就是MCU的“应急响应系统”。你想想看,CPU正在跑主循环,突然来了个按键、定时器溢出或者串口数据,它得立刻放下手头的事去处理。这个“放下手头事、跳转到指定地址执行”的过程,全靠一张表——向量表。

做Bootloader,最核心的一个动作就是向量表重定向。没有它,你的APP永远跑不起来。

4.1 向量表的结构与默认位置

大部分Cortex-M内核的MCU,向量表默认放在0x00000000地址。这张表里存的是啥?

  • 栈顶指针:复位后SP寄存器的初始值
  • 复位向量:程序入口地址
  • NMI、HardFault、MemManage、BusFault、UsageFault:各种异常入口
  • 外设中断向量:TIM、UART、DMA等外设的中断服务函数地址

每个向量占4字节。以STM32为例,前16个是系统异常,后面才是外设中断。整个表的大小取决于芯片支持的中断数量。

关键点:CPU复位后,硬件自动从0x00000000加载栈顶指针,从0x00000004加载PC指针。这就是为什么向量表必须放在起始位置。

4.2 向量表重定向原理

Bootloader在0x08000000,APP在0x08020000。APP的中断向量表当然也在0x08020000。问题是——CPU怎么知道要去那里找向量?

Cortex-M内核提供了一个寄存器:SCB->VTOR(Vector Table Offset Register)。

你只需要把APP的基地址写进VTOR,CPU就会自动从新地址读取向量。就这么简单。

// 以STM32为例,重定向向量表到0x08020000
#define APP_BASE_ADDRESS  0x08020000

SCB->VTOR = APP_BASE_ADDRESS;

嗯,这里要注意:VTOR必须对齐到512字节边界(有些芯片要求更高,比如1024字节)。为什么?因为向量表大小是固定的,对齐是为了硬件查找方便。

我曾经踩过的坑:有一次我把APP地址设成了0x08020080,没对齐。结果一进中断就HardFault。查了两天才发现是VTOR没对齐。从那以后,我每次写VTOR之前都会检查地址的低9位是不是0。

4.3 中断服务函数注册

中断服务函数怎么注册?说白了就是把函数地址填到向量表的对应位置。

在标准库或HAL库中,我们通常用__attribute__((interrupt))或者WEAK声明。但做Bootloader时,有个细节容易被忽略:

  • Bootloader的中断服务函数:只在Bootloader阶段有效
  • APP的中断服务函数:跳转到APP后,中断向量表已经重定向,Bootloader的中断函数不再被调用

所以,你不需要在Bootloader里实现所有外设的中断函数。只需要实现跳转相关的(比如按键检测、通信超时等)。

我的个人习惯:在Bootloader里只开一个定时器中断做看门狗喂狗,和一个串口中断做固件接收。其他中断全部关闭。这样既简单又安全。

4.4 双Bank启动中的中断处理

双Bank启动,就是芯片内部有两块Flash区域:Bank0和Bank1。一个跑Bootloader,一个跑APP。或者两个都跑APP,做A/B升级。

这时候中断处理就有点意思了。

场景 向量表位置 VTOR设置 注意事项
Bootloader运行 Bank0起始地址 默认或Bank0地址 只处理必要中断
APP运行(Bank1) Bank1起始地址 Bank1地址 确保Bank1的向量表完整
回滚到旧APP Bank0的APP区 Bank0的APP区地址 注意Bank0可能被Bootloader占用

双Bank启动时,中断处理的核心问题只有一个:中断来了,CPU该找哪个向量表?

答案很简单:看VTOR指向哪里。

所以,在跳转到APP之前,你必须把VTOR改成APP的基地址。否则,中断一来,CPU还是去Bootloader的向量表找函数,结果要么找不到,要么跑错地方。

避坑指南:我曾经在一个双Bank项目里,跳转前忘了改VTOR。结果APP跑得挺好,但只要一按按键(外部中断),系统就死机。后来发现是中断向量还在指向Bootloader,而Bootloader里根本没实现这个按键中断函数。嗯,从那以后,我把VTOR重定向写进了跳转函数的第一步。

4.5 中断向量表重定向的完整流程

我总结了一个标准流程,每次做Bootloader都这么干:

  1. 关闭全局中断:跳转前关掉所有中断,防止跳转过程中被打断
  2. 清理中断标志:确保没有挂起的中断
  3. 设置VTOR:写入APP的基地址
  4. 设置栈指针:从APP向量表加载新的栈顶指针
  5. 跳转到APP复位向量:从APP向量表加载PC指针
  6. APP初始化:APP重新配置中断、外设、时钟等
// 标准跳转函数示例
void jump_to_app(uint32_t app_address)
{
    uint32_t stack_ptr;
    uint32_t reset_vector;
    void (*app_entry)(void);

    // 1. 关闭全局中断
    __disable_irq();

    // 2. 清理中断标志(具体实现取决于芯片)
    // ...

    // 3. 重定向向量表
    SCB->VTOR = app_address;

    // 4. 从APP向量表加载栈顶指针
    stack_ptr = *(volatile uint32_t *)app_address;

    // 5. 从APP向量表加载复位向量
    reset_vector = *(volatile uint32_t *)(app_address + 4);

    // 6. 设置主栈指针
    __set_MSP(stack_ptr);

    // 7. 跳转到APP
    app_entry = (void (*)(void))reset_vector;
    app_entry();
}

注意:跳转前一定要确保外设时钟、DMA、中断控制器等已经恢复到复位状态。否则APP初始化时可能会遇到奇怪的问题。

4.6 向量表与中断管理知识图谱

下面这张图是我自己画的,把向量表重定向、中断注册和双Bank启动的关系理清楚了。

向量表与中断管理知识图谱 向量表与中断管理知识图谱 向量表 (Vector Table) 默认位置 0x00000000 VTOR 重定向 中断服务函数注册 双Bank启动中的中断处理 Bank0: Bootloader Bank1: APP 回滚机制 核心原则: • 中断向量表必须对齐到512/1024字节边界 • 跳转前必须重定向VTOR,否则中断会跑飞

4.7 常见问题与避坑

  • 中断进不去:检查VTOR是否设置正确,地址是否对齐
  • 跳转后死机:检查栈指针是否从APP向量表正确加载
  • 双Bank切换后中断混乱:确认两个Bank的向量表都完整,且VTOR指向正确
  • 中断优先级配置丢失:APP初始化时要重新配置NVIC

我的经验:调试中断问题,最有效的方法是先关掉所有中断,一个一个开。这样能快速定位是哪个中断出了问题。另外,用逻辑分析仪抓中断引脚波形,比看代码快多了。

向量表重定向是Bootloader的基石。搞懂了它,中断管理就没什么神秘的了。记住一句话:VTOR指向哪里,CPU就去哪里找中断服务函数。