前言
按鍵處理是學習單片機的必修課之一。一次按鍵的過程,並非是一個理想的有一定寬度的電平脈沖,而是在按下、彈起過程中存在抖動,只有在中間階段電平信號是穩定的。一次典型的按鍵過程是醬紫的:
在抖動過程中,電平信號高低反復變化,如果你的按鍵檢測是檢測下降沿或上升沿或者是用外部中斷檢測按鍵,都可能在抖動時重復檢測到多次按鍵。這就是在未消抖的按一次鍵顯示值加1的程序中,出現按一次鍵顯示值+2、+3甚至加更多的原因。
對於按鍵消抖,常用的有硬件消抖和軟件消抖。本文是我個人對按鍵處理的一些常見方法的總結,由於我本人不太懂硬件,所以這里只討論獨立按鍵的軟件消抖實現。水平有限,如有錯誤請不吝指正。
硬件環境
本文代碼均在單片機STC90C516RD+、晶振12.0MHz硬件環境下試驗通過。
帶消抖的簡單的按鍵處理
最簡單的消抖處理就是在首次檢測到電平變化后加一個延時,等待抖動停止后再次檢測電平信號。這也是大多數單片機教程講述的消抖方式。但在實際應用中基本不用這種方式,原因后面講,先看代碼:

//方法一:帶消抖的簡單的按鍵處理 #include <reg52.h> #define GPIO_KEY P1 //8個獨立按鍵IO口 #define GPIO_LED P0 //8個LED燈,用於顯示鍵值 unsigned char ScanKey(); void DelayXms(unsigned char x); void main() { unsigned char key; GPIO_LED = 0x00; //初始化LED while (1) { key = ScanKey(); //讀取鍵值 // if (0xff != key) //若有鍵按下,則更新LED的狀態 GPIO_LED = ~key; //點亮LED } } unsigned char ScanKey() { unsigned char keyValue = 0xff; //賦初值,0xff表示沒有鍵按下 GPIO_KEY = 0xff; //給按鍵IO口置位 if (0xff != GPIO_KEY) //檢查按鍵IO口的電平,如有鍵按下則不為0xff { DelayXms(15); //延時15ms,濾掉抖動。一般按鍵的抖動時間在10ms~20ms if (0xff != GPIO_KEY) //再次檢查按鍵IO口的電平 { keyValue = GPIO_KEY; //重復檢測后表明有鍵按下,讀取鍵值 } // while (0xff != GPIO_KEY) ; //等待按鍵彈起 } return keyValue; } void DelayXms(unsigned char X) { unsigned char i, j; do { i = 2; j = 240; do { while (--j); } while (--i); } while (--X); }
可以看到,在首次檢測到電平由1->0后,延時了10ms,等待抖動過去,然后再檢測按鍵的電平。你也許已經注意到了,在延時10ms期間,單片機閑置了,就暫停在那里等待延時完成,這對處理能力本就緊張的單片機來說無疑是個巨大的浪費。特別是當你在用單片機同時運行數碼管動態掃描等對時序要求高功能的時候,按鍵消抖延時期間程序暫停了,數碼管也就熄滅了,嚴重影響顯示效果。
利用定時器消抖的按鍵處理
為了避免單純Delay()消抖所產生的問題,可以采用定時器來進行延時,這樣就不用讓單片機在那里干等了。
一種簡單的實現方式是設置一個全局狀態變量,用來標志定時器延時時間已到,在第一次檢測到電平變化時開啟定時器,檢測到定時器延時時間已到時關閉定時器並再次進行按鍵檢測。代碼如下:

