晨輝教你輕松學(xué)51--------按鍵篇
對于一個由單片機(jī)為核心構(gòu)成的系統(tǒng)而言。輸入通道是相當(dāng)重要的??梢钥吹綆缀趺恳粯踊趩纹瑱C(jī)的產(chǎn)品都有人機(jī)交互的部分。如各種儀器設(shè)備上的各種按鈕和開關(guān),以及我們手機(jī)上的鍵盤,MP3上的按鍵等等。最常見的輸入部分,莫非就是按鍵了。對于大多數(shù)初學(xué)者而言,編寫一個好的按鍵程序是一件頗為頭疼的事情。于是乎在網(wǎng)上亂搜一氣,程序倒是找到了不少,但是看了半天依然是不明白。或者在某某論壇上面發(fā)帖“跪求XX按鍵程序,大蝦幫忙……”如果你偶然間進(jìn)了這個論壇,又偶然看到了這個帖子,而且恰好你對按鍵程序的寫法也不是很清楚,那么我希望你能夠靜靜的看完這個帖子。如果你覺得對你很有幫助,那么我希望你能夠在以后的日子中能夠堅(jiān)持到這個論壇來,一起交流學(xué)習(xí),分享自己學(xué)習(xí)過程中的喜悅或者一起探討棘手的問題,這是我寫這個帖子的最大的初衷了。OK,不能再說了,再說就變成水帖了。那么我們開始吧。
按鍵的種類很多。不過原理基本相似。下面我們以一種輕觸開關(guān)為例講解按鍵程序的寫法。 ![]() 一般情況下,按鍵與單片機(jī)的連接如下面這幅圖所示。 ![]() (圖中電阻值一般去4.7k~10k之間,對于內(nèi)部端口有上拉電阻的單片機(jī)則可省略此電阻) 單片機(jī)對于按鍵的按下與否則是通過檢測相應(yīng)引腳上的電平來實(shí)現(xiàn)的。對于上圖而言,當(dāng)P17引腳上面的電平為低時,則表示按鍵已經(jīng)按下。反之,則表明按鍵沒有按下。我們在程序中只要檢測到了P17引腳上面的電平為低了,就可以判斷按鍵按下。呵呵,簡單吧。等會,您先別樂呵,話還沒說完呢。下面我們來看看,當(dāng)按鍵按下時,P17引腳上面的波形是怎么變化的。 ![]() 上圖是一個理想波形圖,當(dāng)按鍵按下時,P17口的電平馬上被拉低到0V了。當(dāng)然理想的東西都是不現(xiàn)實(shí)的。所以我們還是看看現(xiàn)實(shí)的波形圖吧。 ![]() 看出什么區(qū)別來了沒。呵呵,只要你不是傻子我相信都能看出其中的區(qū)別。由于按鍵的機(jī)械特性。當(dāng)按鍵閉合時,并不能馬上保存良好的接觸,而是來回彈跳。這個時間很短,我們的手根本感覺不出來。但是對于一秒鐘執(zhí)行百萬條指令的單片機(jī)而言,這個時間是相當(dāng)?shù)拈L了。那么在這段抖動的時間內(nèi),單片機(jī)可能讀到多次高低電平的變化。如果不加任何處理的話,就會認(rèn)為已經(jīng)按下,或者松開很多次了。而事實(shí)上,我們的手一直按在按鍵上,并沒有重復(fù)按動很多次。要想能夠正確的判斷按鍵是否按下就要避開這段抖動的時間。根據(jù)一般按鍵的機(jī)械特點(diǎn),以及按鍵的新舊程度等而言,這段抖動的時間一般在5MS~20MS之間。 看到這里你明白了該如何做了吧。 看看下面的這個流程圖,你應(yīng)該不陌生吧。 ![]() 這個流程是好多教科書上的做法??上?,誤導(dǎo)了好多人。為什么呢。因?yàn)樗揪蜎]有考慮實(shí)際情況。我們根據(jù)這幅流程圖來寫它的代碼看看。 unsigned char v_ReadKey_f( void ) { unsigned char KeyPress ; if( P17 == 0) { Delay(20) ; //延時20MS If( P17 == 0) { KeyPress = 1 ; While( !P17) ; //等待釋放 } else KeyPress = 0 ; } } 這樣一個程序,相信對很多初學(xué)者而言都不陌生。因?yàn)楹枚鄷匣径际沁@樣的一個流程和寫法。可是當(dāng)有一天,我們想做一個數(shù)碼管加按鍵調(diào)整的時鐘,發(fā)現(xiàn)當(dāng)我們按鍵按下去的時候,數(shù)碼管就不亮了。為什么呢。原因就在這個鍵盤掃描函數(shù)。平常沒有按鍵按下還好。一旦有鍵按下,它先是浪費(fèi)了CPU的大部分時間(就是那個什么事情都沒做的延時20MS函數(shù))然后,又霸占CPU( 就是哪個死死等在那里的while(P17);語句)直到按鍵釋放。對于這種情況我們是忍無可忍的,那么就讓我們徹底的拋棄它吧。那么到底按鍵掃描函數(shù)改如何寫呢……..所謂眾里尋她千百度,驀然回首,那人卻在燈火闌珊處。如果我們把CPU延時的那20MS拿出來去做其它事情,那么不就充分利用CPU的時間了嗎。而一般情況下我們只要前沿去抖動就可以了。也就是說了,我們只需在按鍵按下后去抖就可以了,對于按鍵的釋放抖動可以不必要過于關(guān)注。當(dāng)然這主要和應(yīng)用的場合有關(guān)。一個能有效識別按鍵按下并支持連發(fā)功能的按鍵已經(jīng)能夠應(yīng)用到大多數(shù)的場合了。 下面以四個獨(dú)立按鍵的處理程序?yàn)槔齺碇v解(支持單擊和連發(fā)) #include"regx52.h" sbit KeyOne = P1^0 ; sbit KeyTwo = P1^1 ; sbit KeyThree = P1^2 ; sbit KeyFour = P1^3 ; #define uint16 unsigned int #define uint8 unsigned char #define NOKEY 0xff #define KEY_WOBBLE_TIME 500 //去抖動時間(待定) #define KEY_OVER_TIME 15000 //等待進(jìn)入連擊時間(待定),該常數(shù)要比正常 //按鍵時間要長,防止非目的性進(jìn)入連擊模式 #define KEY_QUICK_TIME 1000 //等待按鍵抬起的連擊時間(待定) void v_KeyInit_f( void ) { KeyOne = 1 ; //按鍵初始化(相應(yīng)端口寫1) KeyTwo = 1 ; KeyThree = 1 ; KeyFour = 1 ; } uint8 u8_ReadKey_f(void) { static uint8 LastKey = NOKEY ; //保存上一次的鍵值 static uint16 KeyCount = 0 ; //按鍵延時計(jì)數(shù)器 static uint16 KeyOverTime = KEY_OVER_TIME ; //按鍵抬起時間 uint8 KeyTemp = NOKEY ; //臨時保存讀到的鍵值 KeyTemp = P1 & 0x0f ; //讀鍵值 if( KeyTemp == 0x0f ) { KeyCount = 0 ; KeyOverTime = KEY_OVER_TIME ; return NOKEY ; //無鍵按下返回NOKEY } else { if( KeyTemp == LastKey ) //是否第一次按下 { if( ++KeyCount == KEY_WOBBLE_TIME ) //不是第一次按下,則判斷//抖動是否結(jié)束 { return KeyTemp ; //去抖動結(jié)束,返回鍵值 } else { if( KeyCount > KeyOverTime ) { KeyCount = 0 ; KeyOverTime = KEY_QUICK_TIME ; } return NOKEY ; } } else //是第一次按下則保存鍵值,以便下次執(zhí)行此函數(shù)時與讀到的鍵值作比較 { LastKey = KeyTemp ; //保存第一次讀到的鍵值 KeyCount = 0 ; //延時計(jì)數(shù)器清零 KeyOverTime = KEY_OVER_TIME ; return NOKEY ; } } } 下面是我測試用的主程序(相關(guān)頭文件未列出,僅僅作測試演示用) void main(void) { uint8 KeyValue ; int16 Count ; v_LcdInit_f() ; v_KeyInit_f() ; CLS LOCATE(3, 1) PRINT("Key Test") LOCATE(6, 2) SHOW_ICON while(1) { KeyValue = u8_ReadKey_f() ; if( KeyValue != NOKEY ) { LOCATE(1, 2) if( KeyValue == 0x0e )Count++ ; if( KeyValue == 0x0d )Count-- ; if( KeyValue == 0x0b )Count = 0 ; if( KeyValue == 0x07 )Count = 0 ; HIDE_ICON PRINTD(Count, 5) LOCATE(6, 2) } else { //SHOW_ICON } } } 每次執(zhí)行讀鍵盤函數(shù)時,只是對一些標(biāo)志進(jìn)行判斷,然后退出。因此能夠充分的利用CPU的資源。同時可以處理連發(fā)按鍵。此按鍵掃描按鍵函數(shù)可以直接放在主函數(shù)中。如果感覺按鍵太過靈敏或者遲鈍則改一下相關(guān)消抖動的宏定義即可。此函數(shù)也可以通過中斷標(biāo)志位進(jìn)行定時的掃描。此時,需要添加一個定時標(biāo)志位,并將相關(guān)消抖動的和連擊時間的宏定義改小即可。然后在主程序類似下面這樣寫即可 if( KeyTime ) //定時掃描時間到 { KeyValue = u8_ReadKey_f() ; } 具體的工作就交給您去完成啦。 看看效果: 按鍵單擊 ![]() 連發(fā)時候的截圖 ![]() 至此,關(guān)于單個按鍵的學(xué)習(xí)就告一段落了,您是否已經(jīng)明白了。如果您還不明白,那么把這個程序好好的看看,并畫下流程圖,分析分析。估計(jì)您就會恍然大悟。關(guān)鍵是思路要轉(zhuǎn)換過來。 下面我們來看看多個按鍵的情況吧 一般情況下,如果多個按鍵每個都直接接在單片機(jī)的I/O上的話會占用很多的I/O資源。比較合理的一種做法是,按照行列接成矩陣的形式。按鍵接在每一個的行列的相交處。這樣對于m行n列的矩陣,可以接的按鍵總數(shù)是m*n。這里我們以常見的4*4矩陣鍵盤來講解矩陣鍵盤的編程。 ![]() 上圖就是矩陣鍵盤的一般接法。 這里我們要介紹一種快速的鍵盤掃描法:線反轉(zhuǎn)法(或者稱為行列翻轉(zhuǎn)法)。具體流程如下。首先,讓單片機(jī)的行全部輸出0,列全部輸出1,讀取列的值(假設(shè)行接P3口的高四位,列接低四位)。即P3= 0x0f ; 此時讀列的值,如果有鍵按下,則相應(yīng)的列讀回來的值應(yīng)該為低。譬如此時讀回來的值為 0x0e ; 即按鍵列的位置已經(jīng)確定。這時反過來,把行作為輸入,列作為輸出,即P0 = 0xf0 ;此時再讀行的值,如果按鍵仍然被按下,則相應(yīng)的行的值應(yīng)該為低,如果此時讀回來的值為0xe0,則確定了行的位置 。說到這里,您應(yīng)該笑了,知道了一個按鍵被按下的行和列的位置,那么就可以肯定確定它的位置了。我們把讀回來的行值和列值進(jìn)行或運(yùn)算。即 0xe0 | 0x 0e 即 0xee。那么0xee就是我們按下的按鍵的鍵值了。怎么樣。只需幾步就可以判斷所有的鍵值,簡單吧。下面再結(jié)合一個例子具體看看。 /****************************************** * 此模塊所需相關(guān)支持庫 * ******************************************/ #include"regx52.h" #define uint8 unsigned char #define uint16 unsigned int /**************************************** * 與硬件連接相關(guān)的定義及宏定義和操作宏 * *****************************************/ #define KEYBOARD P3 //鍵盤連接到單片機(jī)上的端口位置 #define READ_ROW_ENLABLE KEYBOARD = 0x0f ; //讀端口之前先把相應(yīng)口置位(由基本51單片機(jī)特性決定的) #define READ_COL_ENLABLE KEYBOARD = 0xf0 ; // 根據(jù)實(shí)際硬件連接情況修改 /***************************************** * 模塊內(nèi)相關(guān)的宏定義及常數(shù)宏 * ******************************************/ #define NOKEY 0xff //定義無鍵按下時的返回值 #define DELAY_COUNT 2 //消抖時間常數(shù) /***************************************** * 此模塊所需的全局或者外部變量 * *****************************************/ bit bdata StartScan = 0 ;//此變量需放在定時中斷中置位 /***************************************** * 按鍵掃描函數(shù),按下去后經(jīng)去抖,確定按下 * * 則返回鍵值0~15;無鍵按下則返回0xff ; * * 此函數(shù)需要定時器的支持(去抖....) * *****************************************/ uint8 u8_KeyBoardScan_f() { static uint8 DelayCount = 0 ; uint8 KeyValueRow = 0 ; uint8 KeyValueCol = 0 ; uint8 KeyValue = 0 ; if( StartScan ) //開始掃描,StartScan在定時中斷中置位 { StartScan = 0 ; //清除開始掃描標(biāo)志位,避免多次重復(fù)執(zhí)行掃描程序 //讀入按鍵狀態(tài)前先向相應(yīng)端口寫1(由基本51單片機(jī)硬件結(jié)構(gòu)決定) READ_ROW_ENLABLE if( ( KEYBOARD & 0x0f ) != 0x0f ) //判斷是否有鍵按下 { DelayCount++; if( DelayCount <= DELAY_COUNT ) //有鍵按下則判斷延時去抖的時間是否達(dá)到 { return NOKEY ; } else //消除了抖動 { if( ( KEYBOARD & 0x0f ) != 0x0f ) //再次判斷是否按鍵真的按下 { DelayCount = 0 ; //確定按下后,延時去抖計(jì)時器清0 KeyValueRow = KEYBOARD & 0x0f ; //取得行碼 //準(zhǔn)備讀列,先向相應(yīng)端口寫1(由基本51單片機(jī)硬件結(jié)構(gòu)決定) READ_COL_ENLABLE if ( (KEYBOARD & 0xf0) != 0xf0 ) //反轉(zhuǎn),讀列碼 { KeyValueCol = KEYBOARD & 0xf0 ; //取得列碼 //合并取得的行碼和列碼,即是相應(yīng)按鍵的鍵值 switch( KeyValueCol | KeyValueRow) { case 0x77 : KeyValue = 0 ; break ; case 0xb7 : KeyValue = 1 ; break ; case 0xd7 : KeyValue = 2 ; break ; case 0xe7 : KeyValue = 3 ; break ; case 0x7b : KeyValue = 4 ; break ; case 0xbb : KeyValue = 5 ; break ; case 0xdb : KeyValue = 6 ; break ; case 0xeb : KeyValue = 7 ; break ; case 0x7d : KeyValue = 8 ; break ; case 0xbd : KeyValue = 9 ; break ; case 0xdd : KeyValue = 10 ;break ; case 0xed : KeyValue = 11 ;break ; case 0x7e : KeyValue = 12 ;break ; case 0xbe : KeyValue = 13 ;break ; case 0xde : KeyValue = 14 ;break ; case 0xee : KeyValue = 15 ;break ; default : return NOKEY ; } return KeyValue ; } else { DelayCount = 0 ; return NOKEY ; } } else { DelayCount = 0 ; return NOKEY ; } } } else { DelayCount = 0 ; return NOKEY ; } } } void v_T0_Isr_f( void ) interrupt INTERRUPT_TIMER2_OVERFLOW { StartScan = 1 ; } /*************************************************** *模塊調(diào)試 * ***************************************************/ //主函數(shù)僅作演示用,主函數(shù)除按鍵掃描外的函數(shù)并沒在這里給出 void v_Init_T2_f( void ) { T2CON = 0x04 ; T2MOD = 0x00 ; TH2 = 0xd8 ; RCAP2H = 0xd8 ; TL2 = 0xf0 ; RCAP2L = 0xf0 ; ET2 = 1 ; TR2 = 1 ; } void main( void ) { uint8 readkey = 0 ; v_Init_T2_f( ) ; v_LcdInit_f( ); LOCATE( 1, 1) PRINT("4*4KeyBoard Test") EA = 1 ; LOCATE( 3, 2) while( 1 ) { SHOW_ICON readkey = u8_KeyBoardScan_f() ; if( readkey != NOKEY) { PRINTN( readkey , 2) LOCATE( 3, 2) continue ; } else { continue ; } } } 呵呵,按鍵掃描程序已經(jīng)注釋的很詳細(xì)了。我就不多費(fèi)嘴舌了。如果有不清楚的地方,歡迎跟帖討論。 下面是按鍵測試的截圖 ![]() ![]() 我的自己搭建的實(shí)驗(yàn)板 ![]() OK,Enioy it !自此按鍵檢測告一段落。下次如果再講按鍵。將會討論另外一種按鍵的寫法:基于狀態(tài)機(jī)的按鍵程序設(shè)計(jì)。歡迎討論。 |
|