Arduino – 中斷功能

我們之前曾經利用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分鐘走回來檢查一次,那麼會有二種情況:

  1. 走過來時水早已開了。         大部份的情況。
  2. 走過來時水剛剛好開了,。   機率很低。

又如果我們很忙,必須利用這三分鐘時間順便拖地洗衣兼看看連續劇,那麼,三分鐘的間隔有可能被打亂,更有可能拖長了煮開水這個工作的完成時間,最後造成開水煮得不理想,甚至於蒸發了大部份的水或者地板噴濺得滿地都是。那麼如果有一個機制(例如水開時會發出笛音的鳴笛壺),能夠在水煮開時自動通知我們,這樣不是很好嗎?不但省了每隔幾分鐘就要走過來檢查的動作,我們更有時間可從容的完成其它想作的事情。

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("有人!");

}

3 Comments

迴響已關閉。