2016年9月12日 星期一

Arduino 按鈕開關測試 (二) : 硬體中斷法 (Interrupt)

做完 Arduino 按鈕開關實驗才發現, 一個這麼簡單的按鈕竟然這麼難搞, 印證了物理與數學之間確實有一道鴻溝. 數理邏輯嚴謹, 一就是一, 二就是二, 但在物理機械特性上就沒這麼黑白分明了. 物理世界有一個慣性, 所以一個作用不會一次到位, 按鈕的彈跳現象就是彈性與慣性作用下的結果, 微控器必須不斷追蹤 I/O 輸入的狀態變化, 將雜訊濾除掉, 取得穩態訊號後才能做出符合期望的控制動作, 這種方式叫做輪詢法 (Polling). 前次的按鈕開關測試全部都是這種方法, 參考 :

#  Arduino 按鈕開關測試 (一) : 輪詢法

輪詢法必須在 loop() 主迴圈中持續追蹤輸入狀態, 會一直占用主程序資源 (執行時間與記憶體空間). 同時, 由於處理器必須主動檢查周邊狀態, 使得 MPU 的負荷與時間延遲都增加, 如果後面還有其他對 timing 要求很高的程序的話就不太好了.

其實微控器要偵測周邊狀態的變化, 還有一個辦法, 就是使用硬體外部中斷, 透過一個自訂函數來處理中斷事件, 處理器是被動接受中斷要求才去服務周邊, 這樣可大幅提高處理器工作效率. 這兩種 I/O 處理方式比較如下 :
  1. Polling (輪詢法) :
    微控器主動檢查輸入腳的狀態變化, 亦即在 loop() 主迴圈中不斷地檢查輸入腳, 若發現有變化就處理, 耗費系統資源較多.
  2. Interrupt (中斷法) :
    微控器不須在主迴圈檢查周邊設備狀態, 而是被動因應周邊中斷的觸發, 將目前的狀態存入堆疊, 暫停現在執行中的程序去處理中斷事件, 控制權移轉到中斷處理函數, 處理完再從堆疊取回被中斷程序繼續執行原先的程序, 因此所耗費之系統資源少. 
我先將 Arduino 的中斷功能大致整理如下 :

一般微控器大都有特定之數位接腳可觸發外部硬體中斷, Arduino 各種板子的硬體中斷因其使用之微控器不同而有差異, 如下表所示 :

 板子 微控器 中斷 0 中斷 1 中斷 2 中斷 3 中斷 4
 UNO/NANO/Pro mini ATmega328P D2 D3
 Leonardo/Micro ATmega32u4 D3 D2 D0 D1 D7
 Mega2560 ATmega2560 D2  D3 D21 D20 D19

採用 ATmega328P 微控器的 UNO, NANO, 或 Pro mini 都只有兩個硬體中斷 D2 與 D3. 除此之外, Arduino 第一個基於 ARM 架構的 Due 採用 Atmel SAM3X8E ARM Cortex-M3 處理器, 其 54 個數位接腳每一個都具有外部硬體中斷功能, 參考 :

https://www.arduino.cc/en/Reference/AttachInterrupt

這些特定數位接腳的外部硬體中斷功能預設是關閉的, 必須使用 attachInterrupt() 函數予以設定並啟用. 其 API 如下, 具有三個參數 :

attachInterrupt(digitalPinToInterrupt(pin), ISR, mode);

其中第一個參數是呼叫 digitalPinToInterrupt() 函數並傳入數位腳編號, 也可以不呼叫此函數直接傳入腳位編號 (不建議); 第二個參數是自訂的中斷服務函數名稱 (Interrupt Service Routine); 第三個參數則是中斷觸發模式, 有 LOW, RISING, FALLING, CHANG, 以及 HIGH (只能用於 Arduino Due).

當外部硬體中斷發生後, ATmega328 處理器會將程式暫存器推入堆疊保存, 然後跳到 ISR 所在位址執行 ISR 的第一條指令, 這樣總共需要 82 個時脈週期. 參考 :

http://www.gammon.com.au/interrupts (超詳細)

