stm32單片機按鍵消抖、長按、鬆開終極解決方案

如果有更好的解決方案或是發現天神的方案有問題,歡迎大家熱烈討論!

明確按鍵的使用環境和終極目標

使用環境

首先我們的按鍵使用在有操作系統的環境中,不能使用佔用CPU的延時函數,使用操作系統的延時每20ms對按鍵進行一次檢測。

終極目標

我們的按鍵需要實現的終極目標是檢測按鍵按下、長按、鬆開、長松(一般沒啥用)。按鍵的按下我們希望按下一次,程序中只反應出一次按下來,而不是唰唰響應了一長串,同樣鬆開也是。對於長按我們希望在按鍵按下後過一會才反應出來,這個是需要唰唰一直響應的,只要不鬆開程序就一直反應出長按。長松和長按是一樣的。


按鍵的程序信號、邏輯狀態、物理狀態、開啓計數、關閉計數

根據我們的環境和目標,天神總結出來我們的按鍵需要有1+4個信息來記錄按鍵的狀態,也就是標題中的程序信號、邏輯狀態、物理狀態、開啓計數、關閉計數。其中程序信號寫成信號是因爲這些信號不需要存儲,通過邏輯狀態、物理狀態的關係直接返回。對此天神畫了一個圖進行分析:在這裏插入圖片描述
以下進行詳細介紹(不含長松狀態):

  1. 程序信號
    程序信號有四種:關閉、開啓、長按、等待
    其中等待狀態是爲了當按鍵已經按下或是鬆開後程序不再重複響應而設置

  2. 邏輯狀態
    邏輯狀態有三種:關閉、開啓、長按
    沒有等待狀態

  3. 物理狀態
    物理狀態是按鍵按下後單片機IO口接收到的狀態,是有抖動的
  4. 開啓計數與關閉計數
    將這兩個計數分開是爲了在開啓和關閉時都做出消抖,並且開啓計數可以用來作爲對長按的延遲計數。當狀態爲(0,1)或者(1,0)時計數,出現抖動即發生(0,0)或(1,1)狀態時清零。

此外還有兩個狀態所對應幾種實際情況,按照圖中順序總結一下分別爲

  1. 關閉情況
  2. 開啓抖動
  3. 開啓計數累加情況
  4. 開啓兼長按檢測情況
  5. 長按情況
  6. 關閉抖動
  7. 關閉計數累加情況

首先要明確,程序最後接收到的信號,與按鍵的邏輯狀態是有區別的。如圖中所示,按鍵的邏輯狀態只有三種,關閉、開啓、長按,並沒有等待狀態。這樣是爲了方便進行分析。程序最後接收到的信號與按鍵的邏輯狀態關係是:
1. 在邏輯狀態的上升沿,程序接收到開啓信號
2. 在邏輯狀態的下降沿,程序接收到關閉信號
3. 在邏輯狀態爲長按時,程序也接收到長按信號
4. 其餘時間程序都接收到等待信號




其次看圖的左下角部分,分別反映的是邏輯狀態、物理狀態所對應的實際情況。需要注意的是,兩者爲(1,1)時的狀態,在按鍵的關閉抖動過程中也有出現,兩者爲(0,0)的狀態,在按鍵的開啓抖動過程中也有出現。因此需要在這兩個狀態中分別對關閉計數和開啓計數清零。同時,兩者爲(1,1)的狀態正是要檢測按鍵是不是進入長按的時刻,因此要對開啓計數進行累加,到達給定值後切換到(2,1)狀態。


最後是右上角對於一個完整的按鍵開啓、長按、關閉過程的具體分析,用不同顏色代表了各個實際情況,應該一目瞭然,就不多做解釋了。

具體代碼(在stm32上實現)

根據以上分析,天神得出結論,對於每一個按鍵都需要存儲它的邏輯狀態、物理狀態、開啓計數、關閉計數,最後反饋給程序的信號是由這些狀態計算而來。注意是每一個按鍵,也就是說如果你有10個按鍵,就需要存10組。爲此定義一個結構體:

//按鍵狀態結構體,存儲四個變量
typedef struct
{
   
    
 	uint8_t KeyLogic;
	uint8_t KeyPhysic;
 	uint8_t KeyONCounts;
 	uint8_t KeyOFFCounts;
}KEY_TypeDef;

一些宏定義,如果你的開關時按下低電平,鬆開高電平就把KEY_OFF,KEY_ON對調一下就ok了。

//宏定義
#define    	KEY_OFF	   		0
#define    	KEY_ON	   	 	1
#define    	KEY_HOLD		2
#define		KEY_IDLE		3
#define		KEY_ERROR		10

#define		HOLD_COUNTS			50
#define 	SHAKES_COUNTS		5

創建一個結構體數組,用來對應每一個實際按鍵,我這裏有兩個。

//按鍵結構體數組,初始狀態都是關閉
static KEY_TypeDef Key[2] =
	{
   
    {
   
    KEY_OFF, KEY_OFF, 0, 0},
	 {
   
    KEY_OFF, KEY_OFF, 0, 0}};

接下里是關鍵的key_scan()函數,這個函數要在操作系統的任務中循環執行,因此其中不能有阻塞延時。

/*
 * 函數名:Key_Scan
 * 描述  :檢測是否有按鍵按下
 * 輸入  :GPIOx:gpio的port
 *		   GPIO_Pin:gpio的pin
 * 輸出  :KEY_OFF、KEY_ON、KEY_HOLD、KEY_IDLE、KEY_ERROR
 */
 
