前言
按键作为十分常见的硬件外设,从一开始就会接触到,但是之前使用的都是硬延时来判断单双击和长按的,不仅会阻塞程序运行还会浪费性能,但是如果使用硬件定时器的话就有点浪费资源了。不久以前我在ESP32的一个项目上写过一个利用软件定时器的案件事件判断方法,但是需要建构在Freertos上,所以不是十分方便。现在给出我最近采用的方法。
需要的组件
任意按键 + 软件定时器组件
软件定时器组件我使用的是我之前一篇博客中提到的方法,再次基础上加了一点点改动。原文连接
源代码
定时器组件.h
#ifndef _TIMER_H
#define _TIMER_H
typedef struct{
unsigned int soft_tim;//用于软件定时器计时
unsigned char tim_init; //打开和关闭定时器
unsigned int soft_tim_threshold; //定时器触发阈值
void (*callback)(void * data); //触发的回调函数
void *callbackData;
}Tim;
void soft_Tim_open(Tim * tim);
void soft_Tim_close(Tim * tim);
void soft_Tim_clean(Tim * tim);
void soft_Tim_heartbeat(Tim *tim);
void soft_Tim_init(int num,Tim *tim,void (*callback)(void * data));
#endif
定时器组件.c
#include "timer.h"
void soft_Tim_open(Tim * tim){//打开定时器
tim->tim_init=1;
}
void soft_Tim_close(Tim * tim){//关闭定时器
tim->tim_init=0;
}
void soft_Tim_clean(Tim * tim){//清除定时器计数
tim->soft_tim=0;
}
//简易软件定时器回调 num为多少秒回调一次,时基为1ms tim为为实例化的定时器对象
//num触发阈值
void soft_Tim_heartbeat(Tim *tim){
if(tim->tim_init){
tim->soft_tim++;
if(tim->soft_tim>=tim->soft_tim_threshold){
tim->soft_tim=0;
if((tim->callback)!=0)//防止未初始化
tim->callback(tim->callbackData);
}
}
}
//初始化定时器
void soft_Tim_init(int num,Tim *tim,void (*callback)(void * data)){
tim->callback=callback;
tim->soft_tim=0;
tim->soft_tim_threshold=num;
tim->tim_init=1;
}
改动说明:在原来的基础上现在定时器的回调函数会带入一个指针,用来传递一些参数进去。这个指针需要在使用的时候指向需要传递的参数的地址。
接下来是正式key.h
#ifndef _KEY_H
#define _KEY_H
#include "timer.h"
typedef enum{
Free_state=0, //空闲状态
Dithering_state,//消抖状态
Chack_state, //检测状态
Wait_state, //多击等待状态 此状态关闭检测定时器,最终定时器状态不变
Key_null, //用于向Key_envent抛出的事件类型 无事件
Key_click, //单击
Key_Dlick, //双击
Key_Llick //长按
}KEY_State;
typedef struct KEY{
Tim keychack,keyfinal; //用于定时触发按键状态检查,用于最后抛出事件,两个定时器分别为检测定时器和最终定时器
unsigned int Dithering_time; //消抖时间
unsigned int Double_click_time; //双击判定时间
unsigned int Incessant_click_time; //连续检测间隔时间
void (*Falling_edge_trigger)(struct KEY* keynum);//下降沿触发
unsigned char (*KEY_EventGet)(struct KEY* keynum);//用户最终调用 获取按键事件
void (*KEY_heartbeat)(struct KEY* keynum);//按键的心跳,放在1ms中断
KEY_State Thiskey_state; //按键状态
int (*Key_StateGet)(void);//获取按键当前状态 和物理层对接
unsigned int KeyUp_count;//按键在双击判定时间内的抬起次数
unsigned char Time_out; //时间耗尽标志位 0 未耗尽 1 耗尽
unsigned char Key_envent; //这个属于私有变量 按键状态更新后会向此值抛出事件
}M_KEY;
void key_Init(M_KEY* keynum,int (*Key_StateGet)(void));
#endif
key.c
#include "key.h"
//此函数放在按键状态由1转变为0的中断中
void Falling_edge_trigger_callbrack(struct KEY* keynum){
if(keynum->Thiskey_state==Free_state){
keynum->keychack.soft_tim_threshold=keynum->Dithering_time;//将检测定时器的触发时间设置为设定好的消抖时间
keynum->keychack.tim_init=1;//打开检查定时器
keynum->keychack.soft_tim=0;//初始化检查定时器默认计数器值
keynum->keyfinal.soft_tim_threshold=keynum->Double_click_time;//将抛出事件定时器的触发时间设置为双击最终判定时间
keynum->keyfinal.tim_init=1;//打开最终定时器
keynum->keyfinal.soft_tim=0;//初始化最终定时器的默认计数器值
keynum->Thiskey_state=Dithering_state;
keynum->Time_out=0;
}
else if(keynum->Thiskey_state==Wait_state){
keynum->keychack.soft_tim_threshold=keynum->Dithering_time;//重新将检测定时器设置为消抖时间
keynum->keychack.tim_init=1;//打开检测定时器
keynum->Thiskey_state=Dithering_state;
}
}
//检查定时器的回调函数
void KEY_chacktimerCallBrack(void *data){
M_KEY* key=(M_KEY*)data;
int key_state=0;
key_state=key->Key_StateGet();
if(key->Thiskey_state==Dithering_state){//如果按键此时的状态在消抖状态
if(key_state==0){//此时按键状态为按下
key->Thiskey_state=Chack_state;//进入检测状态
key->keychack.soft_tim_threshold=key->Incessant_click_time; //将检测时间间隔改为连续检测时间间隔
}else{//判定为抖动干扰,非人为操作
key->Thiskey_state=Free_state;//按键状态转换为空闲状态->此操作会关闭俩个按键定时器
key->keychack.soft_tim=0;
key->keyfinal.soft_tim=0;//两个定时器清零
}
}
else if(key->Thiskey_state==Chack_state){//如果是检测状态
if(key_state==0){//如果是按下
return;
}
else{//如果是抬起
if(key->Time_out==0){//时间没有耗尽
key->KeyUp_count++;
key->Thiskey_state=Wait_state; //状态变成等待状态在时间耗尽前检测下一次按键事件
}
}
}
else if(key->Thiskey_state==Wait_state){//如果进入了等待状态
key->keychack.tim_init=0;//关闭检测定时器
key->keychack.soft_tim=0;//将检测定时器清零
}
}
//最终定时器的回调函数
void KEY_finaltimerCallBrack(void *data){
M_KEY* key=(M_KEY*)data;
key->Time_out=1;
if(key->KeyUp_count==0)
key->Key_envent=Key_Llick;
else if(key->KeyUp_count==1)
key->Key_envent=Key_click;
else if(key->KeyUp_count==2)
key->Key_envent=Key_Dlick;
else
key->Key_envent=Key_Dlick;
key->KeyUp_count=0;
key->Thiskey_state=Free_state;
key->keyfinal.tim_init=0;//关闭最终定时器
key->keychack.soft_tim=0;
key->keyfinal.soft_tim=0;//两个定时器清零
}
//此函数放入1ms定时中断中
void KEY_heartbeat(M_KEY* keynum){
if(keynum->Thiskey_state!=Free_state){//如果按键的状态不处于空闲状态 就开启俩定时器
soft_Tim_heartbeat(&(keynum->keychack));
soft_Tim_heartbeat(&(keynum->keyfinal));
}
}
/*此函数为最终用户调用的获取按键事件函数
返回值如下
0 无按键事件
1 单击事件
2 双击事件
9 长按事件
*/
unsigned char KEY_EventGet(struct KEY* keynum){
if(keynum->Key_envent==Key_null){
keynum->Key_envent=Key_null;
return 0;
}
else if(keynum->Key_envent==Key_click){
keynum->Key_envent=Key_null;
return 1;
}
else if(keynum->Key_envent==Key_Dlick){
keynum->Key_envent=Key_null;
return 2;
}
else if(keynum->Key_envent==Key_Llick){
keynum->Key_envent=Key_null;
return 9;
}
else {
keynum->Key_envent=Key_null;
return 0;
}
}
/*
参数说明:
keynum 你所定义的按键对象,别忘了传入的是一个指针
Key_StateGet 传入一个函数指针,该函数需要实现调用它就可以返回这个按键的状态 0 为按下 1为抬起
注意;这个函数不关心按键的实际情况到底是按下是高电平还是按下是低电平,只要按下时这个函数返回0 抬起时返回1就行
*/
void key_Init(M_KEY* keynum,int (*Key_StateGet)(void)){
keynum->Dithering_time=50; //设置消抖时间
keynum->Double_click_time=500; //设置双击检测时间
keynum->Incessant_click_time=5; //设置连续检测间隔时间
keynum->Thiskey_state=Free_state; //设置按键初始为空闲状态
keynum->keychack.callbackData=keynum; //设置按键检查定时器的数据域的数据为按键对象本身,该数据会被带入按键检查定时器的回调函数中
keynum->keyfinal.callbackData=keynum;
keynum->Key_StateGet=Key_StateGet;
keynum->KEY_heartbeat=KEY_heartbeat;
keynum->Key_envent=Key_null; //设定初始按键事件
keynum->KEY_EventGet=KEY_EventGet;
keynum->Falling_edge_trigger=Falling_edge_trigger_callbrack;
keynum->keychack.callback=KEY_chacktimerCallBrack;
keynum->keyfinal.callback=KEY_finaltimerCallBrack;
keynum->Time_out=0; //时间未耗尽
}
使用方法
第一步
定义一个按键
M_KEY key_PB1;
第二步
对接按键读取函数
int Read_Key(void){
return HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
}
//注意此函数需要在所定义的心跳开始之前被初始化,负责会由于按键默认参数没有初始化导致错误
key_Init(&key_PB1,Read_Key);
第三步
将下降沿触发函数放在相应的中断中或者其它地方,只要是按键由抬起转变为按下那一瞬间能运行这个函数就行
key_PB1.Falling_edge_trigger(&key_PB1);
第四步
将按键心跳放在1ms中断就行,或者其它能1ms运行一次的地方。
key_PB1.KEY_heartbeat(&key_PB1);
第五步
开始使用,直接使用以下函数就行,如果触发了相应的按键事件则会返回对应的状态码
返回值如下
0 无按键事件
1 单击事件
2 双击事件
9 长按事件
按键状态由这个函数读取之后会自动清零,如果不读取就会保留最新的一次状态。
key_PB1.KEY_EventGet(&key_PB1);
测试记录
2023-9-9 新增
考虑到经过多次测试贴片按键几乎不发生抖动现象,所以以上处理方式可以简化