本文隸屬於AVR單片機教程系列。html
中斷,是單片機的精華。git
當一個事件發生時,CPU會中止當前執行的代碼,轉而處理這個事件,這就是一箇中斷。觸發中斷的事件成爲中斷源,處理事件的函數稱爲中斷服務程序(ISR)。ide
中斷在單片機開發中有着舉足輕重的地位——沒有中斷,不少功能就沒法實現。好比,在程序幹別的事時接受UART總線上的輸入,而uart_scan_char
等函數只會接收調用該函數後的輸入,先前的則會被忽略。利用中斷,咱們能夠在每次接受到一個字節輸入時把數據存放到緩衝區中,程序能夠從緩衝區中讀取已經接收的數據。函數
AVR單片機支持多種中斷,包括外部引腳中斷、定時器中斷、總線中斷等。每個中斷被觸發時,經過中斷向量表跳轉到對應ISR。若是一箇中斷對應的ISR不存在,連接器會把復位地址放在那裏,若是這個中斷被響應程序就會復位(但單片機不會復位)。ui
那麼,咱們之前從未寫過ISR,但常常改變引腳電平,爲何沒有復位呢?由於中斷默認是不開啓的。要啓用一箇中斷,須要讓兩個位於不一樣寄存器中的位爲1
,一個是中斷對應的中斷使能位,每一箇中斷都有各自的位,另外一個是全局中斷使能位,位於寄存器SREG
中,不能直接存取,須要經過定義在<avr/interrupt.h>
頭文件中的sei()
函數開全局中斷,相對地,cli()
用於關全局中斷。spa
先來寫第一個帶中斷的程序吧。從原理圖中能夠看到,PB2
旁邊標明瞭INT2
,表示PB2
引腳可用於外部中斷2。把一個按鍵鏈接到PB2
引腳上,即開發板最下方的7P排母的最右邊。利用中斷,咱們實現每按一次按鍵就翻轉LED狀態的功能。操作系統
#include <avr/io.h> #include <avr/interrupt.h> int main() { PORTB |= 1 << PORTB2; EICRA |= 0b10 << ISC20; EIMSK |= 1 << INT2; DDRC |= 1 << DDC4; sei(); while (1) ; } ISR(INT2_vect) { PORTC ^= 1 << PORTC4; }
ISC21:0
兩位指定外部中斷的類型,這裏設置爲降低沿,即按鍵按下時觸發;INT2
位使能外部中斷2;所有初始化完成後,sei()
啓用全局中斷,而後單片機就會相應按鍵按下的事件了。code
ISR(INT2_vect)
指示這個函數是外部中斷2的ISR。每一箇中斷ISR都有本身的名字,由數據手冊12章Source
一欄的內容加上_vect
組成,這個名字能夠當成函數名字來使用。htm
若是多箇中斷同時觸發,單片機會先響應優先級高的。一些單片機支持自定義的優先級,但在AVR單片機中,只有簡單的地址低的優先級高的規則。blog
中斷能夠被中斷嗎?在AVR單片機中,執行一箇中斷處理函數會自動地關閉全局中斷,此時程序不會被中斷,但能夠手動地sei()
使中斷能夠被處理。程序是否相應中斷僅取決於該中斷是否被啓用,與其優先級無關。
固然,中斷不是完美的。其一,你也許已經發現上面的程序不能很好的工做,有時候明明按下了按鍵,燈卻一閃就滅。這是由於,按鍵存在抖動,比單片機時鐘週期長,能觸發多箇中斷。之前把button_down()
放在main
函數的while
循環裏時就沒有這個問題,正是循環中的delay
濾除了這種抖動。
其二,進入和退出中斷,除了須要CPU幾個週期來改變PC(程序計數器,當前執行指令的地址)外,還須要保護和恢復現場,包括SREG
寄存器與ISR中用到的通用寄存器。下面這段彙編代碼能夠在Solution Explorer
中Output Files\xxx.lss
中找到。
00000094 <__vector_3>: #include <avr/io.h> #include <avr/interrupt.h> ISR(INT2_vect) { 94: 1f 92 push r1 96: 0f 92 push r0 98: 0f b6 in r0, 0x3f ; 63 9a: 0f 92 push r0 9c: 11 24 eor r1, r1 9e: 8f 93 push r24 a0: 9f 93 push r25 PORTC ^= 1 << PORTC4; a2: 98 b1 in r25, 0x08 ; 8 a4: 80 e1 ldi r24, 0x10 ; 16 a6: 89 27 eor r24, r25 a8: 88 b9 out 0x08, r24 ; 8 } aa: 9f 91 pop r25 ac: 8f 91 pop r24 ae: 0f 90 pop r0 b0: 0f be out 0x3f, r0 ; 63 b2: 0f 90 pop r0 b4: 1f 90 pop r1 b6: 18 95 reti
這段代碼沒必要理解,更不用會寫。94
到a0
行是保護現場,依次將寄存器r1
、r0
、SREG
(即0x3f
)、r24
和r25
push進棧,把r1
清零,一共用了12個週期,還要加上響應中斷的4個週期;a2
到a8
是恢復現場,把這些寄存器原來的值逆序地從棧上pop出來,用了15個週期;而只有中間aa
到b6
的語句是用於執行用戶代碼的,在總共35個週期中只佔4個週期。
固然,這個比例很小是由於這個ISR過於簡單。可是,ISR更復雜也意味着有更多寄存器須要push和pop,中斷的響應時間更長。
這個例子並無中斷效率低下的意思,而是代表不能過於頻繁地依賴中斷。好比接下來要講的定時器中斷,我一般設置爲1ms間隔,只有一次到0.1ms,再快恐怕就起不到定時的做用了。
定時器,顧名思義,定時用的。以前咱們在main
函數的while (1)
循環中,每一個週期執行一些代碼,而後延時一個固定的時長。我也曾見過根據該次週期的工做量來計算延時時長的操做,但畢竟寫BASIC的人學得也basic吧,這種作法的定時仍不精確。利用定時器中斷(其實沒必要中斷),咱們能夠實現精確的定時,使每一週期的時間嚴格相同。
若是對操做系統有一點了解,就會知道操做系統須要進行任務調度。然而,任務在執行時,並不知道本身該什麼時候被調度走。實際上,是操做系統在定時器中斷中打斷了任務的正常執行,而後進行調度。定時器中斷是操做系統的基礎。
在AVR單片機定時器的各類模式中,普通模式和CTC模式經常使用於產生定時器中斷。咱們仍然以定時/計數器0爲例。
在普通模式中,使用TIMER0_OVF
中斷,頻率爲\(\frac {f_{CPU}} {256 \cdot N}\),\(N\)爲分頻係數。這樣產生的定時器中斷精確但不確切,由於N
的取值是很離散的。若是隻須要在中斷中進行外設輪詢的話,普通模式就足夠了。
若是在ISR的第一行就給TCNT0
賦值,或是使用TIMER0_COMPA
中斷並在起始處寫TCNT0 = 0
,那麼能夠改變中斷頻率,但因爲有編譯器插入的保護現場的代碼的存在,這種定時不夠精確,而CTC模式解決了這個問題。
在CTC模式中,使用TIMER0_COMPA
中斷,頻率精確地爲\(\frac {f_{CPU}} {N \cdot (OCR0A + 1)}\)(注意沒有蜂鳴器頻率公式中的\(2\))。
還須要提醒一句,若是想要中斷被響應,必須保證main
函數不退出,由於編譯器會在退出處加上一句cli()
。最簡單的方法是在main
函數的最後加上一句while (1);
。
數碼管的動態掃描須要每隔一段時間就換一位點亮是一件很煩人的事,尤爲是在操控其餘外設的程序已經比較複雜的時候。我原本想把中斷完美地拖到第二期再講,沒想到本身也受不了動態掃描的折磨,在某個版本的庫中就放出了segment_auto
函數來接管這項工做。它正是使用了定時器中斷。
實現思路很簡單,把要顯示的數據放在客戶和庫能夠共同取用的變量中,在中斷裏逐位顯示,只要中斷夠快,就能夠實現動態掃描,使每一位看起來都在亮。
#include <avr/io.h> #include <avr/interrupt.h> #include <ee1/segment.h> void segment_int_init() { // other initializations, ex. pins TCCR0A = 0b10 << WGM00; // CTC mode TCCR0B = 0b0 << WGM02 | 0b100 << CS00; // divide by 256 OCR0A = 97; // ~1ms TIMSK0 = 1 << OCIE0A; // compare match A interrupt sei(); } static uint8_t segment_int_data[SEGMENT_DIGIT_COUNT]; void segment_int_display(/* ... */) { // store the display pattern in segment_int_data } ISR(TIMER0_COMPA_vect) { static uint8_t cur = 0; // display the cur-th digit according to segment_int_data if (++cur == SEGMENT_DIGIT_COUNT) cur = 0; }
若是你把以上代碼放在可執行程序的項目中,那徹底沒有問題,但若是是放在一個靜態庫項目中,而後在可執行程序項目中引用它,那麼定時器中斷的ISR是不會連接進程序的。這是由於,從連接器的角度來說,這個ISR歷來沒有被調用過,所以就被當成無用的函數扔掉了。爲了讓連接器把ISR連接進程序,咱們須要在main
會執行的代碼中調用它,最簡單地:
if (0) TIMER0_COMPA_vect();
放在初始化中,既達到了目的,又沒有運行時的負擔。
試着寫一個庫,管理開發板引出的16個引腳的外部中斷。
研究定時器中斷與PWM的關係。
改進ADC一講中最後一個例程,把main
函數還給客戶。