單片機按鍵掃描

新型的按鍵掃描程序 
不過我在網上游逛了很久,也看過不少源程序了,沒有發現這種按鍵處理辦法的蹤跡,所以,我將他共享出來,和廣大同僚們共勉。我非常堅信這種按鍵處理辦法的便捷和高效,你可以移植到任何一種嵌入式處理器上面,因爲C語言強大的可移植性。 
同時,這裏面用到了一些分層的思想,在單片機當中也是相當有用的,也是本文的另外一個重點。 
對於老鳥,我建議直接看那兩個表達式,然後自己想想就會懂的了,也不需要聽我後面的自吹自擂了,我可沒有班門弄斧的意思,hoho~~但是對於新手,我建議將全文看完。因爲這是實際項目中總結出來的經驗,學校裏面學不到的東西。 
以下假設你懂C語言,因爲純粹的C語言描述,所以和處理器平臺無關,你可以在MCS-51,AVR,PIC,甚至是ARM平臺上面測試這個程序性能。當然,我自己也是在多個項目用過,效果非常好的。 
好了,工程人員的習慣,廢話就應該少說,開始吧。以下我以AVR的MEGA8作爲平臺講解,沒有其它原因,因爲我手頭上只有AVR的板子而已沒有51的。用51也可以,只是芯片初始化部分不同,還有寄存器名字不同而已。 
核心算法: 
unsigned char Trg; 
unsigned char Cont; 
void KeyRead( void ) 
{ 
    unsigned char ReadData = PINB^0xff;   // 1 
    Trg = ReadData & (ReadData ^ Cont);      // 2 
    Cont = ReadData;                                // 3 
} 



