[HowTo] STM32 中斷 & 外部中斷 教學

我想,在學習一個晶片的時候,一開始除了玩玩流水燈(也就是LED接連閃爍)再來就是按鈕控制LED了。許多一開始的教學都會使用Polling的方式來讀取Pin的狀態,意思就是寫一個if函數在main的無窮回圈裡面一直”Poll”(或者叫做Check)按鈕的值來決定LED的狀態。因為我覺得這種教學,不但讀完之後沒學到東西(除了知道怎麼讀取Pin的狀態以外),又浪費時間,所以我在這就直接教STM32的中斷及外部中斷,並用它來控制LED(或作其他事)。

首先我們來淺談中斷的用意。我們來假設你今天在看電視好了,無止盡的一直看,突然電話響了,你必須站起來接起電話,講完後回到沙發上繼續看電視。看電視代表的是原本所再做的工作,電話響了就是所謂的中斷,而講電話就是中斷服務流程(Interrupt Service Routine (ISR))。若講細一點,電話響了叫做中斷產生事件,講電話就是中斷服務流程。透過這個比喻我想各位對中斷的概念有了一個雛形。

STM32的中斷產生事件有很多種,例如系統計時器、記憶存取錯誤、外部重置、還有很多內建的設備(Peripheral(USART, I2C, DMA…etc. )) 。我們可以想像這些產生事件的信號是被核心在程式進行中不斷的被監控的,而當事件發生時,核心能對他做正確地處理。說得更細一點的話,核心會先將目前執行程式的計數器放入堆疊區,然後執行對應的ISR,完畢後把原本的程式計數器放回來然後繼續原本的流程。剛剛所講解的我們都不必去寫那些程式,因為那是ARM公司幫我們做好的部分,我們只要知道如何正確地設定這些中斷,之後就讓他自己執行就好了。

由於中斷的可能性有很多種,我在這就講解外部中斷。意思救是中斷事件由一個外部的信號所產生,或者說一個Pin的狀態的改變。這又會牽扯到電路是怎麼設計的,我們先來看看底下這幾個圖:

Buttons

Fig. 1的設計是按鈕按下輸出高態,否則輸出低態。

Fig. 2的設計是按鈕按下輸出低態,否則輸出高態。

Fig. 3的設計是按鈕按下為低態,否則浮空(未知狀態)。

該多說明的應該是圖三吧,一個好的信號應為高態或低態,而為什麼會有人想設計成圖三那樣呢?其實是因為STM32的輸入可以設定為內部提升(Input Pull Up)或內部接地(Input Pull Down)當然他中間也會串一個電阻(詳細的阻值我忘了,可在datasheet找到),所以這樣可以省一個零件。

但基於大部份的設計都為圖一或圖二為主,我在這就以圖一當範例來設定吧。

接下來要說明的是ARM Cortex-M3的中段管理,有一個很花俏的名字,叫做”Nested Vector Interrupt Controller”,對岸翻成“嵌套向量中斷控制器”,而我們就叫他NVIC吧。NVIC管理了中斷的優先順序,某中斷的啓用與否,以及中斷等待位元的設定及清除。所以在設定一個中斷時,一定要設定他的NVIC,才能正確運作。Cortex-M3把每個中斷分配了兩個優先順序,分別為主順序和次要順序,數字較小的優先。所以如果有兩個中斷擁有同樣的主要順序,擁有較小的次要順序中斷會優先被執行。而如果一個中斷正在執行,核心收到一個有較小的主要順序中斷的話,則原先的中段就會被搶走。

以上說明了STM32的中斷的來龍去脈,還沒講的是每個中斷源的設定,因為每種中斷產生的方法不一樣所以是一個case by case的設定。而在這次的教學我就已外部中斷為例來做講解。

STM32提供了20種的的外部中斷來源,雖然很多時候都指向了同樣的向量(也就是為什麼我們需要中斷等待位元來分辨是哪個Pin觸發了)。參考以下的圖:(由ST RM0008截取)

Screen Shot 2013-05-07 at 11.23.49 PM

從這個圖我們看得出來,PA0~PG0所產生的外部中斷事件都會指向EXTI0這個向量(向量就當中斷發生時程式計數器會指向的位置)PA1~PG1指向EXTI1,以此類推。也就是說,如果PA0或PC0或者說Px0所產生的向量,都會去執行EXTI0的ISR,而在這個ISR裡面我們再去看中斷等待位元來分辨到底是哪個Pin所產生的中斷(不懂沒關係,等等看到code就懂了)。

如果我想要設定一個外部中斷,我需要做以下的步驟:

  1. 設定對應向量的NVIC。
  2. 對於GPIO,設定AFIO_EXTICRx寄存器來選擇正確的Pin(例如,PA0)
  3. 設定觸發條件(上緣,下緣或上下緣)。

幸好,這些事情都由ST所提供的Standard Peripheral Library內寫好了,我們只要知道如何使用他的函數庫就可以正確地設定了。以下已PA0為例:

//結構宣告

GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;

//設定PA0的GPIO

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);

GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPU; //內部拉高
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;

GPIO_Init(GPIOA, &GPIO_InitStructure);

