痞子衡嵌入式:以i.MXRT1xxx的GPIO模塊爲例談談中斷處理函數(IRQHandler)的標準流程


  你們好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給你們介紹的是以i.MXRT的GPIO模塊爲例談談中斷處理函數(IRQHandler)的標準流程html

  在痞子衡舊文 《串口(UART)自動波特率識別程序設計與實現(中斷)》裏,咱們利用了 GPIO 模塊內部集成的 I/O 邊沿檢測功能完成了 RXD 信號降低沿的捕捉,這裏涉及到了 GPIO 中斷處理函數。中斷處理函數 IRQHandler 是嵌入式裏很是特殊的一類函數,它們是嵌入式系統可以實時完成任務的關鍵所在,任何一箇中斷處理函數都須要被謹慎對待。git

  上面那篇舊文裏,痞子衡寫的 GPIO 中斷處理函數實際上是有一點瑕疵的,雖然不影響最終波特率識別功能,但其並非標準流程寫法。今天痞子衡就和你們聊一聊什麼是中斷處理函數的標準流程:微信

1、GPIO模塊中斷簡介

  GPIO 基本上能夠說是 MCU 裏最入門級的外設了,咱們先來簡單看一下 i.MXRT1011 裏 GPIO 模塊功能。函數

1.1 GPIO 通常設計

  i.MXRT 裏每組 GPIO 最大包含 32 個 Pin,正好對應 32bit 寄存器,下面是 GPIO 三大基礎寄存器:測試

GDIR[31:0] - 配置 Pin 的輸入/輸出方向(僅當 IOMUXC 裏配置爲 GPIO 模式)
DR[31:0]   - 設置 Pin 輸出電平
PSR[31:0]  - 保存 Pin 輸入電平(以 ipg_clk_s 時鐘來採樣)

  操做上述 GPIO 外設寄存器的前提條件是在 IOMUXC 模塊裏已將 Pin 功能模式配爲 GPIO (由於每一個 Pin 可能被多種外設UART/Timer等複用)。好比文章開頭說起的那篇舊文裏咱們用於波特率檢測的 GPIO_09 引腳,它有以下八種複用功能,其中 Alt5 功能是 GPIO。ui

  將 GPIO_09 引腳設爲 GPIO 功能模式後,還須要根據應用場景進一步配置其 Pad 屬性,下圖是 Pad 內部電路結構,咱們能夠配置的屬性有不少,好比驅動強度、速度等級、上下拉等,這些也是在 IOMUXC 模塊裏完成的。.net

  在串口波特率識別檢測場景裏,咱們須要在 IOMUXC 模塊裏將 GPIO_09 引腳配置爲 GPIO 模式,而且相應配置 Pad 屬性(主要是使能內部上拉,由於串口信號 Idle 狀態是高電平),示例代碼以下:設計

#include "fsl_iomuxc.h"

void io_pin_config(void)
{
    CLOCK_EnableClock(kCLOCK_Iomuxc);           /* iomuxc clock (iomuxc_clk_enable): 0x03U */

    IOMUXC_SetPinMux(
        IOMUXC_GPIO_09_GPIOMUX_IO09,            /* GPIO_09 is configured as GPIOMUX_IO09 */
        0U);                                    /* Software Input On Field: Input Path is determined by functionality */

    IOMUXC_SetPinConfig(
        IOMUXC_GPIO_09_GPIOMUX_IO09,            /* GPIO_09 PAD functional properties : */
        0x01B0A0U);                             /* Slew Rate Field: Slow Slew Rate
                                                    Drive Strength Field: R0/4
                                                    Speed Field: fast(150MHz)
                                                    Open Drain Enable Field: Open Drain Disabled
                                                    Pull / Keep Enable Field: Pull/Keeper Enabled
                                                    Pull / Keep Select Field: Pull
                                                    Pull Up / Down Config. Field: 100K Ohm Pull Up
                                                    Hyst. Enable Field: Hysteresis Enabled */
}

1.2 GPIO 中斷設計

  若是僅僅是控制 I/O 輸入輸出電平,那 GPIO 外設功能也太簡陋了。爲了讓 GPIO 外設具有更大的應用價值,IC 設計者每每會爲其加入邊沿檢測功能,以下圖藍框標出的寄存器(這些寄存器僅在 Pin 方向被配置爲輸入時有效):調試

EDGE_SEL[31:0] - 配置是否使能 Pin 雙邊沿檢測
ICRx[31:0]     - 配置 Pin 低電平/高電平/上升沿/降低沿四種檢測模式(僅當 EDGE_SEL 裏沒使能雙邊沿)

