学习何为中断以及如何通过中断来检测GPIO的电平变化
不管是现实世界还是STM32的程序里,总会有这样那样的事情打断我们,令我们不得不放下手头的工作去处理那些更紧急的事情.STM32也要随时准备着去处理一些我们为其规定的各种突发事件处理完成后还要继续执行之前正在执行的任务,而这些可以打断正常工作流程去处理的任务,我们就将其称为"中断"

对于STM32芯片来说,可以产生中断的事件多种多样,例如某个指令执行出错,某个定时器时间结束,串口接收到了数据,或者某个GPIO口检测到了电平变化等等等等
我们就先来试试检测GPIO口电平变化的中断,或者我们也可以叫它外部中断(EXTI,EXTernal Interrupt).外部中断,顾名思义就是触发源来自外部的中断,而STM32芯片与外部沟通的媒介,就是一根根GPIO引脚。
设想一下我们有这样一个需求,红色小灯要以4秒围殴周期循环闪烁,亮两秒灭两秒。当KEY1按下时,绿色小灯要翻转亮灭状态
需求实现
首先是新建工程

选择芯片,可以直接在收藏里面查找

给工程起名就叫interrupt(中断)

参照原理图,将代表红绿小灯的PB0 PA7都设置为GPIO_Output,并设置好用户标签,代表按键KEY1的PB12设置为GPIO_Input,也设置用户标签


最后Ctrl+s保存并生成代码,在while死循环中,先来一段红色小灯循环亮灭的代码
HAL_GPIO_WritePin(LED_RED_GPIO_Port,LED_RED_Pin,GPIO_PIN_SET);
HAL_Delay(2000);
HAL_GPIO_WritePin(LED_RED_GPIO_Port,LED_RED_Pin,GPIO_PIN_RESET);
HAL_Delay(2000);
再来一段按键按下后翻转绿色小灯的代码
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
{
HAL_GPIO_TogglePin(LED_GREEN_GPIO_Port,LED_GREEN_Pin);
while(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET);
}
编译下载之后可以看一下效果,我们发现按下KEY1后绿色小灯并没有亮起,不停的按或者长按不松才会在某一瞬间亮起完全达不到我们预期的效果,回看代码我们会发现,代码中存在两个延迟函数HAL_Delay,HAL_Delay会延迟等待我们设定的时间后在执行下面的代码,所以每次循环都要等待2+2=4秒,才会执行一次判断按键的代码,而且按键的判断瞬间就会完成,接下来就又是漫长的4秒等待,所以当按键按下时,程序大概率在执行HAL_Delay而不是按键判断,这也就造成了按键大概率失效的现象。

那有没有一种方法,让STM32即使正在执行HAL_Delay等这种耗时任务,也能快速响应类似于按键按下这种突发情况呢,当然要,那就是我们的主角:中断

回到Cube MX界面,将PB12由原先的GPIO_Input改为GPIO_EXTI12,也就是第12号外部中断线,修改了GPIO类型后,还需要重新设置用户标签

在来到System Core下的GPIO对PB12进行详细设置

点击GPIO mode的选择框,会有六个选项,其中前三个与中断有关,分别是上升沿触发中断,下降沿触发中断,以及上升沿/下降沿都触发中断。所谓上升沿即使某个GPIO口读取到的电平从低电平变到了高电平,而下降沿当然就是从高电平变成了低电平。

KEY1按下时,PB12会读取到低电平,所以在按键按下的瞬间,必然会有一个从高电平到低电平的变化,也即是所谓的下降沿,因而我们选择下降沿触发中断

这样当按键按下,电平下降的一瞬间,中断就会被触发,从而执行我们为其设定的中断处理函数,然后,我们还需要点击NVIC也就是中断控制器,勾选上开启中断向量EXTI15_10

保存并生成代码,首先删除旧的按键判断代码,然后在Core/Src文件夹中,我们可以找到stm32f1xx_it.c文件,其后缀it就代表它是与interrupt(中断)相关的文件。

此文件的最底部,有一个Cube MX帮我们自动生成的函数EXTI15_10_IRQHandler,它就是我们按下按键触发中断后STM32会调用执行的中断处理函数。
稍微捋一下就是,当STM32正在正常执行while死循环,控制红色小灯的循环闪烁,这是若按键按下,则按下的瞬间产生的下降沿就会被STM32捕捉到了,从而触发PB12引脚对应的EXTI12中断,因此中断又会调用执行对应的中断处理函数,也就是EXTI15_10_IRQHandler,所以只要在这个函数中翻转绿色小灯的亮灭,这样就能实现我们想要的结果了,当然了中断函数执行完成后,将会接着执行正常的while死循环,如果我们再次按下按键,就又会执行中断处理函数,翻转绿色小灯,而且由于中断处理函数中代码简单,只翻转小灯亮灭,瞬间就能执行完成,因而也不会对红色小灯的闪烁造成什么影响。


回到Cube IDE,在EXTI15_10_IRQHandler的第一个USER CODE注释对中,我们写下绿色小灯翻转亮灭的代码
void EXTI15_10_IRQHandler(void)
{
HAL_GPIO_TogglePin(LED_GREEN_GPIO_Port,LED_GREEN_Pin);
}
编译下载,看一下效果,可以看到在红色小灯正常闪烁的过程中触发中断,翻转绿色小灯亮灭,不过在多次尝试后,我们还是会发现有些不完美,经常会出现按键按下后,绿色小灯并没有翻转的情况,这其实是因为在设计电路时硬件电路的消抖电容,对于本次的中断应用来说,还是有些捉襟见肘,所以我们尝试对进行软件消抖。
按键按下或者抬起时的抖动都会有下降沿,从而触发中断,所以我们先延时10ms等待抖动过去,在这10ms中,由于中断处理函数没有执行完成,所以EXTI12中断一直处于占用状态,也就不会产生新的EXTI12中断,10ms后,我们再来判断当前KEY1的电平是否是低电平,来判断按键是因为按下的下降沿触发还是因为抬起时抖动中的下降沿触发,并在确认是按键按下后翻转绿色小灯电平。
void EXTI15_10_IRQHandler(void)
{
HAL_Delay(10);
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
{
HAL_GPIO_TogglePin(LED_GREEN_GPIO_Port,LED_GREEN_Pin);
}
}
再次编译下载查看效果,我们可以看到,不仅绿色小灯没有亮起,连红色小灯也不再闪烁。
那么,这是怎么回事呢?其实,这涉及到了发生多个中断时的优先级问题,因为HAL_Delay函数需要依赖一个叫做System tick timer(系统滴答)的中断,这个中断可以为其提供1ms的时钟基准,但此中断的优先级比我们所触发的中断的优先级低一些,也就导致了其不能在我们的中断处理函数执行时执行,从而使得HAL_Delay函数无法正常执行,程序也就卡死在了这行代码上。
解决的办法也很简单,回到Cube IDE,让System tick timer的优先级数字小于我们的EXTI15_10即可,保存并生成代码,这次不需要修改代码,直接编译运行,这次程序就运行正常了。红色小灯以4秒为周期循环闪烁,按键也可以正常控制绿色小灯。
PS:在学习或者练手的小工程中,通过中断来实现按键操作还算是可以接受的,但是在正规项目中,直接在中断中实现按键逻辑,尤其是在中断中调用HAL_Delay函数,是一种不被推荐的做法。这是因为我们需要尽可能的保证中断任务尽快执行完成,以将中断对正常执行流程的影响降到最低。因而对于按键,我们往往还会有一些更加巧妙的方式来实现这就需要继续往下学习提升了。