1、按鍵識別算法的作用
????????按鍵識別算法在不同的技術和應用背景下有不同的作用,但其核心目標都是準確、可靠地檢測和區分用戶通過物理或虛擬按鍵所執行的操作。按鍵識別算法在各類電子設備及系統中起到至關重要的作用,它確保了人機交互的有效性和準確性,提升了用戶體驗,并保證了系統的正常運行。按鍵識別算法也有較多的種類,在這一部分主要介紹獨立按鍵的雙擊、短按、長按識別算法。? ? ? ?
????????雙擊、長按、短按一般是用來擴展按鍵功能,讓人機交互界面用起來更方便。比如手機觸摸屏的長按APP圖標時,會出現卸載軟件等功能選擇,而單擊時則是進入應用程序。此外,還有滑動等其他功能操作。雖然上述舉例目前是在手機觸摸屏中非常常見的,但其最早是應用于實體按鍵中的,后面擴展延伸到了觸摸屏的使用中,它們的實現本質邏輯框架是同一套,相信以前玩過MP5、PSP游戲機的讀者對這個是最有體會的。除了上述的場景中,還有我們的自拍桿、鍵盤、鼠標等等無一不是按鍵算法的身影。本文的下一部分開始將詳細剖析雙擊、短按、長按等按鍵算法實現的邏輯原理。
????????短按:一般定義為按鍵按下并快速釋放的操作,持續時間較短,按下時不會立即觸動動作,而是過一個較短的時間才觸發動作。
????????長按:通常是指按鍵保持按壓狀態超過短按時間閾值的操作,持續時間較長,松開時觸發動作。
????????雙擊:較短的時間內,連續按下兩次才會觸發動作。
2、按鍵電平時序圖
????????在這一部分主要是使用了24MHz 8通道的邏輯分析儀通過PulseView軟件對按鍵的三種狀態進行采樣觀察。如果有想進一步了解邏輯分析儀的使用和波形解析,可以參考本人在之前寫的一篇博客文章,已經對邏輯分析儀進行了較為詳細的講解。
邏輯分析儀使用配置,PulseView通信波形解析-CSDN博客https://blog.csdn.net/weixin_49337111/article/details/135602908?spm=1001.2014.3001.5501
????????提醒:這一部分所提到的檢測時間范圍都是可以靈活變動的,并不是說一成不變,最終的效果都是為了讓實現按鍵狀態識別更加的快速、準確。
(1)短按
????????通過邏輯分析儀觀測一連串的按鍵短按的電平時序圖
????????通過邏輯分析儀的測量工具可知,本次短按測量的持續時間為159ms
????????通過邏輯分析儀的測量工具可知,本次短按測量的持續時間為74ms
????????后續又經過多次測量,發現大部分短按時間是在160ms左右,考慮到實際日常使用過程中,有些用戶按下按鍵的速度可能比較緩慢,因此本文中將按鍵短按設定為90ms~250ms的閾值。
????????如果檢測出按鍵按下的時間>=90ms且<=250ms時,則會被認定為按鍵短按。程序則是進入短按的邏輯處理狀態。
(2)長按
????????通過邏輯分析儀觀測一連串的按鍵長按的電平時序圖。從邏輯時序圖中可以非常明顯的看出,長按相對于短按來說,就是按下時間延長,其余操作并沒有什么區別。因此在檢測算法編寫時,只需要判斷一個臨界值,超過該值,便可以判定為按鍵長按操作。
????????通過邏輯分析儀的測量工具可知,本次長按測量的持續時間為781ms
????????通過邏輯分析儀的測量工具可知,本次長按測量的持續時間為697ms
? ? 經過網上大量的資料查閱和實際按下的情況對比,長按的時間一般大于500ms,因此在本文的按鍵識別算法中,將臨界時間設定為500ms。? ?
(3)雙擊
????????通過邏輯分析儀觀測按鍵雙擊的時序圖。
????????
????????通過邏輯分析儀的測量工具可知,第一次按下結束后到第二次按下開始的時間間隔為74ms。
????????通過邏輯分析儀的測量工具可知,第一次按下結束后到第二次按下開始的時間間隔為199ms。
????????又經過多次測量發現,雙擊的第一次按下松開后到第二次按下開始的時間間隔較多分布在200ms以內,而考慮到一些用戶操作動作較為緩慢,及誤觸發的風險、硬件和軟件的響應能力。因此將雙擊檢測時間設置為了250ms,如果250ms內沒有檢測到按下第二次短按,則是會判斷為單擊,否則就是觸發雙擊。
? ? ? ? 通過上面三種模式的電平時序圖可知,按鍵檢測必須要實現毫秒級定時才實現較為精準的按鍵識別算法。
3、按鍵識別算法代碼
????????因按鍵識別算法有一定的難度,所以本文聚焦于按鍵算法的核心實現。如果需要實現按鍵識別算法,需要具備定時器的基礎,掌握GPIO引腳的使用,知道如何配置毫秒級定時器,中斷等。
? ? ? ? 在按鍵檢測算法代碼編寫時,考慮到了移植性的問題,因在對不同的平臺進行適配時,修改對電平狀態讀取和電平狀態判斷后,其余的檢測算法部分變動不需要很大既可以實現按鍵檢測算法功能。
????????本文中使用的是STM32G431RB6開發板進行按鍵識別算法開發編寫,如果對STM32的定時器配置不熟悉,可以參考本人在此前寫的博客文章。
STM32標準庫+HAL庫 | 高精度動態調節PWM輸出頻率+占空比_hal庫中設置pwm輸出頻率和占空比的函數-CSDN博客https://blog.csdn.net/weixin_49337111/article/details/135671353?spm=1001.2014.3001.5501? ? ? ? 本文所呈現的檢測效果是將定時器配置為了10ms檢測一次,且配置了4個按鍵,如果有更多的按鍵或更少的按鍵,將其中的循環次數修改和數組大小修改即可。
(1)狀態機
????????狀態機(Finite State Machine,FSM)是一種理論模型,用于描述一個系統在不同時間點可能存在的不同狀態以及引起狀態轉移的條件。它在計算機科學、電子工程、自動化控制、編譯原理等多個領域有著廣泛應用。
????????狀態機中有幾個術語:state(狀態)?、transition(轉移)?、action(動作)?、transition condition(轉移條件)?。
? ? ? ? 狀態機的內容大致了解一下,在本文使用了狀態機的思想對按鍵檢測算法進行編寫。其中的狀態有三種,分別是消抖裁決狀態、單擊狀態、雙擊狀態。如果是單擊狀態,則會判斷是長按還是短按,如果是短按,又會進行狀態轉移到雙擊,進行是否即將雙擊檢測。
(2)長按、短按識別算法
? ? ? ? JUDGE狀態主要是用于消除按鍵的抖動狀態,而SINGLE狀態中,則是對按鍵的長按、短按邏輯進行檢測判斷。
//按鍵狀態結構體
typedef struct key_state{int8_t short_flag; //短按flagint8_t long_flag; //長按flagint8_t level_state; //電平狀態 int8_t judge_flag; //裁決抖動int32_t press_time_cnt; //按下時間}key_state_t;enum {JUDGE = 0, SINGLE = 1};//狀態機的狀態類型key_state_t g_key[4] = {0}; //按鍵狀態全局變量//定時器回調函數,已經配置為10ms觸發一次定時器中斷回調
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM4) //已設置10ms觸發一次定時器中斷{static int32_t press_time_cnt[4] = {0}; //按下持續計數值g_key[0].level_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0); //讀取引腳電平值g_key[1].level_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1); g_key[2].level_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2); g_key[3].level_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0); for(int i=0; i<4; i++){switch(g_key[i].judge_flag){case JUDGE:if(g_key[i].level_state == GPIO_PIN_RESET) //主要用于消抖{g_key[i].judge_flag = 1;}break;case SINGLE://第一次按下,長按、短按if(g_key[i].level_state == GPIO_PIN_RESET) //按鍵按下{press_time_cnt[i]++;}else if(g_key[i].level_state == GPIO_PIN_SET) //按鍵松開{if(press_time_cnt[i] >= 50) //長按(按下持續時間>=500ms){g_key[i].long_flag = 1; //長按標志置1g_key[i].short_flag = 0;g_key[i].two_flag = 0;}else if(press_time_cnt[i] >= 7) //短按{g_key[i].short_flag = 1; //短按標志置1g_key[i].long_flag = 0;g_key[i].two_flag = 0;}g_key[i].judge_flag = 0;press_time_cnt[i] = 0; //清空按下和松開間的計數}break;}}}}
//頭文件int main(void)
{//其它代碼,如初始化,函數調用,變量定義//主函數中調用長短按鍵算法部分代碼while (1){for(int i=0; i<4; i++){if(g_key[i].short_flag == 1){g_key[i].short_flag = 0;printf("\r\nKEY %d 短按\r\n", i+1);}else if(g_key[i].long_flag == 1){g_key[i].long_flag = 0;printf("\r\nKEY %d 長按\r\n", i+1);} }}
}
????????短按、長按、檢測效果圖
(3)雙擊、長按、短按算法
//按鍵狀態結構體
typedef struct key_state{int8_t short_flag; //短按flagint8_t long_flag; //長按flagint8_t two_flag; //雙擊flagint8_t level_state; //電平狀態int32_t start_time; //按下時間int32_t end_time; //松開時間int8_t press_cnt; //按下次數int32_t time_gap; //時間間隔(兩次按下)int8_t judge_flag; //按鍵裁決
}key_state_t;enum {JUDGE = 0, SINGLE = 1, DBCLICK = 2}; //狀態機的狀態類型
extern key_state_t g_key[4]; //按鍵狀態全局變量//定時器回調函數,已經配置為10ms觸發一次定時器中斷回調
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM4) //已設置10ms觸發一次定時器中斷{static int32_t press_time_cnt[4] = {0}; //按下持續計數值static int32_t wait_press_cnt[4] = {0}; //等待按鍵按下計數值g_key[0].level_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);//讀取引腳電平值g_key[1].level_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1); g_key[2].level_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2); g_key[3].level_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0); for(int i=0; i<4; i++){switch(g_key[i].judge_flag){case JUDGE:if(g_key[i].level_state == GPIO_PIN_RESET) //按下{if(g_key[i].press_cnt == 0)g_key[i].judge_flag = 1;elseg_key[i].judge_flag = 2;}else if(g_key[i].level_state == GPIO_PIN_SET) //松開{ if(g_key[i].press_cnt == 1) //雙擊的第一次按下已經松開了{g_key[i].judge_flag = 2;}}break;case SINGLE: //第一次按下,長按、短按if(g_key[i].level_state == GPIO_PIN_RESET) //按鍵按下{press_time_cnt[i]++;}else if(g_key[i].level_state == GPIO_PIN_SET) //按鍵松開{if(press_time_cnt[i] > 50) //長按(按下持續時間>=500ms){g_key[i].long_flag = 1; //長按標志置1g_key[i].short_flag = 0;g_key[i].two_flag = 0;g_key[i].press_cnt = 0; //長按則清除雙擊的按下按鍵計數 }else if(press_time_cnt[i] > 9) //短按(持續時間>=90ms){g_key[i].short_flag = 0; //短按標志需要進一步判斷,在一定時間內,如果沒有第二次按下,則是短按g_key[i].long_flag = 0;g_key[i].two_flag = 0;if(g_key[i].press_cnt == 0){g_key[i].press_cnt = 1; //記錄雙擊的第一次按下}}g_key[i].judge_flag = 0; press_time_cnt[i] = 0; //清空按下和松開間的計數}break;case DBCLICK:if(g_key[i].level_state == GPIO_PIN_SET) //按鍵松開{if(wait_press_cnt[i]++ >= 25) //250ms內未按下第二次,說明為短按{g_key[i].short_flag = 1; //短按標志位置1g_key[i].long_flag = 0;g_key[i].two_flag = 0;g_key[i].press_cnt = 0;g_key[i].judge_flag = 0;wait_press_cnt[i] = 0; }}else if(g_key[i].level_state == GPIO_PIN_RESET) //第二次按鍵按下{if((press_time_cnt[i]++ >=7) && (wait_press_cnt[i] >= 5)) //50~250ms內第二次按下且按下的時間大于70ms,則是雙擊{g_key[i].two_flag = 1; //雙擊標志置1g_key[i].short_flag = 0;g_key[i].long_flag = 0;g_key[i].press_cnt = 0;g_key[i].judge_flag = 0;wait_press_cnt[i] = 0;press_time_cnt[i] = 0;}}break;}}}}
//頭文件int main(void)
{//其它代碼,如初始化,函數調用,變量定義//主函數中,調用按鍵識別算法部分邏輯代碼while (1){for(int i=0; i<4; i++){if(g_key[i].short_flag == 1){g_key[i].short_flag = 0;printf("\r\nKEY %d 短按\r\n", i+1);}else if(g_key[i].long_flag == 1){g_key[i].long_flag = 0;printf("\r\nKEY %d 長按\r\n", i+1);}else if(g_key[i].two_flag == 1){g_key[i].two_flag = 0;printf("\r\nKEY %d 雙擊\r\n", i+1); }}}return 0;
}
????????短按、長按、雙擊檢測效果圖