1. 程式人生 > 其它 >外設驅動庫開發筆記39:按鍵操作驅動

外設驅動庫開發筆記39:按鍵操作驅動

  按鍵在我們的專案中是經常使用到的元件。一般來說,我們都是在用到按鍵時直接針對編碼,但這樣每次都做很多重複性的工作。所以在這裡我們考慮做一般性抽象得到一個可應用於按鍵操作的通用性驅動程式。

1、功能概述

  按鍵操作在我們的產品種經常用到,一般都是在特定的應用環境中直接有針對性的操作。但這些按鍵的操作往往有很多的共性,這就為程式碼複用提供了可能。

1.1、按鍵的定義

  在開始考慮按鍵操作之前,我們先來分析一下究竟什麼是按鍵。按鍵一般來講就是用於訊號輸入的按鈕,通過響應它的操作我們可以實現想要的功能。但我們這裡所說的按鍵不僅包括普通的單體按鍵,還包括如組合鍵、鍵盤等。

  對於這些種類的按鍵它們的形態、功能或許有較大的差異,但我們可以對它們所進行的操作卻很類似。這也是我們能夠統一考慮它們的基礎。

1.2、原理分析

  我們已經給我們要操作的按鍵劃分了範圍,在此基礎上我們簡單分析實現按鍵操作的基本原理。

  首先我們來考慮按鈕操作的原理,其實很簡單,無非就是按下或者彈起兩種狀態。至於按鈕本身是常開或者常閉,是低電平有效還是高電平有效都沒有問題,我們只要能檢測出其狀態就可以了。我們考慮按鍵的按下、彈起、連擊和長按等狀態,如下圖:

  其次我們來考慮按鍵狀態的儲存。在系統中的多個按鍵需要操作時,如何處理響應事件就會是一個問題。我們考慮以先入先出佇列來儲存按鍵的狀態,進而根據狀態進行操作。我們需要設計一個佇列,這是一個先入先出的佇列,擁有一定的儲存空間和讀寫操作指標,具體如下圖所示:

  在上圖中,當讀指標與寫指標一樣時,則表示佇列為空。當寫入一個數據,則寫指標加一;當讀出一個數據,則讀指標加一;當讀指標遇到寫指標則表示在沒有資料了。

  最後來說一說按鍵狀態的響應。所謂響應其實就是對不同的狀態我們來處理不同的事件。對於每個按鍵我們根據其狀態定義事件。在不同的事件中處理我們需要的功能。

  在上圖中,狀態和時間都可以在我們的物件中宣告,但具體的實現形式在應用中完成。

2、驅動設計與實現

  我們已經簡單分析了按鍵的基本操作原理,接下來我們將以此為基礎來分析並設計按鍵操作的通用驅動方法。

2.1、物件定義

  我們依然採用基於物件的操作方式。當然前提是我們得到了可用於操作的物件,所以我們先來分析一下如何抽象面向按鍵操作的物件。

2.1.1、定義物件型別

  一般來講,一個物件會包括屬性和操作。接下來我們就從這兩個方面來考慮按鍵物件問題。

  首先我們來考慮按鍵物件的屬性問題。我們的系統中總有多個按鍵,為了區分這些按鍵我們為每一個按鍵分配一個ID,用於區別這些按鍵。所以我們將按鍵ID作為其一個屬性。對於按鍵操作我們一般都會有軟體濾波來實現消抖,我們一如一個濾波計數用以實現這一過程,我們將濾波計數也當作它的一個屬性。長按鍵我們需要預設檢測時長,同時需要一個計數來記錄這一過程,所以我們將其設為屬性。同樣連續按鍵的週期需要預設,而且需要計數來記錄過程,所以也將這兩個作為屬性。當然按鍵當前的狀態,我們也可能需要記錄一下,按鍵按下時的有效電平,我們也需要分辨,這些我們也都將其作為屬性。綜上所述按鍵物件的型別定義如下:

