MultiButton | 一個小巧簡單易用的事件驅動型按鍵驅動模塊

嵌入式開源項目精選專欄

本專欄由Mculover666創建,主要內容爲尋找嵌入式領域內的優質開源項目,一是幫助開發者使用開源項目實現更多的功能,二是通過這些開源項目,學習大佬的代碼及背後的實現思想,提升自己的代碼水平,和其它專欄相比,本專欄的優勢在於:

不會單純的介紹分享項目,還會包含作者親自實踐的過程分享,甚至還會有對它背後的設計思想解讀

目前本專欄包含的開源項目有:

如果您自己編寫或者發現的開源項目不錯,歡迎留言或者私信投稿到本專欄,分享獲得雙倍的快樂!

1. MultiButton

本期給大家帶來的開源項目是 MultiButton,一個小巧簡單易用的事件驅動型按鍵驅動模塊,作者 0x1abin,目前收穫 222 個star,遵循 MIT 開源許可。

這個項目非常精簡,只有兩個文件,可無限量擴展按鍵,按鍵事件的回調異步處理方式可以簡化程序結構,去除冗餘的按鍵處理硬編碼,讓你的按鍵業務邏輯更清晰。

MuliButton 支持如下的按鈕事件:

事件 說明
PRESS_DOWN 按鍵按下,每次按下都觸發
PRESS_UP 按鍵彈起,每次鬆開都觸發
PRESS_REPEAT 重複按下觸發,變量repeat計數連擊次數
SINGLE_CLICK 單擊按鍵事件
DOUBLE_CLICK 雙擊按鍵事件
LONG_RRESS_START 達到長按時間閾值時觸發一次
LONG_PRESS_HOLD 長按期間一直觸發

GIthub地址:https://github.com/0x1abin/MultiButton

2. 使用MultiButton

2.1. 準備一份裸機工程

需要掌握使用HAL庫讀取GPIO輸入的函數、串口的使用、printf重定向、以及systick的使用:

本文中我使用小熊派IoT開發板,主控爲STM32L431RCT6:

配置外部時鐘:

按鍵GPIO配置:

打印串口配置:

時鐘配置:

配置工程,生成代碼,重定向printf,printf可以正常打印後進行下面的步驟

2.2. 移植MultiButton

① 複製MultiButton源碼到裸機工程中:

② 添加MultiButton源碼到項目中:


此時編譯沒有問題。

2.3. 編寫MultiButton應用代碼

在main.c文件中編寫以下代碼。

包含頭文件

/* USER CODE BEGIN Includes */

#include <stdio.h>		//要使用printf
#include "multi_button.h"

/* USER CODE END Includes */

定義一個按鍵結構(按鍵對象)

/* USER CODE BEGIN PV */

//申請一個按鍵結構
struct Button button1;

/* USER CODE END PV */

初始化按鍵對象

初始化按鍵對象使用的API爲:

  • 第一個參數爲剛剛創建的按鍵對象的指針;
  • 第二個參數爲綁定按鍵的GPIO電平讀取接口;
  • 第三個參數爲設置有效觸發電平;

首先在main函數之前實現一個GPIO電平讀取接口:

/* USER CODE BEGIN 0 */

//按鍵狀態讀取接口
uint8_t read_button1_GPIO() 
{
	return HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin);
}

/* USER CODE END 0 */

初始化按鍵對象的代碼在main函數中,while(1)之前編寫,如下:

/* USER CODE BEGIN 2 */
printf("MultiButton Test...\r\n");

//初始化按鍵對象
button_init(&button1, read_button1_GPIO, 0);

/* USER CODE END 2 */

④ 註冊按鍵事件

註冊按鈕事件的API如下:

  • 第一個參數爲按鈕對象指針;
  • 第二個參數爲MultiButton支持的按鈕事件;
  • 第三個參數爲要註冊的該事件回調函數;

MultiButton支持的按鈕事件枚舉如下:

首先在main函數之前定義這兩個事件的回調函數,回調函數有兩種寫法。

第一種適合於按鍵事件較少的情況

//按鍵1按下事件回調函數
void btn1_press_down_Handler(void* btn)
{
	printf("---> key1 press down! <---\r\n");
}

//按鍵1鬆開事件回調函數
void btn1_press_up_Handler(void* btn)
{
	printf("***> key1 press up! <***\r\n");
}

在main函數中,while(1)之前註冊這兩個回調函數:

//註冊按鈕事件回調函數
button_attach(&button1, PRESS_DOWN, btn1_press_down_Handler);
button_attach(&button1, PRESS_UP, btn1_press_up_Handler);

第二種適合於按鍵事件較多的情況,如果每個按鍵都要寫 7 個回調函數,那麼代碼量會非常的大,所以可以將這 7 個回調函數寫在一起,一次性全部註冊,回調函數如下:

void button_callback(void *button)
{
    uint32_t btn_event_val; 
    
    btn_event_val = get_button_event((struct Button *)button); 
    
    switch(btn_event_val)
    {
	    case PRESS_DOWN:
	        printf("---> key1 press down! <---\r\n"); 
	    	break; 
	
	    case PRESS_UP: 
	        printf("***> key1 press up! <***\r\n");
	    	break; 
	
	    case PRESS_REPEAT: 
	        printf("---> key1 press repeat! <---\r\n");
	    	break; 
	
	    case SINGLE_CLICK: 
	        printf("---> key1 single click! <---\r\n");
	    	break; 
	
	    case DOUBLE_CLICK: 
	        printf("***> key1 double click! <***\r\n");
	    	break; 
	
	    case LONG_RRESS_START: 
	        printf("---> key1 long press start! <---\r\n");
	   		break; 
	
	    case LONG_PRESS_HOLD: 
	        printf("***> key1 long press hold! <***\r\n");
	    	break; 
	}
}