//把EXTI0連到PA0

GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);

//設定EXTI0

EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);

//設定NVIC

NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

再來大家會有疑問的是,那麼實際中斷會去執行的程式要寫在哪裡?答案就在stm32f10x_it.c這個檔案內。打開來看只會看到一堆沒有任何功能的函數,這些函數的名稱其實就是中斷向量會執行的函數,那麼我們想要用的中斷向量應該要叫什麼名字呢?這個其實是定義在STM32的啟動程式裡,也就是startup_stm32f10x_md.s這個檔案裡。參考下圖:

Screen Shot 2013-05-08 at 4.39.36 PM

看到 EXTI0_IRQHandler ;EXTI Line 0 這行,就是代表EXTI Line 0中斷所產生的向量函數名稱,所以我們在stm32f10x_it.c裡面加入這個為名稱的函數後,EXTI Line 0產生的中斷就會被執行這個函數了。就像稍早所說的,進入向量後我們要用中斷等待位元來分辨到底是哪個事件(在這裡就是哪個Pin)觸發的中斷,所以我們加上這個if函數,並在最後執行完清除這個等待位元:

void EXTI0_IRQHandler(void) {
if(EXTI_GetITStatus(EXTI_Line0) != RESET) { //分辨中段來源
//實際執行的code放這
}
EXTI_ClearITPendingBit(EXTI_Line0); //清除等待位元
}
}

如此一來就完成了一個完整的中斷設定!如果以我的這個範例,我想要按一下按鈕,LED亮,再按一下,LED滅。另外,我加了一個除彈跳的機制(稍後講解),所以我的EXTI0的ISR應該是這樣寫:

void EXTI0_IRQHandler(void) {
if(EXTI_GetITStatus(EXTI_Line0) != RESET) {
Delay(0xFFFF); //debounce
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)) { //debounce
if(btn) {
btn=0;
GPIO_SetBits(GPIOB, GPIO_Pin_8);
}
else {
btn++;
GPIO_ResetBits(GPIOB, GPIO_Pin_8);
}
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
}

如此一來,這次的功能就完成了!來說明一下按鈕的彈跳問題吧!

我們現在所使用的機械式按鈕,靠的是金屬接觸來導通,而在接觸的那當下,不可能很完美的馬上從高電位變低電位(或低變高),一定會有彈跳的效應,如下圖:

debounce

這兩張圖都是未經過處理的按鈕轉態信號,我們看得出來他並不是個完美的轉換,這會導致晶片讀取時誤以為我們按了很多下按鈕。最常見的解決辦法應該是在按鈕上並聯一個電容,但是我在這裡使用軟體來解決,節省了零件數。每當進入中斷向量時,程式先等待幾毫秒後,在判斷目前的按鈕狀態,如果是穩定的,在繼續執行。我呼叫了GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)這個函數,他會取讀取指定的GPIO Pin,然後回傳其值。我使用了一個if來判斷這個直是否為1,是的話就繼續執行,否的話就不執行。這樣各位應該瞭解彈跳的問題與解決的辦法了!

經過這次的講解,希望各位讀者對STM32的中斷有了非常清楚的基礎,在未來不管是要做Timer的中斷還是其他事件的中斷應該都難不倒各位。如果有任何問題請盡量發問!謝謝閱讀!

本次教學的程式

6 thoughts on “[HowTo] STM32 中斷 & 外部中斷 教學

  1. Yu-Ting Kao

    不好意思,偶然間發現到這個文章還不錯,剛好也在學習這塊板子。

    想問一下就是說,STM32 可以讓你設定 16 個 還是 20 個外部中斷呢?因為我看圖好像只有16個
    另外就是說,每一個 PA0, PB0 …. 都會對應到 EXTI0,然後在 code 那邊有一行 GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); 這意思是不是 PB0, PC0 … 就不理他了? 只有 PA0 對應到 EXTI0 呢?

    謝謝

    Reply
    1. applefreak111 Post author

      “GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);”
      這行只是在說,要把PA0在EXTI0的中段啟動。所有的Px0(PA0, PB0, PC0…),入果你有設定中斷都會執行EXTI0的中斷函數。
      那下個疑問就是,如果我PA0跟PB0都想要有外部中斷,那怎麼辦?那就只要在EXTI0的中段函數裡面判斷是哪隻腳產生的就好啦!也就是
      “if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0))”來判斷,反正你要除彈跳也是要加這行。
      其實我不確定這樣做到底是不是最好的做法,如果有其它方法可以分享出來!

      Reply
  2. Boltnut Chang

    我正在使用 STM32F072-Discovery, 依 RM0091 Reference manual page 178, SYSCFG_EXTICR1 的EXTI0[3:0] 說明, 應該沒法將 PA0 和 PB0 同時設置為外部中斷源吧.

    Reply
    1. applefreak111 Post author

      您好,我已經有段時間沒寫STM32了,這個疑問我之前也有。若我沒記錯的話,他會觸發同一個中斷副程式,那你只要在那個副程式做判斷事PA0或PB0被觸發就好了。
      用此函數判斷:

      if( GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) )

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s