天天看點

《嵌入式 – GD32開發實戰指南》第6章 按鍵

開發環境:

MDK:Keil 5.30

MCU:GD32F207IK

6.1普通方式

6.1.1普通方式工作原理

按鍵 GPIO 端口有兩個方案可以選擇,一是采用上拉輸入模式,因為按鍵在沒按下的時候,是預設為高電平的,采且内部上拉模式正好符合這個要求。第二個方案是直接采用浮空輸入模式,因為按照硬體電路圖,在晶片外部接了上拉電阻,其實就沒必要再配置成内部上拉輸入模式了,因為在外部上拉與内部上拉效果是一樣的。

《嵌入式 – GD32開發實戰指南》第6章 按鍵

筆者本文将會使用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 中斷向量表見下表。

《嵌入式 – GD32開發實戰指南》第6章 按鍵
《嵌入式 – GD32開發實戰指南》第6章 按鍵

……

完整向量表請參考《GD32F20x_User_Manual_EN_Rev2.4》。

 NVIC 中斷控制器

GD32的中斷如此之多,配置起來并不容易,是以我們需要一個強大而友善的中斷控制器 NVIC (Nested Vectored Interrupt Controller)。NVIC 是屬于 Cortex 核心的器件,不可屏蔽中斷 (NMI)和外部中斷都由它來處理,而 SYSTICK 不是由 NVIC 來控制的。

《嵌入式 – GD32開發實戰指南》第6章 按鍵

 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 最普通的應用就是接上一個按鍵,設定為下降沿觸發,用中斷來檢測按鍵。

《嵌入式 – GD32開發實戰指南》第6章 按鍵

6.2.2 EXTI的寄存器描述

EXTI 寄存器的寄存器主要有6個,下面分别描述。

 中斷使能寄存器(EXTI_INTEN)

《嵌入式 – GD32開發實戰指南》第6章 按鍵

 事件使能寄存器(EXTI_EVEN)

《嵌入式 – GD32開發實戰指南》第6章 按鍵

 上升沿觸發選擇寄存器(EXTI_RTEN)

《嵌入式 – GD32開發實戰指南》第6章 按鍵

注意: 外部喚醒線是邊沿觸發的,這些線上不能出現毛刺信号。在寫EXTI_RTSR寄存器時,在外部中斷線上的上升沿信号不能被識别,挂起位也不會被置位。在同一中斷線上,可以同時設定上升沿和下降沿觸發。即任一邊沿都可觸發中斷。

 下降沿觸發選擇寄存器(EXTI_FTEN)

《嵌入式 – GD32開發實戰指南》第6章 按鍵

注意: 外部喚醒線是邊沿觸發的,這些線上不能出現毛刺信号。在寫EXTI_FTSR寄存器時,在外部中斷線上的下降沿信号不能被識别,挂起位不會被置位。在同一中斷線上,可以同時設定上升沿和下降沿觸發。即任一邊沿都可觸發中斷。

 軟體中斷事件寄存器(EXTI_SWIEV)

《嵌入式 – GD32開發實戰指南》第6章 按鍵

 挂起寄存器(EXTI_PD)

《嵌入式 – GD32開發實戰指南》第6章 按鍵

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 中的中斷向量表定義一緻。

《嵌入式 – GD32開發實戰指南》第6章 按鍵

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.關注公衆号[嵌入式實驗樓]