超屌的按键处理方式(类思想,状态机,高移植性)

怎么能把按键处理玩出花?按键处理作为一个基础入门实验,大部分人在刚接触单片机的时候都会自己写一份,开始我们利用延时消抖,后来发现在大的工程当中,延时消抖在没有加入操作系统来调度的情况下,无疑是一种很浪费资源的做法。再后来我们开了定时器去扫描,确实比较靠谱,但是一但设计到复杂的组合按键,长按短按双击等,就需要我们去费很大的功夫去进行逻辑判断。

在网上看到了很多很棒的方法,即把底层寄存器的配置抽离出来,采用状态机思想去进行逻辑判断,可以有效地实现各种复杂的按键处理。借鉴这种思想,完成了自己的按键处理函数。这里直接上代码,再讲解。

.h 头文件:

#ifndef __KEY_H
#define __KEY_H	 
#include "sys.h" 

/**********************************************************************/
#define KEY0_RCCclock 		RCC_AHB1Periph_GPIOE
#define KEY0_PinPort  		GPIOE
#define KEY0_WhichPin 		GPIO_Pin_2
#define KEY0_PinStatus 		GPIO_PuPd_UP      //上拉
#define KEY0_shortPress		Key0_ShortCallback
#define KEY0_longPress		Key0_LongCallback

#define KEY1_RCCclock 		RCC_AHB1Periph_GPIOE
#define KEY1_PinPort  		GPIOE
#define KEY1_WhichPin 		GPIO_Pin_3
#define KEY1_PinStatus 		GPIO_PuPd_UP       //上拉
#define KEY1_shortPress		Key1_ShortCallback
#define KEY1_longPress		Key1_LongCallback
/**********************************************************************/
#define KEY_MAXNUM    4    //最大按键数
#define KEY_TIMER_MS  2    //扫描时间间隔
#define KEY_DELAY_MS	10   //消抖完成标志位   
#define KEY_PRESS_STATUS 0 //即按下标志位
#define KEY_LONG_STATUS (1000/KEY_DELAY_MS*3)    //即按下3s及判定为长按
#define KEY_DOUBLE_HIT_MAX  (100/KEY_DELAY_MS*3) //连击判定时间最大值为300ms
//#define KEY_DOUBLE_HIT_MIN  (100/KEY_DELAY_MS*1) //连击判定事件最小值为100ms
/**********************************************************************/
#define KEY_NODOWN 0x0000 //无按键按下
#define KEY_DOWN   0x1000 //有按键按下
#define KEY_UP		 0x2000 //按键短按标志位 
#define KEY_LIAN   0x4000 //按键连按标志位 
#define KEY_LONG   0x8000 //按键长按标志位
/**********************************************************************/

/*
	这三个函数的作用分别是:
	1、设置a某一位的值 G_SET_BIT
	2、清楚a某一位的值 G_CLEAR_BIT
	3、获得a某一位的值 G_IS_BIT_SET
*/
#define G_SET_BIT(a,b)                	(a |= (1 << b))
#define G_CLEAR_BIT(a,b)              	(a &= ~(1 << b))
#define G_IS_BIT_SET(a,b)             	(a & (1 << b))

/**********************************************************************/
//定义了一个回调指针,即根据发生的事件,
typedef void (*KeyCallback_Pointer) (void);

/**********************************************************************/
//单个按键对象结构体
__packed typedef struct
{
	uint8_t  Key_Num;//共有多少个按键对象
	uint32_t Key_RccPeriphConfig;//按键对象时钟
	GPIO_TypeDef* KeyPort;//按键所在IO口组
	uint32_t Key_WhichPin;//第几个IO引脚
	GPIOPuPd_TypeDef Key_PinStatus;//IO引脚的状态
	KeyCallback_Pointer shortPress;//定义一个函数指针指向短按回调函数
	KeyCallback_Pointer longPress;//定义一个函数指针指向长按回调函数
	
}keyTypeDef_t;//单个按键对象结构体!

/**********************************************************************/
//多个按键对象结构体(总)
__packed typedef struct
{
	u8 KeyTotolNum; //按键总数累计
	keyTypeDef_t* singleKey;//按键对象的指针!
	
}keysTypeDef_t;//多个按键对象结构体!

/**********************************************************************/
//双击枚举!
typedef enum {Keyd_Wait_Flag = 0,Keyd_End_Flag = 1,Keyd_IDLE_Flag = 2}keyd_Status;

//双击结构体!
typedef struct
{
	keyd_Status Keyd_Flag;
	uint16_t First_KeyVal;
	uint16_t Key_Double_Hit_Count;
}Keyd_t;


