一種實用的單片機按鍵檢測方式

《手把手教你學51單片機》第八課講解按鍵相關內容的時候,介紹了一種狀態檢測掃描按鍵的辦法,既可以檢測按鍵,又可以有效消抖。但是部分同學以前沒有接觸過類似的思想和方式,所以可能接受起來有點難度,這裡我再詳細給講解分析一下,如果教程第八課你已經徹底明白,那麼這裡可以不用再學習了,如果還模模糊糊,可以再鞏固一下。

1、獨立按鍵

常用的按鍵電路有兩種形式,獨立式按鍵和矩陣式按鍵,獨立式按鍵比較簡單,它們各自與獨立的輸入線相連接,如下圖所示。

一種實用的單片機按鍵檢測方式

4條輸入KeyIn1、KeyIn2、KeyIn3、KeyIn4接到單片機的IO口上,當按鍵K1按下時,+5V通過電阻R1然後再通過按鍵K1最終進入GND形成一條通路,那麼這條線路的全部電壓都加到了R1這個電阻上,KeyIn1這個引腳就和GND等電位,是個低電平。當鬆開按鍵後,線路斷開,就不會有電流通過,那麼KeyIn1和+5V就應該是等電位,是一個高電平。我們就可以讀取通過KeyIn1這個IO口的高低電平來判斷是否有按鍵按下,獨立按鍵的原理還是很簡單的。

2、矩陣按鍵

在某一個系統設計中,如果需要使用很多的按鍵時,做成獨立按鍵會大量佔用IO口,因此我們引入了矩陣按鍵的設計方式,如下圖所示,用了8個IO口實現了16個按鍵檢測的電路。

一種實用的單片機按鍵檢測方式

上圖,一共有4組按鍵,我們只看其中一組,如下圖所示。大家認真看一下,如果KeyOut1輸出一個低電平,KeyOut1就相當於是GND,是否相當於4個獨立按鍵呢。當然這時候KeyOut2、KeyOut3、KeyOut4都必須輸出高電平,它們都輸出高電平才能保證與它們相連的三路按鍵不會對這一路產生干擾,大家可以對照兩張原理圖分析一下。

一種實用的單片機按鍵檢測方式

同理,可以將KeyOut1,KeyOut3,KeyOut4都拉高,把KeyOut2拉低,來讀取KEY5到KEY8的值。


關於按鍵掃描的具體程序部分,大家可以去參考教程,我這裡只把一段摘出來給大家講一下,部分同學對其中一條語句有所疑問。

while (1)

{

if (KEY4 != backup) //當前值與前次值不相等說明此時按鍵有動作

{

if (backup == 0) //如果前次值為0,則說明當前是由0變1,即按鍵彈起

{

cnt++; //按鍵次數+1

if (cnt >= 10)

{ //只用1個數碼管顯示,所以加到10就清零重新開始

cnt = 0;

}

P0 = LedChar[cnt]; //計數值顯示到數碼管上

}

backup = KEY4; //更新備份為當前值,以備進行下次比較

}

}

大家注意程序中加粗的部分,這一句是在 if (KEY4 != backup) 這一句的循環範圍內,因此只要按鍵發生任何變化,按鍵的備份值backup這個變量,都會隨著更新一次。


3、按鍵消抖

按鍵抖動的產生原理和造成的影響,這裡不再贅述。那麼重點研究一下如何消抖。初學者通常採用的辦法是用delay來延時的辦法,這種是一種演示實驗的辦法,做實際開發是萬萬不能用的,因為CPU一直在delay裡工作,不能做其他事情,是一種很低級的辦法。

下面介紹一種狀態採集的辦法,這種辦法不是把消抖單獨拿出來處理,而是讀的過程中,通過連續讀取多次來確認當前的按鍵狀態。通過多次判斷,那麼就可以消除抖動帶來的影響。這樣講有點繞,還是用例子。