Arduino 的中斷相關函數如下表 :

 函數 說明
 attachInterrupt(int, ISR, mode) 指派中斷服務函式
 int=中斷編號, 0 或 1
 ISR=中斷服務函式名稱
 mode=中斷模式 (LOW, CHANGE, RISING, FALLING)
 detachInterrupt(int) 移除指定腳位之中斷功能, int=中斷編號, 0 或 1
 noInterrupt() 停止全部中斷功能 (除 reset 外)
 interrupts() 重新啟用全部中斷功能

其中後面三個函數都是需要動態控制中斷功能時才會用到, 例如當要取得目前的 LED 狀態時, 我們不想此變數被中斷改變, 這時就可以先停止所有中斷 (Reset 除外), 取得變數值後再恢復 :

noInterrupts();  //disable all interrupts
boolean state=ledState;  //get volatile variable set by ISR
interrupts();  //enable interrupt

要注意的是暫停不要太久, 否則會影響計時器的運作 (計數器會溢位). 而 detachInterrupt(int) 則是移除指定腳位 (0/1) 的中斷功能.

中斷處理函數 ISR 係自訂, 但與一般的自訂函數不同, 必須符合下列限制 :
  1. ISR 不可以有參數亦無傳回值.
  2. ISR 要儘可能地短, 最好在五行指令以內, 以免中斷時間過長.  
  3. 不要在 ISR 內使用與時間有關的函式如 millis(), micro(), 與 delay(), 因為這些函數也是依賴中斷功能去計數或計時 (delay 是依賴 millis, millis 是依賴計時器), 在 ISR 內呼叫它們毫無作用 (micro 只是剛開始有效). 只有 delayMicroseconds() 因為不使用計數器可在 ISR 內正常運作. 如果在 ISR 內必須使用時間延遲功能, 必須自行撰寫時間延遲函數. 
  4. 在 ISR 執行期間, 序列埠輸出如 Serial.print() 可能會遺失一些資料, 因為新版 Arduino IDE 使用中斷來做序列埠輸出入, 故不要在 ISR 內使用序列埠指令. 
  5. 因 ISR 不能有參數, 故必須透過全域變數與主程式或其他函數分享資料. 這些可能在 ISR 內被改變其值之全域變數務必加上 volatile (會被改變的) 關鍵字
  6. 中斷 0 與中斷 1 具有相同優先權, 但當一個中斷發生時預設其他中斷會被忽略 (因為中斷功能會被禁能), 直到目前的中斷結束為止. 若要讓其他中斷恢復有效, 必須呼叫 interrupts() 函數來致能. 
參考 :

attachInterrupt()

關於第 6 項我持保留意見, 因為根據下面這篇文章裡的 ATmega328 中斷向量優先表, MPU 在執行每道指令後會檢查這張中斷向量表, 首先檢查是否有按下 Reset 鍵, 接著檢查中斷 0, 然後是中斷 1 ... 如果都沒發生中斷, 就執行下一道指令 (可見我們的外部硬體中斷只是 26 種中斷裡的兩個而已). 所以看起來中斷 0 是比中斷 1 優先的 :

http://www.gammon.com.au/interrupts (超棒的)

特別注意, ISR 內的全域變數必須宣告為 volatile 是因為編譯器的程式碼優化功能, 在 ISR 中可能會破壞程式碼原本規劃的邏輯, 使得執行功能異常. 例如在趙英傑的 "Arduino 互動設計入門 2" 附錄 D 就舉了一個範例 :

int a, b;
int c=a+b;
int d=a+b;

如果 C 編譯器的優化選項有勾選的話, 那麼上面的程式碼在編譯器執行優化後會變成 :

int a, b;
int c=a+b;
int d=c;

因為編譯器覺得既然 c 與 d 都是 a 與 b 之和, 就不需要浪費時間再做一次 a+b 運算. 這在一般程式不會有問題, 但若 a 或 b 的值會在中斷處理函數中被改變的話, 則 d 的值就會跟 c 不一樣了, 因為萬一執行完 c=a+b 之後發生中斷, 這時系統暫存器狀態會被推入堆疊中保存, 然後程式計數器會被導向中斷處理函數 ISR, 如果 a 或 b 在中斷處理函數內被改變其值, 則當中斷結束, 控制權交回主程式繼續執行 d=c 指令時, 這時 d 的值將無法反映 a 或 b 已經改變的現況, 仍與 c 一樣是舊的數據. 如果 a, b 加上 volatile 宣告的話, 就可以避免這個問題, 這個 volatile 就是通知編譯器, 這兩個全域變數不要進行優化 :

