基於狀態轉移的獨立按鍵程序設計

從網上搜集而來,出處不可考。感謝原做者。函數

對於按鍵處理講得很透徹。 
oop


 從這一章開始,咱們步入按鍵程序設計的殿堂。在基於單片機爲核心構成的應用系統中,用戶輸入是必不可少的一部分。輸入能夠分不少種狀況,譬若有的系統支持PS2鍵盤的接口,有的系統輸入是基於編碼器,有的系統輸入是基於串口或者USB或者其它輸入通道等等。在各類輸入途徑中,更常見的是,基於單個按鍵或者由單個鍵盤按照必定排列構成的矩陣鍵盤(行列鍵盤)。咱們這一篇章主要討論的對象就是基於單個按鍵的程序設計,以及矩陣鍵盤的程序編寫。 學習

◎按鍵檢測的原理 
常見的獨立按鍵的外觀以下,相信你們並不陌生,各類常見的開發板學習板上隨處能夠看到他們的身影。ui




   
     總共有四個引腳,通常狀況下,處於同一邊的兩個引腳內部是鏈接在一塊兒的,如何分辨兩個引腳是否處在同一邊呢?能夠將按鍵翻轉過來,處於同一邊的兩個引腳,有一條突起的線將他們鏈接一塊兒,以標示它們倆是相連的。若是沒法觀察獲得,用數字萬用表的二極管擋位檢測一下便可。搞清楚這點很是重要,對於咱們畫PCB的時候的封裝頗有益。 
它們和咱們的單片機系統的I/O口鏈接通常以下:編碼





     對於單片機I/O內部有上拉電阻的微控制器而言,還能夠省掉外部的那個上拉電阻。簡單分析一下按鍵檢測的原理。當按鍵沒有按下的時候,單片機I/O經過上拉電阻R接到VCC,咱們在程序中讀取該I/O的電平的時候,其值爲1(高電平); 當按鍵S按下的時候,該I/O被短接到GND,在程序中讀取該I/O的電平的時候,其值爲0(低電平) 。這樣,按鍵的按下與否,就和與該按鍵相連的I/O的電平的變化相對應起來。結論:咱們在程序中經過檢測到該I/O口電平的變化與否,便可以知道按鍵是否被按下,從而作出相應的響應。一切看起來很美好,是這樣的嗎? 

◎現實並不是理想 
在咱們經過上面的按鍵檢測原理得出上述的結論的時候,其實忽略了一個重要的問題,那就是現實中按鍵按下時候的電平變化狀態。咱們的結論是基於理想的狀況得出來的,就如同下面這幅按鍵按下時候對應電平變化的波形圖同樣:spa





     而實際中,因爲按鍵的彈片接觸的時候,並非一接觸就牢牢的閉合,它還存在必定的抖動,儘管這個時間很是的短暫,可是對於咱們執行時間以us爲計算單位的微控制器來講, 
它太漫長了。於是,實際的波形圖應該以下面這幅示意圖同樣。設計






這樣便存在這樣一個問題。假設咱們的系統有這樣功能需求:在檢測到按鍵按下的時候,將某個I/O的狀態取反。因爲這種抖動的存在,使得咱們的微控制器誤覺得是屢次按鍵的按下,從而將某個I/O的狀態不斷取反,這並非咱們想要的效果,假如該I/O控制着系統中某個重要的執行的部件,那結果更不是咱們所期待的。因而乎有人便提出了軟件消除抖動的思想,道理很簡單:抖動的時間長度是必定的,只要咱們避開這段抖動時期,檢測穩定的時候的電平不久能夠了嗎?聽起來確實不錯,並且實際應用起來效果也還能夠。因而,各類各樣的書籍中,在提到按鍵檢測的時候,總也不忘說道軟件消抖。就像下面的僞代碼所描述的同樣。(假設按鍵按下時候,低電平有效) 


If(0 == io_KeyEnter)             //若是有鍵按下了 

     Delayms(20) ;             //先延時20ms避開抖動時期 
     If(0 == io_KeyEnter)         //而後再檢測,若是仍是檢測到有鍵按下 
     { 
         return KeyValue ;           //是真的按下了,返回鍵值 
     } 
     else 

     return KEY_NULL         //是抖動,返回空的鍵值 

     while(0 == io_KeyEnter) ;     //等待按鍵釋放 