/**********************************************************************/

extern u32 key_down;//声明外部变量,表示按键长短按及哪一个按键
extern keysTypeDef_t keys;
extern Keyd_t keyd; 

void Key_Init(void);
void Key0_ShortCallback(void);
void Key0_LongCallback(void);
void Key1_ShortCallback(void);
void Key1_LongCallback(void);
void Key_Scan_TimeConfig(void);
uint16_t keyGet(keysTypeDef_t* keys_t);
void Key_Handle_Task(void);

#endif

最上面的和KEY0/KEY1处理有关的宏定义即是把底层抽离的方式之一,在移植的过程中,只需要修改宏定义即可完成对不同的IO口的初始化。而有关按键处理的函数我们并不需要去做处理,初始化按键处理后,只需要去判断 u32 key_down的不同的位,即可获得当前按键的各种状态。这里说一下key_down这个变量,我假定最多按键处理为4个,那么32位便以4分区,可以分成8各区域,那么每个区域即可标识不同的状态位,这里可以根据项目需求去做更改这个变量的不同位。
/
key_down 共有32位,这里把它分割成不同的区域:
0-3 : 预留区域,这里最多定义4个按键,哪个为1表示状态“绑定”在哪个按键上面
4-7 : 短按判断区,这里最多判断4个,哪个按键在触发短按事件,哪个位置1
8-11 : 长按判断区,这里最多判断4个,哪个按键在触发长按事件,哪个位置1
12-15 : 连击判断区,这里最多判断4个,哪个按键在触发连击事件,哪个位置1
/

从宏定义可以看出,这里是把按键这个事件当成一个类,类对应到单片机上每个按键IO口时,即为实例化了一个按键对象。我们需要几个按键,就去实例化几个IO口即可。每个对象都有两个函数指针,当对应状态产生时,即可调用初始化过程中的函数指针指向的函数。这里编程思想比较类似于C++,只是没有区分共有私有之类的数据部分。函数指针即为公共接口,需要自己去编写。这里我虽然在初始化时规定了指向,但是并没有编写接口(回调)函数,而是直接判断key_down 的相应位去判断按键状态。

.C 工程文件:

#include "key.h"
#include "delay.h" 
#include "stdio.h"

/*
   key_down 共有32位,这里把它分割成不同的区域:
	 0-3   : 预留区域,这里最多定义4个按键,哪个为1表示状态“绑定”在哪个按键上面
	 4-7   : 短按判断区,这里最多判断4个,哪个按键在触发短按事件,哪个位置1
	 8-11  : 长按判断区,这里最多判断4个,哪个按键在触发长按事件,哪个位置1
	 12-15 : 连击判断区,这里最多判断4个,哪个按键在触发连击事件,哪个位置1
*/
u32 key_down = 0;//按键状态标志位,所以的操作都是为了改变这个全局变量

#define GPIO_KEY_NUM 2 							//定义按键成员个数
keyTypeDef_t singKey[GPIO_KEY_NUM];	//定义单个按键成员数组指针
keysTypeDef_t keys;									//定义总的按键模块结构	
Keyd_t keyd; //双击结构体

uint8_t keyCountTime = 0;

uint16_t keyGet(keysTypeDef_t* keys_t)
{
	uint8_t i = 0;
	uint16_t readKey = 0;
	
	//循环读取判断键值
	for(i = 0;i < keys_t->KeyTotolNum ; i++) //初始化了几个按键,则扫描几次
	{
		if(KEY_PRESS_STATUS == GPIO_ReadInputDataBit(keys_t->singleKey[i].KeyPort,keys_t->singleKey[i].Key_WhichPin))
		{
			G_SET_BIT(readKey,keys_t->singleKey[i].Key_Num);//Key_Num即为每个按键对象的“序号”
		}
	}
	return readKey;
}

