一、外部中断/事件控制器(EXTI)结构图
1、结构图分析
外部中断主要由外部中断/事件控制器(External interrupt/event controller, EXTI)控制,它管理了外部中断或者事件的使能与否、触发方式等功能。
( 外部中断/事件控制器(EXTI)结构图 )
下图为中文翻译版
图中有两条从右向左走向的线路,红色线路用于产生中断,黄色线路产生事件。
如上图,信号输入后首先会经过边沿检测电路,这个检测电路会通过查看 上边沿触发 和下边沿触发 寄存器的值,去判断信号到底在哪个边沿采集。(所谓上边沿和下边沿,即电平从低->高 or 从高->低 变化时的那条竖直的边)
经过边沿检测电路之后,就来到上图标号3的位置。这里是个或门连接着输入信号和软件中断事件寄存器。也就是说,除了信号触发中断,我们还可以控制相关的寄存器从而产生中断事件。
过了步骤3或门这一步之后,产生中断和产生事件的流程就有所区别了。
我们先来看看中断的产生过程:
首先中断请求信号会先进入 请求挂起寄存器 。这个寄存器的存在意义在于,如果中断发生时,正在处理同级或高优先级中断,则中断不能立即得到响应,此时中断被悬起。悬挂意味着等待而不是舍去,当优先级高的或者同等级先发生的中断完成后,被挂起的中断才会执行。
落实到stm32这里,可以看到在标号4的位置,是一个与门,所以,中断请教什么时候推送到NVIC控制器执行,就是中断屏蔽寄存器应该控制的事了。
同理,对于事件发生来说,事件是否生成,就是事件屏蔽寄存器的事了。不同点在于,事件的产生没有优先级的概念,所有无需挂起寄存器这么个东西。
2、外部中断线路映像
EXTI控制器支持多达20个软件的中断/事件请求。
我们单片机的外部中断通常是由GPIO的电平跳变引起的中断。
在STM32中,每一个GPIO都可以作为外部中断的触发源,外部中断通用I/O映像一共有16条线,对应着GPIO的0-15引脚,每一条外部中断都可以与任意一组的对应引脚相连,但不能重复使用。
还有另外四个EXTI线的连接方式如下:
EXTI线16连接到PVD输出
EXTI线17连接到RTC闹钟事件
EXTI线18连接到USB唤醒事件
EXTI线19连接到以太网唤醒事件(只适用于互联型产品)
3、中断模式
外部中断支持GPIO的三种电平模式:
上升沿中断:当GPIO的电平从低->高时,引发外部中断。
下降沿中断:当GPIO的电平从高->低时,引发外部中断。
上升沿和下降沿中断(双边沿中断):当GPIO的电平从低->高和从高->低,都能引发外部中断。
二、外部中断控制LED灯
1、原理图
如下图,我们把三个LED灯分别连接在PA0、PA1、PA2上,按键 KEY1 连接在 PA3 上。
2、中断消抖
考虑到由于按键的机械结构具有弹性,按下时开关不会立刻接通,断开时也不会立刻断开,这就导致按键的输入信号在按下和断开时都会存在抖动。
如果不先将抖动问题进行处理,则读取的按键信号可能会出现错误。
因此为了消除这一问题,我们可以通过软件消抖或者硬件消抖两种方式来实现。
2.1 硬件消抖
硬件消抖一般有两种实现方式:“RS触发器”、“电容滤波器”
2.11 RS触发器
在RS触发器中,R是RESET的意思,代表复位,S是SET的意思,代表置位。
利用RS触发器来吸收按键的抖动。当按键未按下时,输出为0,一旦有键按下,触发器立即翻转,输出为1,触电的抖动便不会再对输出产生影响,起到抗抖作用,按键释放时也一样。RS触发电路消抖电路图如下。
注:详细分析可学习数电相关知识
2.12 电容滤波器
将电容并联在按键的两端,利用电容两端的电压不能突变的特性以及放电的延时特性。将产生抖动的电平通过电容吸收掉。从而达到消抖的作用,电容消抖电路图如原理图所示。
具体分析:
按键按下(高电平->低电平):按键按下,电容与按键形成回路,电容开始放电,当电容放电结束后,抖动就基本结束了。在放电期间,PA3处一直向外输出高电平。
主要是因为按键的电阻很小,电容和按键的并联电阻也被拉小,大部分回路电压会加到电阻上,电容电压变小,又由于电容两端电压不能突变,根据 I=dq/dt,带入电容公式q=uc,得出I=C*du/dt,当du/dt<0时,电容电荷减少,即电容放电,放电维持电平至按键抖动结束。
按键松开(低电平->高电平):按键松开,电容左极板接电源,右极板接地,开始充电,当电容充电结束后,抖动也就基本结束了。在电容充电期间,PA3处一直向外输出为低电平。
按键松开时,回路电压基本加在电容端,电容本来两边都是低电平,现在一边接高电平,一边接低电平,就开始充电,充电维持电平至按键抖动结束。
2.2 软件消抖
2.21 延时函数按键消抖
优点:简单方便
缺点:程序在空跑浪费CPU资源、不够精准
值得注意的是:由于我们这里按键是用中断的方式实现,所以不能在中断服务函数里面使用延时函数,因为中断服务函数最基本的要求就是快进快出。
2.22 定时器按键消抖
优点:节约CPU资源
缺点:消耗一个定时器
原理:按键采用中断驱动方式,当按键按下以后触发按键中断,在按键中断中开启一个定时器,定时周期为()ms,当定时时间到了以后就会触发定时器中断,最后在定时器中断处理函数中读取按键的值,如果按键值还是按下状态那就表示这是一次有效的按键。
定时器详情可参考STM32定时器TIM控制_沉默的道路的博客-CSDN博客
3、消抖选择
或许有人觉得用硬件电容滤波器消抖容易点,就焊接一个电容,但其实也是根据实际情况而定。
1. 电容也是钱啊,如果做产品,需求大量时也是一笔消费,主打一个省钱。
2. 中断闲着也是闲着,合理规划好代码也不乱,逻辑也不会变复杂。再复杂的逻辑,拆分好,规划好,都可以条理清晰,一目了然。
3. 再打比方如果在产品中,加电容在按键这玩意上,只要出现一次不可靠,人家就会认为你做的整个东西不可靠。
4. 不能100%依靠硬件消抖,这样究竟要选多大的电容合理,小了没有作用,大了反应慢,充电过程中可不可能存在临界状态,就是1,0的边沿。
当然不管硬件消抖也好软件消抖也罢,只要是人按的都不是绝对可靠,你消抖比如50ms,我乱按总可能抖出那么一两个。还是根据实际情况而定吧。
综上,如果能硬件+软件防抖就比较理想了!
三、程序代码
1、按键中断在CubeMX中配置
将PA3号引脚设置为按键的输入引脚,将其设置为外部中断模式。
将GPIO模式设置为双边沿触发的外部中断,电阻设置为上拉电阻,最后设置用户标签为KEY1。
在NVIC中,勾选开启外部中断。
2、模块化封装led
2.1 宏定义与预处理-led
#ifndef __LED_DRIVER_H__
#define __LED_DRIVER_H__
#include "main.h"
void LED_Init(void);
/*********************
* 开启时钟宏定义(便于代码的移植修改)
**********************/
#define LED_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE();//使用前面的宏(LED_CLK_ENABLE),代替后面的函数
//#define LED2_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE(); //如果是不同的引脚
//#define LED3_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE();
/*********************
* 引脚宏定义
**********************/
#define LED1_Pin GPIO_PIN_0 //使用前面的宏(LED1_Pin),代替后面的函数
#define LED2_Pin GPIO_PIN_1
#define LED3_Pin GPIO_PIN_2
/*********************
* 函数宏定义
**********************/
#define LED1_ON HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_SET);//定义一个宏,通过使用前面的宏(LED1_ON),代替后面的函数,点亮对应灯
#define LED2_ON HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_SET);
#define LED3_ON HAL_GPIO_WritePin(GPIOA,GPIO_PIN_2,GPIO_PIN_SET);
#define LED1_OFF HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_RESET);//定义一个宏,通过使用前面的宏(LED1_OFF),代替后面的函数,关闭对应灯
#define LED2_OFF HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_RESET);
#define LED3_OFF HAL_GPIO_WritePin(GPIOA,GPIO_PIN_2,GPIO_PIN_RESET);
#endif
2.2 主函数-led
#include "led_driver.h"
void LED_Init(void)
{
/*定义GPIO的结构体变量*/
GPIO_InitTypeDef GPIO_InitStruct = {0};
/*使能LED的GPIO对应的时钟,开启时钟(GPIOA)*/
LED_CLK_ENABLE() //用.h文件中定义的宏代替_ _HAL_RCC_GPIOA_CLK_ENABLE();
/*设置参数*/
GPIO_InitStruct.Pin = LED1_Pin|LED2_Pin|LED3_Pin;//选择LED1 LED2 LED3的引脚
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP ;//设置为推挽输出模式
GPIO_InitStruct.Pull = GPIO_NOPULL; //没有上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;//引脚输出速度设置为慢
/*初始化引脚配置,让设置的参数生效*/
HAL_GPIO_Init(GPIOA,&GPIO_InitStruct);//&GPIO_InitStruct定义的结构体
}
3、模块化封装Exti
3.1 宏定义与预处理-EXti
#ifndef __EXTI_DRIVER_H__
#define __EXTI_DRIVER_H__
#include "main.h" //#include "stm32f1xx_hal.h"
void EXTI_Init(void);
#define KEY1_GPIO_CLK_EN() __HAL_RCC_GPIOA_CLK_ENABLE();
#define KEY1_Pin GPIO_PIN_3
uint8_t Getkey1val(void); //返回key1按键状态
#endif
3.2 主函数-Exti
#include "Exti_driver.h"
uint8_t key_val = 0; //定义按键返回值变量
void EXTI_Init(void)
{
/*定义GPIO的结构体变量*/
GPIO_InitTypeDef GPIO_InitStruct = {0};
/*使能LED的GPIO对应的时钟,开启时钟(GPIOA)*/
KEY1_GPIO_CLK_EN() //用.h文件中定义的宏代替__HAL_RCC_GPIOA_CLK_ENABLE();
/*设置参数*/
GPIO_InitStruct.Pin = KEY1_Pin;//选择按键引脚
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING_FALLING ;//设置为双边沿触发外部中断
GPIO_InitStruct.Pull = GPIO_PULLUP; //默认上拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;//引脚输出速度设置为高速
/*初始化引脚配置,让设置的参数生效*/
HAL_GPIO_Init(GPIOA,&GPIO_InitStruct);//&GPIO_InitStruct定义的结构体
HAL_NVIC_SetPriority(EXTI3_IRQn, 0, 0);// 设置外部中断优先级
HAL_NVIC_EnableIRQ(EXTI3_IRQn);// 使能外部中断
}
//外部中断3中断处理函数
void EXTI3_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(KEY1_Pin);
}
//外部中断回调函数。中断触发后,进入此函数,处理该函数中的代码之后,才会退出中断,此函数称为回调函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)// 回调函数
{
if(GPIO_Pin == KEY1_Pin) //确定是否是KEY1键按下
{
key_val = HAL_GPIO_ReadPin(GPIOA, KEY1_Pin);
}
}
//返回key1按键值
uint8_t Getkey1val(void)
{
return key_val;
}
3.21 中断处理/回调函数
在HAL库的底层驱动说明中,有这么两段说明
大概就是,每当产生外部中断时,程序首先会进入外部中断服务函数。在stm32f1xx_it.c中,可以找到函数EXTI0_IRQHandler,它通过调用函数HAL_GPIO_EXTI_IRQHandler()对中断类型进行判断,并对涉及中断的寄存器进行处理,在处理完成后,它将调用中断回调函数。HAL_GPIO_EXTI_Callback(),在中断回调函数中编写在此次中断中需要执行的功能。
4、主函数
/* USER CODE BEGIN Includes */
#include "Exti_driver.h"
#include "led_driver.h"
/* USER CODE END Includes */
//把两个头文件包含进去
/* USER CODE BEGIN 2 */
LED_Init();
EXTI_Init();
/* USER CODE END 2 */
//初始化
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
//如果按下了,就亮
if(Getkey1val() == 0)
{
LED1_ON
LED2_ON
LED3_ON
}
//如果松开了,就熄灭
else
{
LED1_OFF
LED2_OFF
LED3_OFF
}
}
/* USER CODE END 3 */
}
四、运行效果
保存、编译、下载、烧录、上电。
可以看到一开始LED为点亮状态,当按键按下时,灯亮;当按键松开时,灯灭。