按键外部中断
# 外部中断
在前面我们学过了由定时器引发的中断,这是由定时器的一系列现象引发的中断。而今天的外部中断则是由我们自定义的现象引发的中断
# 按钮
首先先来了解一下按钮,这种元件通过上拉电阻与接地,可以在未按下与按下的时候向引脚输入高电平与低电平。利用外部中断可以被电平的上升或下降沿(或者二者都)引发,我们可以将按下或者松开按钮的电平变化设为外部中断的来源。
# 抖动
但是不得不提到的是由于按键的机械结构具有弹性,按下时开关不会立刻接通,断开时也不会立刻断开,这就导致按键的输入信号在按下和断开时都会存在抖动,如果不先将抖动问题进行处理,则读取的按键信号可能会出现错误
为了消除这一问题,可以通过软件消抖和硬件消抖两种方式来实现。这里我们使用软件滤波的实现方法。软件滤波的思想非常简单,抖动产生在按键按下和松开的两个边沿时刻,也叫下降沿(电平从高到低)和上升沿(电平从低到高)时刻,所以只需要在边沿时进行延时,等到按键输入已经稳定再进行信号读取即可。 一般采用软件消抖时,会进行20ms的延时,示波器采集按键波形如图所示:
# 外部中断的核心思想
外部中断是典型的“前后台模式”程序。在后台,也就是while(1)
中,我们实现着有条不紊的普通工作;在前台,程序会接受中断的信号,一旦有中断发生,后台的精力将会转到前台处理中断
所以根据二者的特性,在编写前后台程序时,需要注意尽量避免在前台程序中执行过长或者过于耗时的代码,让前台程序能够尽快执行完毕,以保证其能够实时响应突发的事件,比较繁杂和耗时的任务一般放在后台程序中处理
前台程序 | 后台程序 | |
---|---|---|
运行方式 | 中断 | 循环 |
处理的任务类型 | 突发型任务 | 重复型任务 |
任务的特点 | 任务轻,要求响应及时 | 任务重,稳定执行 |
# 实践配置
# CubeMX
首先将连接着按键的 PA0 引脚设为外部中断模式:
随后将 LED 的三个引脚设为输出模式
接着进入 GPIO 标签,将 PA0 引脚的外部中断参数改为上升与下降沿均可引发中断。且设置为上拉模式(上拉最为常用且在此处可以加强上拉的程度),还可以改一个标签名:
最后,在 NVIC 标签中允许0号线的外部中断发生,随后配置时钟等要素,生成代码:
外部中断的线
在 STM32 中,每一个 GPIO 都可以作为外部中断的触发源,外部中断一共有16条线,对应着 GPIO 的0-15引脚,每一条外部中断都可以与任意一组的对应引脚相连,但不能重复使用。例如外部中断 Line0 可以和 PA0,PB0,PC0 等任意一条0号引脚相连,但如果已经和PA0相连,就不能同时和 PB0,PC0 其他引脚相连
# 工程代码
在文件stm32f4xx_it.c
中,可以看到自动生成的函数EXTI0_IRQHandler
,它通过调用函数HAL_GPIO_EXTI_IRQHandler
对中断类型进行判断,并对涉及中断的寄存器进行处理,在处理完成后,它将调用中断回调函数HAL_GPIO_EXTI_Callback
,在中断回调函数中编写在此次中断中需要执行的功能
所以回调函数HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
就是我们需要在main.c
中重新定义的,执行中断操作的函数,系统为它传入的参数就是外部中断发生的组号
在介绍主要程序之前,我们需要先了解读取引脚高低电平的函数HAL_GPIO_ReadPin()
,它的两个参数分别是引脚所在的 GPIO 总线(A~H),第二个参数是引脚号,详细信息请见这里
下面就介绍主要程序的防抖、防误触与中断程序。主要是通过一个在中断发生与检测过程中定义的标志位与延时完成的:
uint8_t exit_flag = 0;
uint8_t rising_falling_flag;
/**
* @brief 外部中断回调
* @param[in] GPIO_Pin:引脚号
* @retval none
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == KEY_Pin)
{
if(exit_flag == 0)
{
exit_flag = 1;
rising_falling_flag = HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这是中断的返回函数,首先定义的两个变量exit_flag
与rising_falling_flag
分别是中断的阶段的标志位与记录上升沿还是下降沿以判断进行何种操作的变量
在检测到有电平发生变化时,系统跳转到到回调函数之后,首先会检测其发生的组号是否为设定的组号,以防止其他电平信号干扰。随后如果发现exit_flag
的值为 0,即中断还未发生的时候,就将这个标志位改为 1,代表正在延时检测是否真的是人按下了按钮而不是电平扰动,最后先读取一下诱发中断的信号目前是高电平还是低电平,记录在rising_falling_flag
中,随后转入主循环进一步检测
while(1)
{
if(exit_flag == 1)
{
exit_flag = 2;
if(rising_falling_flag == GPIO_PIN_RESET)
{
//消抖
HAL_Delay(20);
if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
{
HAL_GPIO_WritePin(LED_G_GPIO_Port, LED_G_Pin, GPIO_PIN_SET);
exit_flag = 0;
}
else
{
exit_flag = 0;
}
}
else if(rising_falling_flag == GPIO_PIN_SET)
{
//消抖
HAL_Delay(20);
if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_SET)
{
HAL_GPIO_WritePin(LED_G_GPIO_Port, LED_G_Pin, GPIO_PIN_RESET);
exit_flag = 0;
}
else
{
exit_flag = 0;
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
回到主循环之后,先检测exit_flag
是多少以确定刚刚的中断是不是由设定好的组的信号引发的,若是则将这个标志位改为 2,代表正在进行最后的检测。随后查看刚刚的触发信号是高电平还是低电平,以进入不同的检测程序,但是二者的步骤相同:先延时 20ms,随后再检测一次目标引脚的电平是否保持一致,一致则执行设定好的操作(按下开灯,松开关灯);不一致则无操作,但都要将exit_flag
改回 0 以接收下一个中断
提示
第一次看的时候,会有种疑问:while(1)
中先把exit_flag
改为2有用吗?
答案是有用。为了表示中断的检测到底进行到了哪一个阶段,便于在调试的时候查看
完整代码请查看这里 (opens new window)