volatile int a, b;

此外, 被編譯器優化過的變數會放一個副本在暫存器中直接運算, 而非從 RAM 裡面重新載入, 但這樣可能被中斷處理函數覆蓋掉原來的值, 導致得到錯誤的運算結果. 而宣告為 volatile 是告訴編譯器, 此變數隨時會被中斷處理函數改變, 執行時必須重新從 RAM 中載入暫存器, 不可以只依賴存放在暫存器裡的副本 (被優化的變數就是直接取用暫存器裡的副本, 這樣就節省了重新自 RAM 載入的時間).

下面這篇文章對 C 語言的 volatile 有深入說明 :

# C語言: 認識關鍵字volatile

此文中有一段重點 : 一般書籍對 volatile 的用法解釋並不多, 所以造成我們對此修飾詞了解不夠或甚至是誤解. 例如volatile 用在指標時, 下面兩個用法意思不同 :
  1. int * volatile ptr :
    關鍵字 volatile 修飾 ptr, 意思是指標 ptr 本身是 volatile, 但指標所指之內容不是 volatile, 所以編譯器不會對指標本身進行優化, 但對指標內容 *ptr 之運算卻會進行優化.
  2. volatile int *ptr :
    此處 volatile 修飾的是指標內容 *ptr, 因此 volatile 的是指標的內容, 它不會被優化; 但指標本身卻不是 volatile 的, 指標本身的運算是會被優化的.
還有其他 volatile 使用上的陷阱必須注意, 詳參原文. 如果覺得實際執行結果與預期的不同, 有可能是我們對 volatile 的用法了解不夠所致.

OK, 對中斷有了基本了解之後, 就可以進行測試了. 以下實驗我主要是參考了下面四本書的範例進行修改 :
  1. Arduino 輕鬆入門, 葉難, 博碩出版 (第 3-4 節)
  2. Arduino 完全實戰手冊, 王冠勛譯, 博碩出版 (第 2-2-4 節)
  3. Prototyping Lab, 小林茂, 馥林文化 (第 25 章)
  4. Arduino Cookbook 錦囊妙計, 歐萊禮 (第 18-2 節)
  5. 微電腦原理與應用, 黃新賢等, 全華 (第 8 章)
延續上一篇的控制按鈕開關讓 LED 交替明滅的實驗, 將之前輪詢法的測試 1 改為採用中斷法的測試 7 :


測試 7 : 

const byte intPin=2; //interrupt pin
const byte ledPin=13; //built-in LED
volatile boolean state=LOW; //initial value of switch pin
void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(intPin, INPUT_PULLUP); //enable pull-up resistor of input pin
  digitalWrite(ledPin, ledState); //set LED OFF
  attachInterrupt(0int0, LOW); //assign int0
  }

void loop() {
  if (state) {digitalWrite(ledPin, HIGH);} //turn LED on
  else {digitalWrite(ledPin, LOW);}  //turn LED on
  }

void int0() { //interrupt handler
  state=!state; //reverse state
  }

上面程式在 setup() 中用 attachInterrup() 啟動 D2 腳的 LOW 中斷功能, 即當 D2 位準變 LOW 時觸發中斷 0, 執行中斷處理函數 int0(), 將全域變數 state 狀態反轉. 中斷處理函數執行完畢後控制權跳回 loop() 主迴圈時, 就會根據 state 變數的目前狀態來使 LED 明滅. 由於中斷處理函數會改變全域變數 state 之值, 因此必須宣告為 volatile, 以免被編譯器優化而使動作不正常.

程式上傳後執行發現, 跟前一篇的測試 1 一樣,  理論上每按一下按鈕,  D13 LED 應該由亮變滅, 或由滅變亮才對, 但實際上並非一亮一滅規律切換, 而是有時候正常, 有時候又不正常, 是典型的彈跳現象, 可見外部硬體中斷不會幫我們解決彈跳問題. 我參考前一篇測試 3-1 的程式, 為上面的程式加上去彈跳功能, 如下面測試 8-1 :

測試 8-1 :