完了。有沒有一種不可思議的感覺?當然,沒有想懂之前會那樣,想懂之後就會驚歎於這算法的精妙!! 
下面是程序解釋: 
Trg(triger) 代表的是觸發,Cont(continue)代表的是連續按下。 
1:讀PORTB的端口數據,取反,然後送到ReadData 臨時變量裏面保存起來。 
2:算法1,用來計算觸發變量的。一個位與操作,一個異或操作,我想學過C語言都應該懂吧?Trg爲全局變量,其它程序可以直接引用。 
3:算法2,用來計算連續變量。 
看到這裏,有種“知其然,不知其所以然”的感覺吧?代碼很簡單,但是它到底是怎麼樣實現我們的目的的呢?好,下面就讓我們繞開雲霧看青天吧。 
我們最常用的按鍵接法如下:AVR是有內部上拉功能的,但是爲了說明問題,我是特意用外部上拉電阻。那麼,按鍵沒有按下的時候,讀端口數據爲1,如果按鍵按下,那麼端口讀到0。下面就看看具體幾種情況之下,這算法是怎麼一回事。 
(1)       沒有按鍵的時候 
端口爲0xff,ReadData讀端口並且取反,很顯然,就是 0x00 了。 
Trg = ReadData & (ReadData ^ Cont); (初始狀態下,Cont也是爲0的)很簡單的數學計算,因爲ReadData爲0,則它和任何數“相與”,結果也是爲0的。 
Cont = ReadData; 保存Cont 其實就是等於ReadData,爲0; 
結果就是: 
ReadData = 0; 
Trg = 0; 
Cont = 0; 
(2)       第一次PB0按下的情況 
端口數據爲0xfe,ReadData讀端口並且取反,很顯然,就是 0x01 了。 
Trg = ReadData & (ReadData ^ Cont); 因爲這是第一次按下,所以Cont是上次的值,應爲爲0。那麼這個式子的值也不難算,也就是 Trg = 0x01 & (0x01^0x00) = 0x01 
Cont = ReadData = 0x01; 
結果就是: 
ReadData = 0x01; 
Trg = 0x01;Trg只會在這個時候對應位的值爲1,其它時候都爲0 
Cont = 0x01; 
(3)       PB0按着不鬆(長按鍵)的情況 
端口數據爲0xfe,ReadData讀端口並且取反是 0x01 了。 
Trg = ReadData & (ReadData ^ Cont); 因爲這是連續按下,所以Cont是上次的值,應爲爲0x01。那麼這個式子就變成了 Trg = 0x01 & (0x01^0x01) = 0x00 
Cont = ReadData = 0x01; 
結果就是: 
ReadData = 0x01; 
Trg = 0x00; 
Cont = 0x01; 
因爲現在按鍵是長按着,所以MCU會每個一定時間(20ms左右)不斷的執行這個函數,那麼下次執行的時候情況會是怎麼樣的呢? 
ReadData = 0x01;這個不會變,因爲按鍵沒有鬆開 
Trg = ReadData & (ReadData ^ Cont) = 0x01 & (0x01 ^ 0x01) = 0 ,只要按鍵沒有鬆開,這個Trg值永遠爲 0 !!! 
Cont = 0x01;只要按鍵沒有鬆開,這個值永遠是0x01!! 
(4)       按鍵鬆開的情況 
端口數據爲0xff,ReadData讀端口並且取反是 0x00 了。 
Trg = ReadData & (ReadData ^ Cont) = 0x00 & (0x00^0x01) = 0x00 
Cont = ReadData = 0x00; 
結果就是: 
ReadData = 0x00; 
Trg = 0x00; 
Cont = 0x00; 
很顯然,這個回到了初始狀態,也就是沒有按鍵按下的狀態。 
總結一下,不知道想懂了沒有?其實很簡單,答案如下: 
Trg 表示的就是觸發的意思,也就是跳變,只要有按鍵按下(電平從1到0的跳變),那麼Trg在對應按鍵的位上面會置一,我們用了PB0則Trg的值爲0x01,類似,如果我們PB7按下的話,Trg 的值就應該爲 0x80 ,這個很好理解,還有,最關鍵的地方,Trg 的值每次按下只會出現一次,然後立刻被清除,完全不需要人工去幹預。所以按鍵功能處理程序不會重複執行,省下了一大堆的條件判斷,這個可是精粹哦!!Cont代表的是長按鍵,如果PB0按着不放,那麼Cont的值就爲 0x01,相對應,PB7按着不放,那麼Cont的值應該爲0x80,同樣很好理解。 
如果還是想不懂的話,可以自己演算一下那兩個表達式,應該不難理解的。 
因爲有了這個支持,那麼按鍵處理就變得很爽了,下面看應用: 
應用一:一次觸發的按鍵處理 
假設PB0爲蜂鳴器按鍵,按一下,蜂鳴器beep的響一聲。這個很簡單,但是大家以前是怎麼做的呢?對比一下看誰的方便? 
#define KEY_BEEP 0x01 
void KeyProc(void) 
{ 
       if (Trg & KEY_BEEP) // 如果按下的是KEY_BEEP 
    { 
         Beep();            // 執行蜂鳴器處理函數 
    } 
} 


怎麼樣?夠和諧不?記得前面解釋說Trg的精粹是什麼?精粹就是隻會出現一次。所以你按下按鍵的話,Trg & KEY_BEEP 爲“真”的情況只會出現一次,所以處理起來非常的方便,蜂鳴器也不會沒事亂叫,hoho~~~
或者你會認爲這個處理簡單,沒有問題,我們繼續。 
應用2:長按鍵的處理 
項目中經常會遇到一些要求,例如:一個按鍵如果短按一下執行功能A,如果長按2秒不放的話會執行功能B,又或者是要求3秒按着不放,計數連加什麼什麼的功能,很實際。不知道大家以前是怎麼做的呢?我承認以前做的很鬱悶。 
但是看我們這裏怎麼處理吧,或許你會大吃一驚,原來程序可以這麼簡單 
這裏具個簡單例子,爲了只是說明原理,PB0是模式按鍵,短按則切換模式,PB1就是加,如果長按的話則連加(玩過電子表吧?沒錯,就是那個!) 
#define KEY_MODE 0x01    // 模式按鍵 
#define KEY_PLUS 0x02     // 加 
void KeyProc(void) 
{ 
       if (Trg & KEY_MODE) // 如果按下的是KEY_MODE,而且你常按這按鍵也沒有用, 
    {                    //它是不會執行第二次的哦 , 必須先鬆開再按下 
         Mode++;         // 模式寄存器加1,當然,這裏只是演示,你可以執行你想 
                         // 執行的任何代碼 
    } 
    if (Cont & KEY_PLUS) // 如果“加”按鍵被按着不放 
    { 
         cnt_plus++;       // 計時 
         if (cnt_plus > 100) // 20ms*100 = 2S 如果時間到 
         { 
              Func();      // 你需要的執行的程序 
         }           
    } 
} 


