開發環境:
MDK:Keil 5.30
MCU:GD32F207IK
6.1普通方式
6.1.1普通方式工作原理
按鍵 GPIO 端口有兩個方案可以選擇,一是采用上拉輸入模式,因為按鍵在沒按下的時候,是預設為高電平的,采且内部上拉模式正好符合這個要求。第二個方案是直接采用浮空輸入模式,因為按照硬體電路圖,在晶片外部接了上拉電阻,其實就沒必要再配置成内部上拉輸入模式了,因為在外部上拉與内部上拉效果是一樣的。
筆者本文将會使用K2。
6.1.2普通方式實作
主函數代碼如下:
/* Includes*********************************************************************/
#include "gd32f2_systick.h"
#include "gd32f2_led.h"
#include "gd32f2_key.h"
/*
brief main function
param[in] none
param[out] none
retval none
*/
int main(void)
{
//systick init
sysTick_init();
/* configure LED1 GPIO port */
led_init(LED1);
/* configure LED2 GPIO port */
led_init(LED2);
/* configure LED3 GPIO port */
led_init(LED3);
/* configure LED4 GPIO port */
led_init(LED4);
//key init
key_init(KEY_WAKEUP);
while(1)
{
delay_ms(100);
if(key_scan(KEY_WAKEUP))
{
/* turn toggle LED */
led_toggle(LED1);
led_toggle(LED2);
led_toggle(LED3);
led_toggle(LED4);
}
}
}
GPIO 初始化配置
/*
brief configure key
param[in] keynum: specify the key to be configured
arg KEY_TAMPER: tamper key
arg KEY_WAKEUP: wakeup key
arg KEY_USER: user key
param[out] none
retval none
*/
void key_init(key_typedef_enum keynum)
{
/* enable the key clock */
rcu_periph_clock_enable(KEY_CLK[keynum]);
rcu_periph_clock_enable(RCU_AF);
/* configure button pin as input */
gpio_init(KEY_PORT[keynum], GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, KEY_PIN[keynum]);
}
key_init()與 LED 的 GPIO 初始化函數 led_init()類似,差別隻是在這個函數中,要開啟的 GPIO 的端口時鐘不一樣,并且把檢測按鍵用的引腳 Pin 的模式設定為适合按鍵應用的上拉輸入模式(由于接了外部上拉電阻,也可以使用浮空輸入,讀者可自行修改代碼做實驗)。
按鍵消抖
/*
brief return the key state
param[in] keynum: specify the key to be checked
arg KEY_TAMPER: tamper key
arg KEY_WAKEUP: wakeup key
arg KEY_USER: user key
param[out] none
retval the key's GPIO pin value
*/
key_state_enum key_scan(key_typedef_enum keynum)
{
/* check whether the button is pressed */
if(RESET == gpio_input_bit_get(KEY_PORT[keynum], KEY_PIN[keynum]))
{
delay_ms(100);
/* check whether the button is pressed */
if(RESET == gpio_input_bit_get(KEY_PORT[keynum], KEY_PIN[keynum]))
{
while(RESET == gpio_input_bit_get(KEY_PORT[keynum], KEY_PIN[keynum]))
{
return KEY_ON;
}
}
}
return KEY_OFF;
}
相信延時消抖的原理大家在學習其他單片機時就已經了解了,本函數的功能就是掃描輸入參數中指定的引腳,檢測其電平變化,并作延時消抖處理,最終對按鍵消息進行确認。
利用 gpio_input_bit_get() 讀取輸入資料,若從相應引腳讀取的資料等于 0(KEY_ON),低電平,表明可能有按鍵按下,調用延時函數。否則傳回 KEY_OFF,表示按鍵沒有被按下。
延時之後再次利用 gpio_input_bit_get()讀取輸入資料,若依然為低電平,表明确實有按鍵被按下了。否則傳回 KEY_OFF,表示按鍵沒有被按下。
循環調用gpio_input_bit_get() 一直檢測按鍵的電平,直至按鍵被釋放,被釋放後,傳回表示按鍵被按下的标志 KEY_ON。以上是按鍵消抖的流程,調用了一個庫函數 gpio_input_bit_get()。輸入參數為要讀取的端口、引腳,傳回引腳的輸入電平狀态,高電平為 1,低電平為 0。
6.2 EXTI方式
6.2.1 EXTI的工作原理
EXTI(External Interrupt) 就是指外部中斷,通過 GPIO 檢測輸入脈沖,引起中斷事件,打斷原來的代碼執行流程,進入到中斷服務函數中進行處理,處理完後再傳回到中斷之前的代碼中執行。
GD32的中斷和異常
Cortex核心具有強大的異常響應系統,它把能夠打斷目前代碼執行流程的事件分為異常(exception)和中斷(interrupt),并把它們用一個表管理起來,編号為 0 ~ 15 的稱為核心異常,而 16 以上的則稱為外部中斷(外是相對核心而言),這個表就稱為中斷向量表。
而 GD32 對這個表重新進行了編排,把編号從-3 至 6 的中斷向量定義為系統異常,編号為負的核心異常不能被設定優先級,如複位(Reset)、不可屏蔽中斷 (NMI)、硬錯誤(Hardfault)。從編号 7 開始的為外部中斷,這些中斷的優先級都是可以自行設定的。詳細的 GD32 中斷向量表見下表。
……
完整向量表請參考《GD32F20x_User_Manual_EN_Rev2.4》。
NVIC 中斷控制器
GD32的中斷如此之多,配置起來并不容易,是以我們需要一個強大而友善的中斷控制器 NVIC (Nested Vectored Interrupt Controller)。NVIC 是屬于 Cortex 核心的器件,不可屏蔽中斷 (NMI)和外部中斷都由它來處理,而 SYSTICK 不是由 NVIC 來控制的。
NVIC 結構體成員
當我們要使用 NVIC 來配置中斷時,自然想到GD庫肯定也已經把它封裝成庫函數了。查找庫幫助文檔,發現在 gd32f20x_misc查找到一個nvic_irq_enable() 函數。
/*!
\brief enable NVIC request
\param[in] nvic_irq: the NVIC interrupt request, detailed in IRQn_Type
\param[in] nvic_irq_pre_priority: the pre-emption priority needed to set
\param[in] nvic_irq_sub_priority: the subpriority needed to set
\param[out] none
\retval none
*/
void nvic_irq_enable(uint8_t nvic_irq,
uint8_t nvic_irq_pre_priority,
uint8_t nvic_irq_sub_priority)
{
uint32_t temp_priority = 0x00U, temp_pre = 0x00U, temp_sub = 0x00U;
/* use the priority group value to get the temp_pre and the temp_sub */
switch((SCB->AIRCR) & (uint32_t)0x700U) {
case NVIC_PRIGROUP_PRE0_SUB4:
temp_pre = 0U;
temp_sub = 0x4U;
break;
case NVIC_PRIGROUP_PRE1_SUB3:
temp_pre = 1U;
temp_sub = 0x3U;
break;
case NVIC_PRIGROUP_PRE2_SUB2:
temp_pre = 2U;
temp_sub = 0x2U;
break;
case NVIC_PRIGROUP_PRE3_SUB1:
temp_pre = 3U;
temp_sub = 0x1U;
break;
case NVIC_PRIGROUP_PRE4_SUB0:
temp_pre = 4U;
temp_sub = 0x0U;
break;
default:
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2);
temp_pre = 2U;
temp_sub = 0x2U;
break;
}
/* get the temp_priority to fill the NVIC->IP register */
temp_priority = (uint32_t)nvic_irq_pre_priority << (0x4U - temp_pre);
temp_priority |= nvic_irq_sub_priority & (0x0FU >> (0x4U - temp_sub));
temp_priority = temp_priority << 0x04U;
NVIC->IP[nvic_irq] = (uint8_t)temp_priority;
/* enable the selected IRQ */
NVIC->ISER[nvic_irq >> 0x05U] = (uint32_t)0x01U << (nvic_irq & (uint8_t)0x1FU);
}
該函數有三個參數,需要配置的中斷向量,中斷向量搶占優先級和中斷向量的響應優先級。
前面兩個結構體成員都很好了解,首先要用 nvic_irq參數來選擇将要配置的中斷向量。用nvic_irq_pre_priority參數要配置中斷向量的搶占優先級,用nvic_irq_sub_priority參數配置中斷向量的響應優先級。對于中斷的配置,最重要的便是配置其優先級,但 GD32 的同一個中斷向量為什麼需要設定兩種優先級?這兩種優先級有什麼差別?
搶占優先級和響應優先級
GD32的中斷向量具有兩個屬性,一個為搶占屬性,另一個為響應屬性,其屬性編号越小,表明它的優先級别越高。
搶占,是指打斷其他中斷的屬性,即因為具有這個屬性會出現嵌套中斷(在執行中斷服務函數 A 的過程中被中斷 B 打斷,執行完中斷服務函數 B 再繼續執行中斷服務函數A),搶占屬性由nvic_irq_pre_priority參數配置。
而響應屬性則應用在搶占屬性相同的情況下,當 兩個中斷向量的搶占優先級相同時,如 果兩個中斷同時到達,則先處理響應優先級高的中斷,響應屬性 由nvic_irq_sub_priority參數配置。例如,現在有三個中斷向量,見下表。
中斷向量 | 搶占優先級 | 響應優先級 |
A | ||
B | 1 | |
C | 1 | 1 |
若核心正在執行 C 的中斷服務函數,則它能被搶占優先級更高的中斷 A 打斷,由于 B和 C 的搶占優先級相同,是以 C 不能被 B 打斷。但如果 B 和 C 中斷是同時到達的,核心就會首先響應響應優先級别更高的 B 中斷。
NVIC 的優先級組
在配置優先級的時候,還要注意一個很重要的問題,即中斷種類的數量。NVIC 隻可以配置 16 種中斷向量的優先級,也就是說,搶占優先級和響應優先級的數量由一個 4 位的數字來決定,把這個 4 位數字的位數配置設定成搶占優先級部分和響應優先級部分。有 5 組配置設定方式 :
第 0 組: 所有 4 位用來配置響應優先級。即 16 種中斷向量具有都不相同的響應優先級。
第 1 組:最高 1 位用來配置搶占優先級,低 3 位用來配置響應優先級。表示有 21=2 種級别的搶占優先級(0 級,1 級),有 23=8 種響應優先級,即在 16 種中斷向量之中,有8 種中斷,其搶占優先級都為 0 級,而它們的響應優先級分别為 0~7,其餘 8 種中斷向量的搶占優先級則都為 1 級,響應優先級别分别為 0~7。
第 2 組:2 位用來配置搶占優先級,2 位用來配置響應優先級。即 22=4 種搶占優先級,22=4 種響應優先級。
第 3 組:高 3 位用來配置搶占優先級,最低 1 位用來配置響應優先級。即有 8 種搶占優先級,2 種響應 2 優先級。
第 4 組:所有 4 位用來配置搶占優先級,即 NVIC 配置的 24 =16 種中斷向量都是隻有搶占屬性,沒有響應屬性。
要配置這些優先級組,可以采用庫函數 nvic_priority_group_set(),可輸入的參數為NVIC_PRIGROUP_PRE0_SUB4 ~ NVIC_PRIGROUP_PRE4_SUB0,分别為以上介紹的 5 種配置設定組。
于是,有讀者覺得疑惑了, 如此強的 GD32, 所有GPIO都能夠配置成外部中斷,USART、ADC 等外設也有中斷,而 NVIC 隻能配置 16 種中斷向量,那麼在某個工程中使用超過 16 個中斷怎麼辦呢?注意 NVIC 能配置的是 16 種中斷向量,而不是16 個,當工程中有超過 16 個中斷向量時,必然有兩個以上的中斷向量是使用相同的中斷種類,而具有相同中斷種類的中斷向量不能互相嵌套。
GD32的所有 I/O 端口都可以配置為 EXTI 中斷模式,用來捕捉外部信号,可以配置為下降沿中斷、上升沿中斷和上升下降沿中斷這三種模式。它們以圖2所示方式連接配接到 16 個外部中斷 / 事件線上。
EXTI 外部中斷
GD32的所有 GPIO 都引入到 EXTI 外部中斷線上,使得所有的 GPIO 都能作為外部中斷的輸入源。GPIO 與 EXTI 的連接配接方式見下表。
由下表可知,PA0 ~ PG0 連接配接到 EXTI0 、PA1 ~ PG1 連接配接到 EXTI1、……、PA15 ~ PG15 連接配接到 EXTI15。這裡大家要注意的是 :PAx ~ PGx 端口的中斷事件都連接配接到了 EXTIx,即同一時刻 EXTIx 隻能響應一個端口的事件觸發,不能夠同一時間響應所有GPIO 端口的事件,但可以分時複用。它可以配置為上升沿觸發、下降沿觸發或雙邊沿觸發。EXTI 最普通的應用就是接上一個按鍵,設定為下降沿觸發,用中斷來檢測按鍵。
6.2.2 EXTI的寄存器描述
EXTI 寄存器的寄存器主要有6個,下面分别描述。
中斷使能寄存器(EXTI_INTEN)
事件使能寄存器(EXTI_EVEN)
上升沿觸發選擇寄存器(EXTI_RTEN)
注意: 外部喚醒線是邊沿觸發的,這些線上不能出現毛刺信号。在寫EXTI_RTSR寄存器時,在外部中斷線上的上升沿信号不能被識别,挂起位也不會被置位。在同一中斷線上,可以同時設定上升沿和下降沿觸發。即任一邊沿都可觸發中斷。
下降沿觸發選擇寄存器(EXTI_FTEN)
注意: 外部喚醒線是邊沿觸發的,這些線上不能出現毛刺信号。在寫EXTI_FTSR寄存器時,在外部中斷線上的下降沿信号不能被識别,挂起位不會被置位。在同一中斷線上,可以同時設定上升沿和下降沿觸發。即任一邊沿都可觸發中斷。
軟體中斷事件寄存器(EXTI_SWIEV)
挂起寄存器(EXTI_PD)
6.2.3 EXTI方式實作
主函數代碼如下:
/* Includes*********************************************************************/
#include "gd32f2_systick.h"
#include "gd32f2_led.h"
#include "gd32f2_key.h"
/*
brief main function
param[in] none
param[out] none
retval none
*/
int main(void)
{
//systick init
sysTick_init();
/* configure LED1 GPIO port */
led_init(LED1);
/* configure LED2 GPIO port */
led_init(LED2);
/* configure LED3 GPIO port */
led_init(LED3);
/* configure LED4 GPIO port */
led_init(LED4);
//key init
key_init(KEY_WAKEUP, KEY_MODE_EXTI);
while(1)
{
delay_ms(100);
}
}
配置外部中斷
現在我們重點分析 key_init() 這個函數,它完成了配置一個 I/O 為 EXTI 中斷的一般步驟,主要有以下功能 :
1)使能 EXTIx 線的時鐘和第二功能 AFIO 時鐘。
2)配置 EXTIx 線的中斷優先級。
3)配置 EXTI 中斷線 I/O。
4)標明要配置為 EXTI 的 I/O 口線和 I/O 口的工作模式。
5)EXTI 中斷線工作模式配置。
void key_init(key_typedef_enum keynum, keymode_typedef_enum keymode)
{
/* enable the key clock */
rcu_periph_clock_enable(KEY_CLK[keynum]);
rcu_periph_clock_enable(RCU_AF);
/* configure button pin as input */
gpio_init(KEY_PORT[keynum], GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, KEY_PIN[keynum]);
if (keymode == KEY_MODE_EXTI)
{
/* enable and set key EXTI interrupt to the lowest priority */
nvic_irq_enable(KEY_IRQn[keynum], 2U, 0U);
/* connect key EXTI line to key GPIO pin */
gpio_exti_source_select(KEY_PORT_SOURCE[keynum], KEY_PIN_SOURCE[keynum]);
/* configure key EXTI line */
exti_init(KEY_EXTI_LINE[keynum], EXTI_INTERRUPT, EXTI_TRIG_FALLING);
exti_interrupt_flag_clear(KEY_EXTI_LINE[keynum]);
}
}
key_init()代碼中,不僅配置了NVIC ,還對按鍵的GPIO進行了初始化,這部分和按鍵輪詢的設定類似。
接下來,調用 gpio_exti_source_select () 函數把 GPIOA、Pin0 與EXTI連接配接起來。
最後調用 exti_init() 把 EXTI 初始化,函數如下:
/*!
\brief initialize the EXTI
\param[in] linex: EXTI line number, refer to exti_line_enum
only one parameter can be selected which is shown as below:
\arg EXTI_x (x=0..19): EXTI line x
\param[in] mode: interrupt or event mode, refer to exti_mode_enum
only one parameter can be selected which is shown as below:
\arg EXTI_INTERRUPT: interrupt mode
\arg EXTI_EVENT: event mode
\param[in] trig_type: interrupt trigger type, refer to exti_trig_type_enum
only one parameter can be selected which is shown as below:
\arg EXTI_TRIG_RISING: rising edge trigger
\arg EXTI_TRIG_FALLING: falling trigger
\arg EXTI_TRIG_BOTH: rising and falling trigger
\arg EXTI_TRIG_NONE: without rising edge or falling edge trigger
\param[out] none
\retval none
*/
void exti_init(exti_line_enum linex, exti_mode_enum mode, exti_trig_type_enum trig_type)
{
/* reset the EXTI line x */
EXTI_INTEN &= ~(uint32_t)linex;
EXTI_EVEN &= ~(uint32_t)linex;
EXTI_RTEN &= ~(uint32_t)linex;
EXTI_FTEN &= ~(uint32_t)linex;
/* set the EXTI mode and enable the interrupts or events from EXTI line x */
switch(mode) {
case EXTI_INTERRUPT:
EXTI_INTEN |= (uint32_t)linex;
break;
case EXTI_EVENT:
EXTI_EVEN |= (uint32_t)linex;
break;
default:
break;
}
/* set the EXTI trigger type */
switch(trig_type) {
case EXTI_TRIG_RISING:
EXTI_RTEN |= (uint32_t)linex;
EXTI_FTEN &= ~(uint32_t)linex;
break;
case EXTI_TRIG_FALLING:
EXTI_RTEN &= ~(uint32_t)linex;
EXTI_FTEN |= (uint32_t)linex;
break;
case EXTI_TRIG_BOTH:
EXTI_RTEN |= (uint32_t)linex;
EXTI_FTEN |= (uint32_t)linex;
break;
case EXTI_TRIG_NONE:
default:
break;
}
}
AFIO 時鐘
代碼中調用rcu_periph_clock_enable(RCU_AF)表示開啟 AFIO的時鐘。
AFIO (alternate-function I/O),指 GPIO 端口的複用功能,GPIO 除了用作普通的輸入輸出(主功能),還可以作為片上外設的複用輸入輸出,如序列槽、ADC,這些就是複用功能。大多數 GPIO 都有一個預設複用功能,有的 GPIO 還有重映射功能。重映射功能是指把原來屬于 A 引腳的預設複用功能,轉移到B引腳進行使用,前提是 B 引腳具有這個重映射功能。
當把 GPIO 用作 EXTI 外部中斷或使用重映射功能的時候,必須開啟 AFIO 時鐘,而在使用預設複用功能的時候,就不必開啟 AFIO 時鐘了。
編寫中斷服務函數
在這個 EXTI 設定中我們把 PA0 連接配接到内部的 EXTI0,GPIO 配置為上拉輸入,工作在下降沿中斷。在外圍電路上我們将 PA0 接到了 key上。當按鍵沒有按下時,PA0 始終為高,當按鍵按下時 PA0 變為低,進而 PA0 上産生一個下降沿跳變,EXTI0 會捕捉到這一跳變,并産生相應的中斷,中斷服務程式在 gd32f20x_it.c 中實作。gd32f20x_it.c 檔案是專門用來存放中斷服務函數的。檔案中預設隻有幾個關于系統異常的中斷服務函數,而且都是空函數,在需要的時候自行編寫。那麼中斷服務函數名是不是可以自己定義呢?不可以。中斷服務函數的名字必須要與啟動檔案startup_gd32f20x_cl.s 中的中斷向量表定義一緻。
EXTI0_IRQHandler 表示為 EXTI0 中斷向量的服務函數名。于是,我們就可以在 stm32f10x_it.c 檔案中加入名為 EXTI0_IRQHandler() 的函數。
/*!
\brief this function handles external lines 0 interrupt request
\param[in] none
\param[out] none
\retval none
*/
void EXTI0_IRQHandler(void)
{
if(RESET != exti_interrupt_flag_get(EXTI_0))
{
/* turn toggle LED */
led_toggle(LED1);
led_toggle(LED2);
led_toggle(LED3);
led_toggle(LED4);
exti_interrupt_flag_clear(EXTI_0);
}
}
其内容比較容易了解,進入中斷後,調用exti_interrupt_flag_get() 庫函數來重新檢查是否産生了 EXTI_Line 中斷,接下來把 LED 取反,操作完畢後,調用 exti_interrupt_flag_clear()清除中斷标志位再退出中斷服務函數。
6.3實驗現象
編譯好程式後,下載下傳到闆子上,不管是普通方式還是中斷方式,當按在按鍵K2時,LED或亮或滅。
歡迎通路我的網站
BruceOu的哔哩哔哩
BruceOu的首頁
BruceOu的部落格
BruceOu的簡書
BruceOu的知乎
資源擷取方式
1.關注公衆号[嵌入式實驗樓]