第1期 | MultiButton,一個小巧簡單易用的事件驅(qū)動型按鍵驅(qū)動模塊
嵌入式開源項目精選專欄
本專欄由Mculover666創(chuàng)建,主要內(nèi)容為尋找嵌入式領域內(nèi)的優(yōu)質(zhì)開源項目,一是幫助開發(fā)者使用開源項目實現(xiàn)更多的功能,二是通過這些開源項目,學習大佬的代碼及背后的實現(xiàn)思想,提升自己的代碼水平,和其它專欄相比,本專欄的優(yōu)勢在于:
不會單純的介紹分享項目,還會包含作者親自實踐的過程分享,甚至還會有對它背后的設計思想解讀。
目前本專欄包含的開源項目有:
-
SFUD | 一個簡潔實用的開源項目,幫你輕松搞定SPI Flash -
cJSON | 一個輕量級C語言JSON解析器 -
paho | 支持10種語言編寫mqtt客戶端,總有一款適合你!
如果您自己編寫或者發(fā)現(xiàn)的開源項目不錯,歡迎留言或者私信投稿到本專欄,分享獲得雙倍的快樂!
1. MultiButton
本期給大家?guī)淼拈_源項目是 MultiButton,一個小巧簡單易用的事件驅(qū)動型按鍵驅(qū)動模塊,作者 0x1abin,目前收獲 222 個star,遵循 MIT 開源許可。
這個項目非常精簡,只有兩個文件,可無限量擴展按鍵,按鍵事件的回調(diào)異步處理方式可以簡化程序結(jié)構(gòu),去除冗余的按鍵處理硬編碼,讓你的按鍵業(yè)務邏輯更清晰。MuliButton 支持如下的按鈕事件:
事件 | 說明 |
---|---|
PRESS_DOWN | 按鍵按下,每次按下都觸發(fā) |
PRESS_UP | 按鍵彈起,每次松開都觸發(fā) |
PRESS_REPEAT | 重復按下觸發(fā),變量repeat計數(shù)連擊次數(shù) |
SINGLE_CLICK | 單擊按鍵事件 |
DOUBLE_CLICK | 雙擊按鍵事件 |
LONG_RRESS_START | 達到長按時間閾值時觸發(fā)一次 |
LONG_PRESS_HOLD | 長按期間一直觸發(fā) |
GIthub地址:https://github.com/0x1abin/MultiButton
2. 使用MultiButton
2.1. 準備一份裸機工程
需要掌握使用HAL庫讀取GPIO輸入的函數(shù)、串口的使用、printf重定向、以及systick的使用:
-
STM32CubeMX | 04-使用GPIO進行按鍵檢測 -
STM32CubeMX | 06-使用USART發(fā)送和接收數(shù)據(jù)(查詢模式) -
STM32CubeMX | 09-重定向printf函數(shù)到串口輸出的多種方法 -
STM32CubeMX | 27-系統(tǒng)滴答定時器Systick的使用
本文中我使用小熊派IoT開發(fā)板,主控為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 */
② 定義一個按鍵結(jié)構(gòu)(按鍵對象)
/* USER CODE BEGIN PV */
//申請一個按鍵結(jié)構(gòu)
struct Button button1;
/* USER CODE END PV */
③ 初始化按鍵對象
初始化按鍵對象使用的API為:
-
第一個參數(shù)為剛剛創(chuàng)建的按鍵對象的指針; -
第二個參數(shù)為綁定按鍵的GPIO電平讀取接口; -
第三個參數(shù)為設置有效觸發(fā)電平;
首先在main函數(shù)之前實現(xiàn)一個GPIO電平讀取接口:
/* USER CODE BEGIN 0 */
//按鍵狀態(tài)讀取接口
uint8_t read_button1_GPIO()
{
return HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin);
}
/* USER CODE END 0 */
初始化按鍵對象的代碼在main函數(shù)中,while(1)之前編寫,如下:
/* USER CODE BEGIN 2 */
printf("MultiButton Test...\r\n");
//初始化按鍵對象
button_init(&button1, read_button1_GPIO, 0);
/* USER CODE END 2 */
④ 注冊按鍵事件
注冊按鈕事件的API如下:
-
第一個參數(shù)為按鈕對象指針; -
第二個參數(shù)為MultiButton支持的按鈕事件; -
第三個參數(shù)為要注冊的該事件回調(diào)函數(shù);
MultiButton支持的按鈕事件枚舉如下:首先在main函數(shù)之前定義這兩個事件的回調(diào)函數(shù),回調(diào)函數(shù)有兩種寫法。
第一種適合于按鍵事件較少的情況:
//按鍵1按下事件回調(diào)函數(shù)
void btn1_press_down_Handler(void* btn)
{
printf("---> key1 press down! <---\r\n");
}
//按鍵1松開事件回調(diào)函數(shù)
void btn1_press_up_Handler(void* btn)
{
printf("***> key1 press up! <***\r\n");
}
在main函數(shù)中,while(1)之前注冊這兩個回調(diào)函數(shù):
//注冊按鈕事件回調(diào)函數(shù)
button_attach(&button1, PRESS_DOWN, btn1_press_down_Handler);
button_attach(&button1, PRESS_UP, btn1_press_up_Handler);
第二種適合于按鍵事件較多的情況,如果每個按鍵都要寫 7 個回調(diào)函數(shù),那么代碼量會非常的大,所以可以將這 7 個回調(diào)函數(shù)寫在一起,一次性全部注冊,回調(diào)函數(shù)如下:
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;
}
}
使用這種回調(diào)函數(shù)的時候需要在MultiButton的源碼中添加一行代碼:注冊回調(diào)函數(shù)的代碼如下:
//注冊按鈕事件回調(diào)函數(shù)
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函數(shù)中,while(1)之前編寫代碼,啟動按鍵:
//啟動按鍵
button_start(&button1);
⑥ 設置一個5ms間隔的定時器循環(huán)調(diào)用后臺處理函數(shù)
這里就要用到systick了,在main函數(shù)的while(1)循環(huán)中編寫如下代碼:
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
//每隔5ms調(diào)用一次后臺處理函數(shù)
button_ticks();
HAL_Delay(5);
}
/* USER CODE END 3 */
2.4. 實驗現(xiàn)象
編譯、下載之后,每次按下Key1時打印按下提示,松開Key1時打印松開提示:
2.5. 擴展實驗
在注冊回調(diào)函數(shù)時將這按下和松開屏蔽,將單擊和雙擊打開進行測試:
//注冊按鈕事件回調(diào)函數(shù)
//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);
再測試長按:
//注冊按鈕事件回調(diào)函數(shù)
//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. 面向?qū)ο笏枷?/span>
MultiButton中每個按鍵都抽象為了一個按鍵對象,每個按鍵對象是獨立的,系統(tǒng)中所有的按鍵對象使用單鏈表串起來,結(jié)構(gòu)如下:其中在變量后面跟冒號的語法稱為位域,使用位域的優(yōu)勢是節(jié)省內(nèi)存。
比如在這個結(jié)構(gòu)體中,本來 6 個uint8_t 類型的變量需要占用 6 個字節(jié),但使用位域語法后,這6個變量只占用兩個字節(jié):
3.2. 按鍵對象單鏈表
MultiButton自己定義了一個頭指針:
//button handle list head.
static struct Button* head_handle = NULL;
用戶插入一個按鍵對象的代碼如下:
//啟動按鍵
button_start(&button1);
那么,button_start插入新的按鍵對象之后,單鏈表長啥樣呢?
理解了 button_start 的源碼就很好知道答案了:第一次插入時,因為head_hanler 為 NULL,所以只需要執(zhí)行while之后的代碼,按照它的插入于原理,如果再插入一個buuton2按鍵對象,結(jié)果是不是可以猜出來了呢?
沒錯,它長這樣:這樣做是不是有點不符合常理?后插入Button2竟然在button1前面,憑什么?
這又不是排隊搶雞蛋,在前在后沒什么關(guān)系的。只是這樣的插入方法在代碼算法上會非常簡潔,兩行代碼完成插入。
3.3. 狀態(tài)機處理思想
MultiButton中使用狀態(tài)機來處理每個按鍵對象(的狀態(tài)),比如在上述應用中根據(jù)Systick提供的時基信號,每隔5ms調(diào)用一次 button_tick()
,該函數(shù)會依次調(diào)用狀態(tài)機對單鏈表上的所有按鍵對象進行遍歷處理:根據(jù)上一節(jié)的單鏈表講解,系統(tǒng)中定義的鏈表頭指針 head_handle 永遠指向最后一個插入的按鍵對象,所以無需任何參數(shù)即可遍歷整個單鏈表上的對象,非常之牛逼。
使用 button_handler 來對按鍵對象的狀態(tài)進行處理,該函數(shù)源碼如下:
(讀源碼的時候只需要記住該函數(shù)每隔5ms進入一次就很好分析了)
① 讀取當前引腳狀態(tài)
調(diào)用該按鍵對象注冊的讀取狀態(tài)函數(shù)進行讀?。孩?讀取之后,判斷當前狀態(tài)機的狀態(tài),如果有功能正在執(zhí)行(state不為0),則按鍵對象的tick值加1(后續(xù)一切功能的基礎):③ 按鍵消抖(連續(xù)讀取3次,15ms,如果引腳狀態(tài)一直與之前不同,則改變按鍵對象中的引腳狀態(tài)):④ 狀態(tài)機(整個設計的靈魂所在)
4. 項目工程源碼獲取和問題交流
目前我將MultiButton源碼、我移植到小熊派STM32L431RCT6開發(fā)板的工程、移植到STM32Nucleo-STM32G071RB開發(fā)板的工程源碼上傳到了QQ群里(包含好幾份HAL庫,QQ相對速度快點),可以在QQ群里下載,移植有問題也可以在群里交流,當然也歡迎大家分享出來自己移植的工程到QQ群里:
放上QQ群二維碼:
接收更多精彩文章及資源推送,歡迎訂閱我的微信公眾號:『mculover666』。
免責聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!