我们已经了解到了,所谓中断,就是打断程序的正常执行流程,执行一些由突发状况引起的紧急任务,并且通过一个按键的例子简单了解了如何使用外部中断。还留下了几个知识点:NVIC、EXTI、中断向量、中断优先级。

先从我们接触过的EXTI(外部中断)入手,假设我们来到了按键KEY1对应的GPIO口,也就是PB12它的GPIO内部,外部的电平信号进入到GPIO后,来到了输入驱动器。

首先是路过上拉下拉电阻,然后经过施密特触发器转化,最后抵达了输入数据寄存器,或者片上外设,而其实,在接下来电平信号还会抵达这样一个结构,我们称其为外部中断/事件控制器,像这样的结构,在我们使用的STM32F1系列芯片中共有19个。这19个外部中断控制器共用一套寄存器,但其中的连线都是独立的。图中的这些连线,其实有19组,每个外部中断都对应着其中的一组线,所以我们有时也称其为外部中断线,这19个外部中断线的前16个,也就是EXTI0~EXTI15,分别对应着与其编号相同的GPIO口,就是说从PA0,PB0,PC0,PD0进入的电平信号都可以进入EXTI0,然后PA1,PB1,PC1,PD1都是对应着EXTI1,一直到PA15,PB15,PC15,PD15也是对应着EXTI15。

注意:在接下来的语境中,高电平与1等效,低电平与0等效
首先是这一块区域,实际上这块区域与中断无关,而是与"事件(Event)"相关的结构,所谓事件其实是与中断类似的概念。只不过中断信号会抵达处理器中,调用代码进行处理,而事件信号却是送达相应的外设,由外设自行处理。

然后是这一块结构,边沿检测电路可以检测输入的电平信号中有没有发生高低电平的转换,也就是有没有出现上升沿或者下降沿,然后再根据上面两个寄存器的配置,来决定是否向后输出一个高电平信号。

我们之前在Cube MX中选择是上升沿触发中断还是下降沿触发中断,其实就是在配置这两个寄存器,就比如我们给PB12配置了下降沿触发中断,那么上升沿触发选择寄存器的第12位关闭为0,下降沿触发选择寄存器的第12位开启为1.那么接下来当边沿检测电路检测到了下降沿时,就会向后传递一个高电平信号,此信号经过一个或门抵达请求挂起寄存器,或门的特性是两个输入端只要有一个输入为1,则输出就是1,因而软件中断事件寄存器的存在,让我们可以通过程序,模拟产生一个中断,不过这个功能一般也不需要,暂且忽略掉。


注意:请求挂起寄存器是一个需要注意的点,其接收到高电平信号会将对应的位置1,例如我们当前举例的是EXTI12(外部中断线12),因而接收到从第12根外部中断线来的高电平后,请求挂起寄存器的第12位会被置1,然后请求挂起寄存器又会将此位输出到一个与门,与门的特性是当两个输入都为1时,输出才是1,否则便输出0,因而此与门的另一个输入中断屏蔽寄存器,就掌握了此中断的生杀大权,只有它对应位置上为1,输出高电平,请求挂起寄存器的信号才能通过与门,进入到NVIC。

而中断屏蔽寄存器的开启,其实我们在将PB12设置为GPIO_EXTI12时,Cube MX就自动帮我们在生成的代码中完成了,

那么来自请求挂起寄存器的高电平信号继续向后,就到达了中断嵌套最高城------NVIC,又称嵌套向量中断控制器,其主要作用便是掌管着这样一张中断向量表,所谓向量,那便是有方向的量,中断向量的方向就是指向中断处理函数,在所有的外部中断线中,只有EXTI0到EXTI4拥有自己的中断向量,而EXTI5到EXTI9共享中断向量EXTI9_5,EXTI10到EXTI15共享中断向量EXTI15_10,也就是说当来自EXTI12中断线的信号抵达NVIC后,NVIC会找到中断向量EXTI15_10,然后按照其指向,执行EXTI15_10_IRQHandler函数

