2015年5月10日 星期日

探討:Button Debouncing (軟體作法)

接觸彈跳 (contact bounce) 的問題怎麼解決呢?其實有軟體的做法和硬體的作法,首先來看軟體的作法。





最直覺的作法,就是我先讀取一次,隔一段時間後,再讀取一次。如果這兩次的狀態都一樣,就視為狀態更新,否則就是 bouncing。

以下這個範例,是修改自 "超圖解Arduino 互動設計入門(第二版)" 一書的範例。執行的效果也非常好,但我故意按住按鈕抖動開關,仍然有可能觸動狀態改變。

比較不一樣的是,它更改 LED 狀態的時間點,是發生在釋放按鈕的當下。(1 click = press + release) 一般的範例都發生在 press 的時候,這個範例是發生在 release 的時候。

實作概念是偵測到狀態改變的時候,等 20ms 之後,再讀取一次。若讀到的結果跟前次偵測一樣,則判定改變生效。只是這支程式還利用 click 變數判斷是否完成 click 動作。一次 click 的動作是兩次的狀態改變。即 按下->釋放。

const byte ledPin = 13;
const byte buttonPin = 2;

boolean lastButtonState = LOW;
boolean ledState = LOW;
byte click = 0;

void setup()
{
    pinMode(ledPin, OUTPUT);
    pinMode(buttonPin, INPUT);
    lastButtonState = digitalRead(buttonPin); // 儲存系統初始狀態
}

void loop()
{
    boolean reading1 = digitalRead(buttonPin); // 先讀一次

    if (reading1 != lastButtonState) { // 如果發現有改變
        delay(20); // <- de-bouncing, wait & read again 等 20ms 後再讀一次

        boolean reading2 = digitalRead(buttonPin);

        if (reading2 == reading1) { // 如果 20ms 後都一樣, 判定更改生效
            lastButtonState = reading2;
            click++; // click +1, 然後釋放按鈕的時候, 會再進來一次. 所以 click 會加至 =2
        } // else means reading2 is bouncing
    }

    // press & release means 2 click in the code
    // only toggle the LED if the button is release
    if (click == 2) {
        click = 0;
        ledState = !ledState;
        digitalWrite(ledPin, ledState);
    }
}


其實這支程式可以稍微修改一下,讓它變成在按下按鈕的當下,讓 LED 燈的狀態改變。

const byte ledPin = 13;
const byte buttonPin = 2;

boolean lastButtonState = LOW;
boolean ledState = LOW;

void setup()
{
    pinMode(ledPin, OUTPUT);
    pinMode(buttonPin, INPUT);
    lastButtonState = digitalRead(buttonPin);
}

void loop()
{
    boolean reading1 = digitalRead(buttonPin);

    if (reading1 != lastButtonState) {
        delay(20); // <- de-bouncing, wait & read again

        boolean reading2 = digitalRead(buttonPin);

        if (reading2 == reading1) {
            lastButtonState = reading2;
        } // else means reading2 is bouncing

        if (lastButtonState == HIGH) {
            ledState = !ledState;
        }
    }

    digitalWrite(ledPin, ledState);
}


這邊有個地方要注意的是如上面 highlight 的那三行 code,必須寫在判斷狀態確實改變的 code block 中。我第一次撰寫這支程式的時候,寫成下面的樣子:


/* ...上面省略... */

void loop()
{
    boolean reading1 = digitalRead(buttonPin);

    if (reading1 != lastButtonState) {
        delay(20); // <- de-bouncing, wait & read again

        boolean reading2 = digitalRead(buttonPin);

        if (reading2 == reading1) {
            lastButtonState = reading2;
        } // else means reading2 is bouncing
    }

    if (lastButtonState == HIGH) {
        ledState = !ledState;
        digitalWrite(ledPin, ledState);
    }
}


其實這在程式流程上是錯誤的,因為你每次在 17 行判斷 lastButtonState 時,其實是上一次的狀態。而且你無法得知當次的按鈕行為是否有更新到 lastButtonState? 所以應該寫在確認 lastButtonState 更改生效後才改變 ledState。

第二種方式的寫法,是參考 Arduino Cookbook 2nd Edition 裡面的寫法。概念是每次檢查時,只要發現狀態改變,該狀態得持續一段時間才算數。

const int buttonPin = 2;        // the number of the input pin
const int ledPin = 13;         // the number of the output pin
const int debounceDelay = 10;  // milliseconds to wait until stable
int ledState = LOW;

// debounce returns true if the switch in the given pin is closed and stable
boolean debounce(int pin)
{
    boolean state;
    boolean previousState;
    previousState = digitalRead(pin);          // store switch state

    for(int counter=0; counter < debounceDelay; counter++)
    {
        delay(1);                  // wait for 1 millisecond
                                   /* debounceDelay = 10, 所以在 10ms 的時間內, 
                                    * 狀態必須維持一樣才會跳出 for loop (才算穩定)
                                    */
        state = digitalRead(pin);  // read the pin

        if( state != previousState) {
            counter = 0; // reset the counter if the state changes
                         // 只要發現狀態改變, 就重新觀察 10ms
            previousState = state;  // and save the current state
        }
    }
    // here when the switch state has been stable longer than the debounce period
    return state;
}