不知道各位感覺如何?我覺得還是挺簡單的完成了任務,當然,作爲演示用代碼。 
應用3:點觸型按鍵和開關型按鍵的混合使用 
點觸形按鍵估計用的最多,特別是單片機。開關型其實也很常見,例如家裏的電燈,那些按下就不鬆開,除非關。這是兩種按鍵形式的處理原理也沒啥特別,但是你有沒有想過,如果一個系統裏面這兩種按鍵是怎麼處理的?我想起了我以前的處理,分開兩個非常類似的處理程序,現在看起來真的是笨的不行了,但是也沒有辦法啊,結構決定了程序。不過現在好了,用上面介紹的辦法,很輕鬆就可以搞定。 
原理麼?可能你也會想到,對於點觸開關,按照上面的辦法處理一次按下和長按,對於開關型,我們只需要處理Cont就OK了,爲什麼?很簡單嘛,把它當成是一個長按鍵,這樣就找到了共同點,屏蔽了所有的細節。程序就不給了,完全就是應用2的內容,在這裏提爲了就是說明原理~~ 
好了,這個好用的按鍵處理算是說完了。可能會有朋友會問,爲什麼不說延時消抖問題?哈哈,被看穿了。果然不能偷懶。下面談談這個問題,順便也就非常簡單的談談我自己用時間片輪辦法,以及是如何消抖的。
延時消抖的辦法是非常傳統,也就是 第一次判斷有按鍵,延時一定的時間(一般習慣是20ms)再讀端口,如果兩次讀到的數據一樣,說明了是真正的按鍵,而不是抖動,則進入按鍵處理程序。 
當然,不要跟我說你delay(20)那樣去死循環去,真是那樣的話,我衷心的建議你先放下手上所有的東西,好好的去了解一下操作系統的分時工作原理,大概知道思想就可以,不需要詳細看原理,否則你永遠逃不出“菜鳥”這個圈子。當然我也是菜鳥。我的意思是,真正的單片機入門,是從學會處理多任務開始的,這個也是學校程序跟公司程序的最大差別。當然,本文不是專門說這個的,所以也不獻醜了。 
我的主程序架構是這樣的: 
volatile unsigned char Intrcnt; 
void InterruptHandle()    // 中斷服務程序 
{ 
       Intrcnt++;          // 1ms 中斷1次,可變 
} 
void main(void) 
{ 
       SysInit(); 
    while(1)           // 每20ms 執行一次大循環 
    { 
        KeyRead();             // 將每個子程序都掃描一遍 
        KeyProc(); 
        Func1(); 
        Funt2(); 
        … 
        … 
           while(1) 
        { 
              if (Intrcnt>20)     // 一直在等,直到20ms時間到 
              { 
                   Intrcnt="0"; 
                   break;       // 返回主循環 
              } 
        } 
       } 
} 


貌似扯遠了,回到我們剛纔的問題,也就是怎麼做按鍵消抖處理。我們將讀按鍵的程序放在了主循環,也就是說,每20ms我們會執行一次KeyRead()函數來得到新的Trg 和 Cont 值。好了,下面是我的消抖部分:很簡單 
基本架構如上,我自己比較喜歡的,一直在用。當然,和這個配合,每個子程序必須執行時間不長,更加不能死循環,一般採用有限狀態機的辦法來實現,具體參考其它資料咯。 
懂得基本原理之後,至於怎麼用就大家慢慢思考了,我想也難不到聰明的工程師們。例如還有一些處理, 
怎麼判斷按鍵釋放?很簡單,Trg 和Cont都爲0 則肯定已經釋放了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章