吳大拿 從單片機基礎到程序框架 資料下載
“非阻塞”,在處理消抖的時候,必須用到延時,如果此時用阻塞的delay延時就會影響其它任務的運行效率,因此,用非阻塞的定時延時更加有優越性。 “清零式濾波”,在消抖的時候,有兩種境界,第一種境界是判斷兩次電平的狀態,中間插入“固定的時間”延時,這種方法前后一共判斷了兩次,第一次是識別到低電平就進入延時的狀態,第二次是延時后再確認一次是否繼續是低電平的狀態,這種方法的不足是,“固定的時間”全憑經驗值,但是不同的按鍵它們的抖動時間長度是不同的,除此之外,前后才判斷了兩次,在軟件的抗干擾能力上也弱了很多,“密碼等級”不夠高。第二種境界就是“清零式濾波”,“清零式濾波”非常巧妙,抗擾能力超強,它能自動過濾不同按鍵的“抖動時間”,然后再進入一個“穩定時間”的“N次識別判斷”,更加巧妙的是,在“抖動時間”和“穩定時間”兩者時間內,只要發現一次是高電平的干擾,就馬上自動清零計時器,重新開始計時。“穩定時間”一般取20ms到30ms之間,而“抖動時間”是隱藏的,在代碼上并沒有直接描寫出來,但是卻無形地融入了代碼之中,只有慢慢體會才能發現它的存在。 具體的代碼如下,實現的功能是按一次K1或者K2按鍵,就觸發一次蜂鳴器鳴叫。 #include "REG52.H" #define KEY_VOICE_TIME 50 //按鍵觸發后發出的聲音長度 #define KEY_FILTER_TIME 25 //按鍵濾波的“穩定時間”25ms void T0_time(); void SystemInitial(void) ; void Delay(unsigned long u32DelayTime) ; void PeripheralInitial(void) ; void BeepOpen(void); void BeepClose(void); void VoiceScan(void); void KeyScan(void); //按鍵識別的驅動函數,放在定時中斷里 void KeyTask(void); //按鍵任務函數,放在主函數內 sbit P3_4=P3^4; sbit KEY_INPUT1=P2^2; //K1按鍵識別的輸入口。 sbit KEY_INPUT2=P2^1; //K2按鍵識別的輸入口。 volatile unsigned char vGu8BeepTimerFlag=0; volatile unsigned int vGu16BeepTimerCnt=0; volatile unsigned char vGu8KeySec=0; //按鍵的觸發序號,全局變量意味著是其它函數的接口。 void main() { SystemInitial(); Delay(10000); PeripheralInitial(); while(1) { KeyTask(); //按鍵任務函數 } } void T0_time() interrupt 1 { VoiceScan(); KeyScan(); //按鍵識別的驅動函數 TH0=0xfc; TL0=0x66; } void SystemInitial(void) { TMOD=0x01; TH0=0xfc; TL0=0x66; EA=1; ET0=1; TR0=1; } void Delay(unsigned long u32DelayTime) { for(;u32DelayTime>0;u32DelayTime--); } void PeripheralInitial(void) {
} void BeepOpen(void) { P3_4=0; } void BeepClose(void) { P3_4=1; } void VoiceScan(void) { static unsigned char Su8Lock=0; if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0) { if(0==Su8Lock) { Su8Lock=1; BeepOpen(); } else { vGu16BeepTimerCnt--; if(0==vGu16BeepTimerCnt) { Su8Lock=0; BeepClose(); } } } } /* 注釋一: * 獨立按鍵掃描的詳細過程,以按鍵K1為例,如下: * 第一步:平時沒有按鍵被觸發時,按鍵的自鎖標志,去抖動延時計數器一直被清零。 * 第二步:一旦有按鍵被按下,去抖動延時計數器開始在定時中斷函數里累加,在還沒累加到 * 閥值KEY_FILTER_TIME時,如果在這期間由于受外界干擾或者按鍵抖動,而使 * IO口突然瞬間觸發成高電平,這個時候馬上把延時計數器Su16KeyCnt1清零了,這個過程 * 非常巧妙,非常有效地去除瞬間的雜波干擾。以后凡是用到開關感應器的時候, * 都可以用類似這樣的方法去干擾。 * 第三步:如果按鍵按下的時間達到閥值KEY_FILTER_TIME時,則觸發按鍵,把編號vGu8KeySec賦值。 * 同時,馬上把自鎖標志Su8KeyLock1置1,防止按住按鍵不松手后一直觸發。 * 第四步:等按鍵松開后,自鎖標志Su8KeyLock1及時清零(解鎖),為下一次自鎖做準備。 * 第五步:以上整個過程,就是識別按鍵IO口下降沿觸發的過程。 */ void KeyScan(void) //此函數放在定時中斷里每1ms掃描一次 { static unsigned char Su8KeyLock1; //1號按鍵的自鎖 static unsigned int Su16KeyCnt1; //1號按鍵的計時器 static unsigned char Su8KeyLock2; //2號按鍵的自鎖 static unsigned int Su16KeyCnt2; //2號按鍵的計時器
//1號按鍵 if(0!=KEY_INPUT1)//IO是高電平,說明按鍵沒有被按下,這時要及時清零一些標志位 { Su8KeyLock1=0; //按鍵解鎖 Su16KeyCnt1=0; //按鍵去抖動延時計數器清零,此行非常巧妙,是全場的亮點。 } else if(0==Su8KeyLock1)//有按鍵按下,且是第一次被按下。這行很多初學者有疑問,請看專題分析。 { Su16KeyCnt1++; //累加定時中斷次數 if(Su16KeyCnt1>=KEY_FILTER_TIME) //濾波的“穩定時間”KEY_FILTER_TIME,長度是25ms。 { Su8KeyLock1=1; //按鍵的自鎖,避免一直觸發 vGu8KeySec=1; //觸發1號鍵 } } //2號按鍵 if(0!=KEY_INPUT2) { Su8KeyLock2=0; Su16KeyCnt2=0; } else if(0==Su8KeyLock2) { Su16KeyCnt2++; if(Su16KeyCnt2>=KEY_FILTER_TIME) { Su8KeyLock2=1; vGu8KeySec=2; //觸發2號鍵 } } } void KeyTask(void) //按鍵任務函數,放在主函數內 { if(0==vGu8KeySec) { return; //按鍵的觸發序號是0意味著無按鍵觸發,直接退出當前函數,不執行此函數下面的代碼 } switch(vGu8KeySec) //根據不同的按鍵觸發序號執行對應的代碼 { case 1: //1號按鍵 vGu8BeepTimerFlag=0; vGu16BeepTimerCnt=KEY_VOICE_TIME; //觸發按鍵后,發出固定長度的聲音 vGu8BeepTimerFlag=1; vGu8KeySec=0; //響應按鍵服務處理程序后,按鍵編號必須清零,避免一直觸發 break; case 2: //2號按鍵 vGu8BeepTimerFlag=0; vGu16BeepTimerCnt=KEY_VOICE_TIME; //觸發按鍵后,發出固定長度的聲音 vGu8BeepTimerFlag=1; vGu8KeySec=0; //響應按鍵服務處理程序后,按鍵編號必須清零,避免一直觸發 break; } } 【92.2 專題分析:else if(0==Su8KeyLock1)。】 疑問: if(0!=KEY_INPUT1) { Su8KeyLock1=0; Su16KeyCnt1=0; } else if(0==Su8KeyLock1)//有按鍵按下,且是第一次被按下。為什么?為什么?為什么? { Su16KeyCnt1++; if(Su16KeyCnt1>KEY_FILTER_TIME) { Su8KeyLock1=1; vGu8KeySec=1; } } 解答: 首先,我們要明白C語言的語法中, if(條件1) { } else if(條件2) { } 以上語句是一對組合語句,不能分開來看。當(條件1)成立的時候,它是絕對不會判斷(條件2)的。當(條件1)不成立的時候,才會判斷(條件2)。 回到剛才的問題,當程序執行到(條件2) else if(0==Su8KeyLock1)的時候,就已經默認了(條件1) if(0!=KEY_INPUT1)不成立,這個條件不成立,就意味著0==KEY_INPUT1,也就是有按鍵被按下,因此,這里的else if(0==Su8KeyLock1)等效于else if(0==Su8KeyLock1&&0==KEY_INPUT1),而Su8KeyLock1是一個自鎖標志位,一旦按鍵被觸發后,這個標志位會變1,防止按鍵按住不松手的時候不斷觸發按鍵。這樣,按鍵只能按一次觸發一次,松開手后再按一次,又觸發一次。 【92.3 專題分析:if(0!=KEY_INPUT1)。】 疑問:為什么不用if(1==KEY_INPUT1)而用if(0!=KEY_INPUT1)? 解答:其實兩者在功能上是完全等效的,在這里都可以用。之所以本教程優先選用后者if(0!=KEY_INPUT1),是因為考慮到了代碼在不同單片機平臺上的可移植性和兼容性。很多32位的單片機提供的是庫函數,庫函數返回的按鍵狀態是一個字節變量來表示,當被按下的時候是0,但是,當沒有按下的時候并不一定等于1,而是一個“非0”的數值。
【92.4 專題分析:把KeyScan函數放在定時器中斷里。】 疑問:為什么把KeyScan函數放在定時器中斷里? 解答:中斷函數里放的函數或者代碼越少越好,但是KeyScan函數是特殊的函數,是涉及到IO口輸入信號的濾波,濾波就涉及到時間的及時性與均勻性,放在定時中斷函數里更加能保證時間的一致性。比如,蜂鳴器驅動,動態數碼管驅動,按鍵掃描驅動,我個人都習慣放在定時中斷函數里。 【92.5 專題分析:if(0==vGu8KeySec)return。】 疑問:if(0==vGu8KeySec)return是不是多此一舉? 解答:在KeyTask函數這里,if(0==vGu8KeySec)return這行代碼刪掉,對程序功能是沒有影響的,這里之所以多插入這行判斷語句,是因為,當按鍵多達幾十個的時候,避免主函數每次進入KeyTask函數,都挨個掃描判斷switch的狀態進行多次判斷,如果增加了這行if(0==vGu8KeySec)return代碼,就可以直接退出省事,在理論上感覺更加運行高效。其實,不同單片機不同的C編譯器可能對switch語句的翻譯不一樣,因此,這里的是不是更加高效我不敢保證。但是可以保證的是,加了這行代碼也沒有其它副作用。
|