在生活中,我们做一件事时常常被打断,然后自动去处理更紧急的事情。那么在计算机又是如何识别和处理这些紧急事件呢?本文将主要介绍什么是中断,中断服务函数又是什么,遇到中断了硬件和软件是怎么协作的呢?。
一、定义
1、定义
中断(Interrupt)是计算机体系结构中一种硬件驱动的控制转移机制。
当处理器正在执行某条指令流时,外部或内部硬件事件(中断源)向处理器发送请求信号(即:不是CPU主动轮询去查事件有没发生,是硬件主动通知CPU) ,处理器在当前指令执行完毕后,暂停原有指令流,保存执行现场,转去执行预先指定的中断处理程序;处理完成后恢复现场,回到被暂停的指令流继续执行。
2、核心名词
中断源:发出信号的地方(外部引脚、定时器、串口、ADC等)
中断服务函数(ISR):中断来了之后,要跑的那段专门处理代码。
中断标志位:硬件置1,告诉CPU:有中断来了,处理完一般软件/硬件清零。
中断优先级:多个中断同时来,先处理优先级高的;高优先级可以抢占低优先级(中断嵌套)
开/关中断(总中断开关):全局开关,关掉之后所有中断都不响应。
二、中断的作用
相对轮询,中断可以:
及时响应外部事件 :按键触发、外部传感器信号、脉冲输入,不用一直循环检测
解放CPU :没有事件的时候,CPU安心跑主业务,有事才临时切过去处理
精准定时 :定时器中断,用来做固定周期任务(1ms定时、PWM、采样)
通信收发 :串口、I2C、SPI收到数据、发完一帧触发中断,避免轮询卡死
故障紧急处理:硬件错误、电源异常、看门狗等紧急告警。
三、普通函数和中断服务函数ISR区别
有人会认为"ISR不就是一个特殊的函数吗?",虽然语法上看起来像C函数,但是执行模型和调度者完全不一样 。
普通函数是软件调度 的,编译器自动根据函数情况,按需保存用到寄存器,C函数可以传参数,也可以有返回值
中断服务函数是硬件事件驱动的,中断突如其来,CPU不知道当前主程序正在用哪些寄存器。Cortex-M内核硬件会自动压入一部分寄存器,剩下的需要程序员、编译器手动保存(有些编译器__interrupt关键字自动处理)。ISR不是普通return,用专门的中断返回指令(Cortex-M是BX LR,由编译器自动生成),回到被打断的断点,而不是某个函数的调用点。
| 对比项 | 普通函数 | 中断服务函数 |
|---|---|---|
| 触发方式 | 代码主动调用,顺序可控制 | 硬件中断自动触发,触发时机不可预知 |
| 参数、返回值 | 可以有参数、return返回 | 无参数无返回值 |
| 是否抢占 | 不能,顺序执行 | 高优先级可抢占主程序或低优先级中断(由中断优先级控制) |
| 调用位置 | 代码可以随便写调用 | 禁止手动调用 |
| 执行时长 | 无强制限制 | 必须尽量短,禁止阻塞 |
| 返回 | return 回到调用行 | 中断专用返回指令,回到被打断断点 |
| 注册 | 无需注册 | 需要放在中断向量表 |
四、中断服务函数(ISR)底层实现
底层本质:ISR不是CPU自带的一段特殊代码,而是你写好、放在内存里,在中断向量表登记入口地址;当中断请求到来时,硬件自动完成一系列动作,最终跳转到这个入口地址执行你的代码
整个流程分为6个阶段(以Cortex-M为例子):
前提:已配置好外设、打开外设中断、打开NVIC、打开全局中断
阶段1:中断源发起中断请求(硬件)
外设检测到事件,把中断标志位(IF)置1
如果外设的中断使能位打开,就向外设中断控制器(NVIC)发出中断请求信号IRQ
阶段2:处理器判断是否响应中断(在一条指令执行完之后才响应)
CPU有一条硬件规则:不会打断一条正在执行的指令,必须等当前指令执行结束,才检查是否有中断请求。
判断:总中断开关是否开启;这个中断的优先级,是否高于当前正在执行上下文的优先级(这一步硬件自动做,软件代码不参与)
阶段3:硬件自动保存上下文(自动压栈)【Cortex-M特色】
Cortex-M内核硬件自动把一部分寄存器压入当前栈(主栈MSP、进程线PSP)--这是硬件入栈,不需要软件写汇编push
剩下在ISR里用到的其他寄存器,由编译器自动生成PUSH汇编保存(编译器识别到是中断函数,自动补)
同时硬件更新LR特殊值(EXC_RETURN)
51单片机没有硬件自动压栈,全部靠软件在ISR开头手动PUSH、结尾POP
阶段4:查表,跳转到中断服务函数入口(核心:中断向量表)
处理器去访问中断向量表(Vector Table),CPU根据本次中断的中断号,索引向量表,拿到ISR的起始地址,修改PC寄存器,从而开始执行中断服务函数。
向量表本质就是一块连续的内存数组,里面存的全是地址
阶段5:执行中断服务函数(你写的C代码,编译成汇编)
c
void EXTIO_ISRHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0)!=RESET)
{
//业务:读寄存器,置标志
EXTI_ClearITPendingBit(EXTI_Line0);//软件清除中断标志(非常关键哦)
}
}
这里有两种标志:
硬件自动清零:部分外设、读寄存器自动清标志位
软件手动清零:EXTI、很多定时器,必须代码写寄存器清除挂起位,不清会一进一出反复触发中断(中断雪崩)
在Keil里用__irq/GCC用__attribute__((interrupt))标记,告诉编译器:a:这个函数不能用普通函数返回;b:自动保存用到的寄存器;c:结尾生成中断返回指令。
阶段6:中断返回、恢复上下文
ISR末尾,编译器生成BX LR 指令
CPU识别到这是中断返回,根据EXC_RETURN的值:
a从栈里硬件自动弹出之前保存的寄存器
bPC恢复成被打断时原来的地址;
c:回到之前被暂停的代码继续运行
d:退出中断上下文,回到线程上下文。
五、说明
1、国内有些单片机教材,习惯把异常也称为中断。实际两者是差别的:
中断(Interrupt) :事件和CPU指令流不同步,不知道什么时候来(eg:外部引脚、串口接收、定时器溢出),所以是异步的
异常(Exception) :由当前正在执行的指令本身引发,和指令同步(访问非法地址、未定义指令),所以是同步的
2、中断服务函数一定要短,快进快出
中断不要写:delay、printf、长循环、复杂运算
正确做法:读硬件寄存器,只标记一个标志位/发事件信号;复杂逻辑放到主循环或者RTOS任务里去执行。
RTOS中,ISR里不能直接嗲用会阻塞的API(eg:osSemaphoreWait()),只能用ISR专用版本
3、千万不要手动ISR()
手动调用会破坏中断上下文,引发异常或死机
4、ISR不是硬件写的,是程序员写的软件代码
硬件:进入ISR、保存现场、查表跳转、恢复现场这一套流程,是处理器硬件电路和内核逻辑自动完成的。
软件只负责配置中断、写ISR业务、清中断标志。