1. 程式人生 > 其它 >嵌入式裝置中按鍵的硬體防抖, 軟體防抖和按鍵訊息處理

嵌入式裝置中按鍵的硬體防抖, 軟體防抖和按鍵訊息處理

按鈕就是一種配備了彈性裝置的雙狀態開關: 連通和斷開. 由於彈性部件的作用, 大部分時間按鈕是斷開的. 從電路角度看, 按鈕扮演的角色就是開路和短路. 按鈕在嵌入式裝置中是常見元件, 按鈕在按下和釋放時都有可能產生抖動效應, 會導致過程中產生多次短路與開路之間的切換, 對於這個問題, 需要從硬體和軟體方面來解決: 硬體上, 低通濾除抖動, 軟體上, 增加第一次檢測到動作後的 dead time. 按鍵的系統訊息是通過狀態機模型進行處理的.

嵌入式裝置中的按鍵處理

按鍵

按鈕就是一種配備了彈性裝置的雙狀態開關: 連通和斷開. 由於彈性部件的作用, 大部分時間按鈕是斷開的. 從電路角度看, 按鈕扮演的角色就是開路和短路. 按鈕在嵌入式裝置中是常見元件, 通常情況下, 一個按鈕需要有一個弱上拉或下拉電阻, 對於STM32而言, GPIO口已經自帶了弱上拉電阻, 可以在程式中設定是否使用, STC系列的MCU, 要看具體型號和具體的IO口, 例如經典的stc89c51/stc89c52, P0口就是漏極開路的雙向IO口, 使用時當電流流出需外接上拉電阻.

將按鈕連線到MCU通常有兩種方式, 一種是低電平有效, 另一種是高電平有效, 在低電平有效的電路中, 當按鈕按下時, 將在引腳上讀取到邏輯0, 按鈕釋放後讀取的是1; 在高電平有效電路中則正好相反.

上拉/下拉電阻阻值選取

如果將一個IO口等價為一個電容, 那麼低電平有效的等價電路為下圖

如果電阻太小, 電流過大可能會損壞元件, 一般這個阻值在幾K到幾十K歐. 阻值的大小受GPIO的邏輯轉換時間限制, 對於STM32, IO口電容為5pF, 上拉電阻可以為10K歐.

按鍵抖動效應 The bounce effect

按鈕在按下和釋放時都有可能產生抖動效應, 會導致過程中產生多次短路與開路之間的切換, 對於這個問題, 需要從硬體和軟體方面來解決:

  • 硬體上, 低通濾除抖動
  • 軟體上, 增加第一次檢測到動作後的 dead time

硬體處理

硬體防抖動(debouncing)是需要優先考慮的方法, 比軟體方式更穩定和高效. 可以通過在GPIO口和按鍵之間新增一個低通濾波電路實現.

實現低通濾波最簡單的電路就是 RC濾波. 其阻值和容值怎麼計算呢? 取決於抖動的容忍頻率. 可以使用以下計算式

\(f_{LP} = \frac{1}{2 \pi RC} = 0.1 \cdot f_{bounce}\)

低通頻率不能太低, 否則會濾除正常的操作, 在正常情況下, 一個人不太可能以100赫茲的頻率去按按鍵, 所以

  • 將低通頻率設為10KHz, 對應的就是160歐的電阻和100nF的電容, 或1K歐電阻和16nF電容
  • 將低通頻率設為1KHz, 對應的就是1K歐電阻和160nF電容
  • 將低通頻率設為100Hz, 對應的就是10K歐電阻和160nF電容

下面的電路中, 使用了10KR電阻和100nF(104)電容作為硬體防抖處理

軟體處理

軟體處理分兩種情況, 如果僅僅需要檢測短按, 是比較簡單的, 宣告一個volatile static a變數用於表示按鍵狀態, 宣告一個static uint8_t b變數用於計數, 每個迴圈的檢測中, 低電平(假定按下為低電平)b加1, 當b值計數到達一個閾值時表示按鈕按下, 將a置位, 當迴圈中檢測到高電平時將a和b都清零.