/*定義按鍵物件型別*/
typedef struct KeyObject {
 	uint8_t id;							//按鍵的ID
 	uint8_t Count;					//濾波器計數器
 	uint16_t LongCount;			//長按計數器
 	uint16_t LongTime;  		//按鍵按下持續時間, 0 表示不檢測長按
 	uint8_t  State;					//按鍵當前狀態(按下還是彈起)
 	uint8_t  RepeatPeriod;	//連續按鍵週期
 	uint8_t  RepeatCount;		//連續按鍵計數器
 	uint8_t ActiveLevel;		//啟用電平
}KeyObjectType;

除了按鍵物件,其實我們還需要定義一個數據佇列的物件。者如我們前面所說,佇列除了一個數據儲存區外還需要讀寫指標。我們定義如下:

/*定義鍵值儲存佇列的型別*/
typedef struct KeyStateQueue{
  	uint8_t queue[KEY_FIFO_SIZE];		//鍵值儲存佇列
 	uint8_t pRead;		//讀佇列指標
 	uint8_t pWrite;		//寫佇列指標
}KeyStateQueueType;

2.1.2、物件初始化配置

  物件定義之後並不能立即使用我們還需要對其進行初始化。所以這裡我們來考慮按鍵物件的初始化函式。關於物件的初始化,初始化函式需要處理幾個方面的問題。一是檢查輸入引數是否合理;二是為物件的屬性賦初值;三是對物件作必要的初始化配置。據此思路我們設計按鍵物件的初始化函式如下:

/*按鍵讀取初始化*/
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、物件操作

  我們已經抽象了按鍵物件型別,也設計了物件的初始化函式。接下來我們需要考慮使用物件如何實現操作。根據我們前面的分析,操作可分為量個部分:按鍵狀態的檢測和鍵值佇列的操作。

2.2.1、按鍵狀態檢測

  需要週期性的檢測按鍵的狀態以便我們響應按鍵的操作。我們一般10ms檢測一次狀態,並持續一定的濾波週期用於消抖。我們檢測到按鍵的不同狀態後將狀態存入到相關的鍵值佇列中。

/*按鍵週期掃描程式*/
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;
 
 				/*傳送按鍵按下事件訊息*/
 				KeyValueEnQueue((uint8_t)((pKey->id<<2) + KeyDown));
 			}
 
 			if (pKey->LongTime > 0)
 			{
 				if (pKey->LongCount < pKey->LongTime)
 				{
 					/* 傳送按建持續按下的事件訊息 */
 					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傳送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;
 
 				/*傳送按鍵彈起事件訊息*/
 				KeyValueEnQueue((uint8_t)((pKey->id<<2)+ KeyUP));
 			}
 		}
 
 		pKey->LongCount = 0;
 		pKey->RepeatCount = 0;
 	}
}

2.2.2、鍵值佇列的操作

&esmp; 鍵值佇列的操作就簡單了,主要包括資料的寫入、讀出、清空佇列以及佇列是否為空。需要說的是鍵值的儲存,包括量方面類容:按鍵的ID和按鍵的狀態。我們使用一個位元組來儲存這些資訊,前六個位儲存ID,後兩位儲存狀態。具體如下圖所示:

  這樣一種儲存格式,我們最多可以儲存64個按鍵和4種狀態,當然這還要看佇列的大小。

/*鍵值出佇列程式*/
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、驅動的使用

  我們已經設計了按鍵操作的驅動程式,還需要對這一設計進行驗證。這一節我們將以前面的設計為基礎,用一個簡單的應用來驗證。我們設計4個單體按鍵,並由它們生出兩組組合鍵,所以我們的應用程式就是面向這6個按鍵物件進行操作。

3.1、宣告並初始化物件

  在開始面向一個物件的操作之前,我們需要得到這個物件的一個例項。那麼我們要先宣告物件。我們前面已經定義了按鍵物件型別KeyObjectType和儲存鍵值的佇列型別KeyStateQueueType。我們使用這兩個型別先宣告兩個物件變數如下:

  KeyObjectType keys[6];

  KeyStateQueueType keyState;

  聲明瞭物件還需要對變數進行初始化。在驅動的設計中我們已經設計了初始化函式,物件變數的初始化操作就通過這一函式來實現。初始化函式需要一些輸入引數:

  KeyObjectType *pKey,按鍵物件

  uint8_t id,按鍵ID

  uint16_t longTime,長按有效時間

  uint8_t repeatPeriod,連按間隔週期

  KeyActiveLevelType level,按鍵按下有效電平

  在這些引數中pKey為按鍵物件,是我們要初始化的物件。而其它引數只需要根據實際設定輸入就可以了。說一初始化函式可呼叫為:

