開發環境:
MDK:凱爾 5.30
STM32立方體MX:V6.4.0
單片機:STM32F103ZET6
5.1普通方式
5.1.1 普通方式工作原理
按鍵 GPIO 端口有兩個方案可以選擇,一是采用上拉輸入模式,因為按鍵在沒按下的時候,是默認為高電平的,采且內部上拉模式正好符合這個要求。 第二個方案是直接采用浮空輸入模式,因為按照硬件電路圖,在芯片外部接了上拉電阻,其實就沒必要再配置成內部上拉輸入模式了,因為在外部上拉與內部上拉效果是一樣的。
5.1.2普通方式實現-標準庫
完整代碼請參附件,這里只貼出核心代碼。
- GPIO 初始化配置
void Key_GPIO_Config(void)
{
GPIO_InitTypeDefGPIO_InitStructure;
/*開啟按鍵端口(PA)的時鐘*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitStructure.GPIO_Pin= GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode= GPIO_Mode_IPU;//上拉輸入
GPIO_Init(GPIOA,&GPIO_InitStructure);
}
Key_GPIO_Config() 與 LED 的 GPIO 初始化函數 LED_GPIO_Config() 類似,區別只是在這個函數中,要開啟的 GPIO 的端口時鐘不一樣,并且把檢測按鍵用的引腳 Pin 的模式設置為適合按鍵應用的上拉輸入模式(由于接了外部上拉電阻,也可以使用浮空輸入,讀者可自行修改代碼做實驗)。 若 GPIO 被設置為輸入模式,不需要設置 GPIO 端口的最大輸出速度,當然,如果配置了這個速度也沒關系,GPIO_Init() 函數會自動忽略它。 在 RCC_APB2PeriphClockCmd()和 GPIO_InitStructure.GPIO_Pin 的輸入參數設置之中,我們可以用符號“|”,同時配置多個參數。 如:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOG,ENABLE);
輸入參數為RCC_APB2Periph_GPIOB| RCC_APB2Periph_GPIOG ,這樣調用之后,就把 GPIOB 和 GPIOG 的時鐘都開啟了。
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
以上代碼則表示將要同時配置 GPIOG 端口的 Pin5 和 Pin6。
- 按鍵消抖
uint8_t Key_Scan(GPIO_TypeDef*GPIOx,u16 GPIO_Pin,uint8_t Down_state)
{
/*檢測是否有按鍵按下 */
if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin)== Down_state )
{
/*延時消抖*/
Key_Delay(10000);
if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin)== Down_state )
{
/*等待按鍵釋放 */
while(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin)== Down_state);
return KEY_ON;
}
else
returnKEY_OFF;
}
else
returnKEY_OFF;
}
相信延時消抖的原理大家在學習其他單片機時就已經了解了,本函數的功能就是掃描輸入參數中指定的引腳,檢測其電平變化,并作延時消抖處理,最終對按鍵消息進行確認。
- 利用GPIO_ReadInputDataBit() 讀取輸入數據,若從相應引腳讀取的數據等于 0(KEY_ON),低電平,表明可能有按鍵按下,調用延時函數。否則返回 KEY_OFF,表示按鍵沒有被按下。
- 延時之后再次利用GPIO_ReadInputDataBit() 讀取輸入數據,若依然為低電平,表明確實有按鍵被按下了。否則返回 KEY_OFF,表示按鍵沒有被按下。
- 循環調用 GPIO_ReadInputDataBit()一直檢測按鍵的電平,直至按鍵被釋放,被釋放后,返回表示按鍵被按下的標志 KEY_ON。以上是按鍵消抖的流程,調用了一個庫函數GPIO_ReadInputDataBit()。輸入參數為要讀取的端口、引腳,返回引腳的輸入電平狀態,高電平為 1,低電平為 0。
5.1.3普通方式實現-HAL庫
5.1.3.1 STM32Cube生成工程
關于如何使用STM32Cube新建工程在前文已經講解過了,這里直說配置GPIO部分內容。本文要實現按鍵功能,通過按鍵實現LED的亮滅。我門在第一個程序的基礎上進行修改即可,不必每次都新建工程。根據按鍵電路,KEY1的引腳是PA0,我們將PA0的GPIO設置為下拉的輸入模式,保留3個LED的GPIO配置。
初始化基本配置后,我們重新生成工程,接下來按鍵編程。
5.1.3.2具體代碼分析
在看代碼前,我們先看看按鍵掃描編程的流程:
1)使能按鍵引腳時鐘,本文的引腳是PA0;
2)初始化按鍵,即初始化GPIO機構體,在前文已經詳細講解過了;
3)在無限循環中不斷讀取PA0的電平值,同時進行按鍵消抖;
4)判斷按鍵被按下時,進行相應的處理。
- GPIO 初始化配置
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_RESET);
/*Configure GPIO pin : PA0 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/*Configure GPIO pin : PB0 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/*Configure GPIO pins : PG6 PG7 */
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);
}
按鍵與 LED 的 GPIO 初始化函數類似,區別只是在這個函數中,要開啟的 GPIO 的端口時鐘不一樣,并且把檢測按鍵用的引腳 Pin 的模式設置為適合按鍵應用的上拉輸入模式(由于接了外部上拉電阻,也可以使用浮空輸入,讀者可自行修改代碼做實驗)。若 GPIO 被設置為輸入模式,不需要設置 GPIO 端口的最大輸出速度。
- 按鍵狀態監測及按鍵消抖
uint8_t Key_Scan(void)
{
if(HAL_GPIO_ReadPin(KEY_GPIO,GPIO_PIN_0)== KEY_DOWN_LEVEL )
{
HAL_Delay(10);
if(HAL_GPIO_ReadPin(KEY_GPIO,GPIO_PIN_0)== KEY_DOWN_LEVEL )
{
while(HAL_GPIO_ReadPin(KEY_GPIO,GPIO_PIN_0)== KEY_DOWN_LEVEL);
return KEY_DOWN;
}
else
{
returnKEY_UP;
}
}
returnKEY_UP;
}
相信延時消抖的原理大家在學習其他單片機時就已經了解了,本函數的功能就是掃描輸入參數中指定的引腳,檢測其電平變化,并作延時消抖處理,最終對按鍵消息進行確認。
- 利用HAL_GPIO_ReadPin()函數讀取輸入數據,若從相應引腳讀取的數據等于 0(KEY_DOWN),低電平,表明可能有按鍵按下,調用延時函數。否則返回 KEY_UP,表示按鍵沒有被按下。
- 延時之后再次利用 HAL_GPIO_ReadPin()函數讀取輸入數據,若依然為低電平,表明確實有按鍵被按下了。否則返回 KEY_UP,表示按鍵沒有被按下。
- 循環調用HAL_GPIO_ReadPin()函數一直檢測按鍵的電平,直至按鍵被釋放,被釋放后,返回表示按鍵被按下的標志 KEY_DOWN。以上是按鍵消抖的流程,調用了一個庫函數 HAL_GPIO_ReadPin()函數。輸入參數為要讀取的端口、引腳,返回引腳的輸入電平狀態,高電平為 1,低電平為 0。
Main函數如下:
/* USER CODE END Header */
/* Includes------------------------------------------------------------------*/
#include "main.h"
/* Private includes----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* Private typedef-----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
typedef enum{
KEY_UP= 0,
KEY_DOWN= 1,
}KEYState_Type;
/* USER CODE END PTD */
/* Private define------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define KEY_GPIO GPIOA
#define KEY_GPIO_PIN GPIO_PIN_0
#define KEY_DOWN_LEVEL 1
/* USER CODE END PD */
/* Private macro-------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes-----------------------------------------------*/
uint8_t Key_Scan(void);
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entrypoint.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCUConfiguration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and theSystick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
if(KEY_DOWN_LEVEL== Key_Scan())
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0);
HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_6);
HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_7);
}
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief Key Scan
* @retval uint8_t
*/
uint8_t Key_Scan(void)
{
if(HAL_GPIO_ReadPin(KEY_GPIO,GPIO_PIN_0)== KEY_DOWN_LEVEL )
{
HAL_Delay(10);
if(HAL_GPIO_ReadPin(KEY_GPIO,GPIO_PIN_0)== KEY_DOWN_LEVEL )
{
while(HAL_GPIO_ReadPin(KEY_GPIO,GPIO_PIN_0)== KEY_DOWN_LEVEL);
return KEY_DOWN;
}
else
{
returnKEY_UP;
}
}
returnKEY_UP;
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specifiedparameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) !=HAL_OK)
{
Error_Handler();
}
}
/**
* @brief GPIO Initialization Function
* @param None
* @retval None
*/
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_RESET);
/*Configure GPIO pin : PA0 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/*Configure GPIO pin : PB0 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/*Configure GPIO pins : PG6 PG7 */
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function isexecuted in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error returnstate */
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of thesource file and the source line number
* where the assert_paramerror has occurred.
* @param file: pointer to thesource file name
* @param line: assert_param errorline source number
* @retval None
*/
void assert_failed(uint8_t*file, uint32_t line)
{
/* USERCODE BEGIN 6 */
/* User can add his own implementation to report the file name and linenumber,
tex: printf("Wrong parameters value:file %s on line %d\\r\\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
/************************ (C)COPYRIGHT STMicroelectronics *****END OF FILE****/
5.2 EXTI方式
5.2.1 EXTI的工作原理
EXTI(ExternalInterrupt)就是指外部中斷,通過 GPIO 檢測輸入脈沖,引起中斷事件,打斷原來的代碼執行流程,進入到中斷服務函數中進行處理,處理完后再返回到中斷之前的代碼中執行。
- STM32 的中斷和異常
Cortex 內核具有強大的異常響應系統,它把能夠打斷當前代碼執行流程的事件分為異常(exception)和中斷(interrupt),并把它們用一個表管理起來,編號為 0 ~ 15 的稱為內核異常,而 16 以上的則稱為外部中斷(外是相對內核而言),這個表就稱為中斷向量表。
而 STM32 對這個表重新進行了編排,把編號從–3 至 6 的中斷向量定義為系統異常,編號為負的內核異常不能被設置優先級,如復位(Reset)、不可屏蔽中斷(NMI)、硬錯誤(Hardfault)。從編號 7 開始的為外部中斷,這些中斷的優先級都是可以自行設置的。詳細的 STM32 中斷向量表見下表。
- NVIC 中斷控制器
STM32 的中斷如此之多,配置起來并不容易,因此我們需要一個強大而方便的中斷控制器 NVIC (Nested VectoredInterrupt Controller)。NVIC 是屬于 Cortex 內核的器件,不可屏蔽中斷(NMI)和外部中斷都由它來處理,而 SYSTICK 不是由 NVIC 來控制的。
- NVIC 結構體成員
當我們要使用 NVIC 來配置中斷時,自然想到 ST 庫肯定也已經把它封裝成庫函數了。查找庫幫助文檔,發現在 Modules->STM32F10x_StdPeriph_Driver->misc 查找到一個NVIC_Init() 函數。對 NVIC 初始化,首先要定義并填充一個 NVIC_InitTypeDef類型的結構體。這個結構體有 4 個成員,見下表。
結構體成員名稱 | 描述 |
---|---|
NVIC_IRQChannel | 需要配置的中斷向量 |
NVIC_IRQChannelCmd | 使能或關閉相應中斷向量的中斷響應 |
NVIC_IRQChannelPreemptionPriority | 配置相應中斷向量搶占優先級 |
NVIC_IRQChannelSubPriority | 配置相應中斷向量的響應優先級 |
前面兩個結構體成員都很好理解,首先要用 NVIC_IRQChannel 參數來選擇將要配置的中斷向量,用NVIC_IRQChannelCmd 參數來進行使能(ENABLE)或關閉(DISABLE)該中斷。在NVIC_IRQChannelPreemptionPriority 成員要配置中斷向量的搶占優先級,在NVIC_IRQChannelSubPriority需要配置中斷向量的響應優先級。對于中斷的配置,最重要的便是配置其優先級,但 STM32 的同一個中斷向量為什么需要設置兩種優先級?這兩種優先級有什么區別?
- 搶占優先級和響應優先級
STM32 的中斷向量具有兩個屬性,一個為搶占屬性,另一個為響應屬性,其屬性編號越小,表明它的優先級別越高。
搶占,是指打斷其他中斷的屬性,即因為具有這個屬性會出現嵌套中斷(在執行中斷服務函數 A 的過程中被中斷 B 打斷,執行完中斷服務函數 B 再繼續執行中斷服務函數A),搶占屬性由NVIC_IRQChannelPreemptionPriority 的參數配置。
而響應屬性則應用在搶占屬性相同的情況下,當兩個中斷向量的搶占優先級相同時,如果兩個中斷同時到達,則先處理響應優先級高的中斷,響應屬性由NVIC_IRQChannelSubPriority參數配置。例如,現在有三個中斷向量,見下表。
中斷向量 | 搶占優先級 | 響應優先級 |
---|---|---|
A | 0 | 0 |
B | 1個 | 0 |
C | 1個 | 1個 |
若內核正在執行 C 的中斷服務函數,則它能被搶占優先級更高的中斷 A 打斷,由于 B和 C 的搶占優先級相同,所以 C 不能被 B 打斷。但如果 B 和 C 中斷是同時到達的,內核就會首先響應響應優先級別更高的 B 中斷
- NVIC 的優先級組
在配置優先級的時候,還要注意一個很重要的問題,即中斷種類的數量。NVIC 只可以配置 16 種中斷向量的優先級,也就是說,搶占優先級和響應優先級的數量由一個 4 位的數字來決定,把這個 4 位數字的位數分配成搶占優先級部分和響應優先級部分。有 5 組分配方式:
- 第 0 組:所有 4 位用來配置響應優先級。即 16 種中斷向量具有都不相同的響應優先級。
- 第 1 組:最高 1 位用來配置搶占優先級,低 3 位用來配置響應優先級。 表示有 2 ^1^ =2 種級別的搶占優先級(0 級,1 級),有 2 ^3^ =8 種響應優先級,即在 16 種中斷向量之中,有8 種中斷,其搶占優先級都為 0 級,而它們的響應優先級分別為 0 ~ 7,其余 8 種中斷向量的搶占優先級則都為 1 級,響應優先級別分別為 0~7。
- 第 2 組:2 位用來配置搶占優先級,2 位用來配置響應優先級。即 2 ^2^ =4 種搶占優先級,2 ^2^ =4 種響應優先級。
- 第 3 組:高 3 位用來配置搶占優先級,最低 1 位用來配置響應優先級。即有 8 種搶占優先級,2 種響應 2 優先級。
- 第 4 組:所有 4 位用來配置搶占優先級,即 NVIC 配置的 2 ^4^ =16 種中斷向量都是只有搶占屬性,沒有響應屬性。
要配置這些優先級組,可以采用庫函數NVIC_PriorityGroupConfi g(),可輸入的參數為NVIC_PriorityGroup_0 ~NVIC_PriorityGroup_4,分別為以上介紹的 5 種分配組。
于是,有讀者覺得疑惑了,如此強的 STM32,所有GPIO都能夠配置成外部中斷,USART、ADC 等外設也有中斷,而 NVIC 只能配置 16 種中斷向量,那么在某個工程中使用超過 16 個中斷怎么辦呢? 注意 NVIC 能配置的是 16 種中斷向量,而不是16 個,當工程中有超過 16 個中斷向量時,必然有兩個以上的中斷向量是使用相同的中斷種類,而具有相同中斷種類的中斷向量不能互相嵌套。
STM2 單片機的所有 I/O 端口都可以配置為 EXTI 中斷模式,用來捕捉外部信號,可以配置為下降沿中斷、上升沿中斷和上升下降沿中斷這三種模式。 它們以圖 3- 2 所示方式連接到 16 個外部中斷 / 事件線上。
- EXTI 外部中斷
STM32 的所有 GPIO 都引入到 EXTI 外部中斷線上,使得所有的 GPIO 都能作為外部中斷的輸入源。 GPIO 與 EXTI 的連接方式見下圖。
觀察下圖可知,PA0 ~ PG0 連接到 EXTI0 、PA1 ~ PG1 連接到 EXTI1、……、PA15 ~ PG15 連接到 EXTI15。 這里大家要注意的是:PAx ~ PGx 端口的中斷事件都連接到了 EXTIx,即同一時刻 EXTIx 只能響應一個端口的事件觸發,不能夠同一時間響應所有GPIO 端口的事件,但可以分時復用。 它可以配置為上升沿觸發、下降沿觸發或雙邊沿觸發。 EXTI 最普通的應用就是接上一個按鍵,設置為下降沿觸發,用中斷來檢測按鍵。
5.2.2 EXTI的寄存器描述
EXTI 寄存器的寄存器主要有6個,下面分別描述。
- 中斷屏蔽寄存器(EXTI_IMR)
- 事件屏蔽寄存器(EXTI_EMR)
- 上升沿觸發選擇寄存器(EXTI_RTSR)
注意: 外部喚醒線是邊沿觸發的,這些線上不能出現毛刺信號。 在寫EXTI_RTSR寄存器時,在外部中斷線上的上升沿信號不能被識別,掛起位也不會被置位。 在同一中斷線上,可以同時設置上升沿和下降沿觸發。 即任一邊沿都可觸發中斷
- 下降沿觸發選擇寄存器(EXTI_FTSR)
注意: 外部喚醒線是邊沿觸發的,這些線上不能出現毛刺信號。 在寫EXTI_FTSR寄存器時,在外部中斷線上的下降沿信號不能被識別,掛起位不會被置位。 在同一中斷線上,可以同時設置上升沿和下降沿觸發。 即任一邊沿都可觸發中斷。
- 軟件中斷事件寄存器(EXTI_SWIER)
- 掛起寄存器(EXTI_PR)
5.2.3 EXTI方式實現-標準庫
- 部署外部掛起
現在我們重點分析 EXTI_PA0_Config() 這個函數,它完成了配置一個 I/O 為 EXTI 中斷的一般步驟,主要有以下功能:
1)使能 EXTIx 線的時鐘和第二功能 AFIO 時鐘。
2)配置 EXTIx 線的中斷優先級。
3)配置 EXTI 中斷線 I/O。
4)選定要配置為 EXTI 的 I/O 口線和 I/O 口的工作模式。
5)EXTI 中斷線工作模式配置。
void EXTI_PA0_Config(void)
{
GPIO_InitTypeDefGPIO_InitStructure;
EXTI_InitTypeDefEXTI_InitStructure;
/*config the extiline clock and AFIO clock */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA| RCC_APB2Periph_AFIO,ENABLE);
/*config the NVIC */
NVIC_Configuration();
/*EXTI line gpio config*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; // 下拉輸入
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*EXTI line mode config */
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//下降沿觸發中斷
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
}
EXTI_PA0_Config()代碼中,配置好 NVIC 后,還要對 GPIOA 進行初始化,這部分和按鍵輪詢的設置類似。
接下來,調用GPIO_EXTILineConfi g() 函數把 GPIOA、Pin0 設置為 EXTI 輸入線。 選擇好了 GPIO,開始填寫 EXTI 的初始化結構體。 從這些參數的名字,相信讀者已經知道如何把它應用到按鍵檢測中。
1). EXTI_Line =EXTI_Line0 :給 EXTI_Line 成員賦值。 選擇 EXTI_Line0 線進行配置,因為按鍵的 PA0 連接到了 EXTI_Line0。
2). EXTI_Mode =EXTI_Mode_Interrupt :給 EXTI_Mode 成員賦值。 把 EXTI_Line0的模式設置為中斷模式(EXTI_Mode_Interrupt)。 這個結構體成員也可以賦值為事件模式EXTI_Mode_Event ,這個模式不會立刻觸發中斷,而只是在寄存器上把相應的事件標志位置 1,應用這個模式需要不停地查詢相應的寄存器。
3). EXTI_Trigger =EXTI_Trigger_Falling :給 EXTI_Trigger 成員賦值。 把觸發方式(EXTI_Trigger)設置為下降沿觸發(EXTI_Trigger_Falling)。
4) . EXTI_LineCmd =ENABLE :給 EXTI_LineCmd 成員賦值。 把 EXTI_LineCmd 設置為使能。
5)最后調用 EXTI_Init() 把 EXTI 初始化結構體的參數寫入寄存器。
- AFIO 時鐘
代碼中調用RCC_APB2PeriphClockCmd()時還輸入了參數RCC_APB2Periph_AFIO,表示開啟 AFIO的時鐘。
AFIO (alternate-functionI/O),指 GPIO 端口的復用功能,GPIO 除了用作普通的輸入輸出(主功能),還可以作為片上外設的復用輸入輸出,如串口、ADC,這些就是復用功能。 大多數 GPIO 都有一個默認復用功能,有的 GPIO 還有重映射功能。 重映射功能是指把原來屬于 A 引腳的默認復用功能,轉移到 B 引腳進行使用,前提是 B 引腳具有這個重映射功能。
當把 GPIO 用作 EXTI 外部中斷或使用重映射功能的時候,必須開啟 AFIO 時鐘,而在使用默認復用功能的時候,就不必開啟 AFIO 時鐘了。
- NVIC 初始化配置
static voidNVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* Configure one bit for preemption priority */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
/* 配置中斷源 */
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);
}
本代碼中調用了NVIC_PriorityGroupConfi g() 庫函數,把 NVIC 中斷優先級分組設置為第 1 組。 接下來開始向 NVIC 初始化結構體寫入參數 . NVIC_IRQChannel=EXTI0_IRQn,表示要配置的為 EXTI 第 1 線的中斷向量。 因為按鍵 PA0 對應的 EXTI 線為EXTI0。 這些可寫入的參數可以在 stm32f10x.h 文件的 IRQn 類型定義中查找到。 然后配置搶占優先級和響應優先級,因為這個工程簡單,就直接把它設置為最高級中斷。 填充完結構體,別忘記最后要調用 NVIC_Init() 函數來向寄存器寫入參數。 這里要注意的是,如果用的 IO 口是 IO0 ~ IO4,那么對應的中斷向量是 EXTI0_IRQn ~ EXTI4_IRQn,如果用的 IO 是I05 ~ IO9 中的一個的話,對應的中斷向量只能是 EXTI9_5_IRQn, 如果用的 IO 是 I010~IO15中的一個的話,對應的中斷向量只能是 EXTI15_10_IRQn。 舉例:如果 PE5 或者 PE6 作為EXTI 中斷口,那么對應的中斷向量都是 EXTI9_5_IRQn,在同一時刻只能相應來自一個IO 的 EXTI 中斷。
- 編寫中斷服務函數
在這個 EXTI 設置中我們把 PA0 連接到內部的 EXTI0,GPIO 配置為上拉輸入,工作在下降沿中斷。在外圍電路上我們將 PA0 接到了 key上。當按鍵沒有按下時,PA0 始終為高,當按鍵按下時 PA0 變為低,從而 PA0 上產生一個下降沿跳變,EXTI0 會捕捉到這一跳變,并產生相應的中斷,中斷服務程序在 stm32f10x_it.c 中實現。stm32f10x_it.c 文件是專門用來存放中斷服務函數的。文件中默認只有幾個關于系統異常的中斷服務函數,而且都是空函數,在需要的時候自行編寫。那么中斷服務函數名是不是可以自己定義呢?不可以。中斷服務函數的名字必須要與啟動文件startup_stm32f10x_hd.s中的中斷向量表定義一致。
EXTI0_. IRQHandler表示為 EXTI0 中斷向量的服務函數名。 于是,我們就可以在 stm32f10x_it.c 文件中加入名為EXTI0_IRQHandler() 的函數。
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0)!= RESET) //確保是否產生了 EXTI Line 中斷
{
//LED取反
LED_TOGGLE;
EXTI_ClearITPendingBit(EXTI_Line0); //清除中斷標志位
}
}
其內容比較容易理解,進入中斷后,調用庫函數EXTI_GetITStatus() 來重新檢查是否產生了 EXTI_Line 中斷,接下來把 LED 取反,操作完畢后,調用EXTI_ClearITPendingBit()清除中斷標志位再退出中斷服務函數。
5.2.4 EXTI方式實現-HAL庫
5.2.4.1 STM32Cube生成工程
根據按鍵電路,KEY1的引腳是PA0,我們將PA0的GPIO設置為上升沿觸發的外部中斷模式,保留3個LED的GPIO配置。
如上圖中對KEY的GPIO進行了初始化配置,接下來就要進行NVIC配置。 NVIC選項用于設置中斷的優先級,這里先設置優先級組為4位搶占式優先級為1,響應式優先級為0; EXTI[15:10]; EXTI10- EXTI15中短線在中斷向量表中占用同一個優先級,所以EXTI10- EXTI15中斷線優先級都是一樣的,同意配置。 EXTI5 EXTI9情況也是一樣。
最后生成工程文件即可。
5.2.4.2 EXTI代碼分析
在看代碼前,我們先看看按鍵中斷編程的流程:
1)使能AFIO時鐘,設置NVIC優先級NVIC_PRIORITYGROUP_4;
2)使能按鍵引腳PA0,將其設置為上升沿觸發中斷模式并使能下拉;
3)配置按鍵引腳中斷優先級并使能中斷;
4)編寫中斷回調函數,同時進行消抖處理。
- 配置GPIO
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_RESET);
/*Configure GPIO pin : PA0 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/*Configure GPIO pin : PB0 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/*Configure GPIO pins : PG6 PG7 */
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);
/* EXTI interrupt init*/
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
這部分和按鍵輪詢的設置類似,需要對GPIO進行初始化設置。
- AFIO 時鐘
代碼中調用__HAL_RCC_AFIO_CLK_ENABLE()函數,表示開啟 AFIO的時鐘。 這個函數HAL_Init函數調用HAL_MspInit()函數實現的。
AFIO (alternate-functionI/O),指 GPIO 端口的復用功能,GPIO 除了用作普通的輸入輸出(主功能),還可以作為片上外設的復用輸入輸出,如串口、ADC,這些就是復用功能。 大多數 GPIO 都有一個默認復用功能,有的 GPIO 還有重映射功能。 重映射功能是指把原來屬于 A 引腳的默認復用功能,轉移到 B 引腳進行使用,前提是 B 引腳具有這個重映射功能。
當把 GPIO 用作 EXTI 外部中斷或使用重映射功能的時候,必須開啟 AFIO 時鐘,而在使用默認復用功能的時候,就不必開啟 AFIO 時鐘了。
- NVIC 初始化配置
HAL_StatusTypeDef HAL_Init(void)
{
…
/* Set Interrupt Group Priority */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
…
}
static void MX_GPIO_Init(void)
{
….
/* EXTI interrupt init*/
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
本代碼中調用了HAL_NVIC_SetPriorityGrouping()庫函數,把 NVIC 中斷優先級分組設置為4組。 MX_GPIO_Init()函數的最后兩個函數是關于中斷優先級分組和使能中斷的。
HAL_NVIC_SetPriority(),共有三個參數:
1.中斷向量號
中斷向量號在stm32f103xe.h中定義的。
2.搶占優先級:設置了兩位搶占優先級,那么搶占優先級可以是00-11,即0-3。
3.響應優先級:同樣是兩位。
HAL_NVIC_EnableIRQ()函數用于使能外部中斷線,外部中斷線10-15是共用一個中斷向量的。
這里要注意的是,如果用的 IO 口是 IO0 ~ IO4,那么對應的中斷向量是 EXTI0_IRQn ~EXTI4_IRQn,如果用的 IO 是I05 ~ IO9 中的一個的話,對應的中斷向量只能是 EXTI9_5_IRQn, 如果用的 IO 是 I010 ~ IO15中的一個的話,對應的中斷向量只能是 EXTI15_10_IRQn。 舉例:如果 PE5 或者 PE6 作為EXTI 中斷口,那么對應的中斷向量都是 EXTI9_5_IRQn,在同一時刻只能相應來自一個IO 的 EXTI 中斷。
- 編寫中斷服務函數
在這個 EXTI 設置中我們把 PA0 連接到內部的 EXTI0,GPIO 配置為上拉輸入,工作在下降沿中斷。在外圍電路上我們將 PA0 接到了 key上。當按鍵沒有按下時,PA0 始終為高,當按鍵按下時 PA0 變為低,從而 PA0 上產生一個下降沿跳變,EXTI0 會捕捉到這一跳變,并產生相應的中斷,中斷服務程序在 stm32f10x_it.c 中實現。stm32f10x_it.c 文件是專門用來存放中斷服務函數的。文件中默認只有幾個關于系統異常的中斷服務函數,而且都是空函數,在需要的時候自行編寫。那么中斷服務函數名是不是可以自己定義呢?不可以。中斷服務函數的名字必須要與啟動文件startup_stm32f10x_hd.s中的中斷向量表定義一致。
EXTI0_IRQHandler 表示為 EXTI0 中斷向量的服務函數名。于是,我們就可以在 stm32f10x_it.c 文件中加入名為EXTI0_IRQHandler() 的函數。
void EXTI0_IRQHandler(void)
{
/* USER CODE BEGIN EXTI0_IRQn 0 */
/* USER CODE END EXTI0_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
/* USER CODE BEGIN EXTI0_IRQn 1 */
/* USER CODE END EXTI0_IRQn 1 */
}
EXTI0_IRQHandler()函數調用HAL_GPIO_EXTI_IRQHandler()函數,我們進入HAL_GPIO_EXTI_IRQHandler()函數,發現又調用了函數HAL_GPIO_EXTI_Callback(),再此進入HAL_GPIO_EXTI_Callback()函數,HAL_GPIO_EXTI_Callback()就是回調函數。
__weak 是一個弱化標識,帶有這個的函數就是一個弱化函數,就是你可以在其他地方寫一個名稱和參數都一模一樣的函數,編譯器就會忽略這一個函數,而去執行你寫的那個函數; 而UNUSED(GPIO_Pin) ,這就是一個防報錯的定義,當傳進來的GPIO端口號沒有做任何處理的時候,編譯器也不會報出警告。 其實我們在開發的時候已經不需要去理會中斷服務函數了,只需要找到這個中斷回調函數并將其重寫即可而這個回調函數還有一點非常便利的地方這里沒有體現出來,就是當同時有多個中斷使能的時候,STM32CubeMX會自動地將幾個中斷的服務函數規整到一起并調用一個回調函數,也就是無論幾個中斷,我們只需要重寫一個回調函并判斷傳進來的端口號即可。
那么接下來我們就在stm32f4xx_it.c這個文件的最下面添加以下代碼:
voidHAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin==KEY_GPIO_PIN)
{
HAL_Delay(100);
if(HAL_GPIO_ReadPin(KEY_GPIO,KEY_GPIO_PIN)== KEY_DOWN_LEVEL)
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0);
HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_6);
HAL_GPIO_TogglePin(GPIOG,GPIO_PIN_7);
}
}
}
其內容比較容易理解,進入中斷后,調用庫函數 HAL_GPIO_ReadPin來重新檢查是否產生了中斷,接下來把 LED 取反。
5.3實驗現象
編譯好程序后,下載到板子上,不管是普通方式還是中斷方式,當按在按鍵S1時,LED1或亮或滅。
-
電路圖
+關注
關注
10354文章
10723瀏覽量
532206 -
mcu
+關注
關注
146文章
17307瀏覽量
352172 -
ARM
+關注
關注
134文章
9153瀏覽量
368506 -
按鍵
+關注
關注
4文章
223瀏覽量
57633 -
GPIO
+關注
關注
16文章
1216瀏覽量
52267
發布評論請先 登錄
相關推薦
評論