//采用状态机思想,将按键形态分割成不同状态
uint16_t readKeyValue(keysTypeDef_t* keys_t)
{
	static uint8_t   keyCheck = 0;		 
	static uint8_t   keyState = 0;
	static uint8_t   keydCheck = 0;     //双击补偿量,每点击一次按键,则重置为0,这个值过大则表示
	static uint16_t  keyLongCheck = 0;  //长按事件检测标志位
	static uint16_t  keyPrev = 0;       //上一次的键值
	
	uint16_t keyPress = 0;
	uint16_t keyReturn = 0;
	
	
	keyCountTime += KEY_TIMER_MS; //每当进入一次中断,便自增2ms
	if(keyCountTime >= KEY_DELAY_MS) //消抖完成
	{
		keyCountTime = 0;
		keyCheck = 1;
	}
	if(1 == keyCheck) //即每10ms 进行一次按键读取,如果这一次判断按键按下,下一次按键同样为按下状态,则表示确实按下!
	{
		keyCheck = 0;
		keyPress = keyGet(keys_t);//当对应按键按下时,16位对应位置,这里只用了两个,即仅判断第0位和第1位
		switch(keyState)
		{
			case 0://按键未按下态
				if(keyPress != 0)//表示有按键按下
				{
					keyPrev = keyPress; //记录当前按键状态
					keyState = 1;
				}
			break;
			
			case 1://表示有按键按下,判断当前值和上一次的值是否一样,若不一样则为抖动!
				if(keyPress  == keyPrev)//不是抖动
				{
					keydCheck = 0;
					keyState = 2;
					keyReturn = keyPrev | KEY_DOWN;

				}else{
					keyState = 0; //是抖动!返回上一层
				}
				
			case 2:
				if(keyPress != keyPrev)//表示按键已松开,触发一次短按操作!
				{		
					keyd.Keyd_Flag = Keyd_Wait_Flag;//开启双击检测标志位
				}else{
					keyLongCheck++;
					if(keyLongCheck >= KEY_LONG_STATUS)//按下时间超过3s
					{
						keyLongCheck = 0;
						keyState = 3;
						keyReturn = keyPress | KEY_LONG;//返回值标记哪个按键
						return keyReturn;
					}
				}
				keydCheck += 1;
				keyState = 1;//加入这个是为了当有按键按下时清零

				break;
				
			case 3:
				if(keyPress != keyPrev)//一次按键扫描已经完成,等待按键松开
				{
					keyState = 0;
					keydCheck = 0;
					keyd.Keyd_Flag = Keyd_End_Flag;
					keyd.Key_Double_Hit_Count = 0;

				}
				break;
		}
		
		if(keyd.Keyd_Flag == Keyd_Wait_Flag)
		{
			keyd.Key_Double_Hit_Count++;
			if(keyd.Key_Double_Hit_Count >= KEY_DOUBLE_HIT_MAX) //超过300ms 出发了双击事件
			{
				keydCheck = 0;//重新清零双击计数
				keyd.Key_Double_Hit_Count = 0;
				keyd.Keyd_Flag = Keyd_End_Flag;
								
				//超时则返回短按
				keyState = 0; 
				keyLongCheck = 0;
				keyReturn = keyPrev | KEY_LIAN; //标记一次短按成功
				return keyReturn;
				
			}
			//这个判断即小于300ms区间内没有发生双击事件,keydCheck的值是一直在增加的而没有被清零
			else if((keydCheck > 20) && (keyd.Key_Double_Hit_Count < KEY_DOUBLE_HIT_MAX))//表示触发了短按事件 
			{
				keydCheck = 0;//重新清零双击计数
				keyd.Keyd_Flag = Keyd_End_Flag;
				keyd.Key_Double_Hit_Count = 0;
				
				//超时则返回短按
				keyState = 0; 
				keyLongCheck = 0;
				keyReturn = keyPrev | KEY_UP; //标记一次短按成功
				return keyReturn;
			}
			keyd.First_KeyVal = keyPrev;	
		}
	}
	return KEY_NODOWN;
}	

//读取按键返回值,并作出相应的判断
void Key_Scan_2ms(keysTypeDef_t* keys_t)
{
	uint8_t  i = 0;
	uint16_t key_value = 0;
	
	key_value = readKeyValue(keys_t);
	
	if(!key_value) return;
	
	//短按事件触发
	if(key_value & KEY_UP)
	{
		for(i = 0;i < keys_t->KeyTotolNum;i++)//循环扫描看是哪个按键按下
		{
			if(G_IS_BIT_SET(key_value,keys_t->singleKey[i].Key_Num))
			{
				G_SET_BIT(key_down,(keys_t->singleKey[i].Key_Num + 4)); // 4 5 位
				//如果初始化指向了回调函数,
//				if(keys_t->singleKey[i].shortPress)
//				{
//					keys_t->singleKey[i].shortPress();//执行相应的回调函数
//				}
			}
		}
	}
	
	//长按事件触发
	if(key_value & KEY_LONG)
	{
		for(i = 0;i < keys_t->KeyTotolNum;i++)
		{ //判断是否产生长按事件
			if(G_IS_BIT_SET(key_value,keys_t->singleKey[i].Key_Num))
			{
				G_SET_BIT(key_down,(keys_t->singleKey[i].Key_Num + 8)); // 8 9 位
//				if(keys_t->singleKey[i].longPress)
//				{
//					keys_t->singleKey[i].longPress();
//				}
			}
		}
	}
	
	//双击事件触发
	if(key_value & KEY_LIAN)
	{
		for(i = 0;i < keys_t->KeyTotolNum;i++)
		{//判断是否是双击事件
			if(G_IS_BIT_SET(key_value,keys_t->singleKey[i].Key_Num))//判断第0位还是第1位是被置1的
			{
				G_SET_BIT(key_down,(keys_t->singleKey[i].Key_Num + 12)); // 12 13 位
			}
		}
	}
}

