鴻哥是我的一位單片機學(xué)習(xí)的啟蒙老師,在此分享來自于他的一篇文章。
【92.1 獨立按鍵的硬件電路簡介。】
上圖92.1.1 獨立按鍵電路
按鍵有兩種驅(qū)動方式,一種是獨立按鍵,一種是矩陣按鍵。1個獨立按鍵要占用1個IO口,IO口不能共用。而矩陣按鍵的IO口是分時片選復(fù)用的,用少量的IO口就可以驅(qū)動翻倍級別的按鍵數(shù)量。比如,用8個IO口只能驅(qū)動8個獨立按鍵,但是卻可以驅(qū)動16個矩陣按鍵(4x4)。因此,按鍵少的時候就用獨立按鍵,按鍵多的時候就用矩陣按鍵。這兩種按鍵的驅(qū)動本質(zhì)是一樣的,都是靠識別輸入信號的下降沿(或上升沿)來識別按鍵的觸發(fā)。
獨立按鍵的硬件原理基礎(chǔ),如上圖,P2.2這個IO口,在按鍵K1沒有被按下的時候,P2.2口因為單片機內(nèi)部自帶上拉電阻把電平拉高,此時P2.2口是高電平的輸入狀態(tài)。當(dāng)按鍵K1被按下的時候,按鍵K1左右像一根導(dǎo)線連接到電源的負極(GND),直接把原來P2.2口的電平拉低,此時P2.2口變成了低電平的輸入狀態(tài)。編寫按鍵驅(qū)動程序,就是要識別這個電平從高到低的過程,這個過程也叫下降沿。多說一句,51單片機的P1,P2,P3口是內(nèi)部自帶上拉電阻的,而P0口是內(nèi)部沒有上拉電阻的,需要外接上拉電阻。除此之外,很多單片機內(nèi)部其實都沒有上拉電阻的,因此,建議大家在做獨立按鍵電路的時候,養(yǎng)成一個習(xí)慣,凡是按鍵輸入狀態(tài)都外接上拉電阻。
識別按鍵的下降沿觸發(fā)有四大要素:自鎖,消抖,非阻塞,清零式濾波。
“自鎖”,按鍵一旦進入到低電平,就要“自鎖”起來,避免不斷觸發(fā)按鍵,只有當(dāng)按鍵被松開變成高電平的時候,才及時“解鎖”為下一次觸發(fā)做準備。
“消抖”,按鍵是一個機械觸點器件,在接觸的瞬間必然存在微觀上的機械抖動,反饋到電平的瞬間就是“高,低,高,低...”這種不穩(wěn)定的電平狀態(tài)是一種干擾,但是,按鍵一旦按下去穩(wěn)定了之后,這種狀態(tài)就消失,電平就一直保持穩(wěn)定的低電平。消抖的本質(zhì)就是濾波,要把這種接觸的瞬間抖動過濾掉,避免按鍵的“一按多觸發(fā)”。
“非阻塞”,在處理消抖的時候,必須用到延時,如果此時用阻塞的delay延時就會影響其它任務(wù)的運行效率,因此,用非阻塞的定時延時更加有優(yōu)越性。
“清零式濾波”,在消抖的時候,有兩種境界,第一種境界是判斷兩次電平的狀態(tài),中間插入“固定的時間”延時,這種方法前后一共判斷了兩次,第一次是識別到低電平就進入延時的狀態(tài),第二次是延時后再確認一次是否繼續(xù)是低電平的狀態(tài),這種方法的不足是,“固定的時間”全憑經(jīng)驗值,但是不同的按鍵它們的抖動時間長度是不同的,除此之外,前后才判斷了兩次,在軟件的抗干擾能力上也弱了很多,“密碼等級”不夠高。第二種境界就是“清零式濾波”,“清零式濾波”非常巧妙,抗擾能力超強,它能自動過濾不同按鍵的“抖動時間”,然后再進入一個“穩(wěn)定時間”的“N次識別判斷”,更加巧妙的是,在“抖動時間”和“穩(wěn)定時間”兩者時間內(nèi),只要發(fā)現(xiàn)一次是高電平的干擾,就馬上自動清零計時器,重新開始計時?!胺€(wěn)定時間”一般取20ms到30ms之間,而“抖動時間”是隱藏的,在代碼上并沒有直接描寫出來,但是卻無形地融入了代碼之中,只有慢慢體會才能發(fā)現(xiàn)它的存在。
具體的代碼如下,實現(xiàn)的功能是按一次K1或者K2按鍵,就觸發(fā)一次蜂鳴器鳴叫。
1#include "REG52.H" 2 3#define KEY_VOICE_TIME 50 //按鍵觸發(fā)后發(fā)出的聲音長度 4#define KEY_FILTER_TIME 25 //按鍵濾波的“穩(wěn)定時間”25ms 5 6void T0_time(); 7void SystemInitial(void) ; 8void Delay(unsigned long u32DelayTime) ; 9void PeripheralInitial(void) ; 10 11void BeepOpen(void); 12void BeepClose(void); 13void VoiceScan(void); 14void KeyScan(void); //按鍵識別的驅(qū)動函數(shù),放在定時中斷里 15void KeyTask(void); //按鍵任務(wù)函數(shù),放在主函數(shù)內(nèi) 16 17sbit P3_4=P3^4; 18sbit KEY_INPUT1=P2^2; //K1按鍵識別的輸入口。 19sbit KEY_INPUT2=P2^1; //K2按鍵識別的輸入口。 20 21volatile unsigned char vGu8BeepTimerFlag=0; 22volatile unsigned int vGu16BeepTimerCnt=0; 23 24volatile unsigned char vGu8KeySec=0; //按鍵的觸發(fā)序號,全局變量意味著是其它函數(shù)的接口。 25 26void main() 27{ 28SystemInitial(); 29Delay(10000); 30PeripheralInitial(); 31 while(1) 32{ 33 KeyTask(); //按鍵任務(wù)函數(shù) 34 } 35} 36 37void T0_time() interrupt 1 38{ 39VoiceScan(); 40KeyScan(); //按鍵識別的驅(qū)動函數(shù) 41 42TH0=0xfc; 43TL0=0x66; 44} 45 46 47void SystemInitial(void) 48{ 49TMOD=0x01; 50TH0=0xfc; 51TL0=0x66; 52EA=1; 53ET0=1; 54TR0=1; 55} 56 57void Delay(unsigned long u32DelayTime) 58{ 59 for(;u32DelayTime>0;u32DelayTime--); 60} 61 62void PeripheralInitial(void) 63{ 64 65} 66 67void BeepOpen(void) 68{ 69P3_4=0; 70} 71 72void BeepClose(void) 73{ 74P3_4=1; 75} 76 77void VoiceScan(void) 78{ 79 80 static unsigned char Su8Lock=0; 81 82if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0) 83 { 84 if(0==Su8Lock) 85 { 86 Su8Lock=1; 87BeepOpen(); 88 } 89 else 90{ 91 92 vGu16BeepTimerCnt--; 93 94 if(0==vGu16BeepTimerCnt) 95 { 96 Su8Lock=0; 97BeepClose(); 98 } 99 100} 101 } 102} 103 104/* 注釋一: 105* 獨立按鍵掃描的詳細過程,以按鍵K1為例,如下: 106* 第一步:平時沒有按鍵被觸發(fā)時,按鍵的自鎖標(biāo)志,去抖動延時計數(shù)器一直被清零。 107* 第二步:一旦有按鍵被按下,去抖動延時計數(shù)器開始在定時中斷函數(shù)里累加,在還沒累加到 108* 閥值KEY_FILTER_TIME時,如果在這期間由于受外界干擾或者按鍵抖動,而使 109* IO口突然瞬間觸發(fā)成高電平,這個時候馬上把延時計數(shù)器Su16KeyCnt1清零了,這個過程 110* 非常巧妙,非常有效地去除瞬間的雜波干擾。以后凡是用到開關(guān)感應(yīng)器的時候, 111* 都可以用類似這樣的方法去干擾。 112* 第三步:如果按鍵按下的時間達到閥值KEY_FILTER_TIME時,則觸發(fā)按鍵,把編號vGu8KeySec賦值。 113* 同時,馬上把自鎖標(biāo)志Su8KeyLock1置1,防止按住按鍵不松手后一直觸發(fā)。 114* 第四步:等按鍵松開后,自鎖標(biāo)志Su8KeyLock1及時清零(解鎖),為下一次自鎖做準備。 115* 第五步:以上整個過程,就是識別按鍵IO口下降沿觸發(fā)的過程。 116*/ 117void KeyScan(void) //此函數(shù)放在定時中斷里每1ms掃描一次 118{ 119 static unsigned char Su8KeyLock1; //1號按鍵的自鎖 120 static unsigned int Su16KeyCnt1; //1號按鍵的計時器 121 static unsigned char Su8KeyLock2; //2號按鍵的自鎖 122 static unsigned int Su16KeyCnt2; //2號按鍵的計時器 123 124 //1號按鍵 125 if(0!=KEY_INPUT1)//IO是高電平,說明按鍵沒有被按下,這時要及時清零一些標(biāo)志位 126 { 127 Su8KeyLock1=0; //按鍵解鎖 128 Su16KeyCnt1=0; //按鍵去抖動延時計數(shù)器清零,此行非常巧妙,是全場的亮點。 129 } 130 else if(0==Su8KeyLock1)//有按鍵按下,且是第一次被按下。這行很多初學(xué)者有疑問,請看專題分析。 131 { 132 Su16KeyCnt1++; //累加定時中斷次數(shù) 133 if(Su16KeyCnt1>=KEY_FILTER_TIME) //濾波的“穩(wěn)定時間”KEY_FILTER_TIME,長度是25ms。 134 { 135 Su8KeyLock1=1; //按鍵的自鎖,避免一直觸發(fā) 136 vGu8KeySec=1; //觸發(fā)1號鍵 137 } 138 } 139 140 //2號按鍵 141 if(0!=KEY_INPUT2) 142 { 143 Su8KeyLock2=0; 144 Su16KeyCnt2=0; 145 } 146 else if(0==Su8KeyLock2) 147 { 148 Su16KeyCnt2++; 149 if(Su16KeyCnt2>=KEY_FILTER_TIME) 150 { 151 Su8KeyLock2=1; 152 vGu8KeySec=2; //觸發(fā)2號鍵 153 } 154 } 155 156 157} 158 159void KeyTask(void) //按鍵任務(wù)函數(shù),放在主函數(shù)內(nèi) 160{ 161if(0==vGu8KeySec) 162{ 163return; //按鍵的觸發(fā)序號是0意味著無按鍵觸發(fā),直接退出當(dāng)前函數(shù),不執(zhí)行此函數(shù)下面的代碼 164} 165 166switch(vGu8KeySec) //根據(jù)不同的按鍵觸發(fā)序號執(zhí)行對應(yīng)的代碼 167{ 168 case 1: //1號按鍵 169 170 vGu8BeepTimerFlag=0; 171vGu16BeepTimerCnt=KEY_VOICE_TIME; //觸發(fā)按鍵后,發(fā)出固定長度的聲音 172 vGu8BeepTimerFlag=1; 173vGu8KeySec=0; //響應(yīng)按鍵服務(wù)處理程序后,按鍵編號必須清零,避免一致觸發(fā) 174break; 175 176 case 2: //2號按鍵 177 178 vGu8BeepTimerFlag=0; 179vGu16BeepTimerCnt=KEY_VOICE_TIME; //觸發(fā)按鍵后,發(fā)出固定長度的聲音 180 vGu8BeepTimerFlag=1; 181vGu8KeySec=0; //響應(yīng)按鍵服務(wù)處理程序后,按鍵編號必須清零,避免一致觸發(fā) 182break; 183 184} 185}
【92.2 專題分析:else if(0==Su8KeyLock1)?!?/span>
疑問:
1if(0!=KEY_INPUT1) 2 { 3 Su8KeyLock1=0; 4 Su16KeyCnt1=0; 5 } 6 else if(0==Su8KeyLock1)//有按鍵按下,且是第一次被按下。為什么?為什么?為什么? 7 { 8 Su16KeyCnt1++; 9 if(Su16KeyCnt1>KEY_FILTER_TIME) 10 { 11 Su8KeyLock1=1; 12 vGu8KeySec=1; 13 } 14 }
解答:
首先,我們要明白C語言的語法中,
1if(條件1) 2{ 3 4} 5else if(條件2) 6{ 7 8}
以上語句是一對組合語句,不能分開來看。當(dāng)(條件1)成立的時候,它是絕對不會判斷(條件2)的。當(dāng)(條件1)不成立的時候,才會判斷(條件2)。
回到剛才的問題,當(dāng)程序執(zhí)行到(條件2) else if(0==Su8KeyLock1)的時候,就已經(jīng)默認了(條件1) if(0!=KEY_INPUT1)不成立,這個條件不成立,就意味著0==KEY_INPUT1,也就是有按鍵被按下,因此,這里的else if(0==Su8KeyLock1)等效于else if(0==Su8KeyLock1&&0==KEY_INPUT1),而Su8KeyLock1是一個自鎖標(biāo)志位,一旦按鍵被觸發(fā)后,這個標(biāo)志位會變1,防止按鍵按住不松手的時候不斷觸發(fā)按鍵。這樣,按鍵只能按一次觸發(fā)一次,松開手后再按一次,又觸發(fā)一次。
【92.3 專題分析:if(0!=KEY_INPUT1)?!?/span>
疑問:為什么不用if(1==KEY_INPUT1)而用if(0!=KEY_INPUT1)?
解答:其實兩者在功能上是完全等效的,在這里都可以用。之所以本教程優(yōu)先選用后者if(0!=KEY_INPUT1),是因為考慮到了代碼在不同單片機平臺上的可移植性和兼容性。很多32位的單片機提供的是庫函數(shù),庫函數(shù)返回的按鍵狀態(tài)是一個字節(jié)變量來表示,當(dāng)被按下的時候是0,但是,當(dāng)沒有按下的時候并不一定等于1,而是一個“非0”的數(shù)值。
【92.4 專題分析:把KeyScan函數(shù)放在定時器中斷里?!?/span>
疑問:為什么把KeyScan函數(shù)放在定時器中斷里?
解答:中斷函數(shù)里放的函數(shù)或者代碼越少越好,但是KeyScan函數(shù)是特殊的函數(shù),是涉及到IO口輸入信號的濾波,濾波就涉及到時間的及時性與均勻性,放在定時中斷函數(shù)里更加能保證時間的一致性。比如,蜂鳴器驅(qū)動,動態(tài)數(shù)碼管驅(qū)動,按鍵掃描驅(qū)動,我個人都習(xí)慣放在定時中斷函數(shù)里。
【92.5 專題分析:if(0==vGu8KeySec)return。】
疑問:if(0==vGu8KeySec)return是不是多此一舉?
解答:在KeyTask函數(shù)這里,if(0==vGu8KeySec)return這行代碼刪掉,對程序功能是沒有影響的,這里之所以多插入這行判斷語句,是因為,當(dāng)按鍵多達幾十個的時候,避免主函數(shù)每次進入KeyTask函數(shù),都挨個掃描判斷switch的狀態(tài)進行多次判斷,如果增加了這行if(0==vGu8KeySec)return代碼,就可以直接退出省事,在理論上感覺更加運行高效。其實,不同單片機不同的C編譯器可能對switch語句的翻譯不一樣,因此,這里的是不是更加高效我不敢保證。但是可以保證的是,加了這行代碼也沒有其它副作用。
長期商務(wù)合作服務(wù):
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!