IMR[31:0]      - 配置是否使能 Pin 中斷
ISR[31:0]      - 記錄 Pin 中斷狀態

  邊沿檢測功能會涉及中斷響應,在 i.MXRT 裏爲了節省中斷號資源,將 16 個 Pin 編爲一組,這 16 個 Pin 共享一箇中斷號。i.MXRT1011 裏一共 37 個 GPIO(即GPIO1[31:0]、GPIO2[13:0]、GPIO5[0]),因此你在 MIMXRT1011.h 頭文件裏會看到以下中斷號定義:code

typedef enum IRQn {
  /* Core interrupts */
  // ...省略

  /* Device specific interrupts */
  GPIO1_Combined_0_15_IRQn     = 70,
  GPIO1_Combined_16_31_IRQn    = 71,
  GPIO2_Combined_0_15_IRQn     = 72,  // 沒用滿
  GPIO5_Combined_0_15_IRQn     = 73,  // 沒用滿

  // ...省略

} IRQn_Type;

  在串口波特率識別檢測場景裏,咱們須要在 GPIO 模塊裏將 GPIO_09 引腳配置爲輸入模式,且開啓降低沿捕獲中斷,示例代碼以下:

#include "fsl_gpio.h"

void io_func_config(void)
{
    // I/O 配置爲輸入,降低沿捕獲模式
    gpio_pin_config_t sw_config = {
        kGPIO_DigitalInput,
        0,
        kGPIO_IntFallingEdge,
    };

    // 初始化 GPIO1[9] 管腳
    GPIO_PinInit(GPIO1, 9, &sw_config);

    // 使能 GPIO1[9] 管腳中斷
    GPIO_PortEnableInterrupts(GPIO1, 1U << 9);

    // 配置使能系統 GPIO1 中斷
    NVIC_SetPriority(GPIO1_Combined_0_15_IRQn, 1);
    NVIC_EnableIRQ(GPIO1_Combined_0_15_IRQn);
}

2、中斷處理函數(IRQHandler)的標準流程

  上一節鋪墊那麼多,如今終於到了核心的中斷處理函數了,咱們在文章開頭說起的那篇舊文關於串口波特率識別場景裏繼續聊(上位機設置發送波特率爲115200),爲了更好地展現問題,咱們用示波器將相關信號 RXD/TXD 拉出來觀察。

2.1 有問題的中斷處理函數

2.1.1 無效中斷執行

  以下代碼便是咱們以前的中斷處理函數寫法,串口波特率識別接頭暗號是 0x5A、0XA6,從信號時序上看一共有 7 個降低沿,原理上這個中斷處理函數應該被觸發執行 7 次(也是 s_pin_irq_func 執行次數),咱們額外加個輔助調試變量 s_irqCount,按理說識別結束後這個變量值應該等於 7,但實際上它的值是 12,即多進了 5 次中斷,這顯然不太合理。不過合理的是 s_pin_irq_func 確實只執行了 7 次。

// 輔助調試變量1
uint32_t s_irqCount = 0;

void GPIO1_Combined_0_15_IRQHandler(void)
{
    // ****輔助調試1:記錄中斷處理函數觸發執行次數
    s_irqCount++;
    // ****輔助調試2:翻轉 GPIO1[10]
    GPIO1->DR_TOGGLE = 1U << 10;

    uint32_t interrupt_flag = (1U << 9);
    // 僅當GPIO1[9]中斷髮生時
    if ((GPIO_GetPinsInterruptFlags(GPIO1) & interrupt_flag) && s_pin_irq_func)
    {
        // 執行一次回調函數
        s_pin_irq_func();
        // 清除GPIO1[9]中斷標誌
        GPIO_ClearPinsInterruptFlags(GPIO1, interrupt_flag);
    }
}

  爲了進一步定位問題,咱們用另外一個 GPIO1[10] 來輔助,將其配置爲 GPIO 輸出模式,初值爲高,在中斷處理函數裏作一次翻轉,而後用示波器同時抓取 GPIO1[10:9],波形以下,能夠看到中間的每一個降低沿均連續觸發了兩次中斷處理函數的執行:

  這個問題其實跟 ARM Errata 838869 有關,在Cortex-M4/7 上,若是 CPU 執行速度(此處 i.MXRT1011 工做在 500MHz 主頻下)遠遠高於 GPIO 外設寄存器寫入速度(1/4 主頻),中斷處理函數代碼裏在退出前才清中斷標誌位 ISR[9] 的話,會致使中斷標誌位尚未真正被清除掉,CPU 當即又再次執行中斷處理函數(只要 ISR 寄存器裏標誌位仍處於置位狀態)。至於功能回調函數 s_pin_irq_func 沒有被誤執行,是由於中斷處理函數裏有中斷狀態位置起判斷語句,剛好執行到這裏的時候,狀態位 ISR[9] 已經被清除了(但這樣並不可靠)。