乍看上去,確實挺不錯,實際中呢?在實際的系統中,通常是不容許這麼樣作的。爲何呢?首先,這裏的Delayms(20) , 讓微控制器在這裏白白等待了20 ms 的時間,啥也沒幹,考慮我在《學會釋放CPU》一章中所說起的幾點,這是不可取的。其次while(0 == io_KeyEnter) 因此合理的分配好微控制的處理時間,是編寫按鍵程序的基礎。J;更是程序設計中的大忌(極少的特殊狀況例外)。任何非極端狀況下,都不要使用這樣語句來堵塞微控制器的執行進程。本來是等待按鍵釋放,結果CPU就一直死死的盯住該按鍵,其它事情都無論了,那其它事情不幹了嗎?你贊成別人可不會贊成 


◎消除抖動有必要嗎? 

的確,軟件上的消抖確實能夠保證按鍵的有效檢測。可是,這種消抖確實有必要嗎?有人提出了這樣的疑問。抖動是按鍵按下的過程當中產生的,若是按鍵沒有按下,抖動會產生嗎?若是沒有按鍵按下,抖動也會在I/O上出現,我會馬上把這個微控制器錘了,永遠不用這樣一款微控制器。因此抖動的出現即意味着按鍵已經按下,儘管這個電平尚未穩定。因此只要咱們檢測到按鍵按下,便可以返回鍵值,問題的關鍵是,在你執行完其它任務的時候,再次執行咱們的按鍵任務的時候,抖動過程尚未結束,這樣便有可能形成重複檢測。因此,如何在返回鍵值後,避免重複檢測,或者在按鍵一按下就執行功能函數,當功能函數的執行時間小於抖動時間時候,如何避免再次執行功能函數,就成爲咱們要考慮的問題了。這是一個仁者見仁,智者見智的問題,就留給你們去思考吧。因此消除抖動的目的是:防止按鍵一次按下,屢次響應。 

        「從單片機初學者邁向單片機工程師」之KEY主題討論 

                                          基於狀態轉移的獨立按鍵程序設計 


     本章所描述的按鍵程序要達到的目的:檢測按鍵按下,短按,長按,釋放。即經過按鍵的返回值咱們能夠獲取到以下的信息:按鍵按下(短按),按鍵長按,按鍵連_發,按鍵釋放。不知道你們還記得小時候玩過的電子鐘沒有,就是外形相似於CALL 機(CALL )的那種,有一個小液晶屏,還有四個按鍵,功能是時鐘,鬧鐘以及秒錶。在調整時間的時候,短按+鍵每次調整值加一,長按的時候調整值連續增長。小的時候很好奇,這樣的功能究竟是如何實現的呢,今天就讓咱們來剖析它的原理吧。J機,好像是很古老的東西了 
狀態在生活中隨處可見。譬如早上的時候,鬧鐘把你叫醒了,這個時候,你便處於清醒的狀態,立刻你就穿衣起牀洗漱吃早餐,這一系列事情就是你在這個狀態作的事情。作完這些後你會去等車或者開車去上班,這個時候你就處在上班途中的狀態…..中午下班時間到了,你就處於中午下班的狀態,諸如此類等等,在每個狀態咱們都會作一些不一樣的事情,而總會有外界條件促使咱們轉換到另一種狀態,譬如鬧鐘叫醒咱們了,下班時間到了等等。對於狀態的定義出發點不一樣,考慮的方向不一樣,或者會有些許細節上面的差別,可是大的狀態老是相同的。生活中的事物一樣遵循一樣的規律,譬如,用一個智能充電器給你的手機電池充電,剛開始,它是處於快速充電狀態,隨着電量的增長,電壓的升高,當達到規定的電壓時候,它會轉換到恆壓充電。總而言之,細心觀察,你會發現生活中的總總均可以歸結爲一個個的狀態,而狀態的變換或者轉移老是由某些條件引發同時伴隨着一些動做的發生。咱們的按鍵亦遵循一樣的規律,下面讓咱們來簡單的描繪一下它的狀態流程轉移圖。對象

 



   下面對上面的流程圖進行簡要的分析。 
首先按鍵程序進入初始狀態S1,在這個狀態下,檢測按鍵是否按下,若是有按下,則進入按鍵消抖狀態2,在下一次執行按鍵程序時候,直接由按鍵消抖狀態進入按鍵按下狀態3,在此狀態下檢測按鍵是否按下,若是沒有按鍵按下,則返回初始狀態S1,若是有則能夠返回鍵值,同時進入長按狀態S4,在長按狀態下每次進入按鍵程序時候對按鍵時間計數,當計數值超過設定閾值時候,則代表長按事件發生,同時進入按鍵連_髮狀態S5。若是按鍵鍵值爲空鍵,則返回按鍵釋放狀態S6,不然繼續停留在本狀態。在按鍵連_髮狀態下,若是按鍵鍵值爲空鍵則返回按鍵釋放狀態S6,若是按鍵時間計數超過連_發閾值,則返回連_發按鍵值,清零時間計數後繼續停留在本狀態。 
看了這麼多,也許你已經有一個模糊的概念了,下面讓咱們趁熱打鐵,一塊兒來動手編寫按鍵驅動程序吧。 
   下面是我使用的硬件的鏈接圖。 接口



   
   硬件鏈接很簡單,四個獨立按鍵分別接在P3^0------P3^3四個I/O上面。 