一般的按鍵持續時間在100ms以上,抖動的時間,都是在10ms之內發生。那好了,那我們就2ms讀一次按鍵,連續讀8次,那一共是16ms,大於10ms的抖動時間,小於按鍵持續時間100ms。如果這8次狀態是 11111111,那麼就認為當前狀態是按鍵彈起狀態。如果這8次狀態是00000000,那麼就認為當前狀態是按鍵按下狀態。那如果這8次狀態是1和0混合的狀態,那就認為當前的狀態既不是彈起,也不是按下,可能是在剛按下,可能是在抖動過程,也可能是在快抖動完畢,總之狀態是不確定的,這個時候我們就不做判斷,既不認為他是按下,也不認為他是彈起,如下圖所示,一個按鍵從彈起到按下,再到彈起的過程。

一種實用的單片機按鍵檢測方式

同學們注意,我們讀的是連續8次狀態,而並不是間隔8個狀態讀一次,個別同學混淆,舉例說明,數字用十六進制。

第一次:12345678

第二次:23456789

第三次:3456789A

第四次:456789AB

....................................

任何一次判斷,只有全1認為是彈起,全0認為是按下,否則則認為按鍵處於抖動區間,不做判斷。

程序方面我只寫主程序部分,用定時器定時2ms,在中斷內部進行按鍵當前狀態讀取和更新,keybuf用來存儲連續8次的按鍵狀態,然後判斷出來,把最終我們認為是“彈起”還是“按下”這樣的最終狀態結果,賦值給KeySta。那這裡某一個按鍵我們確認是“彈起”還是“按下”加上掃描和消抖判斷一共需要16ms即可確認。

while (1)

{

if (KeySta != backup) //當前值與前次值不相等說明此時按鍵有動作

{

if (backup == 0) //如果前次值為0,則說明當前是彈起動作

{

cnt++; //按鍵次數+1

if (cnt >= 10)

{ //只用1個數碼管顯示,所以加到10就清零重新開始

cnt = 0;

}

P0 = LedChar[cnt]; //計數值顯示到數碼管上

}

backup = KeySta; //更新備份為當前值,以備進行下次比較

}

}

/* T0中斷服務函數,用於按鍵狀態的掃描並消抖 */

void InterruptTimer0() interrupt 1

{

static unsigned char keybuf = 0xFF; //掃描緩衝區,保存一段時間內的掃描值

TH0 = 0xF8; //重新加載初值

TL0 = 0xCD;

keybuf = (keybuf<<1) | KEY4; //緩衝區左移一位,並將當前掃描值移入最低位

if (keybuf == 0x00)

{

KeySta = 0; //連續8次掃描值都為0,可認為按鍵已按下

}

else if (keybuf == 0xFF)

{

KeySta = 1; //連續8次掃描值都為1,可認為按鍵已彈起

}

else

{} //其它情況則說明按鍵狀態尚未穩定,則不對KeySta變量值進行更新

}


這種狀態掃描的辦法是工程中常用的一個辦法,介紹給大家使用。這裡雖然判斷一次按鍵也用了16ms,但是真正單片機在讀按鍵狀態的程序時間是很短的,我們不需要停留在一直等待按鍵狀態發生變化的那個過程中,這個時候單片機可以做很多其他的事情。

矩陣按鍵有16個,如果還是按照2ms採集一次,一次可以採集1組共4個按鍵的情況,一共採集8次的話,需要的總時間 = 2ms*8次*(16/4)= 64ms,這個時間就有點太長了。這裡的時間計算方式,部分同學混淆,所以我把式子詳細列了出來。

那我們對矩陣按鍵的處理方式採取狀態時間間隔減半,確認最終所需要的讀取狀態次數減半處理,時間間隔1ms讀一次,每次可以讀一組4個按鍵,每個按鍵需要採集4次最終確認按鍵是“彈起”還是“按下”,那讀完了的總時間 = 1ms*4次*(16/4)=16ms。但是有一點這裡重點強調(凡事這裡重點強調的,就是有其他同學混淆的),每一個按鍵都是16ms才能確認按鍵狀態,但是掃描時間一旦開啟,是每間隔4ms,按鍵就判斷到一次。

