這一章,我們將向大家介紹如何使用 STM32F1 的外部輸入中斷。在前面幾章的學習
中,我們掌握了 STM32F1 的 IO 口最基本的操作。本章我們將介紹如何將 STM32F1
的 IO 口作為外部中斷輸入,在本章中,我們將以中斷的方式,實現我們在第八章所
實現的功能。本章分為如下幾個部分:
10.1 STM32F1 外部中斷簡介
10.2 硬體設計
10.3 軟體設計
10.4 下載驗證
10.1 STM32F1 外部中斷簡介
STM32F1 的 IO 口在第六章有詳細介紹,而中斷管理分組管理在前面也有詳細的闡述。這
裡我們將介紹 STM32F1 外部 IO 口的中斷功能,通過中斷的功能,達到第八章實驗的效果,即:
通過板載的 4 個按鍵,控制板載的兩個 LED 的亮滅以及蜂鳴器的發聲。
這章的代碼主要分布在 HAL 庫的 stm32f1xx_hal_exti.h 和 stm32f1xx_hal_exti.c 文件中。
這裡我們首先講解 STM32F1 IO 口中斷的一些基礎概念。STM32F1 的每個 IO 都可以作為
外部中斷的中斷輸入口,這點也是 STM32F1 的強大之處。STM32F103 的中斷控制器支持 19
個外部中斷/事件請求。每個中斷設有狀態位,每個中斷/事件都有獨立的觸發和屏蔽設置。
STM32F103 的 19 個外部中斷為:
EXTI 線 0~15:對應外部 IO 口的輸入中斷。
EXTI 線 16:連接到 PVD 輸出。
EXTI 線 17:連接到 RTC 鬧鐘事件。
EXTI 線 18:連接到 USB 喚醒事件。
EXTI 線 19:連接到乙太網喚醒事件。
從上面可以看出,STM32F1 供 IO 口使用的中斷線只有 16 個,但是 STM32F1 的 IO 口卻
遠遠不止 16 個,那麼 STM32F1 是怎麼把 16 個中斷線和 IO 口一一對應起來的呢?於是 STM32
就這樣設計,GPIO 的管教 GPIOx.0~GPIOx.15(x=A,B,C,D,E,F,G,H,I)分別對應中斷線 0~15。這
樣每個中斷線對應了最多 9 個 IO 口,以線 0 為例:它對應了 GPIOA.0、GPIOB.0、GPIOC.0、
GPIOD.0、GPIOE.0、GPIOF.0、GPIOG.0。而中斷線每次只能連接到 1 個 IO 口上,這樣就需要
通過配置來決定對應的中斷線配置到哪個 GPIO 上了。下面我們看看 GPIO 跟中斷線的映射關
系圖:
接下來我們講解使用庫函數配置外部中斷的步驟。
1) 使能 IO 口時鐘,初始化 IO 口為輸入
首先,我們要使用 IO 口作為中斷輸入,所以我們要使能相應的 IO 口時鐘,具體的操作方
法跟我們按鍵實驗是一致的,這裡就不做過多講解。
1) 設置 IO 口模式,觸發條件,開啟 SYSCFG 時鐘,設置 IO 口與中斷線的映射關係。
該步驟如果我們使用標準庫那麼需要多個函數分部實現。而當我們使用 HAL 庫的時候,
則都是在函數 HAL_GPIO_Init 中一次性完成的。例如我們要設置 PA0 連結中斷線 0,並且為上
升沿觸發,代碼為:
GPIO_InitTypeDef GPIO_Initure;
GPIO_Initure.Pin=GPIO_PIN_0; //PA0
GPIO_Initure.Mode=GPIO_MODE_IT_RISING; //外部中斷,上升沿觸發
GPIO_Initure.Pull=GPIO_PULLDOWN; //默認下拉
HAL_GPIO_Init(GPIOA,&GPIO_Initure);
當我們調用 HAL_GPIO_Init 設置 IO 的 Mode 值為 GPIO_MODE_IT_RISING(外部中斷上
升 沿 觸 發 ), GPIO_MODE_IT_FALLING ( 外 部 中 斷 下 降 沿 觸 發 ) 或 者
GPIO_MODE_IT_RISING_FALLING(外部中斷雙邊沿觸發)的時候,該函數內部會通過判斷
Mode 的值來開啟 SYSCFG 時鐘,並且設置 IO 口和中斷線的映射關係。
因為我們這裡初始化的是 PA0,根據圖 10.1.1 可知,調用該函數後中斷線 0 會自動連接到
PA0。如果某個時間,我們又同樣的方式初始化了 PB0,那麼 PA0 與中斷線的連結將被清除,
而直接連結 PB0 到中斷線 0。
2) 配置中斷優先級(NVIC),並使能中斷。
我們設置好中斷線和 GPIO 映射關係,然後又設置好了中斷的觸發模式等初始化參數。既
然是外部中斷,涉及到中斷我們當然還要設置 NVIC 中斷優先級。這個在前面已經講解過,這
裡我們就接著上面的範例, 設置中斷線 0 的中斷優先級並使能外部中斷 0 的方法為:
HAL_NVIC_SetPriority(EXTI0_IRQn,2,0); //搶佔優先級為 2,子優先級為 0
HAL_NVIC_EnableIRQ(EXTI0_IRQn); //使能中斷線 2
上面這段代碼相信大家都不陌生,我們在前面的串口實驗的時候講解過,這裡不再講解。
3) 編寫中斷服務函數。
我們配置完中斷優先級之後,接著要做的就是編寫中斷服務函數。中斷服務函數的名字是
在 HAL 庫中事先有定義的。這裡需要說明一下,STM32F1 的 IO 口外部中斷函數只有 7 個,分
別為:
void EXTI0_IRQHandler();
void EXTI1_IRQHandler();
void EXTI2_IRQHandler();
void EXTI3_IRQHandler();
void EXTI4_IRQHandler();
void EXTI9_5_IRQHandler();
void EXTI15_10_IRQHandler();
中斷線 0-4 每個中斷線對應一個中斷函數,中斷線 5-9 共用中斷函數 EXTI9_5_IRQHandler,中
斷線 10-15 共用中斷函數 EXTI15_10_IRQHandler。一般情況下,我們可以把中斷控制邏輯直接
編寫在中斷服務函數中,但是 HAL 庫把中斷處理過程進行了簡單封裝,請看下面步驟 5 講解。
5) 編寫中斷處理回調函數 HAL_GPIO_EXTI_Callback
在使用 HAL 庫的時候,我們也可以跟使用標準庫一樣,在中斷服務函數中編寫控制邏輯。
但 是 HAL 庫 為 了 用 戶 使 用 方 便 , 它 提 供 了 一 個 中 斷 通 用 入 口 函 數
HAL_GPIO_EXTI_IRQHandler,在該函數內部直接調用回調函數 HAL_GPIO_EXTI_Callback。
我們可以看看 HAL_GPIO_EXTI_IRQHandler 函數定義:
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin);
}
}
該函數實現的作用非常簡單,就是清除中斷標誌位,然後調用回調函數
HAL_GPIO_EXTI_Callback()實現控制邏輯。所以我們編寫中斷控制邏輯將跟串口實驗類似,在
中斷服務函數中直接調用外部中斷共用處理函數 HAL_GPIO_EXTI_IRQHandler,然後在回調函
數 HAL_GPIO_EXTI_Callback 中通過判斷中斷是來自哪個 IO 口編寫相應的中斷服務控制邏輯。
講到這裡,相信大家對 STM32 的 IO 口外部中斷已經有了一定的了解。下面我們再總結一
下配置 IO 口外部中斷的一般步驟:
1)使能 IO 口時鐘。
2)調用函數 HAL_GPIO_Init 設置 IO 口模式,觸發條件,使能 SYSCFG 時鐘以及設置 IO
口與中斷線的映射關係。
3)配置中斷優先級(NVIC),並使能中斷。
4)在中斷服務函數中調用外部中斷共用入口函數 HAL_GPIO_EXTI_IRQHandler。
5)編寫外部中斷回調函數 HAL_GPIO_EXTI_Callback。
通過以上幾個步驟的設置,我們就可以正常使用外部中斷了。
本章,我們要實現同第七章差不多的功能,但是這裡我們使用的是中斷來檢測按鍵,還是
KEY_UP 控制 DS0,DS1 互斥點亮;KEY2 控制 DS0,按一次亮,再按一次滅;KEY1 控制 DS1,
效果同 KEY2;KEY0 則同時控制 DS0 和 DS1,按一次,他們的狀態就翻轉一次。
10.2 硬體設計
本實驗用到的硬體資源和第八章實驗的一模一樣,不再多做介紹了。
10.3 軟體設計
我們直接打開我們的光碟的實驗 3 外部中斷實驗工程,可以看到相比上一個工程,我們的
HARDWARE 目錄下面增加了 exti.c 文件,並且包含了頭文件 exti.h。extic.c 文件代碼如下:
//外部中斷初始化
void EXTI_Init(void)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOA_CLK_ENABLE(); //開啟 GPIOA 時鐘
__HAL_RCC_GPIOE_CLK_ENABLE(); //開啟 GPIOE 時鐘
GPIO_Initure.Pin=GPIO_PIN_0; //PA0
GPIO_Initure.Mode=GPIO_MODE_IT_RISING; //上升沿觸發
GPIO_Initure.Pull=GPIO_PULLDOWN;
HAL_GPIO_Init(GPIOA,&GPIO_Initure);
GPIO_Initure.Pin=GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4; //PE2,3,4
GPIO_Initure.Mode=GPIO_MODE_IT_FALLING; //下降沿觸發
GPIO_Initure.Pull=GPIO_PULLUP;
HAL_GPIO_Init(GPIOE,&GPIO_Initure);
//中斷線 0-PA0
HAL_NVIC_SetPriority(EXTI0_IRQn,2,0); //搶佔優先級為 2,子優先級為 0
HAL_NVIC_EnableIRQ(EXTI0_IRQn); //使能中斷線 0
//中斷線 2-PE2
HAL_NVIC_SetPriority(EXTI2_IRQn,2,1); //搶佔優先級為 2,子優先級為 1
HAL_NVIC_EnableIRQ(EXTI2_IRQn); //使能中斷線 2
//中斷線 3-PE3
HAL_NVIC_SetPriority(EXTI3_IRQn,2,2); //搶佔優先級為 2,子優先級為 2
HAL_NVIC_EnableIRQ(EXTI3_IRQn); //使能中斷線 2
//中斷線 4-PE4
HAL_NVIC_SetPriority(EXTI4_IRQn,2,3); //搶佔優先級為 2,子優先級為 3
HAL_NVIC_EnableIRQ(EXTI4_IRQn); //使能中斷線 4
}
//中斷服務函數
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); //調用中斷處理公用函數
}
void EXTI2_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2); //調用中斷處理公用函數
}
void EXTI3_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_3); //調用中斷處理公用函數
}
void EXTI4_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_4); //調用中斷處理公用函數
}
//中斷服務程序中需要做的事情
//在 HAL 庫中所有的外部中斷服務函數都會調用此函數
//GPIO_Pin:中斷引腳號
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
delay_ms(100); //消抖
switch(GPIO_Pin)
{
case GPIO_PIN_0:
if(WK_UP==1)
{
LED1=!LED1;//控制 LED0,LED1 互斥點亮
LED0=!LED1;
}
break;
case GPIO_PIN_2:
if(KEY2==0) //LED1 翻轉
{
LED1=!LED1;
}
break;
case GPIO_PIN_3:
if(KEY1==0) //同時控制 LED0,LED1 翻轉
{
LED0=!LED0;
LED1=!LED1;
}
break;
case GPIO_PIN_4:
if(KEY0==0)
{
LED0=!LED0;//控制 LED0 翻轉
}
break;
}
}
exti.c 文件總共包含 6 個函數。外部中斷初始化函數 void EXTIX_Init 用來配置 IO 口外部中
斷相關步驟並使能中斷,另一個函數 HAL_GPIO_EXTI_Callback 是外部中斷共用回調函數,用
來處理所有外部中斷真正的控制邏輯。其他 4 個都是中斷服務函數。
void EXTI0_IRQHandler(void)是外部中斷 0 的服務函數,負責 KEY_UP 按鍵的中斷檢測;
void EXTI2_IRQHandler(void)是外部中斷 2 的服務函數,負責 KEY2 按鍵的中斷檢測;
void EXTI3_IRQHandler(void)是外部中斷 3 的服務函數,負責 KEY1 按鍵的中斷檢測;
void EXTI4_IRQHandler(void)是外部中斷 4 的服務函數,負責 KEY0 按鍵的中斷檢測;
下面我們分別介紹這幾個函數。
首先是外部中斷初始化函數 void EXTIX_Init(void),該函數內部主要做了兩件事情。首先
是調用 IO 口初始化函數 HAL_GPIO_Init 來初始化 IO 口,該函數的配置含義請看 10.1 小節中
關於 HAL_GPIO_Init 函數講解。其次是設置中斷優先級並使能中斷線。
接下來我們看看外部中斷服務函數,一共 4 個。所有的中斷服務函數內部都只調用了同樣
一個函數 HAL_GPIO_EXTI_IRQHandler,該函數是外部中斷共用入口函數,函數內部會進行中
斷標誌位清零, 並且調用中斷處理共用回調函數 HAL_GPIO_EXTI_Callback。這在 10.1 小節
我們也有詳細的講解。
最後是外部中斷回調函數 HAL_GPIO_EXTI_Callback,該函數用來編寫真正的外部中斷控
制邏輯。該函數有一個入口參數就是 IO 口序號。所以我們在該函數內部,一般通過判斷 IO 口
序號值來確定中斷是來自哪個 IO 口,也就是哪個中斷線,然後編寫相應的控制邏輯。所以在
該函數內部,我們通過 switch 語句判斷 IO 口來源,例如是來自 GPIO_PIN_0,那麼一定是來自
PA0,因為中斷線一次只能連接一個 IO 口,而四個 IO 口中序號為 0 的 IO 口只有 PA0,所以中
斷線 0 一定是連接 PA0,也就是外部中斷由 PA0 觸發。
exti.h 頭文件裡面主要是一個函數申明,比較簡單,這裡不做過多講解。
接下來我們看看主函數,main 函數代碼如下:
int main(void)
{
HAL_Init(); //初始化 HAL 庫
Stm32_Clock_Init(RCC_PLL_MUL9); //設置時鐘,72M
delay_init(72); //初始化延時函數
uart_init(115200); //初始化串口
LED_Init(); //初始化 LED
KEY_Init(); //初始化按鍵
EXTI_Init(); //初始化外部中斷
while(1)
{
printf("OK\r\n"); //列印 OK 提示程序運行
delay_ms(1000); //每隔 1s 列印一次
}
}
該部分代碼很簡單,先設置延時函數以及串口等外設。然後在初始化完中斷後,點亮 LED0,
就進入死循環等待了,這裡死循環裡面通過一個 printf 函數來告訴我們系統正在運行,在中斷
發生後,就執行相應的處理,從而實現第八章類似的功能。
10.4 下載驗證
在編譯成功之後,我們就可以下載代碼到戰艦 STM32 V3 開發板上,實際驗證一下我們的
程序是否正確。下載代碼後,在串口調試助手裡面可以看到如圖 10.4.1 所示信息:
從圖 10.4.1 可以看出,程序已經在運行了,此時可以通過按下 KEY0、KEY1、KEY2
和KEY_UP 來觀察 DS0、DS1 是否跟著按鍵的變化而變化。