怎麼能把按鍵處理玩出花?按鍵處理作爲一個基礎入門實驗,大部分人在剛接觸單片機的時候都會自己寫一份,開始我們利用延時消抖,後來發現在大的工程當中,延時消抖在沒有加入操作系統來調度的情況下,無疑是一種很浪費資源的做法。再後來我們開了定時器去掃描,確實比較靠譜,但是一但設計到複雜的組合按鍵,長按短按雙擊等,就需要我們去費很大的功夫去進行邏輯判斷。
在網上看到了很多很棒的方法,即把底層寄存器的配置抽離出來,採用狀態機思想去進行邏輯判斷,可以有效地實現各種複雜的按鍵處理。借鑑這種思想,完成了自己的按鍵處理函數。這裏直接上代碼,再講解。
.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的芯片上,具備了很高的移植性,費點兒時間看明白,以後一勞永逸~