文章目錄
- 2、針對該開源框架理解
- 3、分析代碼
- 3.1 再談指針、數組、數組指針
- 3.2 繼續分析源碼
2、針對該開源框架理解
在編寫按鍵模塊的框架中,一定要先梳理按鍵相關的結構體、枚舉等變量。這些數據是判斷按鍵按下、狀態跳轉、以及綁定按鍵事件的核心。
這一部分定義是在驅動層文件 "multi_button.h"
,這個里面的數據類型雖然都是跟按鍵有關的,并且主要是驅動層在使用,但是這個地方需要走出一個誤區:
關于按鍵的相關的結構體、枚舉等變量。這一部分定義是在驅動層文件 "multi_button.h"
,這個里面的數據類型雖然都是跟按鍵有關的,并且主要是驅動層在使用,但是應用層也需要知道按鍵的樣子是什么樣子,這樣的目的是為了保證數據流的有效流動,是必要的。如果不這樣,每一個模塊的數據都是孤立存在的,產生不了聯系,這不算是使用全局變量。
multi_button.h
中定義的按鍵結構體(如 Button
)和事件枚舉(如 PRESS_DOWN
、LONG_PRESS
等)屬于驅動層對應用層的接口規范。
-
?應用層需要知道數據結構格式?:因應用層需創建按鍵實例(如
Button btn1;
)并傳遞給button_init()
,必須了解結構體成員(如pin
、event
)以正確初始化和處理事件回調。 -
?驅動層隱藏實現細節?:雖然結構體定義在頭文件中,但驅動層內部的狀態機邏輯、鏈表管理等實現仍封裝在
.c
文件中,對應用層不可見。
應用層通過read_button_gpio()
回調函數向驅動層提供硬件狀態,驅動層通過事件枚舉(如CLICK
)向應用層傳遞抽象結果。結構體/枚舉作為數據傳遞的載體,是層間通信的契約,確保數據格式一致。 -
按鍵結構體/枚舉在
multi_button.h
中的定義是必要的接口契約,用于保證驅動層與應用層間數據流的有效流動,不屬于全局變量濫用。 -
這種設計在滿足數據交互需求的同時,通過封裝驅動實現細節(如鏈表管理、狀態機),依然符合分層架構的高內聚、低耦合原則。
-
若需進一步隔離,可采用“句柄化”或標準化事件接口,但需權衡實現復雜度與資源消耗。
我覺得一定要帶著這種思想才能深入的去理解該按鍵框架的意義,體會作者的用意,知其所以然。
3、分析代碼
// 按鈕事件類型
typedef enum {BTN_PRESS_DOWN = 0, // 按鈕按下(物理按下動作)BTN_PRESS_UP, // 按鈕釋放(物理釋放動作)BTN_PRESS_REPEAT, // 檢測到重復按下(連按事件)BTN_SINGLE_CLICK, // 單擊完成(短按后釋放)BTN_DOUBLE_CLICK, // 雙擊完成(兩次快速單擊)BTN_LONG_PRESS_START, // 長按開始(達到長按閾值)BTN_LONG_PRESS_HOLD, // 長按保持(持續按住狀態)BTN_EVENT_COUNT, // 事件總數(用于數組定義)BTN_NONE_PRESS // 無事件(空閑狀態)
} ButtonEvent;// 按鈕狀態機狀態
typedef enum {BTN_STATE_IDLE = 0, // 空閑狀態(等待按下)BTN_STATE_PRESS, // 按下狀態(檢測到有效按下)BTN_STATE_RELEASE, // 釋放狀態(等待超時以區分單擊/雙擊)BTN_STATE_REPEAT, // 重復按下狀態(連按計數中)BTN_STATE_LONG_HOLD // 長按保持狀態(持續檢測長按)
} ButtonState;// 按鈕控制結構體
struct _Button {uint16_t ticks; // 時間戳計數器(用于超時判斷)uint8_t repeat : 4; // 重復按下計數(4位,范圍0-15)uint8_t event : 4; // 當前事件類型(4位,對應ButtonEvent)uint8_t state : 3; // 狀態機狀態(3位,對應ButtonState)uint8_t debounce_cnt : 3; // 消抖計數(3位,范圍0-7)uint8_t active_level : 1; // 有效觸發電平(1位,0=低電平有效,1=高電平有效)uint8_t button_level : 1; // 當前按鈕電平(1位,實時GPIO狀態)uint8_t button_id; // 按鈕標識符(用于多按鈕區分)uint8_t (*hal_button_level)(uint8_t button_id); // HAL層按鈕電平讀取函數指針BtnCallback cb[BTN_EVENT_COUNT]; // 事件回調函數數組(按事件類型注冊)Button* next; // 鏈表指針(支持多按鈕管理)
};
主要需要區分的是按鍵狀態機和按鍵事件類型。
按鍵事件的目的是為了和該時間相應的動作綁定,也就是在什么狀態下,執行什么應用邏輯。通過函數 button_attach
進行綁定,
這是調用文件:
button_attach(&btn1, BTN_PRESS_REPEAT, btn1_press_repeat_handler);具體實現是在multi_button.c文件
void button_attach(Button* handle, ButtonEvent event, BtnCallback cb)
{if (!handle || event >= BTN_EVENT_COUNT) return; // parameter validationhandle->cb[event] = cb;
}
其中 handle->cb[event] = cb;
這個目的就是接收傳遞的相關事件處理函數。
并且要想保證傳遞的按鍵處理函數有消息,我們其實是需要定義一個指針函數的,只有這樣傳遞進來的按鍵處理函數入口地址才能被編譯器有效識別為函數入口地址,不然編譯器只是知道這是一個地址。這也就是在之前工作中 胡哥
告訴我的。在函數傳遞的時候無所謂什么,只要是一個地址就行 ,但是在函數定義的時候,定義輸入參數的時候,我們必須要對這個參數是什么類型進行說明,例如是地址,我們就要聲明這是什么地址,是函數指針地址、一個指針變量還是單純的一個數組的入口地址。(可以理解為強轉,也可以理解為告訴編譯器或者告訴這個函數我這個地址本質是什么地址)。根本原因就是上述分析。
所以我們能看到在文件 multi_button.h
這行代碼聲明一個函數指針,就用來定義按鍵事件處理函數的入口地址。
typedef void (*BtnCallback)(Button* btn_handle);
3.1 再談指針、數組、數組指針
此外由于這里使用到了==指針、數組、數組指針==,所以有必要進行說明一下其中的區別與聯系:
在 C 語言中,數組的本質是連續內存塊,其內容完全由定義時的類型決定。
在C語言中,數組確實可以存儲任何類型,但必須明確定義元素類型,這樣才能保證正確訪問內存和調用函數。
數組和指針的本質理解:
數組是什么,數組的本質是連續的內存塊,注意是連續,也就是說可以通過下標進行訪問,并且申請的內存空間是和存儲的數據類型強相關的。本質也是一個個的連續地址。
指針是什么,指針就是地址,如果我們需要存儲一個函數,那么就需要將這個函數存儲到一塊內存空間,相當于是申請了一塊內存,那么編譯器在訪問的時候其實是先找到這個函數的入口地址從而進行訪問。而我們的指針變量就是存儲這個函數入口地址的內存空間,縱使這個函數的內存很大,需要很多個地址存儲,但是體現在指針變量里面就只是保存了一個入口地址,至于剩下的怎么執行完整這個函數,就不需要我們操心了。這個地方就是區別于數組,數組存儲一些內容需要的申請完整的內存空間,來存儲我們需要存儲數組里面的所有內容,也就是都要找到對應的地址空間。
數組是申請內存空間存儲內容,數組元素存儲的是實際數據值,除非該數組是指針數組(如 int* arr[5]
)。
int arr[3] = {10, 20, 30};
那這個10是怎么存儲的吶? 10表示的是0x0A 00 00 00
首先是申請12個字節內存空間,畢竟一個int數據占用的是四個字節,一個地接就需要一個地址存儲。
- 字節 0(低地址):`00001010` → `0x0A`
- 字節 1:`00000000` → `0x00`
- 字節 2:`00000000` → `0x00`
- 字節 3(高地址):`00000000` → `0x00`
這里其實我們經常說的一個字節,指的就是內存空間中的一個地址,對的就是一個實實在在的物理地址。
所以12個字節,其實就是需要12個實實在在的物理地址空間做支撐。
如果是該數組是指針數組,其實數組存儲的原本的10=0x0A 00 00 00此時存儲的是一個地址,畢竟地址也是一個16進制數,所以也只是將這個地址拆分,然后存儲到數組申請的實實在在的物理空間。例如
int a = 10, b = 20;
int* ptr_arr[2] = {&a, &b};
// ptr_arr[0]存儲a的地址(如0x2000),ptr_arr[1]存儲b的地址(如0x2004)
`&a`=`0x1000`32 位系統:地址值占 ?**4 字節**?(如 `0x00001000`) 也是四個字節。
雖然我們存儲的是地址,但是有點類似于int數據類型的存儲。
至此我們明白了數組存內容是怎么儲存的,
接下來再看指針:
指針也是申請內存空間存儲內容:存儲的是地址,不會存儲數據。
定義一個指針變量,其實本質也是在物料地址空間,申請位置,但是需要告訴編譯器我們是什么地址,畢竟內容不一樣需要申請的空間不一樣,但是需要注意的是這個指針變量如果是存儲函數或者是數組,僅僅只存儲它們的入口地址或者是首地址。
如果我們把這個這個函數存在數組中,其實也只是存儲的這個函數的入口地址,但是我們需要將這個數組類型進行聲明,因為只有這樣編譯器才知道我們使用的這個數組存儲的是一個指針數組。
從某種程度來說指針和數組沒有區別,但是又存在一些細微的區別。
數組更像是一種數據集合,固定大小,類型一致。
地址更像是一個地址容器,動態指向。
并且他們訪問形式是不一樣的。
3.2 繼續分析源碼
繼續言歸正傳分析按鍵開源框架:
接下來就是分析按鍵的狀態機跳轉,也就是如何判斷按鍵狀態的核心邏輯。
/*** @brief Button driver core function, driver state machine* @param handle: the button handle struct* @retval None*/static void button_handler(Button* handle)
{uint8_t read_gpio_level = button_read_level(handle);// Increment ticks counter when not in idle stateif (handle->state > BTN_STATE_IDLE) {handle->ticks++;}/*------------Button debounce handling---------------*/if (read_gpio_level != handle->button_level) {// Continue reading same new level for debounceif (++(handle->debounce_cnt) >= DEBOUNCE_TICKS) {handle->button_level = read_gpio_level;handle->debounce_cnt = 0;}} else {// Level not changed, reset counterhandle->debounce_cnt = 0;}/*-----------------State machine-------------------*/switch (handle->state) {case BTN_STATE_IDLE:if (handle->button_level == handle->active_level) {// Button press detectedhandle->event = (uint8_t)BTN_PRESS_DOWN;EVENT_CB(BTN_PRESS_DOWN);handle->ticks = 0;handle->repeat = 1;handle->state = BTN_STATE_PRESS;} else {handle->event = (uint8_t)BTN_NONE_PRESS;}break;case BTN_STATE_PRESS:if (handle->button_level != handle->active_level) {// Button releasedhandle->event = (uint8_t)BTN_PRESS_UP;EVENT_CB(BTN_PRESS_UP);handle->ticks = 0;handle->state = BTN_STATE_RELEASE;} else if (handle->ticks > LONG_TICKS) {// Long press detectedhandle->event = (uint8_t)BTN_LONG_PRESS_START;EVENT_CB(BTN_LONG_PRESS_START);handle->state = BTN_STATE_LONG_HOLD;}break;case BTN_STATE_RELEASE:if (handle->button_level == handle->active_level) {// Button pressed againhandle->event = (uint8_t)BTN_PRESS_DOWN;EVENT_CB(BTN_PRESS_DOWN);if (handle->repeat < PRESS_REPEAT_MAX_NUM) {handle->repeat++;}EVENT_CB(BTN_PRESS_REPEAT);handle->ticks = 0;handle->state = BTN_STATE_REPEAT;} else if (handle->ticks > SHORT_TICKS) {// Timeout reached, determine click typeif (handle->repeat == 1) {handle->event = (uint8_t)BTN_SINGLE_CLICK;EVENT_CB(BTN_SINGLE_CLICK);} else if (handle->repeat == 2) {handle->event = (uint8_t)BTN_DOUBLE_CLICK;EVENT_CB(BTN_DOUBLE_CLICK);}handle->state = BTN_STATE_IDLE;}break;case BTN_STATE_REPEAT:if (handle->button_level != handle->active_level) {// Button releasedhandle->event = (uint8_t)BTN_PRESS_UP;EVENT_CB(BTN_PRESS_UP);if (handle->ticks < SHORT_TICKS) {handle->ticks = 0;handle->state = BTN_STATE_RELEASE; // Continue waiting for more presses} else {handle->state = BTN_STATE_IDLE; // End of sequence}} else if (handle->ticks > SHORT_TICKS) {// Held down too long, treat as normal presshandle->state = BTN_STATE_PRESS;}break;case BTN_STATE_LONG_HOLD:if (handle->button_level == handle->active_level) {// Continue holdinghandle->event = (uint8_t)BTN_LONG_PRESS_HOLD;EVENT_CB(BTN_LONG_PRESS_HOLD);} else {// Released from long presshandle->event = (uint8_t)BTN_PRESS_UP;EVENT_CB(BTN_PRESS_UP);handle->state = BTN_STATE_IDLE;}break;default:// Invalid state, reset to idlehandle->state = BTN_STATE_IDLE;break;}
}
逐段代碼進行分析:
static void button_handler(Button* handle)
{}
uint8_t read_gpio_level = button_read_level(handle);
我們首先要依據GPIO口的高低電平來進行按鍵的判斷按鍵是否按下,因此首先就要將GPIO讀取函數給獲取到,因為在初始化按鍵結構體的時候我們已經裝填了獲取GPIO電平的函數,這個按鍵的結構體包含了按鍵判斷所有的條件,包括但不限于按鍵事件、按鍵狀態、按鍵電平、按鍵什么電平有效等等,前面已經解釋過了,這里就不一一贅述了,只是簡單提醒,要帶著這種思想。并且這些信息都存在了函數的輸入參數里面 handle
并且每一個按鍵的這些時間都是獨立計算的。按鍵與按鍵之間分開計算。
// Increment ticks counter when not in idle stateif (handle->state > BTN_STATE_IDLE) {handle->ticks++;}
這個時間的遞增是怎么實現的?
通俗來講,如果是 5ms
一次調用這個函數,那就可以理解為 5ms
變量 ticks
就會增加1,以此類推,從而實現了時間累計,并將這個是時間應用到下面代碼的邏輯跳轉。
這里在定時器還需要解決一個問題:定時精度依賴?:若 button_ticks()
調用間隔不穩定(如被高優先級中斷阻塞),會導致時間計算誤差。也就是在滴答定時器章節留下的問題。
ARM單片機滴答定時器理解與應用(一)(詳細解析)-CSDN博客
ARM單片機滴答定時器理解與應用(二)(詳細解析)(完)-CSDN博客
接著是按鍵軟件消抖動:
/*------------Button debounce handling---------------*/if (read_gpio_level != handle->button_level) {// Continue reading same new level for debounceif (++(handle->debounce_cnt) >= DEBOUNCE_TICKS) {handle->button_level = read_gpio_level;handle->debounce_cnt = 0;}} else {// Level not changed, reset counterhandle->debounce_cnt = 0;}
這要結合按鍵結構體的初始化,在初始化的時候我們將 handle->button_level
初始化為有效電平的相反數,
handle->button_level = !active_level; // initialize to opposite of active level
假設active_level
?:表示按鍵按下時的有效邏輯電平(例如 0
表示低電平觸發)。
并且假設系統啟動時按鍵處于釋放狀態,也就是按鍵沒有按下,此時物理電平應與有效電平相反(例如:若 active_level=0
,則釋放時應為高電平 1
)。
避免首次掃描的誤觸發?
- 若初始化時
button_level
與active_level
相同(例如均為0
),則首次調用button_handler
時:
read_gpio_level == handle->button_level // 均為0 → 電平“未變化”
即使按鍵實際處于釋放狀態(物理電平應為 1
),框架也會誤判為“按鍵已按下”,導致狀態機錯誤觸發 PRESS_DOWN
事件。
確保消抖機制正確啟動?
- 當按鍵首次被按下時,物理電平從釋放狀態(
1
)變為按下狀態(0
),此時:
read_gpio_level (0) != handle->button_level (1) // 電平變化 → 啟動消抖計數
若未初始化 button_level = !active_level
,首次按下可能因電平“未變化”而跳過消抖計數,直接誤判為穩定狀態。
經過上述的初始化分析:保證了我們的按鍵函數可以準確的進入到消抖動分析,
整體邏輯如下:
A[檢測電平變化] --> B{連續N次相同?}
B – 是 --> C[更新button_level]
B – 否 --> D[重置計數器]
體現在代碼就是:
if (++(handle->debounce_cnt) >= DEBOUNCE_TICKS) {handle->button_level = read_gpio_level;handle->debounce_cnt = 0;}
首先進行次數判斷,為何必須用前置 ++
而非后置?
- 邏輯一致性?:消抖需在連續
DEBOUNCE_TICKS
次變化后立即響應,前置++
確保本次掃描被計入后立刻判斷。 - ?避免漏判?:后置
++
會延遲計數更新,導致本次掃描的檢測結果未被納入當前判斷。
假設連續三次穩定了,那么我們就認為電平穩定了,覆蓋掉初始化的結果,將當前檢測的結果賦值給 handle->button_level = read_gpio_level;
這樣在下一次掃描就不會再執行這個函數,然后將消抖的計數器給清空。
接著就是狀態機的跳轉:
case BTN_STATE_IDLE:if (handle->button_level == handle->active_level) {// Button press detectedhandle->event = (uint8_t)BTN_PRESS_DOWN;EVENT_CB(BTN_PRESS_DOWN);handle->ticks = 0;handle->repeat = 1;handle->state = BTN_STATE_PRESS;} else {handle->event = (uint8_t)BTN_NONE_PRESS;}break;
因為前面已經將電平更新為實際檢測到的 handle->button_level = read_gpio_level;
之所以這樣是因為消抖完成,所以在狀態滿足 if
語句 handle->button_level == handle->active_level
就是說我檢測到的電平和有效電平是一致的,那就是有效按鍵,然后開始清空相關內容,并將狀態設置為按鍵按下 handle->state = BTN_STATE_PRESS;
在消抖的時候按鍵事件一直是 handle->event = (uint8_t)BTN_NONE_PRESS;
,按鍵狀態一直是空閑狀態(等待按下) handle->state = BTN_STATE_IDLE;
,
handle->ticks = 0; 清空的時機需要多留心一下。
然后接著跳轉到:
default:// Invalid state, reset to idlehandle->state = BTN_STATE_IDLE;break;
在消抖過程形成完美閉環。
文章源碼獲取方式:
如果您對本文的源碼感興趣,歡迎在評論區留下您的郵箱地址。我會在空閑時間整理相關代碼,并通過郵件發送給您。由于個人時間有限,發送可能會有一定延遲,請您耐心等待。同時,建議您在評論時注明具體的需求或問題,以便我更好地為您提供針對性的幫助。
【版權聲明】
本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議。這意味著您可以自由地共享(復制、分發)和改編(修改、轉換)本文內容,但必須遵守以下條件:
署名:您必須注明原作者(即本文博主)的姓名,并提供指向原文的鏈接。
相同方式共享:如果您基于本文創作了新的內容,必須使用相同的 CC 4.0 BY-SA 協議進行發布。
感謝您的理解與支持!如果您有任何疑問或需要進一步協助,請隨時在評論區留言。