你们好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给你们介绍的是以i.MXRT的GPIO模块为例谈谈中断处理函数(IRQHandler)的标准流程。html
在痞子衡旧文 《串口(UART)自动波特率识别程序设计与实现(中断)》里,咱们利用了 GPIO 模块内部集成的 I/O 边沿检测功能完成了 RXD 信号降低沿的捕捉,这里涉及到了 GPIO 中断处理函数。中断处理函数 IRQHandler 是嵌入式里很是特殊的一类函数,它们是嵌入式系统可以实时完成任务的关键所在,任何一个中断处理函数都须要被谨慎对待。git
上面那篇旧文里,痞子衡写的 GPIO 中断处理函数实际上是有一点瑕疵的,虽然不影响最终波特率识别功能,但其并非标准流程写法。今天痞子衡就和你们聊一聊什么是中断处理函数的标准流程:微信
GPIO 基本上能够说是 MCU 里最入门级的外设了,咱们先来简单看一下 i.MXRT1011 里 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 */ }
若是仅仅是控制 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); }
上一节铺垫那么多,如今终于到了核心的中断处理函数了,咱们在文章开头说起的那篇旧文关于串口波特率识别场景里继续聊(上位机设置发送波特率为115200),为了更好地展现问题,咱们用示波器将相关信号 RXD/TXD 拉出来观察。
以下代码便是咱们以前的中断处理函数写法,串口波特率识别接头暗号是 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] 已经被清除了(但这样并不可靠)。
这个中断处理函数还有其余问题吗?其实还有,咱们知道中断处理函数的通常原则是快进快出,即在函数里不要执行过多的代码,致使执行时间过长,影响在此期间发生的同类中断被响应。为了便于定位问题,咱们给第一次降低沿中断(时间起点)响应执行里增长额外 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.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.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; } } }
中断处理函数代码改进以后再次用示波器抓取波形,测试结果来看至少没有漏掉第二次降低沿中断,固然实时性仍是没能保证(若是要严格记录第二次中断发生的时刻,显然没法作到),不过对于本文讨论的串口波特率识别应用场景来讲倒并不影响功能。但这种解决方法并非万能的,若是第一次中断处理函数执行期间发生两次及以上同类中断,那仍是会存在漏掉有效中断的状况。
结合上面的问题展现与分析解决,如今咱们来认真探讨下什么是中断处理函数 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(); } }
最后再提一下,在部分 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主页、知乎主页、微信公众号 平台上。
微信搜索"痞子衡嵌入式"或者扫描下面二维码,就能够在手机上第一时间看了哦。