1. 簡單按鍵檢測
記得開始學習單片機的時候,寫的按鍵掃描是這樣的:
if(KEY1 == 0)
{
delay_ms(20);
if(KEY1 == 0)
{
while(KEY1 == 0);
// 按鍵按下處理代碼
}
}
一看,有個20ms消除抖動時間,就是說我要在這里死等20ms,還有等待按鍵釋放,我就是不放,你能怎么樣?沒辦法只能做超時。那我想做長按1s呢?細思極恐,對於實際項目上的應用來說是很糟糕的事情,這不僅會拖慢你整個系統,還會出現,多個按鍵有時檢測不到的問題。有沒有更好的辦法來實現呢?答案是肯定的,想想,如果這個20ms的延時用定時器來做,不就可以了嗎!!!
2. 狀態機
首先我們得了解什么是狀態機?這個當然是問度娘了!!!
狀態機可歸納為4個要素,即現態、條件、動作、次態。這樣的歸納,主要是出於對狀態機的內在因果關系的考慮。"現態"和"條件"是因,"動作"和"次態"是果。詳解如下:
現態:是指當前所處的狀態。
條件:當一個條件被滿足,將會觸發一個動作,或者執行一次狀態的遷移。
動作:條件滿足后執行的動作。動作執行完畢后,可以遷移到新的狀態,也可以仍舊保持原狀態。動作不是必需的,當條件滿足后,也可以不執行任何動作,直接遷移到新狀態。
次態:條件滿足后要遷往的新狀態。"次態"是相對於"現態"而言的,"次態"一旦被激活,就轉變成新的"現態"了。
有了理論的支撐,有沒有發現,狀態機這種機制,其實可以運行到很多場景的啦,不僅僅局限於按鍵。
3. 按鍵枚舉和結構體
按鍵的狀態可以分為:未按、按下、長按、抬起四種。當然你也可以按自己需求去分。廢話不多說,直接上代碼分析。
typedef enum _KEY_STATUS_LIST
{
KEY_NULL = 0x00,
KEY_SURE = 0x01,
KEY_UP = 0x02,
KEY_DOWN = 0x04,
KEY_LONG = 0x08,
}KEY_STATUS_LIST;
我這里定義了一個按鍵狀態的枚舉,包含5個元素,KEY_NULL
表示無動作,KEY_SURE
表示確認狀態,KEY_UP
表示按鍵抬起,
KEY_DOWN
表示按鍵按下,KEY_LONG
表示長按。
typedef enum _KEY_LIST
{
KEY0,
KEY1,
KEY2,
KEY_NUM,
}KEY_LIST;
再來一個枚舉,這里列出你的按鍵,KEY_NUM
可以自動統計你按鍵個數,這里KEY_NUM的值為3,至於為什么我就不多說了,自己問度娘。為什么這里要用枚舉?這里拋個磚,繼續往下看。
typedef struct _KEY_COMPONENTS
{
uint8_t KEY_SHIELD; //按鍵屏蔽0:屏蔽,1:不屏蔽
uint8_t KEY_COUNT; //按鍵長按計數
uint8_t KEY_LEVEL; //虛擬當前IO電平,按下1,抬起0
uint8_t KEY_DOWN_LEVEL; //按下時IO實際的電平
uint8_t KEY_STATUS; //按鍵狀態
uint8_t KEY_EVENT; //按鍵事件
uint8_t (*READ_PIN)(void);//讀IO電平函數
}KEY_COMPONENTS;
extern KEY_COMPONENTS Key_Buf[KEY_NUM];
KEY_SHIELD
按鍵屏蔽用的,0表示按鍵不使用,1表示使用;KEY_COUNT
長按計數器,好比秒表,按開始然后開始計數了;KEY_LEVEL
虛擬按鍵按下的電平,KEY_DOWN_LEVEL
,實際按鍵按下的IO電平,這兩個變量,主要是為了函數封裝進行統一,比如你一個按鍵按下高電平,一個按下低電平,我不管這么多,反正我就和你KEY_DOWN_LEVEL
值進行比較,相等我就認為你按下,然后把KEY_LEVEL
置位,相反就清零;KEY_STATUS
就是我們說的按鍵狀態了,它負責記錄某一時刻按鍵狀態;KEY_EVENT
表示按鍵事件,我這里分了3個事件,有按下、抬起和長按。(*READ_PIN)
是一個函數指針變量,需要把你讀IO的函數接口給它。
最后別忘了,用這個結構體定義變量(這里只是聲明哦),有幾個按鍵就定義幾個結構類型變量。發現沒有我們這里用到了KEY_NUM
,好處一,按鍵增加也不需要改動。
4. 按鍵IO函數
首先得讀IO口的電平吧。
static uint8_t KEY0_ReadPin(void)
{
return _KEY0;
}
static uint8_t KEY1_ReadPin(void)
{
return _KEY1;
}
static uint8_t KEY2_ReadPin(void)
{
return _KEY2;
}
這個很簡單,就是把你的IO口電平返回來給我就可以了。可以根據自己單片機去實現。有了這幾個函數不就可以定義的結構體類型變量了嗎。
KEY_COMPONENTS Key_Buf[KEY_NUM] = {
{1,0,0,0,KEY_NULL,KEY_NULL,KEY0_ReadPin},
{1,0,0,0,KEY_NULL,KEY_NULL,KEY1_ReadPin},
{1,0,0,0,KEY_NULL,KEY_NULL,KEY2_ReadPin},
};
這個就不多說了,對着上面結構體說明看就知道了,我這里按鍵按下的都是低電平。
真正的按鍵IO電平獲取函數在這里
static void Get_Key_Level(void)
{
uint8_t i;
for(i = 0;i < KEY_NUM;i++)
{
if(Key_Buf[i].KEY_SHIELD == 0)
continue;
if(Key_Buf[i].READ_PIN() == Key_Buf[i].KEY_DOWN_LEVEL)
Key_Buf[i].KEY_LEVEL = 1;
else
Key_Buf[i].KEY_LEVEL = 0;
}
}
這個函數主要是實現封裝,兩步走,先判斷按鍵是否使能,每一個按鍵IO電平。如果我添加按鍵這里要改動嗎?完全不需要動。
5. 按鍵狀態機
重點來了,准備了那么多,終於可以上按鍵狀態機代碼實現了。
void ReadKeyStatus(void)
{
uint8_t i;
Get_Key_Level();
for(i = 0;i < KEY_NUM;i++)
{
switch(Key_Buf[i].KEY_STATUS)
{
//狀態0:沒有按鍵按下
case KEY_NULL:
if(Key_Buf[i].KEY_LEVEL == 1)//有按鍵按下
{
Key_Buf[i].KEY_STATUS = KEY_SURE;//轉入狀態1
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
}
else
{
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
}
break;
//狀態1:按鍵按下確認
case KEY_SURE:
if(Key_Buf[i].KEY_LEVEL == 1)//確認和上次相同
{
Key_Buf[i].KEY_STATUS = KEY_DOWN;//轉入狀態2
Key_Buf[i].KEY_EVENT = KEY_DOWN;//按下事件
Key_Buf[i].KEY_COUNT = 0;//計數器清零
}
else
{
Key_Buf[i].KEY_STATUS = KEY_NULL;//轉入狀態0
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
}
break;
//狀態2:按鍵按下
case KEY_DOWN:
if(Key_Buf[i].KEY_LEVEL != 1)//按鍵釋放,端口高電平
{
Key_Buf[i].KEY_STATUS = KEY_NULL;//轉入狀態0
Key_Buf[i].KEY_EVENT = KEY_UP;//松開事件
}
else if((Key_Buf[i].KEY_LEVEL == 1) && (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超過KEY_LONG_DOWN_DELAY沒有釋放
{
Key_Buf[i].KEY_STATUS = KEY_LONG;//轉入狀態3
Key_Buf[i].KEY_EVENT = KEY_LONG;//長按事件
Key_Buf[i].KEY_COUNT = 0;//計數器清零
}
else
{
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
}
break;
//狀態3:按鍵連續按下
case KEY_LONG:
if(Key_Buf[i].KEY_LEVEL != 1)//按鍵釋放,端口高電平
{
Key_Buf[i].KEY_STATUS = KEY_NULL;//轉入狀態0
Key_Buf[i].KEY_EVENT = KEY_UP;//松開事件
Key_Buf[i].KEY_EVENT = KEY_NULL;
}
else if((Key_Buf[i].KEY_LEVEL == 1)
&& (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超過KEY_LONG_DOWN_DELAY沒有釋放
{
Key_Buf[i].KEY_EVENT = KEY_LONG;//長按事件
Key_Buf[i].KEY_COUNT = 0;//計數器清零
}
else
{
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
}
break;
}
}
}
這個函數就是獲取當前所有按鍵狀態,每20ms(用定時器定時)調用一次,就可以了。
//狀態0:沒有按鍵按下
case KEY_NULL:
if(Key_Buf[i].KEY_LEVEL == 1)//有按鍵按下
{
Key_Buf[i].KEY_STATUS = KEY_SURE;//轉入狀態1
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
}
else
{
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
}
break;
這里拿一個按鍵做說明,首先進來先獲取按鍵IO電平,按鍵開始狀態從KEY_NULL
開始(現態),此時按鍵按下(條件),轉入KEY_SURE
(次態),第一步完成退出;
//狀態1:按鍵按下確認
case KEY_SURE:
if(Key_Buf[i].KEY_LEVEL == 1)//確認和上次相同
{
Key_Buf[i].KEY_STATUS = KEY_DOWN;//轉入狀態2
Key_Buf[i].KEY_EVENT = KEY_DOWN;//按下事件
Key_Buf[i].KEY_COUNT = 0;//計數器清零
}
else
{
Key_Buf[i].KEY_STATUS = KEY_NULL;//轉入狀態0
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
}
break;
定時器到20ms,剛好去抖動,進來就從新的狀態KEY_SURE
開始了,再判斷當前按鍵是否還是按下的,如果沒有按下,那就返回KEY_NULL
,說明上次是干擾,如果按鍵是按下的,那就進入真正按下的狀態KEY_DOWN
,同時我們給KEY_EVENT
事件賦值,標識我觸發了按下事件,KEY_COUNT
清零為長按計數做准備,此時退出,你就可以在外面判斷事件這個變量決定是否要執行什么任務(動作);
//狀態2:按鍵按下
case KEY_DOWN:
if(Key_Buf[i].KEY_LEVEL != 1)//按鍵釋放,端口高電平
{
Key_Buf[i].KEY_STATUS = KEY_NULL;//轉入狀態0
Key_Buf[i].KEY_EVENT = KEY_UP;//松開事件
}
else if((Key_Buf[i].KEY_LEVEL == 1)
&& (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超過KEY_LONG_DOWN_DELAY沒有釋放
{
Key_Buf[i].KEY_STATUS = KEY_LONG;//轉入狀態3
Key_Buf[i].KEY_EVENT = KEY_LONG;//長按事件
Key_Buf[i].KEY_COUNT = 0;//計數器清零
}
else
{
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
}
break;
又過20ms進來,這次是從狀態KEY_DOWN
開始,判斷按鍵是否釋放,如果釋放就轉入狀態KEY_NULL
,同時標記事件為KEY_UP
,如果沒被釋放,我們會進行計數,同時清空數據標志,其它不變,因為我們的條件
沒有滿足,不進行狀態遷移,需要注意每次進來沒有變化就清空事件,不然出去你判斷的標記又觸發動作了;
//狀態2:按鍵按下
case KEY_DOWN:
if(Key_Buf[i].KEY_LEVEL != 1)//按鍵釋放,端口高電平
{
Key_Buf[i].KEY_STATUS = KEY_NULL;//轉入狀態0
Key_Buf[i].KEY_EVENT = KEY_UP;//松開事件
}
else if((Key_Buf[i].KEY_LEVEL == 1)
&& (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超過KEY_LONG_DOWN_DELAY沒有釋放
{
Key_Buf[i].KEY_STATUS = KEY_LONG;//轉入狀態3
Key_Buf[i].KEY_EVENT = KEY_LONG;//長按事件
Key_Buf[i].KEY_COUNT = 0;//計數器清零
}
else
{
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
}
break;
同樣20ms后進來,假設長按,還是從KEY_DOWN
開始,計數值累加,當累加到設定值,比如25時,也就是500ms,滿足長按條件
,遷移到長按狀態KEY_LONG
,標記事件為KEY_LONG
;
//狀態3:按鍵連續按下
case KEY_LONG:
if(Key_Buf[i].KEY_LEVEL != 1)//按鍵釋放,端口高電平
{
Key_Buf[i].KEY_STATUS = KEY_NULL;//轉入狀態0
Key_Buf[i].KEY_EVENT = KEY_UP;//松開事件
Key_Buf[i].KEY_EVENT = KEY_NULL;
}
else if((Key_Buf[i].KEY_LEVEL == 1)
&& (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超過KEY_LONG_DOWN_DELAY沒有釋放
{
Key_Buf[i].KEY_EVENT = KEY_LONG;//長按事件
Key_Buf[i].KEY_COUNT = 0;//計數器清零
}
else
{
Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
}
break;
20ms后,進入狀態KEY_LONG
,一樣判斷是否釋放,釋放就進入KEY_NULL
狀態,標記松開事件,否則繼續判斷是否為長按。我這里做的是一直按下,每500ms就返回一個長按事件。
這就是整狀態機實現,這個函數也不需要我們修改,按鍵增加或減少根本不影響我這個函數的實現,這就是我前面做一堆枚舉、結構體、函數封裝的好處,好處二。
6. 按鍵處理函數
前面我們做了一堆標記事件,就是給我們主函數做處理的,下面是我的按鍵處理,放在主循環里就可以了。
void Task_KEY_Scan(void)
{
ReadKeyStatus();
if(Key_Buf[KEY0].KEY_EVENT == KEY_UP)
{
printf("KEY0 Down\n");
}
else if(Key_Buf[KEY0].KEY_EVENT == KEY_LONG)
{
printf("KEY0 Long Down\n");
}
if(Key_Buf[KEY1].KEY_EVENT == KEY_UP)
{
printf("KEY1 Down\n");
}
else if(Key_Buf[KEY1].KEY_EVENT == KEY_LONG)
{
printf("KEY1 Long Down\n");
}
if(Key_Buf[KEY2].KEY_EVENT == KEY_UP)
{
printf("KEY2 Down\n");
}
else if(Key_Buf[KEY2].KEY_EVENT == KEY_LONG)
{
printf("KEY2 Long Down\n");
}
}
這個就比較簡單,就一直循環判斷每個事件的標記,你需要哪種事件就做對比,出現這個標記就執行你的代碼。有沒有發現,我這里可以清楚的知道是哪個按鍵標記的事件,這就是那個枚舉的終極用處,好處三。
到這里就結束了,有什么不足的歡迎大家指出留言下方,有好的建議也可以提出,大家共同學習。好東西分享給大家,后面也會更新一些實用的東西給大家,喜歡就點關注哦!!!_