/*
	该函数为填充单个按键IO口状态函数,入口参数为:
	1、按键IO时钟配置参数 Key_RccPeriphConfig
	2、选择按键IO引脚组 	KeyPort
	3、选择第几个IO口     Key_WhichPin
	4、选择IO口上下拉状态 Key_PinStatus
	5、指向短回调函数     shortPress
	6、指向长回调函数     longPress
*/
keyTypeDef_t KeyInit_One(uint32_t Key_RccPeriphConfig,GPIO_TypeDef* KeyPort,uint32_t Key_WhichPin,\
												 GPIOPuPd_TypeDef Key_PinStatus,KeyCallback_Pointer shortPress,KeyCallback_Pointer longPress)
{
	static int8_t key_total = -1;
	
	keyTypeDef_t Key_TemporaryVar;
	
	Key_TemporaryVar.KeyPort = KeyPort;
	Key_TemporaryVar.Key_Num = ++key_total;//标记了该组按键IO是第几个!
	Key_TemporaryVar.Key_WhichPin = Key_WhichPin;
	Key_TemporaryVar.Key_PinStatus = Key_PinStatus;
	Key_TemporaryVar.Key_RccPeriphConfig = Key_RccPeriphConfig;
	/*指向定义了的长短按回调函数!*/
	Key_TemporaryVar.longPress = longPress;
	Key_TemporaryVar.shortPress = shortPress;
	
	keys.KeyTotolNum++;//在总按键函数中,记录当前装载按键IO个数!
	
	return Key_TemporaryVar;
}

/*
		按键初始化函数
		KEY0   <--> PE2 上拉
		KEY1   <--> PE3 上拉
		KEY2 	 <--> PE4 上拉 
		KEY_UP <--> PA0 下拉
*/
void Key_PartInit(keysTypeDef_t *Keys)
{
	uint8_t temp;
	
	if(NULL == Keys)
	{
		return;
	}
	
	Keys->KeyTotolNum = (Keys->KeyTotolNum > KEY_MAXNUM) ? KEY_MAXNUM : Keys->KeyTotolNum;//限定个数!
	
	for(temp = 0; temp < Keys->KeyTotolNum ; temp++)
	{
		GPIO_InitTypeDef GPIO_InitStructure;
		RCC_AHB1PeriphClockCmd(Keys->singleKey[temp].Key_RccPeriphConfig,ENABLE);
		
		GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;//普通输入模式
		GPIO_InitStructure.GPIO_Pin = Keys->singleKey[temp].Key_WhichPin;
		GPIO_InitStructure.GPIO_PuPd = Keys->singleKey[temp].Key_PinStatus;
		GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100M
		GPIO_Init(Keys->singleKey[temp].KeyPort,&GPIO_InitStructure);
	}
	
	Key_Scan_TimeConfig();
}

//2ms进入一次中断
void Key_Scan_TimeConfig(void)
{
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);  ///使能TIM3时钟
	
  	TIM_TimeBaseInitStructure.TIM_Period = 2000 - 1; 	//自动重装载值
	TIM_TimeBaseInitStructure.TIM_Prescaler=84 - 1;  //定时器分频
	TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
	TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; 
	
	TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);//初始化TIM3
	
	TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); //允许定时器3更新中断
	TIM_Cmd(TIM3,ENABLE); //使能定时器3
	
	NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn; //定时器3中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x01; //抢占优先级1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x03; //子优先级3
	NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
	NVIC_Init(&NVIC_InitStructure);
}

//每2ms进入一次中断
//定时器3中断服务函数
void TIM3_IRQHandler(void)
{
	if(RESET != TIM_GetITStatus(TIM3,TIM_IT_Update)) //溢出中断
	{
		
		Key_Scan_2ms((keysTypeDef_t*)&keys);
		TIM_ClearITPendingBit(TIM3,TIM_IT_Update);  //清除中断标志位
	}
}


