2011年7月7日 星期四

3.1) External Interrupts

實驗目的

練習處理外部中斷。在這個實驗中,你將利用 ISR() 巨集指令設定中斷服務函式 (Interrupt Service Routine, ISR),讓程式在 pin 2 產生外部中斷時自動執行 ISR。

材料
  • Arduino 主板 x 1
  • LED x 1
  • Pushbutton x 1
  • 10K 歐姆電阻 x 1
  • 麵包板 x 1
  • 單心線 x N
接線
  1. 把 LED 接到 pin13,長腳 (陽極) 接到 pin13,短腳 (陰極) 接到 GND
  2. 把 pushbutton 一支腳接到 +5V,另一支腳接到 pin 2 同時接一顆 10K 電阻連到 GND

image

INT0 與 INT1 外部中斷

如下圖所示,ATmega328 有兩個外部中斷,編號 0 (INT0) 在 pin 2 上,而編號 1 (INT1) 在 pin 3 上:

image

我們可以決定什麼時候要觸發 INT0 和 INT1 外部中斷,一共有四種狀況可以選擇:

  • LOW: 當 pin 為 LOW 時觸發中斷
  • CHANGE: 當 pin 狀態改變時觸發中斷,不管是從 HIGH 到 LOW 或從 LOW 到 HIGH
  • FALLING: 當 pin 狀態從 HIGH 到 LOW 時觸發中斷,又稱負緣觸發
  • RISING: 當 pin 狀態從 LOW 到 HIGH 時觸發中斷,又稱正緣觸發

選擇的方法是透過 ECIRA (External Interrupt Control Register A) 這個暫存器:

image

image
▲ 資料來源: ATmega168/328 Datasheet

以 INT0 外部中斷為例,假如我們希望 pin 2 狀態一有改變就觸發中斷,那麼就要把 ISC01 和 ISC00 設成 01:

除此以外,我們還要設定 EIMSK (External Interrupt Mask Register) 暫存器以啟用 INT0 或 INT1 中斷:

image

以 INT0 外部中斷為例,要啟用 INT0 外部中斷,得把 INT0 這個位元設成 1:

程式

先來看使用 attachInterrupt() 函式的版本(attachInterrupt.pde),這我們在「attachInterrupt() 與外部中斷」一文中介紹過,程式的邏輯是,當按鍵被按下時打開 LED 燈號,在按鍵放開時關閉 LED 燈號:

要特別注意是,程式裏的 buttonState 變數是宣告成 volatile,這樣做的目的是告訴 Compiler 不要做最佳化,避免變數狀態不同步。給你一個建議,程式主體跟 ISR 都會用到的變數,盡可能把它宣告成 volatile。

接下來是改成用 EICRA 和 EIMSK 暫存器和 ISR() 巨集指令的版本:

簡單的說,要處理 INT0 外部中斷,有三個步驟要做:

  1. 用 ISR() 巨集指令定義中斷服務函式
  2. 設定 EIMSK 暫存器啟用 INT0 中斷
  3. 設定 EICRA 暫存器決定什麼時候該觸發中斷

_BV() 巨集我們在「2.2) Blink part2」一文中已經介紹過,如果不想使用 _BV(),這樣寫也是可以的:

另外,ISR() 巨集指令也可以改用 SIGNAL() 巨集指令,SIGNAL() 是早期寫 ISR 的巨集指令。建議你盡可能使用 ISR() 巨集指令,因為 ISR() 比較直覺,只不過你必須知道 SIGNAL() 和 ISR() 幾乎是一樣的東西,而且很多前輩留下的 AVR 程式是用 SIGNAL() 巨集指令。

延伸閱讀

6 意見:

張欽棠 提到...

LOW: 當 pin 為 LOW 時觸發中斷
CHANGE: 當 pin 狀態改變時觸發中斷,不管是從 HIGH 到 LOW 或從 LOW 到 HIGH
FALLING: 當 pin 狀態從 "LOW 到 HIGH 時觸發中斷" ,又稱負緣觸發
RISING: 當 pin 狀態從 "LOW 到 HIGH 時觸發中斷" ,又稱正緣觸發

我猜打""部分應該其中有一個是打錯的:)是嗎?

coopermaa 提到...

我寫錯了!這句:

「FALLING: 當 pin 狀態從 "LOW 到 HIGH 時觸發中斷"」

應該是這樣才對:

「FALLING: 當 pin 狀態從 "HIGH到 LOW 時觸發中斷"」

謝謝你的訂正。:)

Sven Wang 提到...

Cooper好~~~

這邊有一個問題一直困擾我!!

已知道有設定一個巨集
#define _BV(x) (1 << x)

所以EICRA &= ~_BV(ISC01);的意思應該是
EICRA = EICRA & 1111 1101
好讓EICRA的第一個bit被設定為0

這邊可得到一個結論"ISC01代表1"
所以也可以推出另外的結論
ISC10代表2
ISC11代表3

那這邊衍生出一個問題
假設我想設定ISC11是1
那為什麼我不能直接下指令ISC11=1;,這樣就好了呢?是否一定要使用EICRA |= _BV(ISC11);
再者, 如果可以很單純的使用ISC11=1;
那是否跟ISC11代表3這樣的設定有矛盾呢?

也就是ISC11=3 <=當使用_BV巨集推論出來的
ISC11=1 <==希望直接設定暫存器裡面的資料

希望Cooper能為我解答,謝謝~~~~
我想我有很多觀念需要釐清XDDD

coopermaa 提到...

這個是 ISC00~ISC11 的巨集定義 (可在 Arduino tools/avr/avr/include/iom328.h 找到):

#define ISC00 0
#define ISC01 1
#define ISC10 2
#define ISC11 3

要注意,這只是定義該巨集是第幾個位元,不是指 "那一個位元" 喔!

也就是說 ISC10 巨集代表它是第 2 個位元,但並不是指 EICRA 的 bit 2

所以如果你寫:

ISC11 = 1;

那麼這段程式碼在巨集代換後會變成:

3 = 1; // 因為 ISC11 為 3

這樣就錯了。

所以沒有辦法那麼方便,還是得乖乖地搭配 _BV() 巨集來設定指定的位元喔

Sven Wang 提到...

謝謝!!
原來有這個先設定了~
#define ISC00 0
#define ISC01 1
#define ISC10 2
#define ISC11 3
thx!!

coopermaa 提到...

You're welcome!