const byte swPin=2;  //switch pin
const byte ledPin=13;  //built-in LED
volatile boolean ledState=LOW;  //initial state of LED
int debounceDelay=200; //debounce delay (ms)

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor of input pin
  digitalWrite(ledPin, ledState); //set LED OFF
  attachInterrupt(0, int0, LOW); //assign int0
  }

void loop() {
  digitalWrite(ledPin, ledState); //toggle LED
  }

void int0() { //interrupt handler
  if (debounced()) { //debounced: reverse LED state
    ledState = !ledState; //reverse LED state
    }
  }

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
  }

主要就是在中斷處理程式裡面先去呼叫 debounced() 函數, 看看是否已經撐過了預定的彈跳期間, 如果是的話就傳回 true, 中斷處理程式反轉 LED 狀態後, 主控權回到 loop() 主迴圈時, LED 就會變換狀態了. 使用中斷就不需要再去判斷 D2 是否為 LOW 了, 因為在 attachInterrupt() 時已指定 LOW 觸發中斷. 注意, 雖然 debounced() 函數中有用到 millis(), 但因為它並不是直接放在中斷處理函數 int0() 裡面, 所以它還是有作用的.

如果使用前一篇測試 4 的 debounced(pin) 函數大致可以, 只是偶而會沒反應 (why?), 如下面測試 8-2 所示 :

測試 8-2 :

const byte swPin=2;  //switch pin
const byte ledPin=13;  //built-in LED
volatile 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 of input pin
  digitalWrite(ledPin, ledState); //set LED OFF
  attachInterrupt(0, int0, LOW); //assign int0
  }

void loop() {
  digitalWrite(ledPin, ledState); //toggle LED
  }

void int0() { //interrupt handler
  if (debounced(swPin)) { //debounced: reverse LED state
    ledState = !ledState; //reverse LED state
    }
  }

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
  }

這個函數與測試 8-1 所用的不同之處在於要傳入按鈕接腳編號, 注意其彈跳時間為 100 毫秒. 實際執行結果發現偶而會動作不正常.

但是如果使用前一篇測試 6-1 的 debounced(pin) 函數卻完全沒反應, 如下測試 8-2 所示 :

測試 8-2 :

const byte swPin=2;  //switch pin
const byte ledPin=13;  //built-in LED
volatile boolean ledState=LOW;  //initial state of LED
int debounceDelay=50; //debounce delay (ms)

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

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor of input pin
  digitalWrite(ledPin, ledState); //set LED OFF
  attachInterrupt(0, int0, LOW); //assign int0
  }

void loop() {
  digitalWrite(ledPin, ledState); //toggle LED
  }

void int0() { //interrupt handler
  if (debounced(swPin)) { //debounced: reverse LED state
    ledState = !ledState; //reverse LED state
    }
  }

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;
  }

奇怪, 怎麼看都沒問題呀! Why? 

總之, 從上面的測試結果來看, 不論是前一篇的輪詢法還是本篇的中斷法, 都能夠的正確地執行出預期邏輯的 ifDebounced() 函數只有測試 3 或 3-1 以及測試 8-1 的這個 (改寫自 "Arduino 完全實戰手冊" 這本書). 似乎簡單才是硬道理啊!

接下來我想測試使用兩個中斷的情況, 我參考了黃新賢等著的 "微電腦原理與應用" 第 8 章的範例改寫, 讓中斷 0 (D2) 與中斷 1 (D3) 同時啟用, 一個控制 D13 LED 快閃 (D2); 另一個控制 D13 LED 慢閃 (D3), 所以我需要兩個按鈕開關分別連接到 D2 (INT 0) 與 D3 (INT 1), 另一端接地, 然後開啟 D2 與 D3 的內建上拉電阻, 以便按鈕未按下時這兩個中斷輸入腳會被上拉到 HIGH 以消除隨機雜訊之影響. 程式如下 :

測試 9 :

const byte swPin2=2;  //switch pin for int0 (D2)
const byte swPin3=3;  //switch pin for int1 (D3)
const byte ledPin=13;  //built-in LED
volatile int blinkRate;  //blink rate of LED
int debounceDelay=200; //debounce delay (ms)

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin2, INPUT_PULLUP); //enable pull-up resistor of input pin D2
  pinMode(swPin3, INPUT_PULLUP); //enable pull-up resistor of input pin D3
  digitalWrite(ledPin, LOW); //set LED OFF
  attachInterrupt(0, int0, LOW); //enable int0 (D2)
  attachInterrupt(1, int1, LOW); //enable int1 (D3)
  }

