硬件环境:STM32G431
软件环境:Keil5
实现方式:HAL库
1.文章背景:
目前很多文章都能实现按键的基本逻辑,但大多还是实现轮询消抖阻塞的方式,轮询消抖阻塞的方式会非常浪费软件资源。当然也有使用中断的方法,但大多都是一个按键对应一个中断的方式,而且也要进行消抖阻塞,但这样在中断阻塞是很危险的,等下进去临界区出不来就老实了,而且也浪费硬件资源,既然如此,有没有一些更好的方法呢?
有的兄弟,有的,接下来我就展示一个学习到的优秀的方法:定时器中断检测按键状态
2. 问题提出
2.1 为什么会有阻塞?
我们知道,因为按键的机械结构问题,当机械触点断开、闭合时由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,这种抖动可能引起我们的芯片进行错误的判断
因此我们需要一个10-50msCPU不会检测的时间,来避开这段抖动。
因此在按下按键的时候一般会加一个Delay函数让CPU避开抖动期。
但在Delay函数中是不会执行其他代码的,因此也浪费了CPU的性能。
本文章的目的是让按键抖动的时候也能让CPU释放出来,并且能实现长按键还是短按键的判断。
2.2 原理讲解
由于要求是按下之后,延时10-50ms后再次检测按键,容易想到用定时器中断,因为定时器中断就是每隔一段时间判断一次,正好可以抵消那段时间,而且在未进入中断的时候,还能执行主程序的代码,不浪费软件资源,多个按键只需要一个定时器中断,合理利用了硬件资源。
那我们既然要第一次进入和第二次进入执行的代码不同,那我们就需要一个switch语句帮助我们分辨我们是第一次进入中断还是第二次进入。
我们将定时器的各种状态用结构体封装起来
keys key[4]={0};
typedef struct{
char sta; //标志位
char ic_key; //捕获按键电平
char state; //0为按键断开,1为短按键,2为长按键
char key_time; //存放的是按键持续的时间,超过0.7s后为长按键
}keys;
3.定时器中断判断按键模式1(长按可不断响应)
3.1 方法讲解(建议先结合一下下面的代码示例分析我的方法)
长按不断响应的方法是再按键按下1超过一定时长的时候就将标志位置长按,由于我们按下的时候其实在定时器结束的时候也会执行按键判断函数,虽然我们按键的状态 state 会再判断完一次就再判断函数置0,但我们的标志位 sta 并没有清零,因此下次进去的还是switch语句的状态2(sta=2)继续置为长按键状态(state=2)。这样不断循环我们就能实现长按不断响应了。
基于上面的分析,我们可以得出我们按键有三种状态:
1.短按
2.一直长按
3.长按结束
3.2 CUBEMX配置
3.2.1 定时器参数初始化
3.2.2设置时钟源
根据上面的条件,我们可以算出我们的计数频率为10ms,刚好与消抖时间间隔相同
3.3 代码示例展示
keys key[4]={0};
typedef struct{
char sta; //标志位
char ic_key; //捕获按键电平
char state; //0为按键断开,1为短按键,2为长按键
char key_time; //存放的是按键持续的时间,超过0.7s后为长按键
}keys;
static void key_scan(void)
{
/*获取按键状态*/
char i;
key[0].ic_key=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
key[1].ic_key=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
key[2].ic_key=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
key[3].ic_key=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
//遍历按键状态
for(i=0;i<4;i++)
{
switch(key[i].sta)
{
case 0:
{
if(key[i].ic_key==GPIO_PIN_RESET)
{
key[i].sta=1;
key[i].key_time=0;
}
break;
}
/*第一次中断和第二次中断间隔10ms,正好可以消抖*/
case 1:
{
if(key[i].ic_key==GPIO_PIN_RESET)
{
key[i].sta=2;
}
else
{
key[i].sta=0;
}
break;
}
case 2:
{
if(key[i].ic_key==GPIO_PIN_SET&&key[i].key_time<70)//判断短按
{
key[i].sta=0;
key[i].state=1;
}
else if(key[i].ic_key==GPIO_PIN_SET&&key[i].key_time>=70)//在第三个if判断已经将长按键判断出来了,这个if判断实现的是将状态清零重新获取下一个按键状态
{
key[i].sta=0;
}
else if(key[i].key_time>70)//一直按着超过一定时间段直接判断为长按键
{
key[i].state=2;
}
key[i].key_time++;
}
break;
}
}
}
4.定时器中断判断按键模式2(长按松开才响应)
4.1 方法讲解
既然我们在长按未松开的时候不希望他实现功能,那我再长按未松开的时候就不要将他的标志位置长按位(state=2)嘛。
根据上面分析,我们得出按键有两种状态
1.松开的时候时间达不到长按的程度(短按)
2.松开的时候时间达到了长按的程度(长按)
4.2代码示例讲解
keys key[4]={0};
typedef struct{
char sta; //标志位
char ic_key; //捕获按键电平
char state; //0为按键断开,1为短按键,2为长按键
char key_time; //存放的是按键持续的时间,超过0.7s后为长按键
}keys;
static void key_scan(void)
{
/*获取按键状态*/
char i;
key[0].ic_key=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
key[1].ic_key=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
key[2].ic_key=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
key[3].ic_key=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
//遍历按键状态
for(i=0;i<4;i++)
{
switch(key[i].sta)
{
case 0:
{
if(key[i].ic_key==GPIO_PIN_RESET)
{
key[i].sta=1;
key[i].key_time=0;
}
break;
}
/*第一次中断和第二次中断间隔10ms,正好可以消抖*/
case 1:
{
if(key[i].ic_key==GPIO_PIN_RESET)
{
key[i].sta=2;
}
else
{
key[i].sta=0;
}
break;
}
case 2:
{
if(key[i].ic_key==GPIO_PIN_SET&&key[i].key_time<70)//判断短按
{
key[i].sta=0;
key[i].state=1;
}
else if(key[i].ic_key==GPIO_PIN_SET&&key[i].key_time>70)//判断长按
{
key[i].state=2;
key[i].sta=0;
}
key[i].key_time++;
}
break;
}
}
}
按键判断要记得将标志位置回0
void key_pd(void)
{
if(key[0].state==1)
{
//按键按下后功能实现
key[0].state=0;
}
else if(key[1].state==1)
{
//按键按下后功能实现
key[1].state=0;
}
else if(key[2].state==1)
{
//按键按下后功能实现
key[2].state=0;
}
else if(key[3].state==1)
{
//按键按下后功能实现
key[3].state=0;
}
if(key[0].state==1)
{
//按键按下后功能实现
key[0].state=0;
}
else if(key[1].state==2)
{
//按键按下后功能实现
key[1].state=0;
}
else if(key[2].state==2)
{
//按键按下后功能实现
key[2].state=0;
}
else if(key[3].state==2)
{
//按键按下后功能实现
key[3].state=0;
}
}