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一旦检测到中断,硬件会自动完成以下动作:
- 保存当前上下文(PC、PSR、LR等寄存器压栈)
- 根据中断号查找向量表,取出对应的ISR地址
- 跳转到ISR执行
避坑指南:我曾经在一个项目里,因为链接脚本写错了,导致中断向量表没有放在正确的地址上。结果一开中断就死机,查了两天才发现是向量表偏移没设对。所以,务必确认你的向量表地址和芯片手册一致。特别是做Bootloader时,向量表重定位(VTOR寄存器)一定要配好。
1.3 中断服务函数(ISR)的触发机制
ISR的触发,不是CPU主动去“轮询”的,而是由硬件事件“推”过来的。整个过程可以分为三个阶段:
阶段一:事件产生
外设检测到某个条件满足,比如定时器计数值到了、GPIO引脚电平变了。外设会向中断控制器(如NVIC、GIC)发送一个中断请求信号。
阶段二:中断仲裁
中断控制器收到请求后,会做两件事:
- 优先级判断:如果有多个中断同时到达,先处理优先级高的。优先级相同的,按中断号小的优先。
- 中断屏蔽检查:如果该中断被屏蔽了(比如全局中断关掉了,或者该中断的使能位没开),那就先挂着,等条件允许了再处理。
这里有个细节很多人会忽略——中断的“挂起”状态。如果一个中断来了但没被及时处理,它会一直挂在挂起寄存器里。直到CPU有空了,或者优先级更高的中断处理完了,它才会被响应。嗯,这就是中断嵌套的基础。
阶段三:CPU响应
CPU在每条指令执行完后,都会检查一下是否有挂起的中断。如果有,并且满足响应条件(比如优先级够高、全局中断使能),CPU就会:
- 自动压栈(保存现场)
- 取向量表中的ISR地址
- 跳转执行ISR
- 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的中断采样窗口,这个值在芯片手册里能找到。
好了,中断的基础概念就聊到这儿。下一节我们会深入中断延迟的测量方法和优化思路。记住一句话:中断是实时系统的灵魂,但用不好就是噩梦。