超屌的按鍵處理方式(類思想,狀態機,高移植性)

怎麼能把按鍵處理玩出花?按鍵處理作爲一個基礎入門實驗,大部分人在剛接觸單片機的時候都會自己寫一份,開始我們利用延時消抖,後來發現在大的工程當中,延時消抖在沒有加入操作系統來調度的情況下,無疑是一種很浪費資源的做法。再後來我們開了定時器去掃描,確實比較靠譜,但是一但設計到複雜的組合按鍵,長按短按雙擊等,就需要我們去費很大的功夫去進行邏輯判斷。

在網上看到了很多很棒的方法,即把底層寄存器的配置抽離出來,採用狀態機思想去進行邏輯判斷,可以有效地實現各種複雜的按鍵處理。借鑑這種思想,完成了自己的按鍵處理函數。這裏直接上代碼,再講解。

.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的芯片上,具備了很高的移植性,費點兒時間看明白,以後一勞永逸~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章