/**********************************************************************/
//回调函数聚集区!!!~!~!
void Key0_ShortCallback(void)
{
	//printf("KEY2短按触发\r\n");
}

void Key0_LongCallback(void)
{
	//printf("KEY2长按触发\r\n");
}

void Key1_ShortCallback(void)
{
	//printf("KEY1短按触发\r\n");
}

void Key1_LongCallback(void)
{
	//printf("KEY1长按触发\r\n");
}
/**********************************************************************/
//就是先填充结构体,然后根据每个数组成员结构体的值去进行相应的初始化函数
void Key_Init(void)
{
	singKey[0] = KeyInit_One(KEY0_RCCclock,KEY0_PinPort,KEY0_WhichPin,KEY0_PinStatus,Key0_ShortCallback,Key0_LongCallback);
	singKey[1] = KeyInit_One(KEY1_RCCclock,KEY1_PinPort,KEY1_WhichPin,KEY1_PinStatus,Key1_ShortCallback,Key1_LongCallback);
	keys.singleKey = (keyTypeDef_t*)&singKey;//指向第一个按键对象
	Key_PartInit(&keys);
	
}

大体的思路是,首先定义了一个结构体数组,keyTypeDef_t singKey[GPIO_KEY_NUM];(GPIO_KEY_NUM的值即为需要初始化的按键个数),然后分别填充这个数组(这里其实直接去改宏定义,函数整体不用动)。最后把总按键处理结构体里的按键对象指针指向这个数组的首地址,即可通过一个结构体,去控制所有的按键对象。然后根据填充的数据对IO口进行初始化,开一个更新中断为2ms的定时器TIM3,以这个时间进行状态机的行为切换(其实是20ms,足够用了)。

有限状态机在逻辑性比较强的程序中非常有用,即通过对按键状态的判断,去切换不同的状态,最后返回相应的按键状态。

怎么确定当前状态是哪个按键呢?即通过调用keyGet()函数,返回一个u16类型的数据,每个按键对应自己的标号位(即KEY0对应第0位,KEY1对应第1位…)。readKeyValue()即根据u16类型的不同位,去进行状态机检测判断,直到返回一个确定的状态。再把一个u16类型的数据(标识了哪个按键和哪个状态)返回给Key_Scan_2ms()这个函数,在这个函数里进行对u16数据的解析,再把上面说的key_down置相应位。至此一个完整的按键处理已经完成,具体的逻辑判断见readKeyValue()这个函数(即状态机处理函数)。我们在外面只需要判断key_down的相应位即可判断是哪个按键发生了哪个事件。

程序其实有点绕,但是到真正移植的时候会发现,只需要改改宏,就能在不同的芯片,不同的设备上,轻松的完成复杂的按键处理。

下面上主程序:

int main(void)
{ 
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
	delay_init(168);		//延时初始化 
	uart_init(115200);	//串口初始化波特率为115200
	LED_Init();		  		//初始化与LED连接的硬件接口 
	
	//初始化按键底层
	Key_Init();
	
	while(1)
	{
		
		if(G_IS_BIT_SET(key_down,4))
		{
			G_CLEAR_BIT(key_down,4);
			printf("KEY2触发短按事件\r\n");
		}
		if(G_IS_BIT_SET(key_down,5))
		{
			G_CLEAR_BIT(key_down,5);
			printf("KEY1触发短按事件\r\n");
		}
		if(G_IS_BIT_SET(key_down,8))
		{
			G_CLEAR_BIT(key_down,8);
			printf("KEY2触发长按事件\r\n");
		}
		if(G_IS_BIT_SET(key_down,9))
		{
			G_CLEAR_BIT(key_down,9);
			printf("KEY1触发长按事件\r\n");
		}
		if(G_IS_BIT_SET(key_down,12))
		{
			G_CLEAR_BIT(key_down,12);
			printf("KEY2触发双击事件\r\n");
		}
		if(G_IS_BIT_SET(key_down,13))
		{
			G_CLEAR_BIT(key_down,13);
			printf("KEY1触发双击事件\r\n");
		}
		
	}
}

在这里插入图片描述
当然组合按键也没有问题,只需要判断标志位是否同时存在即可,下面上个截图:
在这里插入图片描述
至此,一个按键模块就被做了出来,这个模块可以套用在任何stm32的芯片上,具备了很高的移植性,费点儿时间看明白,以后一劳永逸~

发布了12 篇原创文章 · 获赞 11 · 访问量 3803

猜你喜欢

转载自blog.csdn.net/su_fei_ma_su/article/details/105091193