由於51單片機I/O口內部結構的限制,在讀取外部引腳狀態的時候,須要向端口寫1.在51單片機復位後,不須要進行此操做也能夠進行讀取外部引腳的操做。所以,在按鍵的端口沒有複用的狀況下,能夠省略此步驟。而對於其它一些真正雙向I/O口的單片機來講,將引腳設置成輸入狀態,是必不可少的一個步驟。 
下面的程序代碼初始化引腳爲輸入。 
void KeyInit(void) 

     io_key_1 = 1 ; 
     io_key_2 = 1 ; 
     io_key_3 = 1 ; 
     io_key_4 = 1 ;             

根據按鍵硬件鏈接定義按鍵鍵值 
#define KEY_VALUE_1               0x0e 
#define KEY_VALUE_2               0x0d 
#define KEY_VALUE_3                 0x0b 
#define KEY_VALUE_4                 0x07 
#define KEY_NULL                     0x0f 
下面咱們來編寫按鍵的硬件驅動程序。 
根據第一章所描述的按鍵檢測原理,咱們能夠很容易的得出以下的代碼: 
static uint8 KeyScan(void) 

     if(io_key_1 == 0)return KEY_VALUE_1 ; 
     if(io_key_2 == 0)return KEY_VALUE_2 ; 
     if(io_key_3 == 0)return KEY_VALUE_3 ; 
     if(io_key_4 == 0)return KEY_VALUE_4 ; 
     return KEY_NULL ; 

其中io_key_1等是咱們按鍵端口的定義,以下所示: 
sbit io_key_1 = P3^0 ; 
sbit io_key_2 = P3^1 ; 
sbit io_key_3 = P3^2 ; 
sbit io_key_4 = P3^3 ; 

KeyScan()做爲底層按鍵的驅動程序,爲上層按鍵掃描提供一個接口,這樣咱們編寫的上層按鍵掃描函數能夠幾乎不用修改就能夠拿到咱們的其它程序中去使用,使得程序複用性大大提升。同時,經過有意識的將與底層硬件鏈接緊密的程序和與硬件無關的代碼分開寫,使得程序結構層次清晰,可移植性也更好。對於單片機類的程序而言,可以作到函數級別的代碼重用已經足夠了。 
在編寫咱們的上層按鍵掃描函數以前,須要先完成一些宏定義。 
//定義長按鍵的TICK數,以及連_發間隔的TICK數 
#define KEY_LONG_PERIOD         100 
#define KEY_CONTINUE_PERIOD     25 

//定義按鍵返回值狀態(按下,長按,連_發,釋放) 
#define KEY_DOWN                 0x80 
#define KEY_LONG                     0x40 
#define KEY_CONTINUE             0x20 
#define KEY_UP                   0x10 

//定義按鍵狀態 
#define KEY_STATE_INIT             0 
#define KEY_STATE_WOBBLE             1 
#define KEY_STATE_PRESS             2 
#define KEY_STATE_LONG             3 
#define KEY_STATE_CONTINUE       4 
#define KEY_STATE_RELEASE         5 

接着咱們開始編寫完整的上層按鍵掃描函數,按鍵的短按,長按,連按,釋放等等狀態的判斷均是在此函數中完成。對照狀態流程轉移圖,而後再看下面的函數代碼,能夠更容易的去理解函數的執行流程。完整的函數代碼以下: 