第一次:1234

第二次:2345

第三次:3456

第四次:4567

----------------------

#include

sbit ADDR0 = P1^0;

sbit ADDR1 = P1^1;

sbit ADDR2 = P1^2;

sbit ADDR3 = P1^3;

sbit ENLED = P1^4;

sbit KEY_IN_1 = P2^4;

sbit KEY_IN_2 = P2^5;

sbit KEY_IN_3 = P2^6;

sbit KEY_IN_4 = P2^7;

sbit KEY_OUT_1 = P2^3;

sbit KEY_OUT_2 = P2^2;

sbit KEY_OUT_3 = P2^1;

sbit KEY_OUT_4 = P2^0;

unsigned char code LedChar[] = { //數碼管顯示字符轉換表

0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,

0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E

};

unsigned char KeySta[4][4] = { //全部矩陣按鍵的當前狀態

{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}

};

void main()

{

unsigned char i, j;

unsigned char backup[4][4] = { //按鍵值備份,保存前一次的值

{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}

};

EA = 1; //使能總中斷

ENLED = 0; //選擇數碼管DS1進行顯示

ADDR3 = 1;

ADDR2 = 0;

ADDR1 = 0;

ADDR0 = 0;

TMOD = 0x01; //設置T0為模式1

TH0 = 0xFC; //為T0賦初值0xFC67,定時1ms

TL0 = 0x67;

ET0 = 1; //使能T0中斷

TR0 = 1; //啟動T0

P0 = LedChar[0]; //默認顯示0

while (1)

{

for (i=0; i<4; i++) //循環檢測4*4的矩陣按鍵

{

for (j=0; j<4; j++)

{

if (backup[i][j] != KeySta[i][j]) //檢測按鍵動作

{

if (backup[i][j] != 0) //按鍵按下時執行動作

{

P0 = LedChar[i*4+j]; //將編號顯示到數碼管

}

backup[i][j] = KeySta[i][j]; //更新前一次的備份值

}

}

}

}

}

/* T0中斷服務函數,掃描矩陣按鍵狀態並消抖 */

void InterruptTimer0() interrupt 1

{

unsigned char i;

static unsigned char keyout = 0; //矩陣按鍵掃描輸出索引

static unsigned char keybuf[4][4] = { //矩陣按鍵掃描緩衝區

{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},

{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}

};

TH0 = 0xFC; //重新加載初值

TL0 = 0x67;

//將一行的4個按鍵值移入緩衝區

keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1;

keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2;

keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3;

keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4;

//消抖後更新按鍵狀態

for (i=0; i<4; i++) //每行4個按鍵,所以循環4次

{

if ((keybuf[keyout][i] & 0x0F) == 0x00)

{ //連續4次掃描值為0,即4*4ms內都是按下狀態時,可認為按鍵已穩定的按下

KeySta[keyout][i] = 0;

}

else if ((keybuf[keyout][i] & 0x0F) == 0x0F)

{ //連續4次掃描值為1,即4*4ms內都是彈起狀態時,可認為按鍵已穩定的彈起

KeySta[keyout][i] = 1;

}

}

//執行下一次的掃描輸出

keyout++; //輸出索引遞增

keyout = keyout & 0x03; //索引值加到4即歸零

switch (keyout) //根據索引,釋放當前輸出引腳,拉低下次的輸出引腳

{

case 0: KEY_OUT_4 = 1; KEY_OUT_1 = 0; break;

case 1: KEY_OUT_1 = 1; KEY_OUT_2 = 0; break;

case 2: KEY_OUT_2 = 1; KEY_OUT_3 = 0; break;

case 3: KEY_OUT_3 = 1; KEY_OUT_4 = 0; break;

default: break;

}

}

4、長短按鍵

在單片機系統中應用按鍵的時候,如果想連續加很多數字的時候,我們會希望一直按住按鍵,數字就自動持續增加或減小,這就是所謂的長短按鍵應用。