2.1.2 漏掉有效中斷

  這個中斷處理函數還有其餘問題嗎?其實還有,咱們知道中斷處理函數的通常原則是快進快出,即在函數裏不要執行過多的代碼,致使執行時間過長,影響在此期間發生的同類中斷被響應。爲了便於定位問題,咱們給第一次降低沿中斷(時間起點)響應執行裏增長額外 40us 的延時,故意讓其錯過第二次降低沿中斷(3bit * (1s/115200bit) = 26.04us)但不要錯過第三次降低沿中斷(6bit * (1s/115200bit) = 52.08us)。

// 輔助調試變量1
uint32_t s_irqCount = 0;
// 輔助調試變量2
uint32_t s_irqDelay = 40;

void GPIO1_Combined_0_15_IRQHandler(void)
{
    // 輔助調試1:記錄中斷處理函數觸發執行次數
    s_irqCount++;
    // 輔助調試2:翻轉 GPIO1[10]
    GPIO1->DR_TOGGLE = 1U << 10;

    uint32_t interrupt_flag = (1U << 9);
    // 僅當GPIO1[9]中斷髮生時
    if ((GPIO_GetPinsInterruptFlags(GPIO1) & interrupt_flag) && s_pin_irq_func)
    {
        // 執行一次回調函數
        s_pin_irq_func();

        // ****輔助調試3:增長一次 40us 的延時
        if (s_irqDelay)
        {
            microseconds_delay(s_irqDelay);
            s_irqDelay = 0;
        }
        // 清除GPIO1[9]中斷標誌
        GPIO_ClearPinsInterruptFlags(GPIO1, interrupt_flag);
    }
}

  上述代碼測試波形圖以下,這種狀況下波特率識別功能已經不正常,s_irqCount 值爲 11,更關鍵的是 s_pin_irq_func 僅被執行了 6 次,漏掉了 1 次。由於這 40us 的延時,致使第二次降低沿中斷沒有被及時響應,能夠理解爲第一次中斷處理函數執行退出前清除中斷標誌位操做一次性清除了兩次中斷狀態位的置起行爲。

2.2 解決中斷處理函數裏的問題

2.2.1 避免無效中斷執行

  基於 2.1.1 節最後的分析,咱們改進代碼以下:

void GPIO1_Combined_0_15_IRQHandler(void)
{
    // 輔助調試2:翻轉 GPIO1[10]
    GPIO1->DR_TOGGLE = 1U << 10;

    uint32_t interrupt_flag = (1U << 9);
    if ((GPIO_GetPinsInterruptFlags(GPIO1) & interrupt_flag) && s_pin_irq_func)
    {
        s_pin_irq_func();
        GPIO_ClearPinsInterruptFlags(GPIO1, interrupt_flag);
        // ****改進1:中斷標誌清除以後加 DSB 操做或者 poll 狀態寄存器 ISR 確保標誌位已被清除
        __DSB();
    }
}

  中斷處理函數代碼改進以後再次用示波器抓取波形,測試結果就正常了:

2.2.2 避免漏掉有效中斷

  基於 2.1.2 節最後的分析,咱們改進代碼以下:

void GPIO1_Combined_0_15_IRQHandler(void)
{
    // 輔助調試2:翻轉 GPIO1[10]
    GPIO1->DR_TOGGLE = 1U << 10;

    uint32_t interrupt_flag = (1U << 9);
    if ((GPIO_GetPinsInterruptFlags(GPIO1) & interrupt_flag) && s_pin_irq_func)
    {
        // ****改進2:先清除中斷標誌,再執行回調函數
        GPIO_ClearPinsInterruptFlags(GPIO1, interrupt_flag);
        // 改進1:中斷標誌清除以後加 DSB 操做或者回讀狀態寄存器 ISR 確保標誌位已被清除
        __DSB();
        s_pin_irq_func();

        // 輔助調試3:增長一次 40us 的延時
        if (s_irqDelay)
        {
            microseconds_delay(s_irqDelay);
            s_irqDelay = 0;
        }
    }
}

  中斷處理函數代碼改進以後再次用示波器抓取波形,測試結果來看至少沒有漏掉第二次降低沿中斷,固然實時性仍是沒能保證(若是要嚴格記錄第二次中斷髮生的時刻,顯然沒法作到),不過對於本文討論的串口波特率識別應用場景來講倒並不影響功能。但這種解決方法並非萬能的,若是第一次中斷處理函數執行期間發生兩次及以上同類中斷,那仍是會存在漏掉有效中斷的狀況。

2.3 標準的中斷處理函數流程

  結合上面的問題展現與分析解決,如今咱們來認真探討下什麼是中斷處理函數 IRQHandler 的標準流程,痞子衡認爲主要分爲以下四步:第一步是對中斷狀態位的置起作一次確認(可選項,有些外設不必定有狀態位),第二步是當即清除狀態標誌,第三步是確保狀態標誌已被清除,第四步纔是執行真正的中斷處理任務(這個任務執行時間要越短越好,最好就是僅記錄必要的信息,等中斷退出後進入主循環時再具體展開任務),故中斷處理函數標準模板以下:

void xxx_IRQHandler(void)
{
    // Step 1: 檢查狀態標誌位是否有效
    if ((xxx_IsInterruptFlagSet() && s_irq_func)
    {
        // Step 2: 清除狀態標誌位
        xxx_ClearInterruptFlag();
        // Step 3: 確保狀態標誌位已被清除
        __DSB();
        // Step 4: 執行回調函數,時間越短越好
        s_irq_func();
    }
}

3、番外篇 - 神奇的GPIO1[7:0]

  最後再提一下,在部分 i.MXRT 型號上,關於中斷號資源,GPIO1[7:0] 地位與其餘 GPIO 引腳不太同樣,它們還會有專門的中斷號,好比在 MIMXRT1062.h 頭文件裏你能夠看到 GPIO1_INTx_IRQn:

typedef enum IRQn {
  /* Core interrupts */
  // ...省略

  /* Device specific interrupts */
  GPIO1_INT0_IRQn              = 72,               /**< Active HIGH Interrupt from INT0 from GPIO */
  GPIO1_INT1_IRQn              = 73,               /**< Active HIGH Interrupt from INT1 from GPIO */
  GPIO1_INT2_IRQn              = 74,               /**< Active HIGH Interrupt from INT2 from GPIO */
  GPIO1_INT3_IRQn              = 75,               /**< Active HIGH Interrupt from INT3 from GPIO */
  GPIO1_INT4_IRQn              = 76,               /**< Active HIGH Interrupt from INT4 from GPIO */
  GPIO1_INT5_IRQn              = 77,               /**< Active HIGH Interrupt from INT5 from GPIO */
  GPIO1_INT6_IRQn              = 78,               /**< Active HIGH Interrupt from INT6 from GPIO */
  GPIO1_INT7_IRQn              = 79,               /**< Active HIGH Interrupt from INT7 from GPIO */
  GPIO1_Combined_0_15_IRQn     = 80,               /**< Combined interrupt indication for GPIO1 signal 0 throughout 15 */
  GPIO1_Combined_16_31_IRQn    = 81,               /**< Combined interrupt indication for GPIO1 signal 16 throughout 31 */
  GPIO2_Combined_0_15_IRQn     = 82,               /**< Combined interrupt indication for GPIO2 signal 0 throughout 15 */
  GPIO2_Combined_16_31_IRQn    = 83,               /**< Combined interrupt indication for GPIO2 signal 16 throughout 31 */
  GPIO3_Combined_0_15_IRQn     = 84,               /**< Combined interrupt indication for GPIO3 signal 0 throughout 15 */
  GPIO3_Combined_16_31_IRQn    = 85,               /**< Combined interrupt indication for GPIO3 signal 16 throughout 31 */
  GPIO4_Combined_0_15_IRQn     = 86,               /**< Combined interrupt indication for GPIO4 signal 0 throughout 15 */
  GPIO4_Combined_16_31_IRQn    = 87,               /**< Combined interrupt indication for GPIO4 signal 16 throughout 31 */
  GPIO5_Combined_0_15_IRQn     = 88,               /**< Combined interrupt indication for GPIO5 signal 0 throughout 15 */
  GPIO5_Combined_16_31_IRQn    = 89,               /**< Combined interrupt indication for GPIO5 signal 16 throughout 31 */

  // ...省略

} IRQn_Type;

  至此,以i.MXRT的GPIO模塊爲例談談中斷處理函數(IRQHandler)的標準流程痞子衡便介紹完畢了,掌聲在哪裏~~~

歡迎訂閱

文章會同時發佈到個人 博客園主頁CSDN主頁知乎主頁微信公衆號 平臺上。

微信搜索"痞子衡嵌入式"或者掃描下面二維碼,就能夠在手機上第一時間看了哦。

相關文章
相關標籤/搜索