/*按鍵硬體初始化配置*/
static void Key_Init_Configuration(void)
{
  KeyIDType id;
  for(id=KEY1;id<KEYNUM;id++)
  {
 		KeysInitialization(&keys[id],id,KEY_LONG_TIME,0,KeyHighLevel);
  }
}

  關於按鍵ID,我們使用列舉來定義。與我們前面定義的按鍵物件陣列配合能夠起到很好的效果。在這一我們定義按鍵ID為:

/*定義按鍵列舉*/
typedef enum KeyID {
 	KEY1,
 	KEY2,
 	KEY3,
 	KEY4,
	KEY1KEY2,
 	KEY3KEY4,
 	KEYNUM
}KeyIDType;

  按鍵ID作為作為按鍵的唯一標識,不但在我們的按鍵狀態記錄中要使用到,同時也可作為我們按鍵物件陣列的下標來使用。

3.2、基於物件進行操作

  我們定義了物件,接下來就可以基於物件實現我們的應用。對於按鍵操作我們需要考慮2個方面的事情:一是週期型的檢查按鍵狀態並壓如佇列;二是讀取佇列中的按鍵狀態觸發不同的操作。

  首先我們來說一說週期型的檢查按鍵的狀態。我們採用10ms的週期來檢查按鍵,所以我們需要使用定時中端的方式來實現,將如下函式加入到10ms定時中端即可。

/*按鍵掃描程式*/
void KeyScanHandle(void)
{
  KeyIDType id;
  for(id=KEY1;id<KEYNUM;id++)
  {
     KeyValueDetect(&keys[id]);
  }
}

&esmp;&esmp;其實還有一個回撥函式需要實現,其原型如下:

/*檢查某個ID的按鍵(包括組合鍵)是否按下*/
__weak uint8_t CheckKeyDown(KeyObjectType *pKey)

  根據我們定義的按鍵物件和ID列舉我們實現這個回撥函式並不困難,我們實現其如下:

/*檢查某個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;
}

&esmp;&esmp;此外,我們還需要讀取按鍵的狀態並進行相應的響應。我們實現一個簡單的處理函式如下:

/*按鍵處理函式*/
static void KeyProcessing(void)
{
  	uint8_t keyCode;
 	keyCode=KeyValueDeQueue();

 	if(keyCode==((keys[KEY1].id<<2)+KeyDown))
 	{
 		//key1按下時觸發的事件
 	}
 	else if(keyCode==((keys[KEY1].id<<2)+KeyUP))
 	{
 		//key1彈起時觸發的事件
 	}
}

4、應用總結

  我們已經實現了按鍵物件的操作,並在次基礎上實現了簡單的驗證。操作的結果符合我們的期望。而且擴充套件性也很強。

按照我們對資訊儲存方式和訊息佇列的設計,最多可以儲存64個按鍵和4中狀態,當然這需要看定義的佇列的大小。佇列不應太小,太小有可能會造成某些按鍵操不會響應;也不應太大,太大可能會造成操作遲緩和空間浪費。

  在應用中,我們建議定義按鍵ID時最好使用列舉,使用列舉的好處有幾點。一是不會出現重複,每個按鍵能保證有唯一的ID值。二是便於與按鍵物件陣列組合操作,簡化編碼。三是使用列舉擴充套件很方便,程式碼改動比較小。當然,列舉值最好是連續的而且從0開始。

  在使用驅動是還需要注意,檢測按鍵操作是隻對個體單鍵的硬體有效,如果可能也使用陣列操作,能與ID列舉配合使用簡化操作。對於組合鍵要檢測多個物理硬體,但也是對這些但體檢的檢測,所以在硬體上不需要定義。

歡迎關注: