外部中斷實驗
此實驗將外部中斷配置為按鍵輸入,通過按鍵輸入觸發外部中斷,在外部中斷里面實施相應的處理,具體功能:
- 按下KEY0,翻轉LED0狀態
- 按下KEY1,翻轉LED1狀態
- 按下KEY2,同時翻轉LED0和LED1狀態
- 按下KEY_UP,翻轉BEEP狀態
在中斷回調函數里面使用delay進行消抖,導致中斷是阻塞的,不符合中斷快速執行的原則,linux中的按鍵處理是實驗外部中斷+定時器共同實現的,更具普遍性。
弄清楚:
- 中斷在單片機中是如何實現的
- 外部中斷處理流程(程序)
- 如何配置外部中斷
main函數
main函數代碼:
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/BEEP/beep.h"
#include "./BSP/EXTI/exti.h"int main(void)
{ HAL_Init(); /* 初始化HAL庫 */sys_stm32_clock_init(336, 8, 2, 7); /* 設置時鐘,168Mhz */delay_init(168); /* 延時初始化 */usart_init(115200); /* 串口初始化為115200 */led_init(); /* 初始化LED */beep_init(); /* 初始化蜂鳴器 */extix_init(); /* 初始化外部中斷輸入 */LED0(0); /* 先點亮紅燈 */while(1){delay_ms(1000);}
}
在main中主要做的是初始化,然后在while(1)里面死循環等待中斷的到來,HAL_Init是使用HAL庫必須調用的初始化函數;sys_stm32_clock_init、delay_init、usart_init這三個函數是正點原子編寫的SYSTEM初始化函數,配置了系統時鐘,延時函數,串口配置,實現單片機開發常用基本功能;然后調用led_init、beep_init、extix_init,初始化LED引腳、BEEP引腳、EXTI外部中斷,KEY引腳初始化是在extix_init中進行調用的,故未在main中體現,完成所有的硬件初始化后,先將LED0點亮。
本次實驗的核心是extix_init以及中斷處理函數
exti.h
#ifndef __EXTI_H
#define __EXTI_H#include "./SYSTEM/sys/sys.h"/******************************************************************************************/
/* 引腳 和 中斷編號 & 中斷服務函數 定義 */ #define KEY0_INT_GPIO_PORT GPIOE
#define KEY0_INT_GPIO_PIN GPIO_PIN_4
#define KEY0_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) /* PE口時鐘使能 */
#define KEY0_INT_IRQn EXTI4_IRQn
#define KEY0_INT_IRQHandler EXTI4_IRQHandler#define KEY1_INT_GPIO_PORT GPIOE
#define KEY1_INT_GPIO_PIN GPIO_PIN_3
#define KEY1_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) /* PE口時鐘使能 */
#define KEY1_INT_IRQn EXTI3_IRQn
#define KEY1_INT_IRQHandler EXTI3_IRQHandler#define KEY2_INT_GPIO_PORT GPIOE
#define KEY2_INT_GPIO_PIN GPIO_PIN_2
#define KEY2_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) /* PE口時鐘使能 */
#define KEY2_INT_IRQn EXTI2_IRQn
#define KEY2_INT_IRQHandler EXTI2_IRQHandler#define WKUP_INT_GPIO_PORT GPIOA
#define WKUP_INT_GPIO_PIN GPIO_PIN_0
#define WKUP_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口時鐘使能 */
#define WKUP_INT_IRQn EXTI0_IRQn
#define WKUP_INT_IRQHandler EXTI0_IRQHandler
.h文件中將引腳相關的量宏定義,方便理解與更改,并且將中斷函數重定義,如將EXTI2_IRQHandler定義成KEY2_INT_IRQHandler,方便自己編寫維護
exti.c
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/BEEP/beep.h"
#include "./BSP/KEY/key.h"
#include "./BSP/EXTI/exti.h"/*** @brief KEY0 外部中斷服務程序* @param 無* @retval 無*/
void KEY0_INT_IRQHandler(void)
{HAL_GPIO_EXTI_IRQHandler(KEY0_INT_GPIO_PIN); /* 調用中斷處理公用函數 清除KEY0所在中斷線 的中斷標志位 */__HAL_GPIO_EXTI_CLEAR_IT(KEY0_INT_GPIO_PIN); /* HAL庫默認先清中斷再處理回調,退出時再清一次中斷,避免按鍵抖動誤觸發 */
}/*** @brief KEY1 外部中斷服務程序* @param 無* @retval 無*/
void KEY1_INT_IRQHandler(void)
{ HAL_GPIO_EXTI_IRQHandler(KEY1_INT_GPIO_PIN); /* 調用中斷處理公用函數 清除KEY1所在中斷線 的中斷標志位,中斷下半部在HAL_GPIO_EXTI_Callback執行 */__HAL_GPIO_EXTI_CLEAR_IT(KEY1_INT_GPIO_PIN); /* HAL庫默認先清中斷再處理回調,退出時再清一次中斷,避免按鍵抖動誤觸發 */
}/*** @brief KEY2 外部中斷服務程序* @param 無* @retval 無*/
void KEY2_INT_IRQHandler(void)
{ HAL_GPIO_EXTI_IRQHandler(KEY2_INT_GPIO_PIN); /* 調用中斷處理公用函數 清除KEY2所在中斷線 的中斷標志位,中斷下半部在HAL_GPIO_EXTI_Callback執行 */__HAL_GPIO_EXTI_CLEAR_IT(KEY2_INT_GPIO_PIN); /* HAL庫默認先清中斷再處理回調,退出時再清一次中斷,避免按鍵抖動誤觸發 */
}/*** @brief WK_UP 外部中斷服務程序* @param 無* @retval 無*/
void WKUP_INT_IRQHandler(void)
{ HAL_GPIO_EXTI_IRQHandler(WKUP_INT_GPIO_PIN); /* 調用中斷處理公用函數 清除KEY_UP所在中斷線 的中斷標志位,中斷下半部在HAL_GPIO_EXTI_Callback執行 */__HAL_GPIO_EXTI_CLEAR_IT(WKUP_INT_GPIO_PIN); /* HAL庫默認先清中斷再處理回調,退出時再清一次中斷,避免按鍵抖動誤觸發 */
}/*** @brief 中斷服務程序中需要做的事情* 在HAL庫中所有的外部中斷服務函數都會調用此函數* @param GPIO_Pin:中斷引腳號* @retval 無*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{delay_ms(20); /* 消抖 */switch(GPIO_Pin){case KEY0_INT_GPIO_PIN:if (KEY0 == 0){LED0_TOGGLE(); /* LED0 狀態取反 */ }break;case KEY1_INT_GPIO_PIN:if (KEY1 == 0){LED1_TOGGLE(); /* LED1 狀態取反 */ }break;case KEY2_INT_GPIO_PIN:if (KEY2 == 0){LED1_TOGGLE(); /* LED1 狀態取反 */LED0_TOGGLE(); /* LED0 狀態取反 */ }break;case WKUP_INT_GPIO_PIN:if (WK_UP == 1){BEEP_TOGGLE(); /* 蜂鳴器狀態取反 */ }break;default : break;}
}/*** @brief 外部中斷初始化程序* @param 無* @retval 無*/
void extix_init(void)
{GPIO_InitTypeDef gpio_init_struct;key_init();gpio_init_struct.Pin = KEY0_INT_GPIO_PIN;gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; /* 下降沿觸發 */gpio_init_struct.Pull = GPIO_PULLUP;HAL_GPIO_Init(KEY0_INT_GPIO_PORT, &gpio_init_struct); /* KEY0配置為下降沿觸發中斷 */gpio_init_struct.Pin = KEY1_INT_GPIO_PIN;gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; /* 下降沿觸發 */gpio_init_struct.Pull = GPIO_PULLUP;HAL_GPIO_Init(KEY1_INT_GPIO_PORT, &gpio_init_struct); /* KEY1配置為下降沿觸發中斷 */gpio_init_struct.Pin = KEY2_INT_GPIO_PIN;gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; /* 下降沿觸發 */gpio_init_struct.Pull = GPIO_PULLUP;HAL_GPIO_Init(KEY2_INT_GPIO_PORT, &gpio_init_struct); /* KEY2配置為下降沿觸發中斷 */gpio_init_struct.Pin = WKUP_INT_GPIO_PIN;gpio_init_struct.Mode = GPIO_MODE_IT_RISING; /* 上升沿觸發 */gpio_init_struct.Pull = GPIO_PULLDOWN;HAL_GPIO_Init(WKUP_GPIO_PORT, &gpio_init_struct); /* WKUP配置為上升沿觸發中斷 */HAL_NVIC_SetPriority(KEY0_INT_IRQn, 0, 2); /* 搶占0,子優先級2 */HAL_NVIC_EnableIRQ(KEY0_INT_IRQn); /* 使能中斷線4 */HAL_NVIC_SetPriority(KEY1_INT_IRQn, 1, 2); /* 搶占1,子優先級2 */HAL_NVIC_EnableIRQ(KEY1_INT_IRQn); /* 使能中斷線3 */HAL_NVIC_SetPriority(KEY2_INT_IRQn, 2, 2); /* 搶占2,子優先級2 */HAL_NVIC_EnableIRQ(KEY2_INT_IRQn); /* 使能中斷線2 */HAL_NVIC_SetPriority(WKUP_INT_IRQn, 3, 2); /* 搶占3,子優先級2 */HAL_NVIC_EnableIRQ(WKUP_INT_IRQn); /* 使能中斷線0 */}
extix_init函數,先調用key_init將各按鍵IO的時鐘打開,然后再將按鍵IO模式配置為中斷模式,最后配置各中斷優先級及使能。
中斷觸發流程:(以KEY0為例)
當KEY0變為低電平時,會執行EXTI4_IRQHandler,不過此函數在exit.h里面被重定義成KEY0_INT_IRQHandler。
/*** @brief KEY0 外部中斷服務程序* @param 無* @retval 無*/
void KEY0_INT_IRQHandler(void)
{HAL_GPIO_EXTI_IRQHandler(KEY0_INT_GPIO_PIN); /* 調用中斷處理公用函數 清除KEY0所在中斷線 的中斷標志位 */__HAL_GPIO_EXTI_CLEAR_IT(KEY0_INT_GPIO_PIN); /* HAL庫默認先清中斷再處理回調,退出時再清一次中斷,避免按鍵抖動誤觸發 */
}
在函數內先調用HAL_GPIO_EXTI_IRQHandler,這是HAL庫內的公用處理函數,在其中會先進行一次中斷標志位清除,然后調用回調函數(實際的處理函數)。
最后在KEY0_INT_IRQHandler再執行一次中斷標志位清除,也就是說一次中斷處理內總共執行了兩次中斷標志位清除,第一次是在HAL庫的公用處理函數內,第二層是在自己編寫的KEY0_INT_IRQHandler內,HAL庫官方推薦這么寫:在中斷處理首末各執行一次清標志位,以防止誤觸發。
/*** @brief This function handles EXTI interrupt request.* @param GPIO_Pin Specifies the pins connected EXTI line* @retval None*/
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{/* EXTI line interrupt detected */if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != RESET){__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);HAL_GPIO_EXTI_Callback(GPIO_Pin);}
}
先清除中斷標志位,這是第一次清中斷,然后執行回調函數HAL_GPIO_EXTI_Callback,這個回調函數是HAL庫內的虛函數,需要自己重寫,具體如下,正式開始執行邏輯處理
/*** @brief 中斷服務程序中需要做的事情* 在HAL庫中所有的外部中斷服務函數都會調用此函數* @param GPIO_Pin:中斷引腳號* @retval 無*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{delay_ms(20); /* 消抖 */switch(GPIO_Pin){case KEY0_INT_GPIO_PIN:if (KEY0 == 0){LED0_TOGGLE(); /* LED0 狀態取反 */ }break;case KEY1_INT_GPIO_PIN:if (KEY1 == 0){LED1_TOGGLE(); /* LED1 狀態取反 */ }break;case KEY2_INT_GPIO_PIN:if (KEY2 == 0){LED1_TOGGLE(); /* LED1 狀態取反 */LED0_TOGGLE(); /* LED0 狀態取反 */ }break;case WKUP_INT_GPIO_PIN:if (WK_UP == 1){BEEP_TOGGLE(); /* 蜂鳴器狀態取反 */ }break;default : break;}
}
在回調函數中先延時20毫秒以消抖,根據不同的觸發引腳在switch中執行相應的操作。
執行完這個回調函數后,就回到了KEY0_INT_IRQHandler內,然后執行第二次清中斷標志位,一次外部中斷處理就完成了。
實際上外部中斷處理并不復雜,硬件初始化后,程序就等待中斷的到來,IO電平變低,中斷寄存器中的標志位被置高,程序會立刻跳轉到EXTI4_IRQHandler這個函數中,這個函數其實就是一個地址,其在啟動文件內定義,是一個固定的地址,用戶需要在自己的代碼中重寫這個函數,在里面加上業務處理函數,若最后加上一個清中斷標志位的函數,那該中斷可以被重復觸發,反之則只能觸發一次,這個函數被執行完后,程序又立即跳轉到觸發中斷前執行的位置,這就是STM32的外部中斷處理。
HAL庫對EXTI4_IRQHandler這個函數進行多次封裝,所以變得復雜
在STM32中,幾乎每個引腳都可以被配置為外部中斷,那么問題來了,我怎么哪個引腳會執行哪個中斷處理函數?這需要去查詢單片機的參考手冊
原來STM32是依靠引腳位來分割中斷處理函數的,在啟動匯編文件里面也一一對應
當中斷引腳為0~4時,就使用:
EXTI0_IRQHandler
EXTI1_IRQHandler
EXTI2_IRQHandler
EXTI3_IRQHandler
EXTI4_IRQHandler
當中斷引腳為5~9時,就使用:
EXTI9_5_IRQHandler
當中斷引腳為10~15時,就使用:
EXTI15_10_IRQHandler
在HAL庫中如何編寫外部中斷
- 初始化IO引腳,將其配置為中斷上升沿觸發或下降沿觸發
- 使用HAL_NVIC_SetPriority配置中斷優先級并使用HAL_NVIC_EnableIRQ使能其中斷號
- 重寫EXTIx_IRQHandler,在內部加上HAL_GPIO_EXTI_IRQHandler和__HAL_GPIO_EXTI_CLEAR_IT
- 重寫回調函數HAL_GPIO_EXTI_Callback,在內部編寫具體業務處理代碼
Linux中的外部中斷+定時器實現按鍵大概處理思路
當按鍵按下,觸發該按鍵對應的中斷,在其中斷處理函數內啟動一個10ms的定時器中斷,當10ms結束,定時器中斷內再判斷該按鍵電平是否按下,如按下,則將事件標志位置高,在正常程序內查詢事件標志位以做出響應,如判斷為 沒按下,則說明可能是抖動電平,忽略此次中斷觸發。
中斷會打斷單片機當前執行的程序,因此不應在中斷內過多耗費時間,使用中斷來置各種標志位是常用的做法。