1. 中断基础概念:中断的定义、中断向量表、中断服务函数(ISR)的触发机制

好,咱们直接进入正题。中断这玩意儿,说白了就是CPU正在干一件事,突然被一个外部或内部的事件打断,它得停下手里的活,先去处理这个突发事件,处理完了再回来接着干。嗯,就像你正写着代码,同事突然拍你肩膀问个问题——你总得先回答他,对吧?

我刚开始做嵌入式那会儿,对中断的理解就停留在“打断”这个层面。直到有一次调试一个电机驱动,发现电机总是丢步,查了三天才发现是中断响应太慢,导致PWM脉冲没来得及更新。从那以后,我才真正开始重视中断的每一个细节。

1.1 中断的定义

中断,是CPU对系统内外部异步事件的一种响应机制。它允许CPU在执行主程序的同时,能够及时响应优先级更高的事件。

你想想看,如果没有中断,CPU就得不停地轮询各个外设的状态——那效率得多低?就像你每隔一秒就去看看邮箱有没有新邮件,而不是等邮件到了再通知你。中断就是那个“通知”机制。

核心要点:中断的本质是“事件驱动”。CPU不需要主动去问,而是等事件来了再说。这大大提高了系统的实时性和CPU利用率。

中断的来源可以分为三类:

  • 外部中断:来自芯片引脚的电平变化或边沿触发。比如按键按下、传感器信号跳变。
  • 内部中断:来自片内外设。比如定时器溢出、UART接收到数据、ADC转换完成。
  • 软件中断:由程序主动触发。比如ARM的SWI指令、x86的INT指令。常用于系统调用。

我个人习惯把中断比作“紧急通道”。平时走的是普通车道(主程序),一旦有紧急车辆(中断事件),就得让出紧急通道。但这里有个坑——你不能让所有车都走紧急通道,否则普通车道就堵死了。这就是为什么中断服务函数要短小精悍。

1.2 中断向量表

中断向量表,是中断系统的“通讯录”。CPU收到中断信号后,得知道该去找谁处理。这个“通讯录”就记录了每个中断号对应的处理函数地址。

我记得第一次看ARM Cortex-M3的手册时,被那个中断向量表的结构搞得有点懵。其实说白了,就是一张表格:

向量表偏移 中断号 描述 处理函数
0x0000_0000 - 栈顶指针(MSP初始值) 不适用
0x0000_0004 - 复位向量 Reset_Handler
0x0000_0008 - NMI(不可屏蔽中断) NMI_Handler
0x0000_000C - 硬错误 HardFault_Handler
0x0000_0010 0 外部中断0 EXTI0_IRQHandler
0x0000_0014 1 外部中断1 EXTI1_IRQHandler
... ... ... ...

这张表通常放在Flash的起始地址。CPU一旦检测到中断,硬件会自动完成以下动作:

  1. 保存当前上下文(PC、PSR、LR等寄存器压栈)
  2. 根据中断号查找向量表,取出对应的ISR地址
  3. 跳转到ISR执行

避坑指南:我曾经在一个项目里,因为链接脚本写错了,导致中断向量表没有放在正确的地址上。结果一开中断就死机,查了两天才发现是向量表偏移没设对。所以,务必确认你的向量表地址和芯片手册一致。特别是做Bootloader时,向量表重定位(VTOR寄存器)一定要配好。

1.3 中断服务函数(ISR)的触发机制

ISR的触发,不是CPU主动去“轮询”的,而是由硬件事件“推”过来的。整个过程可以分为三个阶段:

阶段一:事件产生

外设检测到某个条件满足,比如定时器计数值到了、GPIO引脚电平变了。外设会向中断控制器(如NVIC、GIC)发送一个中断请求信号。

阶段二:中断仲裁

中断控制器收到请求后,会做两件事:

  • 优先级判断:如果有多个中断同时到达,先处理优先级高的。优先级相同的,按中断号小的优先。
  • 中断屏蔽检查:如果该中断被屏蔽了(比如全局中断关掉了,或者该中断的使能位没开),那就先挂着,等条件允许了再处理。

这里有个细节很多人会忽略——中断的“挂起”状态。如果一个中断来了但没被及时处理,它会一直挂在挂起寄存器里。直到CPU有空了,或者优先级更高的中断处理完了,它才会被响应。嗯,这就是中断嵌套的基础。

阶段三:CPU响应

CPU在每条指令执行完后,都会检查一下是否有挂起的中断。如果有,并且满足响应条件(比如优先级够高、全局中断使能),CPU就会:

  1. 自动压栈(保存现场)
  2. 取向量表中的ISR地址
  3. 跳转执行ISR
  4. ISR执行完毕后,执行中断返回指令(如ARM的BX LR),恢复现场

用代码表示一个典型的ISR结构:

// 以STM32的EXTI0中断为例
void EXTI0_IRQHandler(void)
{
    // 1. 检查中断标志位
    if (EXTI->PR & (1 << 0))
    {
        // 2. 清除中断标志位(这一步千万不能忘!)
        EXTI->PR = (1 << 0);
        
        // 3. 处理中断事件(尽量短小)
        // 比如:设置一个全局标志,或者唤醒一个任务
        g_button_pressed = 1;
    }
}

重要提醒:ISR里不要做耗时操作!我曾经见过有人在ISR里调用printf、做浮点运算、甚至跑延时循环。结果就是系统响应变得极慢,其他中断根本进不来。记住:ISR里只做最必要的事,把耗时操作放到主循环或任务里去做

1.4 中断的触发方式

不同的外设,中断触发方式也不一样。常见的有:

  • 电平触发:只要引脚保持高/低电平,就一直触发中断。适合按键去抖后的稳定信号。
  • 边沿触发:只在电平跳变时触发一次。分上升沿、下降沿、双边沿。适合脉冲信号。
  • 软件触发:通过写寄存器来模拟中断。常用于调试或任务间通信。

我个人习惯用边沿触发,因为它不容易产生重复触发。但要注意——边沿触发可能会丢失信号,如果脉冲太窄,CPU没来得及采样就过去了。所以,信号宽度至少要大于CPU的中断采样窗口,这个值在芯片手册里能找到。

好了,中断的基础概念就聊到这儿。下一节我们会深入中断延迟的测量方法和优化思路。记住一句话:中断是实时系统的灵魂,但用不好就是噩梦