void loop() {
  blinkD13Led(blinkRate);
  }

void int0() { //interrupt handler
  if (debounced()) {blinkRate=200;} //fast flashing
  }

void int1() { //interrupt handler
  if (debounced()) {blinkRate=1000;} //slow flashing
  }

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
  }

void blinkD13Led(int t) {
  digitalWrite(13, HIGH);
  delay(t);            
  digitalWrite(13, LOW);  
  delay(t);            
  }

此程式中我自訂了一個 blinkD13Led() 函數來讓 LED 閃爍一下, 傳入參數 t 可以控制閃爍的頻率. INT 0 與 INT 1 分別設定 LOW 準位觸發中斷, 在各自的中斷處理函數中, 於消除彈跳現象後會更新全域變數 blinkRate 的值, 以便讓 loop() 主迴圈內的 digitWrite() 調整 LED 的閃爍頻率.


左邊按鈕是中斷 1, 接到 D3 (黃線); 右邊是中斷 0, 接至 D2 (白線), 所以按左鍵閃爍變慢, 按右鍵會變快. 兩個若同時按下的話, 右邊的中斷 0 會獲勝 (快閃), 因為照中斷向量順序表來說, 中斷 0 會先被檢出.

這個範例可能引發一個問題, 就是如果一個中斷發生時, 在其 ISR 執行期間會不會被另一個中斷給中斷 (巢狀中斷, nested interrupts)? 答案是不會, 因為當中斷發生時, ATmega328 處理器就會在硬體上將所有中斷禁止 (Reset 除外), 以避免形成無窮的遞迴中斷 (recursive interrupts), 當然實際上不可能無窮啦! 堆疊很快就會爆掉而當機了. 所以進入 ISR 後預設是不會被中斷, 直到 ISR 執行完畢, 處理器再將中斷致能. 當然有特殊原因的話, 也是可以在 ISR 內呼叫 interrupts() 自行將中斷致能, 但要妥善處理好堆疊以免導致非預期結果.

最後我在 Arduino 官網發現這個 PinChangeInt Library, 這是可將 Arduino UNO, NANO, Duemilanove 這三種板子的數位接腳變成具有外部硬體中斷功能的函式庫, 可在下列網址下載 (不要從說明文件底下所連的 Google Code Archive 下載, 那個少了 PinChangeIntConfig.h 這個檔) :

https://github.com/Ltalionis/PinChangeInt

解壓縮後將目錄 PinChangeInt-master 放到 Arduino IDE 安裝目錄的 libraries 下, 但因為此函式庫已經比較舊了, 它使用的 WProgram.h 在新版 IDE 已經改名為 Arduino.h, 所以須編輯 PinChangeInt.cpp 檔, 將其中的 include "WProgram.h" 改成 include "Arduino.h" :

#ifndef WProgram_h
#include "Arduino.h"
#endif

其使用方法參考, 必須匯入 PinChangeInt.h 與 PinChangeIntConfig.h 這兩個函式庫, 並使用 PCintPort::attachInterrupt() 來起始指定腳位的硬體中斷功能 :

PinChangeInt Example

我將上面測試 8-1 的程式改成下列測試 10 :

測試 10 :

#include <PinChangeInt.h>
#include <PinChangeIntConfig.h>

const byte swPin=5;  //switch pin
const byte ledPin=13;  //built-in LED
volatile boolean ledState=LOW;  //initial state of LED
int debounceDelay=200; //debounce delay (ms)

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor of input pin
  digitalWrite(ledPin, ledState); //set LED OFF
  PCintPort::attachInterrupt(swPin, int0, RISING); //Enable PCI on swPin
  }

void loop() {
  digitalWrite(ledPin, ledState); //toggle LED
  }

void int0() { //interrupt handler
  if (debounced()) { //debounced: reverse LED state
    ledState = !ledState; //reverse LED state
    }
  }

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
  }

這裡用 D5 腳的上升緣 (RISING) 既然是 Change, 當然也可以用下降緣 (FALLING) 觸發, 但我實驗結果發現用 RISING 很穩, 用 FALLING 有時不靈光, 不知原因為何 (彈跳期間太短嗎?).