这下我们也就清楚在上一篇文章中为什么我们要将绿色小灯翻转代码写在EXTI15_10_IRQHandler函数中了。
不过还有一点需要注意的是,NVIC是在一直监测某个中断线是否处于激活状态的,当中断处理函数也就是EXTI15_10_IRQHandler运行完成后,NVIC倘若依旧检测到EXTI12中断处于激活状态,就会再次运行EXTI15_10_IRQHandler。
为了让中断处理函数只执行一遍,而不是无限重复,我们需要在中断处理函数中将请求挂起寄存器的对应位清除为0。
不过,回想一下上一节的内容,在EXTI15_10_IRQHandler中的代码,好像只写了关于绿色小灯翻转亮灭的代码,并没有写什么清除请求挂起寄存器,那为什么程序可以正常运行,并没有发生所谓的无限重复呢?其实仔细看我们会发现,在EXTI15_10_IRQHandler中,还有一行Cube MX自动帮我们生成的代码,调用了一个叫做HAL_GPIO_EXTI_IRQHandler的函数,并将KEY1_Pin作为参数传了进去,而KEY1_Pin就是GPIO_Pin_12,那么其对应的外部中断线也就是EXTI12,我们按住Ctrl点击HAL_GPIO_EXTI_IRQHandler,这时我们就会发现它调用了一个名为_HAL_GPIO_EXTI_CLEAR_IT的函数,并将我们传入的参数GPIO_Pin,也就是KEY1_Pin或者EXTI12传给了此函数,而这个函数正是清除请求挂起寄存器的函数,调用此函数,请求挂起寄存器的第12位也就清除为0了,从而不会导致NVIC再次执行EXTI15_10_IRQHandler,如此看来,清除请求挂起寄存器的操作,Cube MX已经为我们做好了,没有特殊情况下,我们也就没有必要再去关心清除请求挂起寄存器的问题了。

那么,刚刚我们所描绘的,只是发生一个中断的情况,如果两个甚至多个中断同时触发呢?如何安排这些中断的执行顺序呢?
在中断向量表中,中断向量不仅有中断处理函数,还有优先级信息,在STM32中,中断向量的优先级分为两层:抢占优先级和响应优先级,优先级的数字越小则代表越优先。

在这样两种情况会需要判断中断优先级。
第一种情况是,当两个中断几乎同时发生,这时NVIC会首先判断两中断向量的抢占优先级,假如A中断的抢占优先级更高,换句话说,A中断的抢占优先级数字小于B中断,则A中断先执行,A中断执行完成后再执行B中断,但假若A中断与B中断的抢占优先级相同,那边再比较二者的响应优先级,由响应优先级更高的中断先执行,例如A B中断抢占优先级相同,但A中断响应优先级更高,则也是A中断先执行,然后才是B中断,当然啦,假如A B中断的响应优先级也相同的话,那就是按照他们在中断向量表中的顺序决定,这在我们并不关心他们的执行顺序的情况下才会发生,否则我们就应该给它们设定不同的优先级。

第二种情况是,A中断已经在执行过程中,B中断信号突然到达,那么这时STM32只会比较两中断的抢占优先级,当B中断的抢占优先级优于A中断时,B中断就会抢先执行,像打断正常执行流程那样,打断A中断,等到B中断执行完成后,再恢复A中断的执行,当然,最后会恢复正常流程的执行。

而倘若B中断抢占优先级弱于A中断或者与A中断相同时,便只能乖乖等A中断执行结束再执行了

再上一节中,我们设置了系统滴答的抢占优先级优于我们的EXTI15_10,就是为了让系统滴答可以在EXTI15_10执行过程中抢占进行,从而为HAL_Delay提供时间基准。

从这几种情况中我们可以看出,响应优先级仅在两中断同时发生时起辅助作用,抢占优先级才是骑主作用的。
STM32为每个中断向量准备了4个二进制位来储存中断优先级信息

在Cube MX中,我们可以自由选择这4位中几位用来设置抢占优先级,几位用来设置响应优先级。默认设置是4位都用来设置抢占优先级。这是每个中断向量的抢占优先级都可以设置为0~15,但响应优先级只能被迫做0.如果设置为2位为抢占优先级,2位为响应优先级,则二者都可以设置为0~3,不过响应优先级的用处不大,所以一般我们保持默认的4位都是抢占优先级即可。

我们从头稍作梳理,从引脚进入的高低电平信号首先由输入驱动器处理,经过输入驱动器处理过的高低电平信号,会进入到边沿检测电路,当边沿检测电路捕获到我们设置的边沿信号后,就会向请求挂起寄存器输出一个高电平信号,请求挂起寄存器对应的位会置1,然后只要我们已经开启了此中断,请求挂起寄存器的信号就可以进入到NVIC,NVIC会找到此中断线对应的中断向量,并执行相应的中断处理函数,并且在中断处理函数中需要清除请求挂起寄存器,以防止中断重复触发,当然,这一操作Cube MX已经自动帮我们完成。
针对多个中断同时发生的情况,我们可以使用中断优先级来规定执行的顺序,响应优先级用处比较小,往往我们只需要设置抢占优先级即可。优先级的数字越小,则优先级越高。
不过刚刚说的是外部中断如何触发的情况,实际上串口USART、定时器TIM、IIC等等外设也可以触发多种相关中断,它们的中断触发与外部中断也大同小异,虽然没有外部中断线这套结构,但也会有相关的请求挂起寄存器和中断屏蔽寄存器,触发中断后依旧需要NVIC通过中断向量找到并执行中断处理函数,中断处理函数也是需要清除请求挂起寄存器。

PS:如果中断模式下需要设置上拉或者下拉,也可以直接在Cube MX中配置