如果需要檢測短按和長按, 就需要三個變數, 除了上面的a和b以外, 再增加一個迴圈計數c. 檢測的每個迴圈中, 先按檢測短按的方式, 做短按判斷, 另外再通過第三個變數記錄短按的次數, 當達到預設的長按判斷的次數閾值時, 判斷為長按. 要注意的是

  1. 短按的置位要由按鈕釋放觸發
  2. 長按的置位由按鈕按下觸發
  3. 長按釋放時, 要避免判斷為短按

下面是一段實際應用中的程式碼, 會在一個間隔10ms的定時器中呼叫, 其中

  • KEY1 為按鍵對應的IO口, 例如P01
  • debounce[0] 為按鍵1對應的防抖延時計數器
  • k1_pressed 當判斷按鍵1為按下時置位, 全域性使用
  • switchcount[0] 按鍵1對應的長按鍵計數器, SW_CNTMAX為判斷閾值
  • k1_long_pressed 當判斷按鍵1為長按時置位, 全域性使用
  • event 按鍵事件, 全域性使用
void read_key1(void)
{
    //未按下時, KEY1處於高電平, 因此debounce為0xFF
    debounce[0] = (debounce[0] << 1) | KEY1;
    if (debounce[0] == 0x00) { // 8次檢測都為0, 按下置位
        k1_pressed = 1;
        if (!k1_long_pressed) { // 如果長按未置位, 計數加1
            switchcount[0]++;
        }
    } else { // 按鍵已鬆開或未按下
        if (k1_pressed) {
            if (!k1_long_pressed) {
                // 如果短按已置位, 但是長按未置位, 按短按發出系統訊息
                event = EV_K1_SHORT;
            }
            // 清理狀態和計數器
            k1_pressed = 0;
            k1_long_pressed = 0;
            switchcount[0] = 0;
        }
    }
    if (switchcount[0] > SW_CNTMAX) {
        // 如果長按計數器已經達到閾值, 長按置位(避免鬆開時發出短按訊息), 發出長按系統訊息
        k1_long_pressed = 1;
        switchcount[0] = 0;
        event = EV_K1_LONG;
    }
}

按鍵訊息處理

按鍵的系統訊息是通過狀態機模型進行處理的, 在每個按鍵處理迴圈中,

  1. 清除全域性訊息
  2. 根據當前的按鍵狀態, 判斷長按和短按對應的下一個狀態
  3. 下一個迴圈, 會跳到對應的按鍵狀態, 再去判斷下一個狀態
  4. 根據按鍵狀態決定當前的顯示模式
void main(void)
{
    //...
    while (true)
    {
        while (!loop_gate); // wait for open every 100ms
        loop_gate = 0; // close gate

        ev = event;
        event = EV_NONE;
        switch (kmode)
        {
            case K_DISP_SEC:
                dmode = D_DISP_SEC;
                if (ev == EV_K2_SHORT) {
                    kmode = K_DISP_ALARM;
                    m_timeout = TIMEOUT_SHORT;
                }
                break;
            //...
            case K_NORMAL:
            default:
                dmode = D_NORMAL;
                
                if (ev == EV_K1_SHORT) {
                    kmode = K_DISP_DATE;
                    m_timeout = TIMEOUT_SHORT;
                } else if (ev == EV_ALARM) {
                    kmode = K_BUZZ_ALARM;
                    m_timeout = TIMEOUT_LONG;
                }
                else if (ev == EV_K1_LONG)
                    kmode = K_SET_MINUTE;
                else if (ev == EV_K2_SHORT)
                    kmode = K_DISP_SEC;
                else if (ev == EV_K2_LONG)
                    kmode = K_SET_ALARM_MINUTE;
        }
//...
    }
}

參考