在嵌入式系統中,用的最多的輸入設備就是按鍵,用戶的應用需求可通過相應按鍵傳遞到系統軟件中,軟件轉而完成用戶請求,實現簡單的人機交互。筆者此處就矩陣按鍵的實現作一個簡單的介紹。
1. 按鍵輸入概述
按鍵是一種常開型按鈕開關,平時鍵的二個觸點處于斷開狀態,按下鍵時它們才閉合。按鍵控制電路就是用來實時監視按鍵,當有鍵接下時,電路監控中的輸入引腳電平發生變化,檢測到這種變化后,控制電路進行按鍵掃描,定位按鍵的位置,并把相關的按鍵信息反饋回上一層應用中。常見的按鍵輸入設計有獨立式按鍵,矩陣式按鍵。獨立式按鍵每個鍵占用一個IO口,電路配置靈活,軟件簡單,但按鍵較多時,IO口浪費大。矩陣式按鍵適用于按鍵數量較多的場合,由行線和列線組成,按鍵位于行列的交叉點上。節省IO口。通常按鍵控制電路通過查詢方式或中斷方式去檢測按鍵的輸入,查詢方式需占用一定的cpu資源,查詢頻率太低可能造成按鍵輸入丟失,太高浪費cpu資源,通常按鍵查詢頻率約50HZ較合適。中斷方式需占用cpu一路外部中斷,但不會占用cpu資源,只要有按鍵按下時,cpu即可馬上檢測到輸入,進行掃描并得到按鍵值。
2. 硬件設計
筆者此處采用4x4的矩陣按鍵設計,當然,矩陣鍵盤可通過四個肖特基二極管構成四輸入的與門(可參考筆者這篇文章<淺談小信號肖特基二極管在數字電路中的應用>),連接到單片機的外部中斷引腳,從而實現中斷方式檢測按鍵輸入。為兼容目前開發板常見的矩陣按鍵設計,筆者把4x4的矩陣按鍵接口接在P1口,通過查詢方式檢測按鍵輸入。
圖2-1 4x4矩陣按鍵
3. 驅動實現
由于我們采用的是查詢方式按鍵設計,因此單片機需一定的頻率去掃描P1口的按鍵,通常這個頻率約50HZ較合適,為保證這個掃描頻率,通常是通過定時器產生時標周期性進行執行掃描。P1.4~P1.7列線通過上拉電阻接到VCC上,P1.0~P1.3行線產生相應的掃描信號,無按鍵,列線處于高電平狀態,有鍵按下,列線電平狀態將由與此列線相連的行線電平決定。行線電平為低,則列線電平為低,行線電平為高,則列線電平為高。
按鍵掃描函數如下,該函數需周期執行,以掃描按鍵的狀態。以51單片機為例,P1.0~P1.3逐行輸出掃描信號,在Key.h模塊頭文件實現接口宏KeyOutputSelect()
#define KeyOutputSelect(Select) {P1 = ~(1<<(Select));}
輸出掃描線后,需要讀取對應掃描線的按鍵狀態(P1.4~P1.7),同樣在Key.h模塊頭文件實現引腳狀態讀取接口宏KeyGetPinState()
#define KeyGetPinState() (P1>> 4)
讀取了對應掃描線下的按鍵引腳狀態,就需判斷哪些引腳電平為0(按下),對讀到的引腳狀態進行取反轉換成對引腳狀態變量進行搜1算法,得到鍵值的速度能達到最快,并且多個按鍵同時按下時也能夠正確得到優先級最高的按鍵。按鍵有效按下會得到0~15的鍵值,無按鍵按下時得到鍵值16。
voidKeyScan()
{
unsigned char i;
unsigned char KeyValue;
unsigned char PinState;
if (KeyState.State == STATE_DISABLE) {
return; // 按鍵禁用時,不對鍵盤進行掃描
}
// 鍵值為0~15,未按鍵鍵值為16,任意多的鍵按下均能
// 正確返回優先級最高的鍵值
KeyValue = 0;
for (i=0; i<4; i++) {
KeyOutputSelect(i); // 輸出掃描線
// 得到對應掃描線時的按鍵狀態
PinState = KeyGetPinState();
// 有鍵按下時,PinState中有0的位置即為鍵值位置
PinState = ~PinState;
// 搜索Pinstate第一個為1的位
if (!(PinState & 0xf)) {
KeyValue += 4;
continue; // 該掃描線沒有按鍵按下,進入下一掃描線
}
// 該掃描線有鍵按下,對半進行檢索1的位置
if (!(PinState & 0x3)) {
KeyValue += 2; // 低2位(P1.4~P1.5)沒有按下
PinState >>= 2; // 移位檢索(P1.6~P1.7)
}
if (!(PinState & 0x1)){
KeyValue += 1;
}
break; // 有鍵按下,退出繼續掃描
}
KeyStore(KeyValue); // 保存按鍵狀態
}
得到了按鍵值后,我們需要對按鍵值進行處理并根據按鍵狀態把可能產生的按鍵消息保存進緩沖區中,以便用戶程序讀取處理。按鍵通常有按下、松手、長按這幾個狀態,需要支持按下檢測、松手檢測、長按、連擊的功能,并且需要對按鍵進行去抖濾波。按鍵的狀態往往會在這幾種情況進行切換,因此,對按鍵進行狀態機編程是相當清晰的思路。我們在KeyStore()函數中實現對按鍵狀態的轉移判斷,在模塊中我們通過按鍵狀態結構變量KeyState來跟蹤記錄按鍵的狀態
typedef struct {
unsigned char State; // 按鍵的各個狀態轉移
unsigned int TimeCount; // 用來跟蹤各個狀態的計時
} KEY_STATE;
static KEY_STATE KeyState; // 按鍵狀態機狀態轉移
檢測到相應的按鍵事件后(KEY_UP、KEY_DOWN、KEY_LONG),需產生相應的按鍵消息保存進按鍵緩存區,通常可以開辟一個按鍵隊列緩存,以便保存多個產生的按鍵消息,不會因用戶代碼未能及時處理按鍵而造成按鍵丟失,筆者此處為避免復雜,以一個按鍵緩沖為例,按鍵事件結構變量KeyBuffer用來保存按鍵消息
typedef struct {
unsigned char Value;
unsigned char State;
} KEY_EVENT;
// 按鍵掃描得到的鍵值存放在KeyBuffer中,包含鍵值及鍵狀態
static volatile KEY_EVENT KeyBuffer;
按鍵消抖以及長按均是需要以時間為判斷標準,我們在模塊中定義消抖時間以及長按時間判決以及相應的狀態宏
// 按鍵的掃描周期為20ms
#define WOBBLE_COUNT 1 // 按鍵消抖計數,1個按鍵掃描周期(20ms)
#define LONG_COUNT 100 // 長按100個掃描周期判斷為長按(2S)
#define STATE_INIT 0x0 // 按鍵初始化狀態
#define STATE_WOBBLE 0x1 // 按鍵消抖狀態
#define STATE_LONG 0x2 // 按鍵長按檢測狀態
#define STATE_RELEASE 0x3 // 按鍵釋放狀態
#define STATE_DISABLE 0x4 // 按鍵禁用狀態
完整的KeyStore()函數實現如下
static voidKeyStore(unsigned char Value)
{
static unsigned char LastValue;
switch (KeyState.State) {
case STATE_INIT: // 初始狀等待按鍵
if (Value < KEY_NULL) {
// 記錄下按下的鍵并進入消抖狀態
LastValue = Value;
KeyState.TimeCount = WOBBLE_COUNT -1;
KeyState.State = STATE_WOBBLE;
}
break;
case STATE_WOBBLE:
if (KeyState.TimeCount) {
KeyState.TimeCount--; // 消抖計時未到
break;
}
// 消抖后再次判斷為同一鍵值則認為鍵按下保存鍵值
// 并進入到長按檢測
來源;21ic
評論