void GetKey(uint8 *pKeyValue) 

     static uint8 s_u8KeyState = KEY_STATE_INIT ; 
     static uint8 s_u8KeyTimeCount = 0 ; 
     static uint8 s_u8LastKey = KEY_NULL ;   //保存按鍵釋放時候的鍵值 
     uint8 KeyTemp = KEY_NULL ; 

     KeyTemp = KeyScan() ;         //獲取鍵值 

     switch(s_u8KeyState) 
     { 
         case KEY_STATE_INIT : 
                 { 
                     if(KEY_NULL != (KeyTemp)) 
                     { 
                         s_u8KeyState = KEY_STATE_WOBBLE ; 
                     } 
                 } 
         break ; 

         case KEY_STATE_WOBBLE :       //消抖 
                 { 
                     s_u8KeyState = KEY_STATE_PRESS ;     
                 } 
         break ; 

         case KEY_STATE_PRESS : 
                 { 
                     if(KEY_NULL != (KeyTemp)) 
                     { 
                         s_u8LastKey = KeyTemp ; //保存鍵值,以便在釋放按鍵狀態返回鍵值 
                         KeyTemp |= KEY_DOWN ;   //按鍵按下 
                         s_u8KeyState = KEY_STATE_LONG ; 
                     } 
                     else 
                     { 
                         s_u8KeyState = KEY_STATE_INIT ; 
                     } 
                 } 
         break ; 

         case KEY_STATE_LONG : 
                 { 
                     if(KEY_NULL != (KeyTemp)) 
                     { 
                         if(++s_u8KeyTimeCount > KEY_LONG_PERIOD) 
                         { 
                             s_u8KeyTimeCount = 0 ; 
                             KeyTemp |= KEY_LONG ;   //長按鍵事件發生 
                             s_u8KeyState = KEY_STATE_CONTINUE ; 
                         } 
                     } 
                     else 
                     { 
                         s_u8KeyState = KEY_STATE_RELEASE ; 
                     } 
                 } 
         break ; 

         case KEY_STATE_CONTINUE : 
                 { 
                     if(KEY_NULL != (KeyTemp)) 
                     { 
                         if(++s_u8KeyTimeCount > KEY_CONTINUE_PERIOD) 
                         { 
                             s_u8KeyTimeCount = 0 ; 
                             KeyTemp |= KEY_CONTINUE ; 
                         } 
                     } 
                     else 
                     { 
                         s_u8KeyState = KEY_STATE_RELEASE ; 
                     } 
                 } 
         break ; 

         case KEY_STATE_RELEASE : 
                 { 
                     s_u8LastKey |= KEY_UP ; 
                     KeyTemp = s_u8LastKey ; 
                     s_u8KeyState = KEY_STATE_INIT ; 
                 } 
         break ; 

         default : break ; 
     } 
     *pKeyValue = KeyTemp ; //返回鍵值     

關於這個函數內部的細節我並不打算花過多筆墨去講解。對照着按鍵狀態流程轉移圖,而後去看程序代碼,你會發現其實思路很是清晰。最能讓人理解透徹的,莫非就是將整個程序本身看懂,而後想象爲何這個地方要這樣寫,抱着思考的態度去閱讀程序,你會發現本身的程序水平會慢慢的提升。因此我更但願的是你可以認認真真的看完,而後思考。也許你會收穫更多。 
無論怎麼樣,這樣的一個程序已經完成了本章開始時候要求的功能:按下,長按,連按,釋放。事實上,若是掌握了這種基於狀態轉移的思想,你會發現要求實現其它按鍵功能,譬如,多鍵按下,功能鍵等等,亦至關簡單,在下一章,咱們就去實現它。 
在主程序中我編寫了這樣的一段代碼,來演示我實現的按鍵功能。 
void main(void) 
{     
     uint8 KeyValue = KEY_NULL; 
     uint8 temp = 0 ; 
       LED_CS11 = 1 ; //流水燈輸出容許 
     LED_SEG = 0 ; 
     LED_DIG = 0 ; 
     Timer0Init() ; 
     KeyInit() ; 
     EA = 1 ; 
     while(1) 
     { 
         Timer0MainLoop() ; 
         KeyMainLoop(&KeyValue) ; 
         
         if(KeyValue == (KEY_VALUE_1 | KEY_DOWN)) P0 = ~1 ; 
         if(KeyValue == (KEY_VALUE_1 | KEY_LONG)) P0 = ~2 ; 
         if(KeyValue == (KEY_VALUE_1 | KEY_CONTINUE)) { P0 ^= 0xf0;} 
         if(KeyValue == (KEY_VALUE_1 | KEY_UP)) P0 = 0xa5 ; 
     } 


     按住第一個鍵,能夠清晰的看到P0口所接的LED的狀態的變化。當按鍵按下時候,第一個LED燈亮,等待2 S後第二個LED亮,第一個熄滅,表示長按事件發生。再過500 ms 第5~8個LED閃爍,表示連按事件發生。當釋放按鍵時候,P0口所接的LED的狀態爲: 
滅亮滅亮亮滅亮滅,這也正是P0 = 0xa5這條語句的功能。進程

相關文章
相關標籤/搜索