# Camera OV7670 第07段 Interrupt中斷函數 ```javascript= // this is called in Arduino loop() function void processFrame() { processedByteCountDuringCameraRead = 0; commandStartNewFrame(uartPixelFormat); noInterrupts(); processFrameData(); interrupts(); frameCounter++; commandDebugPrint("Frame " + String(frameCounter)/* + " " + String(processedByteCountDuringCameraRead)*/); //commandDebugPrint("Frame " + String(frameCounter, 16)); // send number in hexadecimal } ``` ## Interrupt中斷函數 - 第一個是呼叫 digitalPinToInterrupt() 函數並傳入數位腳編號, 也可以不呼叫此函數直接傳入腳位編號 (不建議) - 第二個參數是自訂的中斷服務函數名稱 (Interrupt Service Routine) - 第三個參數則是中斷觸發模式, 有 LOW, RISING, FALLING, CHANG, 以及 HIGH (只能用於 Arduino Due). 當外部硬體中斷發生後, ATmega328 處理器會將程式暫存器推入堆疊保存, 然後跳到 ISR 所在位址執行 ISR 的第一條指令, 這樣總共需要 82 個時脈週期 ### 中斷相關函數 1. attachInterrupt(int, ISR, mode) 指派中斷服務函式 int = 中斷編號, 0 或 1 ISR = 中斷服務函式名稱 mode = 中斷模式 (LOW, CHANGE, RISING, FALLING) 2. detachInterrupt(int) 移除指定腳位之中斷功能, int=中斷編號, 0 或 1 3. noInterrupt() 停止全部中斷功能 (除 reset 外) 4. interrupts() 重新啟用全部中斷功能 其中後面三個函數(interrupts、noInterrupt、detachInterrupt)都是需要動態控制中斷功能時才會用到, 例如當要取得目前的 LED 狀態時, 我們不想此變數被中斷改變, 這時就可以先停止所有中斷 (Reset 除外), 取得變數值後再恢復 : ```javascript= noInterrupts(); //disable all interrupts boolean state=ledState; //get volatile variable set by ISR(獲取 ISR 的 volatile 變數) 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() 函數來致能. 參考網站(https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/): - 關於第 6 項我持保留意見, 因為根據下面這篇文章裡的 ATmega328 中斷向量優先表, MPU 在執行每道指令後會檢查這張中斷向量表 首先檢查是否有按下 Reset 鍵, 接著檢查中斷 0, 然後是中斷 1 ... 如果都沒發生中斷, 就執行下一道指令 (可見我們的外部硬體中斷只是 26 種中斷裡的兩個而已). **所以看起來中斷 0 是比中斷 1 優先的**:+1: 參考網站(http://www.gammon.com.au/interrupts) ### 補充volatile(直接存取原始記憶體地址) - 特別注意, ISR 內的全域變數必須宣告為 volatile 是因為編譯器的程式碼優化功能, 在 ISR 中可能會破壞程式碼原本規劃的邏輯, 使得執行功能異常 例如在趙英傑的 "Arduino 互動設計入門 2" 附錄 D 就舉了一個範例: ```javascript= int a, b; int c=a+b; int d=a+b; ``` - 如果 C 編譯器的優化選項有勾選的話, 那麼上面的程式碼在編譯器執行優化後會變成 : ```javascript= 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 一樣是舊的數據. ```javascript= volatile int a, b; ``` - 如果 a, b 加上 volatile 宣告的話, 就可以避免這個問題, 這個 volatile 就是通知編譯器, 這兩個全域變數不要進行優化 :100: - 此外, 被編譯器優化過的變數會放一個副本在暫存器中直接運算, 而非從 RAM 裡面重新載入, 不過這樣可能被中斷處理函數覆蓋掉原來的值, 導致得到錯誤的運算結果. 使用宣告為 volatile, 是告訴編譯器, 此變數隨時會被中斷處理函數改變, 執行時必須重新從 RAM 中載入暫存器, 不可以只依賴存放在暫存器裡的副本 (被優化的變數就是直接取用暫存器裡的副本, 這樣就節省了重新自 RAM 載入的時間). 參考網站(https://iter01.com/552126.html) 參考網站(http://yhhuang1966.blogspot.com/2016/09/arduino-interrupt.html) ## 範例 中斷函數 ```javascript= 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 } ------------------------------------------------------------- / 在 setup() 中用 attachInterrup() 啟動 D2 腳的 LOW 中斷功能 / ------------------------------------------------------------- / 即當 D2 位準變 LOW 時觸發中斷 0, 執行中斷處理函數 int0() / ------------------------------------------------------------- void loop() { digitalWrite(ledPin, ledState); //toggle LED } void int0() { //interrupt handler if (debounced()) { //debounced: reverse LED state ledState = !ledState; //reverse LED state } } --------------------------------------------------------------------------------------------------------- / 在中斷處理程式裡面先去呼叫 debounced() 函數, 看看是否已經撐過了預定的彈跳期間, 如果是的話就傳回 true/ --------------------------------------------------------------------------------------------------------- / 將全域變數 state 狀態反轉.處理反轉 LED 狀態,中斷處理函數執行完畢後,控制權跳回 loop() 主迴圈(此時LED 就會變換狀態了) / --------------------------------------------------------------------------------------------------------- / 因此必須宣告為 volatile, 以免被編譯器優化而使動作不正常 / --------------------------------------------------------------------------------------------------------- / 且因為使用 中斷函數就不需要再去判斷 D2 是否為 LOW 了, 因為在 attachInterrupt() 時已指定 LOW 觸發中斷/ --------------------------------------------------------------------------------------------------------- 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() 函數中有用到 millis(), 但因為它並不是直接放在中斷處理函數 int0() 裡面, 所以它還是有作用的./ ``` ## 範例 使用兩個 中斷函數 參考了黃新賢等著的 "微電腦原理與應用" 第 8 章的範例改寫, 讓中斷 0 (D2) 與中斷 1 (D3) 同時啟用, 一個控制 D13 LED 快閃 (D2); 另一個控制 D13 LED 慢閃 (D3), 所以需要兩個按鈕開關分別連接到 D2 (INT 0) 與 D3 (INT 1), 另一端接地, 然後開啟 D2 與 D3 的內建上拉電阻, 以便按鈕未按下時這兩個中斷輸入腳會被上拉到 HIGH 以消除隨機雜訊之影響 ```javascript= 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 的閃爍頻率. - 這個範例可能引發一個問題, 就是如果一個中斷發生時, 在其 ISR 執行期間會不會被另一個中斷給中斷 (巢狀中斷, nested interrupts)? 答案是不會, 因為當中斷發生時, ATmega328 處理器就會在硬體上將所有中斷禁止 (Reset 除外), 以避免形成無窮的遞迴中斷 (recursive interrupts), 當然實際上不可能無窮啦! 堆疊很快就會爆掉而當機了. 所以進入 ISR 後預設是不會被中斷, 直到 ISR 執行完畢, 處理器再將中斷致能. 當然有特殊原因的話, 也是可以在 ISR 內呼叫 interrupts() 自行將中斷致能, 但要妥善處理好堆疊以免導致非預期結果. 參考網站(http://yhhuang1966.blogspot.com/2016/09/arduino-interrupt.html) 參考網站(https://iter01.com/552126.html)