文章目錄
- 3、分析代碼
- 3.3 按鍵的插入
- 3.4 按鍵的刪除
- 3.5 繼續分析狀態機核心理解
- 4、寫在最后的總結
- 5、思想感悟篇
- 6、慈悲不渡自絕人
3、分析代碼
3.3 按鍵的插入
// Button handle list headstatic Button* head_handle = NULL;/*** @brief Start the button work, add the handle into work list* @param handle: target handle struct* @retval 0:succeed, -1:already exist, -2:invalid parameter*/int button_start(Button* handle){if (!handle) return -2; // invalid parameterButton* target = head_handle;while (target) {if (target == handle) return -1; // already existtarget = target->next;}handle->next = head_handle;head_handle = handle;return 0;}
主要是最后兩行代碼:
表示的是頭插法,也就是說只要是一個新的按鍵,這個新的按鍵結構體里面永遠存儲的是當前已經存在按鍵鏈表的第一個節點,然后我們的頭結點就被這個新按鍵給覆蓋掉。為什么head_handle
可以被循環替換,這是因為我們前面定義了static關鍵字,它存在于整個程序生命周期,并且還只能在按鍵的文件調用。
但是需要注意的是,這個使用的是頭指針,不是頭結點方式插入。
概念? | ?頭指針 (head_handle )?? | ?頭結點? |
---|---|---|
?定義? | 指向鏈表第一個節點的指針變量(存儲地址) | 位于鏈表首部的輔助節點?(不存儲有效數據) |
?內存占用? | 僅占用一個指針的空間(如 4/8 字節) | 占用完整節點結構體的空間(含數據域和指針域) |
?初始化? | NULL (表示空鏈表) | 需動態分配內存(如 malloc ) |
? | static Button* head_handle = NULL; | 未定義,代碼中未創建頭結點 |
無頭結點節省了內存(尤其在小內存嵌入式設備中關鍵)。 |
3.4 按鍵的刪除
需要注意的是首節點刪除需特殊處理
若刪除鏈表第一個節點(如 btn2
),需更新頭指針:
void delete_first_node() {Button* temp = head_handle;head_handle = head_handle->next; // 更新頭指針free(temp);
}
相當于是直接把第一個結點內部存的下一個節點的直接賦值給當前的head_handle
。
在鏈表頭節點刪除操作中,使用臨時指針 Button* temp = head_handle;
?是必要且關鍵的,直接跳過此步驟僅更新頭指針(如 head_handle = head_handle->next;
)會導致嚴重內存問題。
void delete_first_node() {head_handle = head_handle->next; // 直接更新頭指針
}
- 原頭節點(
head_handle
指向的節點)未被釋放,其內存空間無法被系統回收。 - 在嵌入式系統中,內存資源有限,頻繁操作后可能耗盡內存,引發系統崩潰。
- 若其他代碼持有原頭節點的指針,該指針會指向已失效的內存區域(稱為野指針)。
- 后續訪問野指針(如
head_handle->data
)會導致未定義行為(程序崩潰或數據錯誤)。 - 原頭節點的數據若需清理(如動態分配的子資源),跳過
free(temp)
會遺漏資源釋放
相當于是雖然改變了head_handle 指向的節點,但是之前結點的地址還是存著內容,所以我們需要將這個結點的入口地址傳遞給free(temp);進行釋放。
-
安全釋放內存?
temp
臨時保存原頭節點地址,確保free()
能準確釋放該內存。 -
?避免野指針?
釋放后,temp
生命周期結束,不會遺留野指針(而原head_handle
已更新指向新節點)。 -
?支持資源清理?
若節點包含動態分配的資源(如字符串緩沖區),可在free(temp)
前先釋放其子資源。 -
當執行
temp = head_handle
時,temp
保存了原頭節點的物理內存地址?(如0x0012FF88
)。 -
此后
head_handle = head_handle->next
修改頭指針,但temp
仍持有原節點的地址,確保能精準定位需釋放的內存塊。 -
原節點成為“內存孤島”,程序失去對其訪問權,但內存仍被占用 → ?內存泄漏?(Memory Leak);
-
在長期運行的嵌入式系統中,此類泄漏會累積耗盡內存,導致系統崩潰。
內存的釋放,釋放的是這個地址,而這個地址是存儲在指針變量里面的,注意我們不是釋放指針變量,是釋放指針變量存儲的地址,這個一定要繞過來,別被繞進去了。這一點很關鍵。
3.5 繼續分析狀態機核心理解
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;}
}
基于以上兩個圖片進行相關內容解析
1、如果一個按鍵是按下有效,那么就要避免增加長按功能的。
首先根據這兩個圖可以看出無論是短按或者長按都會產生一下按鍵按下事件,因此如果一個按鍵在設計為長按有效(不論是長按抬起有效還是按下有效)的同時還具有按下有效的功能,都會觸發按鍵按下事件。這樣就會造成功能沖突,也就是在想要長按功能的時候必定會帶來短按功能的啟動,并且在當前狀態機時沒辦法解決的,因此只能在設計的時候避免。
2、該狀態機在按鍵長按模式下,還可以區分按鍵長按抬起有效和按鍵長按按下有效。
因為在按鍵長按的過程中會有劃分了三個事件:
第一個是按鍵長按開始事件(按鍵按下狀態機),這個事件就是按鍵屬性為長按按下有效發生,因為只要滿足了長按的時間閾值就會立即觸發該事件;
第二個是按鍵長按保持事件(按鍵長按保持狀態機),這個事件只是單純的在超過按鍵長按時間閾值以后會一直觸發的事件,保持的時間并沒有明確要求,只要是超過按鍵長按時間閾值以后的時間都屬于按鍵長按保持時間并且會觸發按鍵長安保持事件;
第三個是按鍵長按抬起事件(按鍵長按保持狀態機),這個事件就是表示在按鍵長按保存狀態的時候釋放按鍵,那么就會產生電平跳變,在消抖以后就會判斷出按鍵抬起,此時就是觸發按鍵長按抬起事件。
長按保持這段時間在當前狀態機是沒有辦法衡量的,因為沒有增加相關變量記錄時間。
3、該狀態機按鍵短按單擊完成的判斷時間需要仔細衡量一下。
這是因為在短按按下動作和抬起動作會有消抖因素的存在,因此這一部分其實也是占用時間的,而按鍵短按單擊完成(按鍵釋放狀態機)判斷的時間累計是從該按鍵短按抬起以后開始累計的。并且按鍵單擊完成和雙擊完成都是在按鍵釋放狀態機完成相關事件觸發的。
4、該狀態機Tick時間的更新。
Tick時間的更新條件是(按鍵狀態> 按鍵釋放狀態),也就是說存在一種情況再按鍵長按的時候,Tick是一直增加的,但是在按鍵釋放狀態機和按鍵長按保持狀態機都是沒有清零的,如果按鍵長按松開以后狀態變為按鍵空閑狀態,此時Tick就不會增加, 按照現有代碼清零是在該按鍵下一次按下的時候清空這個Tick。可以在這里進行改進,將Tick清零放在(按鍵長按保持狀態機)按鍵長按抬起的時候。
狀態機:按鍵釋放狀態。
此時間是兩次雙擊最大間隔時間,如果超過這個間隔,就會觸發是按鍵短按單擊事件。
狀態機:按鍵釋放狀態。
此時處于第二次按鍵按下狀態,首先觸發第二次按鍵按下事件,repeat++自增,接著觸發重復按下第二次事件,清空時間tick=0,進入按鍵重復狀態機。
觸發重復按下第二次事件:這個地方可以理解為只要出現第二次按鍵按下的狀態,就認為連擊完成,僅作為理解。
分析:分析按鍵重復狀態機
需要在這里著重分析里面的狀態,因為源碼在這里存在一些不合理之處。
1、按鍵重復狀態機在第二次按鍵按下動作以后并沒有釋放而是一直按下,此時tick是從零自增,那么時間如果超過SHORT_TICKS狀態機跳轉至按鍵按下狀態機BTN_STATE_PRESS,并且一直還是按下,那么最終會進入按鍵長按保持狀態機BTN_STATE_LONG_HOLD,這種情況并無什么影響,也符合邏輯。但是如果我在按鍵長按判斷之前釋放按鍵,此時出現按鍵抬起事件,tick清空并且進入到按鍵釋放狀態機,然后在該狀態機重新執行handle->ticks > SHORT_TICKS判斷,并且由于前面repeat自增,導致repeat=2,那么就會觸發按鍵雙擊完成事件,這在邏輯是不合理的因為這個過程已經過去了很多個時間,準確來說已經不屬于連擊的范疇了。
2、按鍵重復狀態機第二次按鍵抬起, 首先會經過抬起動作消抖,接著會產生一個第二次按鍵抬起事件,并且tick會自增,并且這個時間是理論上是小于handle->ticks < SHORT_TICKS,這是因為上一個狀態是按下,時間會一直自增,接著抬起消抖完成時間不會清空,因為是在SHORT_TICKS時間內完成的抬起,不然就已經跳到了上述分析的情況1,但是由于消抖會有時間,因此也可能存在卡點,那么這種情況就是直接將狀態機跳轉至按鍵釋放狀態,然后產生一個無按鍵按下事件,這種情況也是無傷大雅,符合邏輯。接著我們繼續分析符合理論情況的時間小于SHORT_TICKS,首先將ticks清空,然后狀態機跳轉至按鍵釋放狀態BTN_STATE_RELEASE,最后在延時一個SHORT_TICKS時間,由于此時repeat=2,因此完美的執行按鍵雙擊完成事件,
假如一個按鍵是按下有效,那么就要避免增加長按功能的,因為這種設計會造成功能沖突,也就是說當這個按鍵按下的時候,首先會觸發按下有效事件,接著會觸發長按事件功能。
如果想把一個按鍵集成短按有用和長按都有不同的功能,那其實就需要將短按設計為抬起有效,這樣如果在長按的過程中沒有抬起,那就不會觸發短按功能,按鍵按下的時間累計達到長按閾值,就會觸發長按功能,這樣就完美的避免了長按和短按同時存在,長按會先觸發短按功能。
4、寫在最后的總結
根據開源按鍵框架MultiButton-master,在basic_example.c文件里面有一個buttons_init函數,其作用是首先是初始化一個 button_init(&btn1, read_button_gpio, 1, 1);但是這個函數 是在multi_button.c里面,因此為了避免暴露bnt1,傳入的是這個結構體的首地址,完成初始化以后,更新鏈表 button_start(&btn1); button_start(&btn2);同樣這兩個函數也在multi_button.c里面,而在文件multi_button.c里面static Button* head_handle = NULL;鏈表用來接收bt1和bt2,并且在調用按鍵處理函數的時候 simulate_button_press(1, 100);并沒有傳入btn1和btn2,并且這個函數是實在multi_button.c里面,所以他實際執行 void button_ticks(void) { Button* target; for (target = head_handle; target; target = target->next) { button_handler(target); } }里面的head_handle這個鏈表已經包含了btn1和btn2,,這樣既保證了按鍵的數據流動,又讓處理過程在basic_example.c不可見,并且bt1和btn2對于multi_button.c也是不可見。 這就是模塊化編程的意義。
5、思想感悟篇
后續還會再更新一篇文章單獨聊一聊這個按鍵框架的思想,為什么要這么做,試圖體會作者的思想,進而觸類旁通,舉一反三。
加油!!!!!!!
6、慈悲不渡自絕人
未完待續…
文章源碼獲取方式:
如果您對本文的源碼感興趣,歡迎在評論區留下您的郵箱地址。我會在空閑時間整理相關代碼,并通過郵件發送給您。由于個人時間有限,發送可能會有一定延遲,請您耐心等待。同時,建議您在評論時注明具體的需求或問題,以便我更好地為您提供針對性的幫助。
【版權聲明】
本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議。這意味著您可以自由地共享(復制、分發)和改編(修改、轉換)本文內容,但必須遵守以下條件:
署名:您必須注明原作者(即本文博主)的姓名,并提供指向原文的鏈接。
相同方式共享:如果您基于本文創作了新的內容,必須使用相同的 CC 4.0 BY-SA 協議進行發布。
感謝您的理解與支持!如果您有任何疑問或需要進一步協助,請隨時在評論區留言。