使用這種回調函數的時候需要在MultiButton的源碼中添加一行代碼:

註冊回調函數的代碼如下:

//註冊按鈕事件回調函數

button_attach(&button1, PRESS_DOWN,       button_callback);
button_attach(&button1, PRESS_UP,         button_callback);
//button_attach(&button1, PRESS_REPEAT,     button_callback);
//button_attach(&button1, SINGLE_CLICK,     button_callback);
//button_attach(&button1, DOUBLE_CLICK,     button_callback);
//button_attach(&button1, LONG_RRESS_START, button_callback);
//button_attach(&button1, LONG_PRESS_HOLD,  button_callback);

⑤ 啓動按鍵
啓動按鍵的API如下:

接着在main函數中,while(1)之前編寫代碼,啓動按鍵:

//啓動按鍵
button_start(&button1);

⑥ 設置一個5ms間隔的定時器循環調用後臺處理函數

這裏就要用到systick了,在main函數的while(1)循環中編寫如下代碼:

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		//每隔5ms調用一次後臺處理函數
		button_ticks();
		HAL_Delay(5);
  }
  /* USER CODE END 3 */

2.4. 實驗現象

編譯、下載之後,每次按下Key1時打印按下提示,鬆開Key1時打印鬆開提示:

2.5. 擴展實驗

在註冊回調函數時將這按下和鬆開屏蔽,將單擊和雙擊打開進行測試:

//註冊按鈕事件回調函數
//button_attach(&button1, PRESS_DOWN,       button_callback);
//button_attach(&button1, PRESS_UP,         button_callback);
//button_attach(&button1, PRESS_REPEAT,     button_callback);
button_attach(&button1, SINGLE_CLICK,     button_callback);
button_attach(&button1, DOUBLE_CLICK,     button_callback);
//button_attach(&button1, LONG_RRESS_START, button_callback);
//button_attach(&button1, LONG_PRESS_HOLD,  button_callback);


再測試長按:

	//註冊按鈕事件回調函數
	//button_attach(&button1, PRESS_DOWN,       button_callback);
	//button_attach(&button1, PRESS_UP,         button_callback);
	//button_attach(&button1, PRESS_REPEAT,     button_callback);
	//button_attach(&button1, SINGLE_CLICK,     button_callback);
	//button_attach(&button1, DOUBLE_CLICK,     button_callback);
	button_attach(&button1, LONG_RRESS_START, button_callback);
	button_attach(&button1, LONG_PRESS_HOLD,  button_callback);

3. MultiButton設計思想解讀

3.1. 面向對象思想

MultiButton中每個按鍵都抽象爲了一個按鍵對象,每個按鍵對象是獨立的,系統中所有的按鍵對象使用單鏈表串起來,結構如下:

其中在變量後面跟冒號的語法稱爲位域,使用位域的優勢是節省內存

比如在這個結構體中,本來 6 個uint8_t 類型的變量需要佔用 6 個字節,但使用位域語法後,這6個變量只佔用兩個字節

3.2. 按鍵對象單鏈表

MultiButton自己定義了一個頭指針

//button handle list head.
static struct Button* head_handle = NULL;

用戶插入一個按鍵對象的代碼如下:

//啓動按鍵
button_start(&button1);

那麼,button_start插入新的按鍵對象之後,單鏈表長啥樣呢?

理解了 button_start 的源碼就很好知道答案了:

第一次插入時,因爲head_hanler 爲 NULL,所以只需要執行while之後的代碼,

按照它的插入於原理,如果再插入一個buuton2按鍵對象,結果是不是可以猜出來了呢?

沒錯,它長這樣:

這樣做是不是有點不符合常理?後插入Button2竟然在button1前面,憑什麼?

這又不是排隊搶雞蛋,在前在後沒什麼關係的。只是這樣的插入方法在代碼算法上會非常簡潔,兩行代碼完成插入。

3.3. 狀態機處理思想

MultiButton中使用狀態機來處理每個按鍵對象(的狀態),比如在上述應用中根據Systick提供的時基信號,每隔5ms調用一次 button_tick(),該函數會依次調用狀態機對單鏈表上的所有按鍵對象進行遍歷處理:

根據上一節的單鏈表講解,系統中定義的鏈表頭指針 head_handle 永遠指向最後一個插入的按鍵對象,所以無需任何參數即可遍歷整個單鏈表上的對象,非常之牛逼。

使用 button_handler 來對按鍵對象的狀態進行處理,該函數源碼如下:

(讀源碼的時候只需要記住該函數每隔5ms進入一次就很好分析了)

讀取當前引腳狀態

調用該按鍵對象註冊的讀取狀態函數進行讀取:

② 讀取之後,判斷當前狀態機的狀態,如果有功能正在執行(state不爲0),則按鍵對象的tick值加1(後續一切功能的基礎):

按鍵消抖(連續讀取3次,15ms,如果引腳狀態一直與之前不同,則改變按鍵對象中的引腳狀態):

狀態機(整個設計的靈魂所在)

4. 項目工程源碼獲取和問題交流

目前我將MultiButton源碼、我移植到小熊派STM32L431RCT6開發板的工程、移植到STM32Nucleo-STM32G071RB開發板的工程源碼上傳到了QQ羣裏(包含好幾份HAL庫,QQ相對速度快點),可以在QQ羣裏下載,有問題也可以在羣裏交流,當然也歡迎大家分享出來自己移植的工程到QQ羣裏

放上QQ羣二維碼:

接收更多精彩文章及資源推送,歡迎訂閱我的微信公衆號:『mculover666』。

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