還有一個比較新的 PCI Manager 函式庫也可以將其他 D2/D3 以外的數位接腳變成具有外部硬體中斷功能, 下載網址與使用範例如下 :

PciManager

將解壓縮後的目錄 arduino-pcimanager-master 複製到 Arduino IDE 安裝目錄的 libraries 下, 參考範例將上面測試 10 改為測試 11 如下 :

測試 11 :

#include <PciManager.h>
#include <PciListenerImp.h>

const byte swPin=5;  //switch pin
const byte ledPin=13;  //built-in LED
volatile boolean ledState=LOW;  //initial state of LED
int debounceDelay=200; //debounce delay (ms)

void onPinChange(byte changeKind);  //declare self-defined function
PciListenerImp listener(swPin, onPinChange);  //create lister object

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(swPin, INPUT_PULLUP); //enable pull-up resistor of input pin
  digitalWrite(ledPin, ledState); //set LED OFF
  PciManager.registerListener(swPin, &listener);  //register change event on swPin
  }

void loop() {
  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
  }

void onPinChange(byte changeKind) { //interrupt handler
  if (debounced()) { //debounced: reverse LED state
    ledState = !ledState; //reverse LED state
    }
  }

這裡需匯入 PciManager.h 與 PciListenerImp.h 這兩個函數, 然後在建立 PciListenerImp 物件之前必須先宣告自訂的事件處理函數, 否則會出現如下錯誤 :

pcimanager:7: error: 'onPinChange' was not declared in this scope

 PciListenerImp listener(INPUT_PIN, onPinChange);

當然也可以把 onPinChange() 函數放到建立 listener 物件之前, 但這樣結構上有點怪. 測試結果也是 OK 的, 只是這個沒辦法選擇是要上升或下降時觸發中斷而已, 因為我們這裡使用上拉電阻, 所以是下降時觸發; 如果用外部下拉電阻的話, 就會變成上升觸發了.

好了, 關於 Arduino 外部硬體中斷功能的測試大概就是這樣了, 其他比較深奧的需要對 AVR 處理器內部深入了解後才看得懂.

參考 :

# Arduino – 中斷功能 (寫得好)
# 從 Arduino 到 AVR 晶片(2) -- Interrupts 中斷處理 (深入韌體)
關於中断(Interrupt)的一些五四三... 中斷 . . (精闢)
Do interrupts interrupt other interrupts on Arduino?
# Global manipulation of the interrupt flag
# EXTERNAL INTERRUPTS ON THE ATmega168/328
# Using millis() and micros() inside an interrupt routine


2016-10-27 補充 :

今天在 "Arduino 從零開始學" 這本書上的 4.2.7 硬體中斷這節看到定時中斷, 作者介紹了 FlexiTimer2.h 與 MsTimer2.h 這兩個定時函式庫, 可以很方便地使用定時中斷. 其範例如下 :

#include <MsTimer2.h>
int pin=13;
volatile int state = LOW;  
void setup() {
  pinMode(pin, OUTPUT);
  MsTimer2::set(500, blink);            
  MsTimer2::start();      
  }

void loop() {
  digitalWrite(pin, state);
  }

void blink() {
  state = !state;                            
  }

此程式利用定時中斷每 0.5 秒會改變 state 變數之值, 從而使 D13  LED 閃爍. MsTimer2.h 可在此書範例原始碼 zip 檔解開後的 Libraries 目錄中找到, 上面範例在 4-5 資料夾下, 請至碁峰網站下載 :

http://books.gotop.com.tw/download/ACH018200