void setup()
{
    pinMode(buttonPin, INPUT);
    pinMode(ledPin, OUTPUT);
    digitalWrite(ledPin, ledState);
}

void loop()
{
    if (debounce(buttonPin)) {
        ledState = !ledState;
        digitalWrite(ledPin, ledState);
    }
}


執行結果有好一點,但其實你還是需要按久一點,才有 "高的機率" 可以改變 LED 狀態。以我按按鈕的速度來測試,不是每次都能夠判讀正確的。且程式使用 delay(),表示在 delay() 執行的過程中,程式是被 block 住的。如果把 delay(1) 拿掉。就變成了連續 10 次結果相同,而不是 10ms 內結果相同。

其實 Arduino 的 Example 裡面也有關於 Debounce 的範例:
概念跟 Arduino Cookbook 應該是相同的:每次偵測到按鍵狀態改變時,利用 millis() 記下當時的時間戳記。然後經過 debounceDelay (ms) 時間後,若狀態還是改變,則改變生效,否則判定為 bounce 訊號。

const int buttonPin = 2;    // the number of the pushbutton pin
const int ledPin = 13;      // the number of the LED pin

// Variables will change:
int ledState = LOW;          // the current state of the output pin
int buttonState;             // the current reading from the input pin
int lastButtonState = LOW;   // the previous reading from the input pin

// the following variables are long's because the time, measured in miliseconds,
// will quickly become a bigger number than can be stored in an int.
long lastDebounceTime = 0;  // the last time the output pin was toggled
long debounceDelay = 50;    // the debounce time; increase if the output flickers

void setup()
{
    pinMode(buttonPin, INPUT);
    pinMode(ledPin, OUTPUT);

    // set initial LED state
    digitalWrite(ledPin, ledState);
}

void loop()
{
    // read the state of the switch into a local variable:
    int reading = digitalRead(buttonPin);

    // check to see if you just pressed the button
    // (i.e. the input went from LOW to HIGH),  and you've waited
    // long enough since the last press to ignore any noise:

    // If the switch changed, due to noise or pressing:
    if (reading != lastButtonState) {
        // reset the debouncing timer
        lastDebounceTime = millis(); // 紀錄時間戳記
    }

    if ((millis() - lastDebounceTime) > debounceDelay) { // 經過 debounceDelay 時間後
        // whatever the reading is at, it's been there for longer
        // than the debounce delay, so take it as the actual current state:

        /* 若在這邊塞一顆閃爍的 LED 燈 debug,
         * 會發現閒置 (按鈕未按下) 時, 程式會一直跑進這個 block.
         */

        // if the button state has changed:
        if (reading != buttonState) { // 經過 debounceDelay 時間後再判斷一次
            buttonState = reading;

            // only toggle the LED if the new button state is HIGH
            if (buttonState == HIGH) {
                ledState = !ledState;
            }
        }
    }

    // set the LED:
    digitalWrite(ledPin, ledState);

    // save the reading.  Next time through the loop,
    // it'll be the lastButtonState:
    lastButtonState = reading;
}


這支程式執行的效果,是這本篇所有範例中最好的,幾乎 100% 即時正確地反應按按鈕的行為。當我故意在短時間內狂按按鈕時,程式也會忽略那些極短時間內的改變。

每次都要為了處理 contact bounce,勢必得多寫很多 code,於是有些前人已經將 debounce 的功能包裝成 Libraries 啦。 前往 http://www.arduino.cc/en/Reference/Libraries 找尋 "Debounce"。

Debounce 的頁面中提到:
Debounce is outdate please use Bounce instead
所以這個功能的最新版已經移到 "Bounce" 這個 Library 啦。

LibraryList 清單中,點選 "Buttons & Debouncing" 可以得到很多跟按鈕 & 消彈跳相關的 Libraries。這裡要用的是 Bounce,而且目前有最新版 Bounce2 了。



OK, 現在利用 Bounce2 的函式庫,再把剛剛的程式拿來改一下:

#include <Bounce2.h>

const int buttonPin = 2;    // the number of the pushbutton pin
const int ledPin = 13;      // the number of the LED pin

int ledState = LOW;          // the current state of the output pin
Bounce debouncer = Bounce(); // Instantiate a Bounce object

void setup()
{
    pinMode(buttonPin, INPUT);
    pinMode(ledPin, OUTPUT);

    debouncer.attach(buttonPin);
    debouncer.interval(20);      /* 你可以調整這個數值, 然後測試看看 
                                  * 然後你會發現當數字越大時, 例如 500
                                  * 你需要按住久一點, LED 才會有反應
                                  */

    digitalWrite(ledPin, ledState); // set initial LED state
}

void loop()
{
    debouncer.update(); // Update the Bounce instance

    if (debouncer.fell()) { // Call code if Bounce fell (transition from HIGH to LOW)
        ledState = !ledState;
        digitalWrite(ledPin, ledState);
    }
}


泥砍砍~ 程式碼是不是簡潔許多?

沒有留言: