《手把手教你學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 <reg52.h>
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,自然給你定薪水待遇的時候,印象分就會低。如果你寫出這種實用的實際開發的程序給領導一看,領導很可能對你另眼相看。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!