我們之前曾經利用Arduino製作了一些範例,例如當動物接近時會自動拍攝的裝置、能顯示目前垃圾量並且當手接近時會自動開啟蓋子的智慧型垃圾桶、還有採用無線方式搜集各種環境資訊上傳雲端的機房環控系統等等,初識這些東西可能讓人感覺很有趣和新奇,不過,我們自己在設計這些系統的時候,還是總覺得運作上隱約有點不對勁。
例如以下方標準的Arduino架構來說,我們的主程式都是放在loop的無窮迴圈中運作。
void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
}
因此,如果要偵測外部環境,我們會連接數個感測器然後在loop迴圈中讀取它們的值,並依據結果進行相對應的動作,這樣反覆運作直到電源被關閉為止,看起來似乎很完美,理論上應該是一個井然有序的運作架構。
然而卻不然,隨著要求的功能愈來愈多,愈來愈多需要執行的程式碼加到函式中,慢慢的我們發現Arduino運作起來似乎再那麼靈敏、好像變得有些遲頓了,應該感應到的動作沒有偵測到、一些透過RF傳送的data也loss,這是怎麼一回事?
INTERRUPT 中斷
原因就在於我們沒有利用INTERRUPT功能。
舉個生活的例子來說明吧,當我們在煮開水時,三不五時總要走過來看一下確定水開了沒,假設我們3分鐘走回來檢查一次,那麼會有二種情況:
- 走過來時水早已開了。 ← 大部份的情況。
- 走過來時水剛剛好開了,。 ← 機率很低。
又如果我們很忙,必須利用這三分鐘時間順便拖地洗衣兼看看連續劇,那麼,三分鐘的間隔有可能被打亂,更有可能拖長了煮開水這個工作的完成時間,最後造成開水煮得不理想,甚至於蒸發了大部份的水或者地板噴濺得滿地都是。那麼如果有一個機制(例如水開時會發出笛音的鳴笛壺),能夠在水煮開時自動通知我們,這樣不是很好嗎?不但省了每隔幾分鐘就要走過來檢查的動作,我們更有時間可從容的完成其它想作的事情。
Arduino的情況也是如此,我們在一個迴圈當中要求它作了太多的事情,導致它來不及應付。
ARDUINO 的中斷支援
其實,像上述中在loop迴圈裏不停的反覆進行讀取查看狀態的方式,我們稱之為「polling」(輪詢),而當狀態改變時會自動通知我們的機制,則是「interrupt」(中斷)。Arduino提供了20個GPIO接腳可連接外部設備,但可不是每一支腳都擁有中斷的功能,僅有少數幾支有支援中斷。想想也是,除了少數重要的大事,我們日常生活上都需要每件事都要主動通知的機制嗎?
下面為各種Arduino版本,其中斷編號所對應的腳位:例如,Nano的D2, D3腳位,其中斷編號分別為0, 1。
Models |
中斷0 |
中斷1 |
中斷2 |
中斷3 |
中斷4 |
中斷5 |
UNO |
2 |
3 |
||||
MEGA 2560 |
2 |
3 |
21 |
20 |
19 |
18 |
Leonardo |
3 |
2 |
0 |
1 |
||
Pro Mini |
2 |
3 |
||||
Nano |
2 |
3 |
一般來說,使用ATmega328處理器的Arduino板子,只有兩個擁有中斷功能(我們俗稱外部中斷)的腳位,大部份常見的Arduino型號都是此類。ATmega2560處理器的則有6個,而比較特殊的是Leonardo,它採用內建USB介面的Atmega32u4微處理器,可支援四個外部中斷。
中斷觸發的方式
當擁有外部中斷功能的腳位,其訊號(也就是電壓)改變或處於某訊號時,就會觸發微處理器去執行一個所謂「中斷服務常式(ISR, Interrupt Service Routine)」的函式,這個函式我們可以自行定義,再由系統當中斷觸發時自動去執行。
既然觸發的時機是當依據訊號狀態,那麼,就可能會有如下的五種情況:
A)FALLING(當訊號下降時觸發)
B)RISING(當訊號上升時觸發)
C)CHANGE(當訊號改變時觸發)
D)LOW(當訊號處在低電位時持續觸發)
E)HIGH(當訊號處在高電位時持續觸發)
最後一種「HIGH」只有Leonardo板子有支援,剩下的前四種當中,則又以FALLING、RISING、CHANGE最常用到。
如何使用外部中斷
在Arduino使用外部中斷功能其實是相當容易的,只要在setup()函式中,利用attachInterrupt指令來指定要綁定中斷功能的腳位、觸發時要執行的函式名稱、以及觸發的方式,這樣就可以了。
例如:attachInterrupt(0, swISR, CHANGE);
- 表示在第0腳位啟用外部中斷,當有CHANGE狀態發生時,就執行swISR函式。
編寫外部中斷函式的注意事項
外部中斷發生時所執行的函式我們一般稱為「中斷服務常式(Interrupt Service Routine)」,或簡稱為ISR,因此,ISR指的就是一個中斷發生時會去執行的函式。它的寫法與一般的函式大同小異,唯一不同的地方就是,在ISR函式內部會改變其值的變數,在宣告時,我們必須在前面加上Volatile這個關鍵字。
Volatile這個單字原意就是易變的意思,在趙英傑所著的「Arduino互動設計入門」這本書的附錄D中,對於使用Volatile的原因以及它在中斷服務中所扮演的角色作了很詳細的解釋,推薦大家可以買來翻翻看。
例如,假設sw這個變數會在ISR函式中被變更為HIGH,那麼,當初我們在宣告時,就應作如下的宣告:
正確:Volatile boolean sw = LOW;
錯誤:boolean sw = LOW;
為什麼要作這個宣告呢?大家可以參考趙英傑所著的「Arduino互動設計入門」一書的附錄D,當中有清楚的說明,在此就不作節錄了;主要觀念在於,當一個變數加入了Volatile的宣告,那麼程式在Complier時就不會去最佳化與此變數相關的程式碼,以避免最佳化後,該變數的值在程式執行中斷處理函數時發生了變化,卻因已最佳化而未即時同步更新該變數值。
舉例示範
下方我以PIR模組為例,先宣告一個volatile 的pirVal變數,用來儲存目前PIR的值,然後在中斷0的腳位(PIR)註冊一個Interrup,當該腳位的值發生變化時執行pirDetect_init(),因此,當PIR一偵測到有人,便立刻會執行相關動作,而不會等到loop迴圈的工作執行完畢。
unsigned char PIRPin = 2; volatile boolean pirVal = 0; //目前PIR狀態 void pirDetect_init() { pirVal = digitalRead(PIRPin); } void setup() { pinMode(PIRPin, INPUT); attachInterrupt(0, pirDetect_init, CHANGE); //注意, 0代表的是中斷編號,不是腳位哦!D2腳位的中斷編號為0, 所以此處需放0 Serial.begin(9600); } void loop() { if(pirVal) Serial.println("有人!"); }
謝謝, check了一下的確寫錯了, 感謝提醒.
你好,您的中斷腳位有寫錯
MEGA 2560 應該是 2、3、21、20、19、18