//方法二:定時器延時消抖的按鍵處理 #include <reg52.h> #define GPIO_KEY P1 //8個獨立按鍵IO口 #define GPIO_LED P0 //8個LED燈,用於顯示鍵值 unsigned char timeUp = 0; //標志位 unsigned char th0Value = (65536 - 15000) / 256; //15ms的定時器初值高8位 unsigned char tl0Value = (65536 - 15000) % 256; //15ms的定時器初值低8位 unsigned char ScanKey(); void InitialTimer0(); void main() { unsigned char key; GPIO_LED = 0x00; //初始化LED InitialTimer0(); while (1) { key = ScanKey(); //讀取鍵值 if (0xff != key) //若有鍵按下,則更新LED的狀態 GPIO_LED = ~key; //點亮LED } } void InitialTimer0() //12MHz { ET0 = 1; //打開定時器0 EA = 1; //打開系統總中斷開關 TMOD &= 0xF0; //清空定時器0的工作模式參數 TMOD |= 0x01; //設置定時器0的工作模式為模式1,16位定時器 TH0 = th0Value; //設置定時高8位初值 TL0 = tl0Value; //設置定時低8位初值 TF0 = 0; //清除TF0溢出標志 TR0 = 0; //關閉定時器0 } void Timer0Interrupt() interrupt 1 { TH0 = th0Value; //設置定時高8位初值 TL0 = tl0Value; //設置定時低8位初值 timeUp = 1; //定時器標志位置1 } unsigned char ScanKey() { unsigned char keyValue = 0xff; //賦初值,0xff表示沒有鍵按下 if (0 == TR0 && 0xff != GPIO_KEY) { timeUp = 0; //定時器標志位置0 TH0 = th0Value; //設置定時高8位初值 TL0 = tl0Value; //設置定時低8位初值 TR0 = 1; //開啟定時器0,開始計時 } if (1 == timeUp) { TR0 = 0; //關閉定時器0 keyValue = GPIO_KEY; //讀取鍵值 } return keyValue; }
另一種方法是利用定時器0,每2ms左右中斷一次,在中斷服務程序中進行多次按鍵檢測,當檢測到10次按鍵按下狀態時,則認為發生了一次有效的按鍵按下動作。這種方式與上一種相比,進行了多次檢測,提高了按鍵檢測的准確性。實現代碼如下:

//方法三:定時器多次檢測的按鍵處理 #include <reg52.h> #define GPIO_KEY P1 //8個獨立按鍵IO口 #define GPIO_LED P0 //8個LED燈,用於顯示鍵值 unsigned char keyCur = 0xff; //暫存當前鍵值 unsigned char keyPress = 0; //按鍵按下狀態標識 unsigned char th0Value = (65536 - 2000) / 256; //2ms的定時器初值高8位 unsigned char tl0Value = (65536 - 2000) % 256; //2ms的定時器初值低8位 unsigned char ScanKey(); void InitialTimer0(); void main() { unsigned char key; GPIO_LED = 0x00; //初始化LED keyCur = 0xff; //初始化 InitialTimer0(); while (1) { key = ScanKey(); //讀取鍵值 if (0xff != key) //若有鍵按下,則更新LED的狀態 GPIO_LED = ~key; //點亮LED } } void InitialTimer0() //12MHz { ET0 = 1; //打開定時器0 EA = 1; //打開系統總中斷開關 TMOD &= 0xF0; //清空定時器0的工作模式參數 TMOD |= 0x01; //設置定時器0的工作模式為模式1,16位定時器 TH0 = th0Value; //設置定時高8位初值 TL0 = tl0Value; //設置定時低8位初值 TF0 = 0; //清除TF0溢出標志 TR0 = 1; //開啟定時器0 } void Timer0Interrupt() interrupt 1 { static unsigned char counter = 0; //輔助計數 static unsigned char keyLast = 0xff; //記錄上一次掃描時的鍵值 TH0 = th0Value; //設置定時高8位初值 TL0 = tl0Value; //設置定時低8位初值 keyCur = GPIO_KEY; //暫存當前鍵值 if (0xff != keyCur && keyCur == keyLast) //當前掃描時有鍵按下且與上一次按下的一致,則累加 counter ++; else { counter = 0; keyLast = keyCur; } if (10 == counter) //連續10次均有鍵按下且按按鍵未變,則認為時一次有效的按鍵 keyPress = 1; else keyPress = 0; } unsigned char ScanKey() { if (1 == keyPress) return keyCur; //讀取鍵值 else return 0xff; }
至此,簡單的按鍵單擊實現實現告一段落。但往往實際中,我們不只要實現單擊,還要實現雙擊、長按、連發等等功能,特別是在那些小尺寸、無法設置多個按鍵的項目中,一個按鍵往往需要通過不同的操作實現不同的功能。要實現這些復雜的功能,就需要引入一種設計模式——有限狀態機模式。敬請期待下一篇:單片機按鍵處理方式(二)——狀態機按鍵實現單擊、雙擊、長按、連發(挖坑,待填)
歡迎關注本人的個人博客YoungCoding.top