【說在前面的話】
按鍵作為單片機的輸入設備,可以向單片機輸入數據、傳輸命令等,是設置參數和控制設備的常用接口。所以,學會按鍵驅動也是初學者必不可少的能力。說到按鍵驅動程序,大家應該也不陌生,而一般的按鍵驅動流程圖如下
這里,可能有人會問,為什么要延時10ms啊?
那是因為按鍵被按下時,不會像理想的情況非0即1,而是會有抖動,如下圖
當機械按鍵被按下或松開時,會有10ms的抖動時間,所以要延時10ms來消去波形抖動(* ̄︶ ̄)
知道了這個,一般初學者編寫的按鍵驅動程序如下:
//延時1ms
void Delay1ms() { //@12.000MHz
unsigned char i, j;
i = 12;
j = 169;
do{
while (--j);
} while (--i);
}
//ms延時
void delay_ms(int ms){
char i = 0;
for(i = 0; i < ms; i++){
Delay1ms();
}
}
char get_key(){
//檢測按鍵是否被按下
if(KEY1 == 0){
//延時10ms
delay_ms(10);
//再次檢測按鍵是否被按下
if(KEY1 == 0){
//等待按鍵松開
while(KEY1 == 0){ }
delay_ms(10);
return 1;
}
}
return 0;
}
像這種按鍵驅動程序也很簡單,作為基礎學習和一些簡單的系統中還可以,但是在很多的產品設計中,這種按鍵程序還是有很大的不足和缺陷。因為他不僅采用了軟件延時使單片機效率降低而且還在那里死等按鍵松開,系統的實時性也變得很差。為此,有人提出了 一種基于狀態機的按鍵驅動程序 ,很好地解決了上述程序的缺陷。下面我們就簡單講一下什么是狀態機。
【狀態機簡介】
對于學電子的同學,首先接觸到的狀態機應該是在數字邏輯電路( 簡稱數電 )中,狀態機的分析方法被應用于時序邏輯電路的設計中,其實狀態機的思想對我們的軟件設計也很有用,首先簡單介紹一下狀態機,它是由有限的狀態和相互之間的遷移構成的。在任何時候,只能處于狀態機的某一個狀態,當接收到一個轉移事件時,狀態機進行狀態的轉移。
下面,就以按鍵驅動為例,畫出他的狀態轉移圖,如下
有了狀態轉移圖,那我們就用程序實現一下這個按鍵驅動程序。從圖中我們知道按鍵驅動程序由3個狀態,剛好可以用C語言的switch case語句來實現這3個狀態,而狀態間的遷移就可以用if條件判斷語句來實現。知道了這個,那我們就動手實現一下。
基于狀態機的按鍵驅動程序
首先,打開原理圖,看一下按鍵接到了單片機的哪個管腳,如下
我們以按鍵1為例,接到了單片機的P33腳,當按鍵 按下時為低電平 , 松開為高電平 ,基于此我們的按鍵程序如下:
sbit KEY1 = P3^3;//key1
char get_key(){
//保存按鍵狀態
static char key_flag = 0;
//軟件延時計時器
static unsigned int s_Counter = 0;
switch(key_flag){
//狀態0為無按鍵按下
case 0:
if(KEY1 == 0){
//如果有按鍵按下,轉為狀態1
key_flag = 1;
}
break;
//狀態1為延時消抖
case 1:
s_Counter++;
if(s_Counter > 1000){
//延時10ms,計時器清零
s_Counter = 0;
if(KEY1 == 0){
//如果按鍵被按下,轉為狀態2
key_flag = 2;
return 1;
}else{
//如果按鍵未按下,轉為狀態0
key_flag = 0;
}
}
break;
//狀態2為等待按鍵釋放
case 2:
if(KEY1 == 1){
//如果按鍵松開,轉為狀態0
key_flag = 0;
}
break;
}
return 0;
}
- 注意,每個case結束后都有一個break
- 第18行,s_Counter > 1000相當于延時10ms,當然這個1000是隨便給的值,大家要根據具體情況設置此值,如果測試小于10ms就可以加大此值,我們只是為了說明用s_Counter 可以延時。
- 第24行,在延時去抖完成后就返回了1(相當于按鍵按下),這樣做的好處就是可以提高按鍵響應速度。當然也可以在狀態2按鍵松開后返回1。
基于狀態機的按鍵驅動程序我們就簡單寫完了,相信大家也get到重點了,這個只是簡單實現了按鍵的單擊,當然,我們也可以實現按鍵的雙擊和長按。
哈哈,在編寫驅動之前,我們先細化一下需求,首先區分單擊和長按,這個很簡單,規定一個時間就可以,我們定為1秒鐘。即按下時間小于1秒為單擊,大于1秒為長按。
那雙擊怎么辦呢?
我們規定,當第一次按下持續時間小于500ms內松開按鍵,在之后500ms內又按下按鍵,此時為雙擊事件。這里有兩點需要注意,1、第一次按下的時間不能超過500ms,否則就被判斷為單擊或長按。2、在第一次按下松開后開始計時,如果500ms內沒有按鍵再次按下則為單擊。按鍵雙擊的原理如下圖所示
按鍵雙擊和長按的需求我們講完了,接下來畫出他的狀態轉移圖,如下
哈哈哈,還可以吧,沒那么復雜。相信大家應該能看懂。這里有必要說一下狀態5,判斷雙擊其實就是第二次按下延時10ms消抖,如果確實按下則為雙擊否則為單擊。好了,看看程序怎么實現吧,如下
#define DELAY_10ms 500
#define DELAY_500ms 10000
char get_key3(){
static char key_flag = 0;
static unsigned int s_Counter = 0;
switch(key_flag){
case 0://無按鍵按下
if(KEY1 == 0){
key_flag = 1;
}
break;
case 1://延時10ms消抖
s_Counter++;
if(s_Counter > DELAY_10ms){
s_Counter = 0;
if(KEY1 == 0){
key_flag = 2;
}else{
key_flag = 0;
}
}
break;
case 2://計時500ms,等待按鍵松開
s_Counter++;
if(s_Counter > DELAY_500ms){//500ms內按鍵未松開
s_Counter = 0;
key_flag = 6;
}
if(KEY1 == 1){//500ms內按鍵松開
s_Counter = 0;
key_flag = 3;
}
break;
case 3://按鍵松開,延時10ms消抖
s_Counter++;
if(s_Counter > DELAY_10ms){
s_Counter = 0;
key_flag = 4;
}
break;
case 4://等待雙擊
s_Counter++;
if(s_Counter > DELAY_500ms){
s_Counter = 0;
key_flag = 7;//500ms內按鍵未按下
}
if(KEY1 == 0){//500ms內按鍵被按下
s_Counter = 0;
key_flag = 5;
}
break;
case 5://延時10ms消抖
s_Counter++;
if(s_Counter > DELAY_10ms){
s_Counter = 0;
if(KEY1 == 0){
key_flag = 8;//等待按鍵松開
return 2;//雙擊
}else{
key_flag = 7;//單擊
}
}
break;
case 6:
s_Counter++;
if(s_Counter > DELAY_500ms){
s_Counter = 0;
key_flag = 8;//等待按鍵松開
return 3;//長按
}
if(KEY1 == 1){
s_Counter = 0;
key_flag = 7;
}
break;
case 7://單擊
key_flag = 8;
return 1;//單擊
break;
case 8://等待按鍵松開
if(KEY1 == 1){
s_Counter = 0;
key_flag = 0;
}
break;
}
return 0;
}