此實驗我主要是參考了下面四本書的範例進行修改 :
- Arduino 輕鬆入門, 葉難, 博碩出版 (第 3-3, 3-4 節)
- Arduino 完全實戰手冊, 王冠勛譯, 博碩出版 (第 2-2-4 節)
- Prototyping Lab, 小林茂, 馥林文化 (第 25 章)
- Arduino Cookbook 錦囊妙計, 歐萊禮 (第 5-2, 5-3 節)
同一側兩隻腳在按鈕尚未按下時是開路的, 按下時為通路; 而對面的兩隻腳則永遠是連通的, 我們通常是使用同一側的兩個接腳, 電路結構如下圖 :
第一個實驗是要測試按鈕按下時, Arduino 內建的 D13 LED 會交替明滅, 即原來是暗的變亮, 原來是亮的變暗, 接線方式為將 Arduino D2 設定為使用內建上拉電阻之輸入腳, 並連線至按鈕開關一腳, 而按鈕開關另一腳則接地, 使按鈕按下時 Arduino 之 D2 接地, 放開時則被內建上拉電阻拉到 HIGH. 程式如下 :
測試 1 :
const byte swPin=2; //switch pin
const byte ledPin=13; //built-in LED
boolean ledState=LOW; //initial state of LED
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor
digitalWrite(ledPin, ledState); //set LED OFF
}
void loop() {
boolean swState=digitalRead(swPin);
if (swState==LOW) { //switch pressed
if (ledState==HIGH) {ledState=LOW;} //ON:turn LED OFF
else {ledState=HIGH;} //OFF:turn LED ON
digitalWrite(ledPin, ledState); //toggle LED
}
}
因為要記住 D13 LED 目前狀態, 因此必須用一個全域變數 ledState 來記錄, 預設狀態為 LOW, 所以程式開始執行後, 跑完 setup() 時 LED 是暗的. 這裡我們使用 INPUT_PULLUP 將 D2 腳設為內建上拉電阻的輸入腳, 這樣當 D2 浮接 (floating) 時, 其位準會被上拉至 HIGH.
注意, 開啟上拉電阻是必要的, 因為當輸入腳是浮接時, digitalRead() 讀到的是隨機的雜訊, 如果 D2 只設為 INPUT 而且浮接 (不接 GND 也不接 +5V), 則送電後 D13 LED 會不斷閃爍, 因為在 loop() 主迴圈中, 每一次迴圈讀到的 D2 值有時是 HIGH, 有時是 LOW, 於是看起來就是隨機地在閃爍, 只要將上面測試 1 中的 INPUT_PULLUP 改為 INPUT 即知, 如下面影片所示 :
若 D2 為 INPUT 模式, 沒有上拉電阻的話, 上電後 LED 就隨機閃爍, 直到按下按鈕被拉到 LOW 才停止, 但一放開變浮接又開始閃了. 關於上拉電阻更詳細說明參考 :
# Arduino Internal Pull-Up Resistor Tutorial (超棒)
上拉電阻有一個舊式用法, 先用 pinMode() 設定 DIO 為 INPUT 腳, 然後再用 digitalWrite() 輸出 HIGH 即可啟用 Arduino 數位接腳的內建上拉電阻 :
pinMode(swPin, INPUT);
digitalWrite(swPin, HIGH);
不過這是還未支援 INPUT_PULLUP 的舊版 (1.0.1 以前) Arduino IDE 之用法, 現在應該使用 INPUT_PULLUP 為宜. Arduino 的 DIO 若設為 INPUT, 其輸入阻抗約 100M 歐姆, 而內建的上拉電阻, 根據 "Arduino Cookbook 錦囊妙計" 5-2 節末尾描述, 其值約在 20K~50K 歐姆之間. 關於上拉電阻說明, 參考 :
# pinMode()
# Input Pullup Serial
# Arduino內部的上拉電阻
# 使用Arduino 的pull-up 電路
# 從按鈕開關看上拉pull-up電阻下拉電阻是蝦密碗糕
# https://www.arduino.cc/en/Tutorial/InputPullupSerial
因為 loop() 執行速度很快, 所以在按鈕按下的瞬間, digitalRead() 已經讀取到數十次到數百次 HIGH 與 LOW 切換的值了, 因此如果在到達穩定狀態前讀取到偶數次的 HIGH-LOW 變換, 那麼 LED 就明滅偶數次, 看起來 LED 就好像沒變換狀態; 如果是奇數次, 就跟程式邏輯相符, LED 會變換狀態. 如果在上面測試 1 程式中加入計數器, 就可以知道在按下按鈕時迴圈已跑多少圈了, 參考 "Arduino 輕鬆入門" 第 3-3 節 :
測試 2 :
const byte swPin=2; //switch pin
const byte ledPin=13; //built-in LED
boolean ledState=LOW; //initial state of LED
int count=0; //bounce counter
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor
digitalWrite(ledPin, ledState); //set LED OFF
Serial.begin(9600);
}
void loop() {
boolean swState=digitalRead(swPin);
if (swState==LOW) { //switch pressed
if (ledState==HIGH) {ledState=LOW;} //ON:turn LED OFF
else {ledState=HIGH;} //OFF:turn LED ON
digitalWrite(ledPin, ledState); //toggle LED
Serial.print("Low count:");
Serial.println(count); //output counter value
count++; //increment bounce counter
}
}
此程式加入一個全域變數 count 來計算在 D2 為 LOW 期間 loop() 迴圈已跑幾次 (會累計), 通常按即放開至少都十幾次以上, 當然按著越久就越多次. 序列埠監控視窗擷取訊息如下 :
Low count:0
Low count:1
Low count:2
Low count:3
Low count:4
Low count:5
Low count:6
Low count:7
Low count:8
Low count:9
Low count:10
Low count:11
Low count:12
Low count:13
Low count:14
解決這個問題的辦法最直接的是時間延遲法, 意即當偵測到按鈕被按下, D2 位準變 LOW 後, 延遲一段時間若狀態還是 LOW, 表示彈跳情況已消失, 按鈕已穩定在被按下狀態, 這時再去反轉 LED 明滅狀態, 我參考 "Arduino 輕鬆入門" 第 3-4 節範例, 將其修改為下面測試 3 :
測試 3 :
const byte swPin=2; //switch pin
const byte ledPin=13; //built-in LED
boolean ledState=LOW; //initial state of LED
int debounceDelay=200; //debounce delay (ms)
unsigned long lastMillis; //record last millis
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor
digitalWrite(ledPin, ledState); //set LED OFF initially
}
void loop() {
boolean swState=digitalRead(swPin);
if (swState==LOW) { //switch pressed
if (debounced()) { //debounced: reverse LED state
ledState = !ledState; //reverse LED state
digitalWrite(ledPin, ledState); //toggle LED
}
}
}
boolean debounced() { //check if debounced
unsigned long currentMillis=millis(); //get current elapsed time
if ((currentMillis-lastMillis) > debounceDelay) {
lastMillis=currentMillis; //update lastMillis with currentMillis
return true; //debounced
}
else {return false;} //not debounced
}
在此程式中我添加了一個自訂的 debounced() 函數來檢查現在的毫秒數 (程式開始執行以來) 跟上一次紀錄之毫秒數 (即上一次 D2 為 LOW 之時間) 是否已超過預期的彈跳時間 bounceDelay, 如果是, 表示按鈕已穩定地被按下, 就可以將 LED 狀態反轉了; 否則表示彈跳還沒結束, 不進行反轉.
在上面程式中我定義了一個全域變數 lastMillis 來記錄上一次 D2 為 LOW的時間, 其實也可以將此變數設為 static 變數放在 debounced() 內, 效果是一樣的, 因為跳出函數後, 宣告為 static 的區域變數不會消失, 會被保留下來, 下次呼叫時可再次存取. 參考 "Arduino 完全實戰手冊" 第 2-2-4 節 :
測試 3-1 :
const byte swPin=2; //switch pin
const byte ledPin=13; //built-in LED
boolean ledState=LOW; //initial state of LED
int debounceDelay=50; //debounce delay (ms)
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor
digitalWrite(ledPin, ledState); //set LED OFF
}
void loop() {
boolean swState=digitalRead(swPin);
if (swState==LOW) { //switch pressed
if (debounced()) { //debounced: reverse LED state
ledState = !ledState; //reverse LED state
digitalWrite(ledPin, ledState); //toggle LED
}
}
}
boolean debounced() { //check if debounced
static unsigned long lastMillis=0; //record last millis
unsigned long currentMillis=millis(); //get current elapsed time
if ((currentMillis-lastMillis) > debounceDelay) {
lastMillis=currentMillis; //update lastMillis with currentMillis
return true; //debounced
}
else {return false;} //not debounced
}
同樣是使用時間延遲, 我在 "Arduino Cookbook 錦囊妙計" 第 5-2 節找到不一樣的做法 (主要是 debounced 函數), 程式改編如下 :
測試 4 :
const byte swPin=2; //switch pin
const byte ledPin=13; //built-in LED
boolean ledState=LOW; //initial state of LED
int debounceDelay=100; //debounce delay (ms)
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor
digitalWrite(ledPin, ledState); //set LED OFF
}
void loop() {
if (debounced(swPin)) { //switch pressed and statble for 50 ms
ledState =! ledState; //reverse LED state
digitalWrite(ledPin, ledState); //toggle LED
}
}
boolean debounced(int pin) {
boolean currentState; //current pin state
boolean previousState; //previous pin state
previousState=digitalRead(pin); //record current state as previous
for (int i=0; i<debounceDelay; i++) { //detect if stable
delay(1); //delay 1 ms
currentState=digitalRead(pin); //get current state
if (currentState != previousState) { //still unstable
i=0; //reset counter
previousState=currentState; //updtae previous state
}
}
if (currentState==LOW) {return true;} //switch pressed (pull-up)
else {return false;} //switch released
}
這裡 debounced() 要傳入欲偵測的腳位編號, 函數內宣告了兩個布林變數來儲存目前與上一次的開關狀態, 然後以 debounceDelay 當迴圈終止值每 1 毫秒去偵測現在開關狀態是否與上一次變化時所儲存的狀態相同, 如果不同, 表示還在彈跳, 就會重新計數, 並持續記錄狀態. 如果迴圈結束時, 開關狀態是 LOW, 就回傳 true 表示按鈕已穩定被按下, 否則傳回 false.
也可以使用 Bounce2 函式庫來處理彈跳問題, 在 "Arduino 輕鬆入門" 3-4 有介紹, 但那是比較舊的 Bounce 函式庫, 現在已更新為 Bounce2 了. 此函式庫可從下列網址下載 :
# http://playground.arduino.cc/Code/Bounce
將下載之 zip 檔解壓縮到 Arduino IDE 安裝目錄的 libraries 下, 重開 IDE 即可看到 "檔案/範例" 下有 Bounce2 的範例. 根據此範例改寫為如下測試 5 :
測試 5 :
#include <Bounce2.h>
const byte swPin=2; //switch pin
const byte ledPin=13; //built-in LED
boolean ledState=LOW; //initial state of LED
int debounceDelay=50; //debounce delay
Bounce debouncer=Bounce(); //create bounce object
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor
digitalWrite(ledPin, ledState); //set LED OFF
debouncer.attach(swPin); //monitor button switch
debouncer.interval(debounceDelay); //set debounce interval
}
void loop() {
debouncer.update();
int swState=debouncer.read();
if (swState==LOW) {
ledState = !ledState; //reverse LED state
digitalWrite(ledPin, ledState); //toggle LED
}
}
與舊版 Bounce 不同的地方是, 在產生全域的 Bounce 物件時不要再傳入參數, 設定要偵測的腳位與彈跳時間要在 setup() 中分別用 Bounce 物件的 attach() 與 interval() 函數來設定. 此函式庫其實就像是一個濾波器, 會把彈跳期間的雜訊濾掉, 輸出穩定的 HIGH-LOW 或 LOW-HIGH 狀態變化. 使用此程式必須在 loop() 中不斷呼叫 Bounce 物件的 update() 來更新濾波結果, 然後再呼叫 read() 將偵測到的最新狀態讀取進來. 我照 "Arduino 輕鬆入門" 3-4 節範例去試結果不會動作, 那是舊版寫法, 新版的 Bounce2 要用新的寫法才行.
其實我有找到另外一個 Bounce 函式庫如下, 不過這更舊, 因為它的 Bounce.cpp 裡還在用 WProgram.h, 須改為 Arduino.h, 這麼麻煩我就不用了 :
# https://www.pjrc.com/teensy/td_libs_Bounce.html
# WProgram.h 这个文件是做什么用的?哪能下载到?
最後, 我發現 Arduino IDE 的 "檔案/範例" 裡就有 Debounce 的程式, 它的作法稍有不同, 我把它稍作整理便於閱讀如下 :
測試 6 :
const int buttonPin=2; //the number of the pushbutton pin
const int ledPin=13; //the number of the LED pin
int ledState=HIGH; //the current state of the output pin
int buttonState; //previous stable state of the input pin
int lastButtonState=LOW; //the previous reading from the input pin
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(ledPin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP); //enable pullup resistor
digitalWrite(ledPin, ledState);
}
void loop() {
int reading=digitalRead(buttonPin);
if (reading != lastButtonState) { //button state changed
lastDebounceTime=millis(); //update last debounce time
}
if ((millis() - lastDebounceTime) > debounceDelay) { //overtime
if (reading != buttonState) { //button state has changed
buttonState=reading; //update previous stable button state
if (buttonState == LOW) { //button pressed
ledState = !ledState; //reverse LED state
}
}
}
digitalWrite(ledPin, ledState);
lastButtonState=reading; //update last button state
}
原理大致與上面類似, 不同之處是這裡它使用了兩個全域變數來記錄按鈕狀態 : buttonState 紀錄前一次的穩定狀態位準, 而 lastButtonState 則記錄動態的最近狀態. 如果目前狀態與最近狀態不同, 表示還在彈跳當中, 就更新最近去彈跳時間 lastDebounceTime 為目前已執行時間 millis(), 如果狀態相同, 表示可能已趨於穩定, 最近去彈跳時間 lastDebounceTime 就不會被更新, 以便迴圈持續偵測這個穩定是否能撐過 debounceDelay, 若撐過了還須比較現在狀態與上次穩定狀態是否不同, 是的話才進一步去看目前狀態是否為 LOW (按下了). 此程式乍看之下覺得其演算法有點難懂, 但實際測試發現, 在設定 50 毫秒延遲時, 其執行結果完全正確, 且一直壓住按鈕, LED 也不會像上面測試 3, 4 那樣閃爍.
我把上面測試 6 改成用 debounced() 函數來判斷是否已去除彈跳 :
測試 6-1 :
const int buttonPin=2; //the number of the pushbutton pin
const int ledPin=13; //the number of the LED pin
int ledState=HIGH; //the current state of the output pin
int buttonState; //previous stable state of the input pin
int lastButtonState=LOW; //the previous reading from the input pin
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(ledPin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP); //enable pullup resistor
digitalWrite(ledPin, ledState);
}
void loop() {
if (debounced(buttonPin)) { //debounced
ledState = !ledState; //reverse LED state
}
digitalWrite(ledPin, ledState);
}
boolean debounced(int pin) { //check if debounced
boolean debounced=false; //default
int reading=digitalRead(pin); //current button state
if (reading != lastButtonState) { //button state changed
lastDebounceTime=millis(); //update last debounce time
}
if ((millis() - lastDebounceTime) > debounceDelay) { //overtime
if (reading != buttonState) { //button state has changed
buttonState=reading; //update previous stable button state
if (buttonState == LOW) { //button pressed
debounced=true;
}
}
}
lastButtonState=reading; //update last button state
return debounced;
}
功能與測試 6 相同, 但這樣感覺比較有結構.
總之, 彈跳現象經過上面如此處理後, 執行起來大都正常了, 但也不是百分之百符合程式所期望的邏輯, 主要的關鍵在於 debouceDelay 要設幾毫秒為宜, 設得太短或太長都沒效果. 經我多次測試結果, 測試 3 與 3-1 要設 200 ms 延遲; 測試 4 要設 100 ms 延遲時, 而測試 6 與 6-1 則要設 50 ms 延遲, 這樣正確率最高, 幾乎無失誤; 而使用 Bounce2 函式庫最糟糕, 延遲從 20, 40, 60, ... 到 200 都試過, 沒有一個設定值是滿意的, 好像跟測試 1 差不多.
參考 :
# Arduino練習:以開關切換LED明滅狀態
Hi ,
回覆刪除debounced函數以多個變數代入,怎麼沒有辦法動阿,
有拆開每PIN去查,但不能執行
if (debounced_v2(buttonPin) == HIGH ){
Serial.println(btn_state);
Serial.print("buttonPin : "); Serial.println(buttonPin);
}
else if (debounced_v2(buttonPin2) == HIGH ){
Serial.println(btn_state2);
Serial.print("buttonPin2 : "); Serial.println(buttonPin2);
}
請問您這 debounced_v2() 是上面哪一個函數呢? 我都測試過 ok 哩!
回覆刪除debounced_v2()是另外copy出來的,作為與debounced()來比較
回覆刪除我用的是 1.6.12版的ide
以example 6 來看,問題在於全區變數的存取,後來再用笨的點方法,就是多一組變數,就可以解決這些狀況了,感謝
回覆刪除int buttonState; //previous stable state of the input pin
int lastButtonState = LOW; //the previous reading from the input pin
long lastDebounceTime = 0; //the last time the output pin was toggled