4. 中断机制详解:中断向量表、中断优先级、中断嵌套与共享中断

中断,说白了就是CPU正在干一件事,突然被一个紧急事件叫停,转去处理这个事件,处理完再回来接着干。这个机制,是嵌入式系统的灵魂。没有中断,你的MCU就得不停地轮询,效率低得吓人。

我刚开始做驱动开发那会儿,觉得中断不就是个回调函数嘛,有啥难的?结果第一次调试一个多中断源的系统,CPU直接跑飞了。嗯,从那以后,我再也不敢小看中断了。

4.1 中断向量表:CPU的紧急联络簿

中断向量表,你可以把它想象成一本“紧急联络簿”。CPU收到中断信号后,第一件事就是翻这本簿子,找到对应的处理函数地址,然后跳过去执行。

这张表通常放在Flash或RAM的起始地址。每个中断源对应一个表项,里面存的就是中断服务函数(ISR)的入口地址。

核心要点:

  • 向量表地址由芯片厂商固定,但有些芯片允许重映射(比如STM32的VTOR寄存器)
  • 表项大小通常是4字节(32位MCU)或8字节(64位MCU)
  • 第0个表项是栈顶指针,第1个是复位向量,后面才是各种外设中断

我在一个项目中遇到过,Bootloader和App各自维护一套中断向量表。App启动时,必须把向量表重映射到App的起始地址。忘了做这一步,中断一进来就跳到Bootloader的ISR里去了,系统直接崩溃。这个坑,我踩过。

// STM32 重映射中断向量表示例
#define APP_BASE_ADDRESS 0x08010000

void jump_to_app(void)
{
    // 关闭全局中断
    __disable_irq();
    
    // 重映射向量表到App区域
    SCB->VTOR = APP_BASE_ADDRESS;
    
    // 设置栈指针
    __set_MSP(*(uint32_t*)APP_BASE_ADDRESS);
    
    // 跳转到App的复位向量
    void (*app_reset_handler)(void) = (void (*)(void))(*(uint32_t*)(APP_BASE_ADDRESS + 4));
    app_reset_handler();
}

4.2 中断优先级:谁更紧急谁先上

多个中断同时发生时,谁先被处理?这就靠优先级来决定了。优先级高的中断,CPU会优先响应。

不同芯片的优先级机制不太一样。ARM Cortex-M系列用的是抢占优先级+子优先级的方式。抢占优先级决定能否打断当前中断,子优先级决定同抢占优先级下的响应顺序。

优先级分组 抢占优先级位数 子优先级位数 说明
NVIC_PriorityGroup_0 0 4 全部为子优先级,无抢占
NVIC_PriorityGroup_1 1 3 2级抢占,8级子优先级
NVIC_PriorityGroup_2 2 2 4级抢占,4级子优先级
NVIC_PriorityGroup_3 3 1 8级抢占,2级子优先级
NVIC_PriorityGroup_4 4 0 16级抢占,无子优先级

我的建议:除非你有非常特殊的需求,否则直接用NVIC_PriorityGroup_4(16级抢占)。这样优先级管理最简单,不容易出错。我见过太多人在优先级分组上玩花样,最后把自己绕晕了。

4.3 中断嵌套:高优先级打断低优先级

中断嵌套,就是高优先级的中断可以打断正在执行的低优先级中断。处理完高优先级中断后,再回来继续执行低优先级中断。

为什么会这样?你想想看,如果定时器中断正在处理,突然来了一个紧急的DMA传输完成中断,难道要等定时器处理完再响应DMA吗?那数据可能就丢了。

中断嵌套的流程是这样的:

  1. 低优先级ISR正在执行
  2. 高优先级中断触发
  3. CPU保存当前ISR的上下文(压栈)
  4. 跳转到高优先级ISR执行
  5. 高优先级ISR执行完毕,恢复上下文(出栈)
  6. 继续执行低优先级ISR

注意:中断嵌套层数不能太多。每嵌套一层,就要压一次栈。如果嵌套太深,栈空间可能不够用。我曾经在一个项目中,中断嵌套了4层,结果栈溢出了,系统跑飞。后来我把一些不紧急的中断优先级调低,减少了嵌套层数,问题就解决了。

4.4 共享中断:多个外设共用一个中断线

有些芯片的中断线是有限的,多个外设可能共用一个中断号。比如GPIO的PA0、PB0、PC0可能都映射到同一个EXTI0中断线。这时候,ISR里就得自己判断到底是哪个引脚触发了中断。

共享中断的处理方式:

  • 在ISR中轮询各个外设的中断标志位
  • 谁置位了,就处理谁
  • 处理完后清除对应的中断标志位
// 共享中断示例:多个GPIO共用一个EXTI中断线
void EXTI0_IRQHandler(void)
{
    // 检查PA0是否触发
    if (EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        // 处理PA0中断
        GPIO_PA0_Handler();
        // 清除中断标志
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
    
    // 检查PB0是否触发
    if (EXTI_GetITStatus(EXTI_Line0) != RESET)  // 注意:同一个中断线
    {
        // 处理PB0中断
        GPIO_PB0_Handler();
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
    
    // 检查PC0是否触发
    if (EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        // 处理PC0中断
        GPIO_PC0_Handler();
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

避坑指南:我曾经在共享中断里犯过一个低级错误——先清除了中断标志位,再去处理业务逻辑。结果在处理过程中,同一个中断又触发了,但标志位已经被清掉了,导致中断丢失。正确的做法是:先读取并保存中断状态,再清除标志位,最后根据保存的状态去处理业务。

4.5 中断设计的实战原则

做了这么多年驱动开发,我总结了几条中断设计的原则,分享给你:

  • ISR要短小精悍:ISR里只做最紧急的事,比如读取数据、设置标志位。耗时的处理放到主循环或任务里去做。
  • 优先级分配要合理:时间敏感的中断(如定时器、DMA)给高优先级,非紧急的中断(如按键)给低优先级。
  • 注意临界区保护:如果ISR和主循环共享数据,记得用关中断或信号量来保护。
  • 避免在ISR中调用阻塞函数:比如printf、延时函数,这些都会拖慢ISR的执行速度。

我的习惯:我会在ISR里只做三件事——读取硬件寄存器、清除中断标志、设置一个volatile标志位。主循环里轮询这个标志位,再做具体处理。这样ISR的执行时间可以控制在几微秒以内,系统响应非常快。

4.6 中断机制知识体系图

下面这张图,把中断机制的核心知识点串起来了。你可以对照着看,加深理解。

中断机制知识体系 中断机制 中断向量表 存储ISR入口地址 可重映射(VTOR) 中断优先级 抢占优先级 子优先级 中断嵌套 高优先级打断低优先级 注意栈深度 共享中断 轮询中断标志位 先读状态再清标志

这张图把中断机制的四个核心知识点都串起来了。你写驱动的时候,对照着这张图来设计中断逻辑,基本不会出大问题。


可可精品资料库