20 則留言:

  1. 你好
    請叫個問題
    如果我有多個程式在跑
    今天我想中斷其中一支
    應該怎麼做
    因為我現在一按下開關後就全部停擺了

    回覆刪除
  2. 不太了解, 您說有多個程式在跑, 但 Arduino 同一時間只能跑一個程序哩!

    回覆刪除
  3. 不好意思可能我表達上有問題
    我的意思是說我有一段程序依序判斷在跑
    然後我今天想要中段裡面其中一支付程式該怎麼中斷?

    回覆刪除
  4. 中斷副程式嗎? 我不太能理解, 副程式只能單線被呼叫, 沒辦法同時呼叫. 您的意思是指多執行緒嗎? Arduino 沒有多執行緒喔!

    回覆刪除
  5. 我的意思是假若好幾支副程式A程式B程式C程式D程式依序被呼叫
    然後B程式值行的時間太久所以我想中斷後直接看到C程式的東西該怎麼寫這段?

    回覆刪除
  6. 你好請問一下 中斷動作結束後 應該是接續著中斷前的程式? 還是會重頭開始跑?

    如果我想讓他中斷後重頭開始跑 該怎麼做?

    回覆刪除
  7. Dear 楊家瑋 :
    中斷處理程式完畢後是回到被中斷的那個地方繼續執行喔, 因為中斷發生時, 執行中的狀態 (程式計數器, 暫存器的值等) 會被放進堆疊中暫存, 中斷處理程式跑完就會從堆疊裡把被中斷前的狀態取出復原, 程式計數器恢復原值, 所以是回到被中斷的地方繼續執行.

    回覆刪除
  8. Dear 傅勁崴 :
    請問您的意思是中斷後往下一個副程式執行還是指定執行 C 副程式呢? 中斷其實只是臨時出去做一下雜務, 做完要回來原來的地方往下執行, 無法直接跳到另一個副程式.

    回覆刪除
  9. Dear 傅勁崴 : 我猜你的意思是要結束現在耗時的副程式, 直接跳到下一個副程式, 我回覆在這一篇 :

    http://yhhuang1966.blogspot.tw/2017/06/arduino_14.html

    回覆刪除
  10. 很有用的文章,獲益良多,感謝。

    回覆刪除
  11. 你好,謝謝你這些優質的文章!

    請問,中斷能不能自己用 digitalWrite 用程式來自己啟動它?
    如: digitalWrite(InterruptPin, HIGH)

    謝謝!

    回覆刪除
  12. Dear Stonez : 當然可以! 只要將一支 DIO 接到 InterruptPin 便可用程式去控制中斷. 不過外部硬體中斷存在的主要意義是讓 cpu 不用費心去觀照外部狀態, 請問您用程式控制中斷的原因是甚麼呢?

    回覆刪除
  13. 您好,
    我現在要做一個當蜂鳴器播音樂時,按下一個按鈕觸發中斷(音樂停止),中斷(音樂停止)持續10分鐘後,重頭開始跑程式,請問程式要如何寫?

    回覆刪除
  14. 請問我用了睡眠模式,然後使用INT0外部喚醒,但是我又需要使用INT0這一隻腳接的按鈕當功能鍵用,這樣要怎做呢? 現在的狀況是,休眠後,按下按鈕會醒來,但是一按就會一直產生中斷,無法繼續原有的loop指令

    回覆刪除
  15. Arduino UNO/Nano/Pro mini 都有兩個中斷接腳啊! 可用 int1

    回覆刪除
  16. INT1 用到了PWM輸出功能了, 目前剩下INT0 可以用..
    請問有什麼方式可以公用一隻腳嗎?
    我在loop() 中使用detachInterrupt(0) ;關閉中斷功能,5秒後開啟interrupts()進入powerdown模式

    powerdown前,按鍵功能OK, 但是進入睡眠再恢復後,按鍵功能就消失了

    回覆刪除
  17. Sorry, 這我沒試過, 短時間內也沒時間測試, 不過我找到一篇文章或許可以試試, 他們使用 Enerlib 與 LowPower 函式庫控制單一中斷的睡眠.

    https://read01.com/zh-tw/MyJk7.html#.XZvXA0YzY2w

    回覆刪除
  18. 您好,您在文中提到"如果在 ISR 內要使用時間延遲功能, 必須自行撰寫時間延遲函數",意思是指自行撰寫一個有呼叫delay或是millis的函數,並且該函數在ISR內被呼叫是可以正常運作的嗎?目前我是想在ISR內呼叫一個副程式,該副程式中使用了Dr. Monk所開發的Timer函式庫(http://srmonk.blogspot.com/2012/01/arduino-timer-library.html)。這個函式庫也是以millis為基礎所編寫完成的。 感謝!

    回覆刪除
  19. 您好, ISR 內只能用 delayMicroseconds(), 因為 delay() 與 millis() 都會用到中斷計時器, 無效.

    回覆刪除
  20. 中斷副程式 可否寫判斷式

    回覆刪除