uint8_t Key_Scan(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{
   
    
	KEY_TypeDef *KeyTemp;

	//檢查按下的是哪一個按鈕
	switch ((uint32_t)GPIOx)
	{
   
    
	case ((uint32_t)KEY1_GPIO_PORT):
		switch (GPIO_Pin)
		{
   
    
		case KEY1_GPIO_PIN:
			KeyTemp = &Key[0];
			break;

		//port和pin不匹配
		default:
			printf("error: GPIO port pin not match\r\n");
			return KEY_IDLE;
		}
		break;

	case ((uint32_t)KEY2_GPIO_PORT):
		switch (GPIO_Pin)
		{
   
    
		case KEY2_GPIO_PIN:
			KeyTemp = &Key[1];
			break;

		//port和pin不匹配
		default:
			printf("error: GPIO port pin not match\r\n");
			return KEY_IDLE;
		}
		break;

	default:
		printf("error: key do not exist\r\n");
		return KEY_IDLE;
	}

	/* 檢測按下、鬆開、長按 */
	KeyTemp->KeyPhysic = GPIO_ReadInputDataBit(GPIOx, GPIO_Pin);

	switch (KeyTemp->KeyLogic)
	{
   
    
	
	case KEY_ON:
		switch (KeyTemp->KeyPhysic)
		{
   
    
		
		//(1,1)中將關閉計數清零,並對開啓計數累加直到切換至邏輯長按狀態
		case KEY_ON:
			KeyTemp->KeyOFFCounts = 0;
			KeyTemp->KeyONCounts++;
			if (KeyTemp->KeyONCounts >= HOLD_COUNTS)
			{
   
    
				KeyTemp->KeyONCounts = 0;
				KeyTemp->KeyLogic = KEY_HOLD;
				return KEY_HOLD;
			}
			return KEY_IDLE;
			
		//(1,0)中對關閉計數累加直到切換至邏輯關閉狀態
		case KEY_OFF:
			KeyTemp->KeyOFFCounts++;
			if (KeyTemp->KeyOFFCounts >= SHAKES_COUNTS)
			{
   
    
				KeyTemp->KeyLogic = KEY_OFF;
				KeyTemp->KeyOFFCounts = 0;
				return KEY_OFF;
			}
			return KEY_IDLE;

		default:
			break;
		}

	case KEY_OFF:
		switch (KeyTemp->KeyPhysic)
		{
   
    
		
		//(0,1)中對開啓計數累加直到切換至邏輯開啓狀態
		case KEY_ON:
			(KeyTemp->KeyONCounts)++;
			if (KeyTemp->KeyONCounts >= SHAKES_COUNTS)
			{
   
    
				KeyTemp->KeyLogic = KEY_ON;
				KeyTemp->KeyONCounts = 0;

				return KEY_ON;
			}
			return KEY_IDLE;
			
		//(0,0)中將開啓計數清零
		case KEY_OFF:
			(KeyTemp->KeyONCounts) = 0;
			return KEY_IDLE;
		default:
			break;
		}

	case KEY_HOLD:
		switch (KeyTemp->KeyPhysic)
		{
   
    
		
		//(2,1)對關閉計數清零
		case KEY_ON:
			KeyTemp->KeyOFFCounts = 0;
			return KEY_HOLD;
		//(2,0)對關閉計數累加直到切換至邏輯關閉狀態
		case KEY_OFF:
			(KeyTemp->KeyOFFCounts)++;
			if (KeyTemp->KeyOFFCounts >= SHAKES_COUNTS)
			{
   
    
				KeyTemp->KeyLogic = KEY_OFF;
				KeyTemp->KeyOFFCounts = 0;
				return KEY_OFF;
			}
			return KEY_IDLE;

		default:
			break;
		}

	default:
		break;
	}
	
	//一般不會到這裏
	return KEY_ERROR;
}

最後在主程序中對按鍵進行循環檢測,天神使用的是FREERTOS操作系統。

static void DataProcess_Task(void *parameter)
{
   
    

    while (1)
    {
   
    
        switch (Key_Scan(KEY1_GPIO_PORT, KEY1_GPIO_PIN))
        {
   
    
        case KEY_ON:
            printf("Key1ON\n");
            break;
        
        case KEY_HOLD:
            printf("Key1HOLD\n");
            break;
        case KEY_OFF:
            printf("Key1OFF\n");
            break;
        case KEY_ERROR:
            printf("error\n");
            break;
        default:

            break;
        }

        switch (Key_Scan(KEY2_GPIO_PORT, KEY2_GPIO_PIN))
        {
   
    
        case KEY_ON:
            printf("Key2ON\n");
            break;
        
        case KEY_HOLD:
            printf("Key2HOLD\n");
            break;
        
        case KEY_OFF:
            printf("Key2OFF\n");
            break;
        case KEY_ERROR:
            printf("error\n");
            break;
        default:
          
            break;
        }
        

        vTaskDelay(20);
    }
}

效果

在這裏插入圖片描述

可以看到,沒有多餘的ON和OFF回來,同時我們的代碼也高度對稱,堪稱完美。當然兩個按鍵同時按下也沒有問題,不過調試的截圖不容易看出來兩個的效果就沒有放圖片。如果需要還可以添加長松狀態,代碼就會完全對稱!太棒了!

歡迎大家討論學習,指出天神的不足或者提出更好的方案!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章