按鍵在我們的項目中是經(jīng)常使用到的組件。一般來說,我們都是在用到按鍵時直接針對編碼,但這樣每次都做很多重復(fù)性的工作。所以在這里我們考慮做一般性抽象得到一個可應(yīng)用于按鍵操作的通用性驅(qū)動程序。
1、功能概述
按鍵操作在我們的產(chǎn)品種經(jīng)常用到,一般都是在特定的應(yīng)用環(huán)境中直接有針對性的操作。但這些按鍵的操作往往有很多的共性,這就為代碼復(fù)用提供了可能。
1.1、按鍵的定義
在開始考慮按鍵操作之前,我們先來分析一下究竟什么是按鍵。按鍵一般來講就是用于信號輸入的按鈕,通過響應(yīng)它的操作我們可以實現(xiàn)想要的功能。但我們這里所說的按鍵不僅包括普通的單體按鍵,還包括如組合鍵、鍵盤等。
對于這些種類的按鍵它們的形態(tài)、功能或許有較大的差異,但我們可以對它們所進行的操作卻很類似。這也是我們能夠統(tǒng)一考慮它們的基礎(chǔ)。
1.2、原理分析
我們已經(jīng)給我們要操作的按鍵劃分了范圍,在此基礎(chǔ)上我們簡單分析實現(xiàn)按鍵操作的基本原理。
首先我們來考慮按鈕操作的原理,其實很簡單,無非就是按下或者彈起兩種狀態(tài)。至于按鈕本身是常開或者常閉,是低電平有效還是高電平有效都沒有問題,我們只要能檢測出其狀態(tài)就可以了。我們考慮按鍵的按下、彈起、連擊和長按等狀態(tài),如下圖:
其次我們來考慮按鍵狀態(tài)的存儲。在系統(tǒng)中的多個按鍵需要操作時,如何處理響應(yīng)事件就會是一個問題。我們考慮以先入先出隊列來存儲按鍵的狀態(tài),進而根據(jù)狀態(tài)進行操作。我們需要設(shè)計一個隊列,這是一個先入先出的隊列,擁有一定的存儲空間和讀寫操作指針,具體如下圖所示:
在上圖中,當讀指針與寫指針一樣時,則表示隊列為空。當寫入一個數(shù)據(jù),則寫指針加一;當讀出一個數(shù)據(jù),則讀指針加一;當讀指針遇到寫指針則表示在沒有數(shù)據(jù)了。
最后來說一說按鍵狀態(tài)的響應(yīng)。所謂響應(yīng)其實就是對不同的狀態(tài)我們來處理不同的事件。對于每個按鍵我們根據(jù)其狀態(tài)定義事件。在不同的事件中處理我們需要的功能。
在上圖中,狀態(tài)和時間都可以在我們的對象中聲明,但具體的實現(xiàn)形式在應(yīng)用中完成。
2、驅(qū)動設(shè)計與實現(xiàn)
我們已經(jīng)簡單分析了按鍵的基本操作原理,接下來我們將以此為基礎(chǔ)來分析并設(shè)計按鍵操作的通用驅(qū)動方法。
2.1、對象定義
我們依然采用基于對象的操作方式。當然前提是我們得到了可用于操作的對象,所以我們先來分析一下如何抽象面向按鍵操作的對象。
2.1.1、定義對象類型
一般來講,一個對象會包括屬性和操作。接下來我們就從這兩個方面來考慮按鍵對象問題。
首先我們來考慮按鍵對象的屬性問題。我們的系統(tǒng)中總有多個按鍵,為了區(qū)分這些按鍵我們?yōu)槊恳粋€按鍵分配一個ID,用于區(qū)別這些按鍵。所以我們將按鍵ID作為其一個屬性。對于按鍵操作我們一般都會有軟件濾波來實現(xiàn)消抖,我們一如一個濾波計數(shù)用以實現(xiàn)這一過程,我們將濾波計數(shù)也當作它的一個屬性。長按鍵我們需要預(yù)設(shè)檢測時長,同時需要一個計數(shù)來記錄這一過程,所以我們將其設(shè)為屬性。同樣連續(xù)按鍵的周期需要預(yù)設(shè),而且需要計數(shù)來記錄過程,所以也將這兩個作為屬性。當然按鍵當前的狀態(tài),我們也可能需要記錄一下,按鍵按下時的有效電平,我們也需要分辨,這些我們也都將其作為屬性。綜上所述按鍵對象的類型定義如下:
/*定義按鍵對象類型*/
typedef struct KeyObject {
uint8_t id;//按鍵的ID
uint8_t Count;//濾波器計數(shù)器
uint16_t LongCount;//長按計數(shù)器
uint16_t LongTime; //按鍵按下持續(xù)時間, 0 表示不檢測長按
uint8_t State;//按鍵當前狀態(tài)(按下還是彈起)
uint8_t RepeatPeriod;//連續(xù)按鍵周期
uint8_t RepeatCount;//連續(xù)按鍵計數(shù)器
uint8_t ActiveLevel;//激活電平
}KeyObjectType;
除了按鍵對象,其實我們還需要定義一個數(shù)據(jù)隊列的對象。者如我們前面所說,隊列除了一個數(shù)據(jù)存儲區(qū)外還需要讀寫指針。我們定義如下:
/*定義鍵值存儲隊列的類型*/
typedef struct KeyStateQueue{
uint8_t queue[KEY_FIFO_SIZE];//鍵值存儲隊列
uint8_t pRead;//讀隊列指針
uint8_t pWrite;//寫隊列指針
}KeyStateQueueType;
2.1.2、對象初始化配置
對象定義之后并不能立即使用我們還需要對其進行初始化。所以這里我們來考慮按鍵對象的初始化函數(shù)。關(guān)于對象的初始化,初始化函數(shù)需要處理幾個方面的問題。一是檢查輸入參數(shù)是否合理;二是為對象的屬性賦初值;三是對對象作必要的初始化配置。據(jù)此思路我們設(shè)計按鍵對象的初始化函數(shù)如下:
/*按鍵讀取初始化*/
void KeysInitialization(KeyObjectType *pKey,uint8_t id,uint16_t longTime, uint8_t repeatPeriod,KeyActiveLevelType level)
{
if(pKey==NULL)
{
return;
}
pKey->id=id;
pKey->Count=0;
pKey->LongCount=0;
pKey->RepeatCount=0;
pKey->State=0;
pKey->ActiveLevel=level;
pKey->LongTime=longTime;
pKey->RepeatPeriod=repeatPeriod;
}
2.2、對象操作
我們已經(jīng)抽象了按鍵對象類型,也設(shè)計了對象的初始化函數(shù)。接下來我們需要考慮使用對象如何實現(xiàn)操作。根據(jù)我們前面的分析,操作可分為量個部分:按鍵狀態(tài)的檢測和鍵值隊列的操作。
2.2.1、按鍵狀態(tài)檢測
需要周期性的檢測按鍵的狀態(tài)以便我們響應(yīng)按鍵的操作。我們一般10ms檢測一次狀態(tài),并持續(xù)一定的濾波周期用于消抖。我們檢測到按鍵的不同狀態(tài)后將狀態(tài)存入到相關(guān)的鍵值隊列中。
/*按鍵周期掃描程序*/
void KeyValueDetect(KeyObjectType *pKey)
{
if (CheckKeyDown(pKey))
{
if (pKey->Count < KEY_FILTER_TIME)
{
pKey->Count = KEY_FILTER_TIME;
}
else if(pKey->Count < 2 * KEY_FILTER_TIME)
{
pKey->Count++;
}
else
{
if (pKey->State == 0)
{
pKey->State = 1;
/*發(fā)送按鍵按下事件消息*/
KeyValueEnQueue((uint8_t)((pKey->id<<2) + KeyDown));
}
if (pKey->LongTime > 0)
{
if (pKey->LongCount < pKey->LongTime)
{
/* 發(fā)送按建持續(xù)按下的事件消息 */
if (++pKey->LongCount == pKey->LongTime)
{
/* 鍵值放入按鍵FIFO */
KeyValueEnQueue((uint8_t)((pKey->id<<2) + KeyLong));
}
}
else
{
if (pKey->RepeatPeriod > 0)
{
if (++pKey->RepeatCount >= pKey->RepeatPeriod)
{
pKey->RepeatCount = 0;
/*長按鍵后,每隔10ms發(fā)送1個按鍵*/
KeyValueEnQueue((uint8_t)((pKey->id<<2) + KeyDown));
}
}
}
}
}
}
else
{
if(pKey->Count > KEY_FILTER_TIME)
{
pKey->Count = KEY_FILTER_TIME;
}
else if(pKey->Count != 0)
{
pKey->Count--;
}
else
{
if (pKey->State == 1)
{
pKey->State = 0;
/*發(fā)送按鍵彈起事件消息*/
KeyValueEnQueue((uint8_t)((pKey->id<<2)+ KeyUP));
}
}
pKey->LongCount = 0;
pKey->RepeatCount = 0;
}
}
2.2.2、鍵值隊列的操作
鍵值隊列的操作就簡單了,主要包括數(shù)據(jù)的寫入、讀出、清空隊列以及隊列是否為空。需要說的是鍵值的存儲,包括量方面類容:按鍵的ID和按鍵的狀態(tài)。我們使用一個字節(jié)來存儲這些信息,前六個位存儲ID,后兩位存儲狀態(tài)。具體如下圖所示:
這樣一種存儲格式,我們最多可以存儲64個按鍵和4種狀態(tài),當然這還要看隊列的大小。
/*鍵值出隊列程序*/
uint8_t KeyValueDeQueue(void)
{
uint8_t result;
if(keyState.pRead==keyState.pWrite)
{
result=0;
}
else
{
result=keyState.queue[keyState.pRead];
if(++keyState.pRead>=KEY_FIFO_SIZE)
{
keyState.pRead=0;
}
}
return result;
}
/*鍵值入隊列程序*/
void KeyValueEnQueue(uint8_t keyCode)
{
keyState.queue[keyState.pWrite]=keyCode;
if(++keyState.pWrite >= KEY_FIFO_SIZE)
{
keyState.pWrite=0;
}
}
3、驅(qū)動的使用
我們已經(jīng)設(shè)計了按鍵操作的驅(qū)動程序,還需要對這一設(shè)計進行驗證。這一節(jié)我們將以前面的設(shè)計為基礎(chǔ),用一個簡單的應(yīng)用來驗證。我們設(shè)計4個單體按鍵,并由它們生出兩組組合鍵,所以我們的應(yīng)用程序就是面向這6個按鍵對象進行操作。
3.1、聲明并初始化對象
在開始面向一個對象的操作之前,我們需要得到這個對象的一個實例。那么我們要先聲明對象。我們前面已經(jīng)定義了按鍵對象類型KeyObjectType和存儲鍵值的隊列類型KeyStateQueueType。我們使用這兩個類型先聲明兩個對象變量如下:
KeyObjectType keys[6];
KeyStateQueueType keyState;
聲明了對象還需要對變量進行初始化。在驅(qū)動的設(shè)計中我們已經(jīng)設(shè)計了初始化函數(shù),對象變量的初始化操作就通過這一函數(shù)來實現(xiàn)。初始化函數(shù)需要一些輸入?yún)?shù):
KeyObjectType *pKey,按鍵對象
uint8_t id,按鍵ID
uint16_t longTime,長按有效時間
uint8_t repeatPeriod,連按間隔周期
KeyActiveLevelType level,按鍵按下有效電平
在這些參數(shù)中pKey為按鍵對象,是我們要初始化的對象。而其它參數(shù)只需要根據(jù)實際設(shè)置輸入就可以了。說一初始化函數(shù)可調(diào)用為:
/*按鍵硬件初始化配置*/
static void Key_Init_Configuration(void)
{
KeyIDType id;
for(id=KEY1;idid++)
{
KeysInitialization(&keys[id],id,KEY_LONG_TIME,0,KeyHighLevel);
}
}
關(guān)于按鍵ID,我們使用枚舉來定義。與我們前面定義的按鍵對象數(shù)組配合能夠起到很好的效果。在這一我們定義按鍵ID為:
/*定義按鍵枚舉*/
typedef enum KeyID {
KEY1,
KEY2,
KEY3,
KEY4,
KEY1KEY2,
KEY3KEY4,
KEYNUM
}KeyIDType;
按鍵ID作為作為按鍵的唯一標識,不但在我們的按鍵狀態(tài)記錄中要使用到,同時也可作為我們按鍵對象數(shù)組的下標來使用。
3.2、基于對象進行操作
我們定義了對象,接下來就可以基于對象實現(xiàn)我們的應(yīng)用。對于按鍵操作我們需要考慮2個方面的事情:一是周期型的檢查按鍵狀態(tài)并壓如隊列;二是讀取隊列中的按鍵狀態(tài)觸發(fā)不同的操作。
首先我們來說一說周期型的檢查按鍵的狀態(tài)。我們采用10ms的周期來檢查按鍵,所以我們需要使用定時中端的方式來實現(xiàn),將如下函數(shù)加入到10ms定時中端即可。
/*按鍵掃描程序*/
void KeyScanHandle(void)
{
KeyIDType id;
for(id=KEY1;idid++)
{
KeyValueDetect(&keys[id]);
}
}
其實還有一個回調(diào)函數(shù)需要實現(xiàn),其原型如下:
/*檢查某個ID的按鍵(包括組合鍵)是否按下*/
__weak uint8_t CheckKeyDown(KeyObjectType *pKey)
根據(jù)我們定義的按鍵對象和ID枚舉我們實現(xiàn)這個回調(diào)函數(shù)并不困難,我們實現(xiàn)其如下:
/*檢查某個ID的按鍵(包括組合鍵)是否按下*/
uint8_t CheckKeyDown(KeyObjectType *pKey)
{
/* 實體單鍵 */
if (pKey->id < KEY1KEY2)
{
uint8_t i;
uint8_t count = 0;
uint8_t save = 255;
/* 判斷有幾個鍵按下 */
for (i = 0; i < KEY1KEY2; i++)
{
if (KeyPinActive(pKey))
{
count++;
save = i;
}
}
if (count == 1 && save == pKey->id)
{
return 1;/* 只有1個鍵按下時才有效 */
}
return 0;
}
/* 組合鍵 K1K2 */
if (pKey->id == KEY1KEY2)
{
if (KeyPinActive(&keys[KEY1]) && KeyPinActive(&keys[KEY2]))
{
return 1;
}
else
{
return 0;
}
}
/* 組合鍵 K3K4 */
if (pKey->id == KEY3KEY4)
{
if (KeyPinActive(&keys[KEY3]) && KeyPinActive(&keys[KEY4]))
{
return 1;
}
else
{
return 0;
}
}
return 0;
}
此外,我們還需要讀取按鍵的狀態(tài)并進行相應(yīng)的響應(yīng)。我們實現(xiàn)一個簡單的處理函數(shù)如下:
/*按鍵處理函數(shù)*/
static void KeyProcessing(void)
{
uint8_t keyCode;
keyCode=KeyValueDeQueue();
if(keyCode==((keys[KEY1].id<<2)+KeyDown))
{
//key1按下時觸發(fā)的事件
}
else if(keyCode==((keys[KEY1].id<<2)+KeyUP))
{
//key1彈起時觸發(fā)的事件
}
}
4、應(yīng)用總結(jié)
我們已經(jīng)實現(xiàn)了按鍵對象的操作,并在次基礎(chǔ)上實現(xiàn)了簡單的驗證。操作的結(jié)果符合我們的期望。而且擴展性也很強。
按照我們對信息存儲方式和消息隊列的設(shè)計,最多可以存儲64個按鍵和4中狀態(tài),當然這需要看定義的隊列的大小。隊列不應(yīng)太小,太小有可能會造成某些按鍵操不會響應(yīng);也不應(yīng)太大,太大可能會造成操作遲緩和空間浪費。
在應(yīng)用中,我們建議定義按鍵ID時最好使用枚舉,使用枚舉的好處有幾點。一是不會出現(xiàn)重復(fù),每個按鍵能保證有唯一的ID值。二是便于與按鍵對象數(shù)組組合操作,簡化編碼。三是使用枚舉擴展很方便,代碼改動比較小。當然,枚舉值最好是連續(xù)的而且從0開始。
在使用驅(qū)動是還需要注意,檢測按鍵操作是只對個體單鍵的硬件有效,如果可能也使用數(shù)組操作,能與ID枚舉配合使用簡化操作。對于組合鍵要檢測多個物理硬件,但也是對這些但體檢的檢測,所以在硬件上不需要定義。
-
按鍵
+關(guān)注
關(guān)注
4文章
223瀏覽量
57661 -
對象
+關(guān)注
關(guān)注
1文章
38瀏覽量
17419 -
驅(qū)動設(shè)計
+關(guān)注
關(guān)注
1文章
111瀏覽量
15314
發(fā)布評論請先 登錄
相關(guān)推薦
評論