當檢測到一個按鍵產生按下動作後,馬上執行一次相應的操作,同時在程序裡記錄按鍵按下的持續時間,該時間超過1秒後(主要是為了區別短按和長按這兩個動作,因短按的時間通常都達到幾百ms),每隔200ms(如果你需要更快那就用更短的時間,反之亦然)就自動再執行一次該按鍵對應的操作,這就是一個典型的長按鍵效果。

程序代碼摘自《手把手教你學51單片機》教程第十課,詳細代碼有想了解的可以去看,這裡只把和長短按鍵有關係的部分代碼摘錄出來。

void KeyDriver()

{

unsigned char i, j;

static unsigned char pdata backup[4][4] = { //按鍵值備份,保存前一次的值

{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}

};

static unsigned long pdata TimeThr[4][4] = { //快速輸入執行的時間閾值

{1000, 1000, 1000, 1000}, {1000, 1000, 1000, 1000},

{1000, 1000, 1000, 1000}, {1000, 1000, 1000, 1000}

};

for (i=0; i<4; i++) //循環掃描4*4的矩陣按鍵

{

for (j=0; j<4; j++)

{

if (backup[i][j] != KeySta[i][j]) //檢測按鍵動作

{

if (backup[i][j] != 0) //按鍵按下時執行動作

{

KeyAction(KeyCodeMap[i][j]); //調用按鍵動作函數

}

backup[i][j] = KeySta[i][j]; //刷新前一次的備份值

}

if (KeyDownTime[i][j] > 0) //檢測執行快速輸入

{

if (KeyDownTime[i][j] >= TimeThr[i][j])

{ //達到閾值時執行一次動作

KeyAction(KeyCodeMap[i][j]); //調用按鍵動作函數

TimeThr[i][j] += 200; //時間閾值增加200ms,以準備下次執行

}

}

else //按鍵彈起時復位閾值時間

{

TimeThr[i][j] = 1000; //恢復1s的初始閾值時間

}

}

}

}

void KeyScan()

{

unsigned char i;

static unsigned char keyout = 0; //矩陣按鍵掃描輸出索引

static unsigned char keybuf[4][4] = { //矩陣按鍵掃描緩衝區

{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},

{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}

};

//將一行的4個按鍵值移入緩衝區

keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1;

keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2;

keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3;

keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4;

//消抖後更新按鍵狀態

for (i=0; i<4; i++) //每行4個按鍵,所以循環4次

{

if ((keybuf[keyout][i] & 0x0F) == 0x00)

{ //連續4次掃描值為0,即4*4ms內都是按下狀態時,可認為按鍵已穩定的按下

KeySta[keyout][i] = 0;

KeyDownTime[keyout][i] += 4; //按下的持續時間累加

}

else if ((keybuf[keyout][i] & 0x0F) == 0x0F)

{ //連續4次掃描值為1,即4*4ms內都是彈起狀態時,可認為按鍵已穩定的彈起

KeySta[keyout][i] = 1;

KeyDownTime[keyout][i] = 0; //按下的持續時間清零

}

}

//執行下一次的掃描輸出

keyout++; //輸出索引遞增

keyout &= 0x03; //索引值加到4即歸零

switch (keyout) //根據索引,釋放當前輸出引腳,拉低下次的輸出引腳

{

case 0: KEY_OUT_4 = 1; KEY_OUT_1 = 0; break;

case 1: KEY_OUT_1 = 1; KEY_OUT_2 = 0; break;

case 2: KEY_OUT_2 = 1; KEY_OUT_3 = 0; break;

case 3: KEY_OUT_3 = 1; KEY_OUT_4 = 0; break;

default: break;

}

}


不管你以後遇到的是什麼樣的按鍵,這種掃描按鍵狀態的思路,提供你參考學習,以後再也不要在實際工程中用delay函數來消抖了,那樣會讓領導一看你就是新手,用的方法很low,自然給你定薪水待遇的時候,印象分就會低。如果你寫出這種實用的實際開發的程序給領導一看,領導很可能對你另眼相看。


分享到:


相關文章: