欢迎来到Introzo百科
Introzo百科
当前位置:网站首页 > 技术 > 皮子恒嵌入式:以i.MXRT1xxx GPIO模块为例讲一下中断处理函数(IRQHandler)的标准流程

皮子恒嵌入式:以i.MXRT1xxx GPIO模块为例讲一下中断处理函数(IRQHandler)的标准流程

日期:2023-10-02 16:06


  大家好,我是皮子恒,一个认真科技的痞子。今天皮子恒给大家介绍以i.MXRT的GPIO模块为例,讲一下中断处理函数(IRQHandler)的标准流程

  在皮子恒的旧文章《串口(UART)自动波特率识别程序设计与实现(中断)》中,我们利用GPIO模块内部集成的I/O边沿检测功能来捕获RXD信号的下降沿,其中涉及到GPIO中断处理功能。中断处理函数IRQHandler是嵌入式系统中非常特殊的一类函数。它们是嵌入式系统能否实时完成任务的关键。任何中断处理函数都需要谨慎对待。

  上面的老文章中,皮子恒写的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] - 设置引脚输出电平
PSR[31:0] - 保存引脚输入电平(使用 ipg_clk_s 时钟采样)

  操作上述GPIO外设寄存器的前提是IOMUXC模块中已将Pin功能模式配置为GPIO(因为每个Pin可能被多个外设UART/Timer等复用)。比如我们在文章开头提到的旧文章中用于波特率检测的GPIO_09引脚就有以下8个复用功能,其中Alt5功能就是GPIO。

  将GPIO_09引脚设置为GPIO功能模式后,需要根据应用场景进一步配置其Pad属性。下图是Pad的内部电路结构。我们可以配置的属性有很多,比如驱动强度、速度等级、上下下拉等,这些也是在IOMUXC模块中完成的。

  在串口波特率识别检测场景中,我们需要在IOMUXC模块中将GPIO_09引脚配置为GPIO模式,并相应配置Pad属性(主要是使能内部上拉,因为串口信号空闲状态为高功率平),示例代码如下:

#include "fsl_iomuxc.h"

无效 io_pin_config(无效)
{
    CLOCK_EnableClock(kCLOCK_Iomuxc); /* iomuxc 时钟 (iomuxc_clk_enable): 0x03U */

    IOMUXC_SetPinMux(
        IOMUXC_GPIO_09_GPIOMUX_IO09, /* GPIO_09 配置为 GPIOMUX_IO09 */
        0U); /* 现场软件输入:输入路径由功能决定 */

    IOMUXC_SetPinConfig(
        IOMUXC_GPIO_09_GPIOMUX_IO09, /* GPIO_09 PAD 功能属性: */0x01B0A0U); /* 转换速率字段:慢转换速率
                                                    驱动强度场:R0/4
                                                    速度场:快速(150MHz)
                                                    漏极开路启用字段:漏极开路禁用
                                                    拉动/保持启用字段:拉动/保持器启用
                                                    拉动/保留选择字段:拉动
                                                    上拉/下拉配置。场:100K 欧姆上拉
                                                    海斯特。启用字段:迟滞启用*/
}

1.2 GPIO中断设计

  如果只是控制I/O输入输出电平,GPIO外设功能就太简陋了。为了让 GPIO 外设有更大的应用价值,IC 设计者常常为其添加边沿检测功能,比如下图蓝框标注的寄存器(这些寄存器只有在 Pin 方向配置为输入时才有效):

EDGE_SEL[31:0] - 配置是否启用Pin双边沿检测
ICRx[31:0] - 配置引脚四检测模式:低电平/高电平/上升沿/下降沿(仅当 EDGE_SEL 中未使能双沿时)

IMR[31:0] - 配置是否使能Pin中断
ISR[31:0] - 记录引脚中断状态

  边沿检测功能涉及中断响应。 i.MXRT 中为了节省中断号资源,将 16 个 Pin 分为一组。这 16 个引脚共享一个中断号。 i.MXRT1011 中共有 37 个 GPIO(即 GPIO1[31:0]、GPIO2[13:0]、GPIO5[0]),因此您将在 MIMXRT1011.h 头文件中看到以下中断号定义:

typedef 枚举 IRQn {
  /* 核心中断 */
  // ...省略

  /* 设备特定中断 */
  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_类型;

  在串口波特率识别检测场景中,我们需要在GPIO模块中将GPIO_09引脚配置为输入模式,并开启下降沿捕获中断。示例代码如下:

#include“fsl_gpio.h”

无效 io_func_config(无效)
{
    // I/O配置为输入,下降沿捕获模式
    gpio_pin_config_t sw_config = {
        kGPIO_数字输入,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;

无效GPIO1_Combined_0_15_IRQHandler(无效)
{
    // ****辅助调试1:记录中断处理函数触发执行的次数s_irqCount++;
    // ****辅助调试2:翻转GPIO1[10]
    GPIO1->DR_TOGGLE = 1U << 10;

    uint32_t 中断标志​​ = (1U << 9);
    //仅当GPIO1[9]中断发生时
    if ((GPIO_GetPinsInterruptFlags(GPIO1) & 中断标志​​) && s_pin_irq_func)
    {
        //执行一次回调函数
        s_pin_irq_func();
        // 清除GPIO1[9]中断标志
        GPIO_ClearPinsInterruptFlags(GPIO1, 中断标志​​);
    }
}

  为了进一步定位问题,我们使用另一个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;

无效GPIO1_Combined_0_15_IRQHandler(无效)
{
    //辅助调试1:记录中断处理函数触发的执行次数
    s_irqCount++;
    //辅助调试2:翻转GPIO1[10]
    GPIO1->DR_TOGGLE = 1U << 10;

    uint32_t 中断标志​​ = (1U << 9);
    //仅当GPIO1[9]中断发生时
    if ((GPIO_GetPinsInterruptFlags(GPIO1) & 中断标志​​) && s_pin_irq_func)
    {
        //执行一次回调函数
        s_pin_irq_func();

        // ****辅助调试3:添加40us延时
        如果(s_irqDelay)
        {
            微秒_延迟(s_irqDelay);
            s_irqDelay = 0;
        }
        // 清除GPIO1[9]中断标志
        GPIO_ClearPinsInterruptFlags(GPIO1, 中断标志​​);
    }
}

  以上代码测试波形如下。这种情况下,波特率识别功能就不再正常了,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 中断标志​​ = (1U << 9);
    if ((GPIO_GetPinsInterruptFlags(GPIO1) & 中断标志​​) && s_pin_irq_func)
    {
        s_pin_irq_func();
        GPIO_ClearPinsInterruptFlags(GPIO1, 中断标志​​);
        // ****改进1:在中断标志位清零后添加DSB操作或轮询状态寄存器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 中断标志​​ = (1U << 9);
    if ((GPIO_GetPinsInterruptFlags(GPIO1) & 中断标志​​) && s_pin_irq_func)
    {
        // ****改进2:先清除中断标志,再执行回调函数
        GPIO_ClearPinsInterruptFlags(GPIO1, 中断标志​​);
        // 改进1:在中断标志位清零后添加DSB操作或者回读状态寄存器ISR,确保标志位已被清零
        __DSB();
        s_pin_irq_func();

        //辅助调试3:添加40us延时
        如果(s_irqDelay)
        {
            微秒_延迟(s_irqDelay);
            s_irqDelay = 0;
        }
    }
}

  改进中断处理函数代码后,我再次使用示波器捕获波形。测试结果表明,至少没有错过第二次下降沿中断。当然,实时性还是不能保证(如果要严格记录第二次中断发生的时刻,显然做不到),但是不影响串口波特率识别应用场景的功能本文讨论了。然而,这个解决方案并不是万能的。如果在第一个中断处理函数执行过程中出现两个或多个相同类型的中断,仍然会出现错过有效中断的情况。

2.3 标准中断处理函数流程

GPIO1_INT2_IRQn = 74, /**< 来自 GPIO 的 INT2 的有效高电平中断 */ GPIO1_INT3_IRQn = 75, /**< 来自 GPIO 的 INT3 的有效高电平中断 */ GPIO1_INT4_IRQn = 76, /**< 来自 GPIO 的 INT4 的有效高电平中断 */ GPIO1_INT5_IRQn = 77, /**< 来自 GPIO 的 INT5 的有效高电平中断 */ GPIO1_INT6_IRQn = 78, /**< 来自 GPIO 的 INT6 的有效高电平中断 */ GPIO1_INT7_IRQn = 79, /**< 来自 GPIO 的 INT7 的有效高电平中断 */ GPIO1_Combined_0_15_IRQn = 80, /**< GPIO1 信号 0 至 15 的组合中断指示 */ GPIO1_Combined_16_31_IRQn = 81, /**< GPIO1 信号 16 至 31 的组合中断指示 */GPIO2_Combined_0_15_IRQn = 82, /**< GPIO2 信号 0 至 15 的组合中断指示 */ GPIO2_Combined_16_31_IRQn = 83, /**< GPIO2 信号 16 至 31 的组合中断指示 */ GPIO3_Combined_0_15_IRQn = 84, /**< GPIO3 信号 0 至 15 的组合中断指示 */ GPIO3_Combined_16_31_IRQn = 85, /**< GPIO3 信号 16 至 31 的组合中断指示 */ GPIO4_Combined_0_15_IRQn = 86, /**< GPIO4 信号 0 至 15 的组合中断指示 */ GPIO4_Combined_16_31_IRQn = 87, /**< GPIO4 信号 16 至 31 的组合中断指示 */ GPIO5_Combined_0_15_IRQn = 88, /**< GPIO5 信号 0 至 15 的组合中断指示 */GPIO5_Combined_16_31_IRQn = 89, /**< GPIO5 信号 16 至 31 的组合中断指示 */ // ...省略 }IRQn_类型;

  至此,以i.MXRT的GPIO模块为例讲一下中断处理函数(IRQHandler)的标准流程,皮子衡介绍完了,掌声在哪里~~~

欢迎订阅

文章将同时发布到我的博客园主页、CSDN主页、知乎主页、微信公众号平台。

微信搜索“痽子hengembedded”或扫描下方二维码即可在手机上立即观看。

关灯