什么是中断处理函数(IRQHandler)的标准流程?
⼤家好,我是痞⼦衡,是正经搞技术的痞⼦。今天痞⼦衡给⼤家介绍的是以i.MXRT的GPIO模块为例谈谈中断处理函数(IRQHandler)的标准流程。
在痞⼦衡旧⽂ ⾥,我们利⽤了 GPIO 模块内部集成的 I/O 边沿检测功能完成了 RXD 信号下降沿的捕捉,这⾥涉及到了 GPIO 中断处理函数。中断处理函数 IRQHandler 是嵌⼊式⾥⾮常特殊的⼀类函数,它们是嵌⼊式系统能够实时完成任务的关键所在,任何⼀个中断处理函数都需要被谨慎对待。
上⾯那篇旧⽂⾥,痞⼦衡写的 GPIO 中断处理函数其实是有⼀点瑕疵的,虽然不影响最终波特率识别功能,但其并不是标准流程写法。今天痞⼦衡就和⼤家聊⼀聊什么是中断处理函数的标准流程:
⼀、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。
将 GPIO_09 引脚设为 GPIO 功能模式后,还需要根据应⽤场景进⼀步配置其 Pad 属性,下图是 Pad 内部电路结构,我们可以配置的属性有很多,⽐如驱动强度、速度等级、上下拉等,这些也是在 IOMUXC 模块⾥完成的。
在串⼝波特率识别检测场景⾥,我们需要在 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 头⽂件⾥会看到如下中断号定义:
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);
}
⼆、中断处理函数(IRQHandler)的标准流程
上⼀节铺垫那么多,现在终于到了核⼼的中断处理函数了,我们在⽂章开头提及的那篇旧⽂关于串⼝波特率识别场景⾥继续聊(上位机设置发送波特率为115200)。
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)
{
microconds_delay(s_irqDelay);
s_irqDelay = 0;
}
// 清除GPIO1[9]中断标志
GPIO_ClearPinsInterruptFlags(GPIO1, interrupt_flag);
}
}
上述代码测试波形图如下,这种情况下波特率识别功能已经不正常,s_irqCount 值为 11,更关键的是 s_pin_irq_func 仅被执⾏了 6
次,漏掉了 1 次。因为这 40us 的延时,导致第⼆次下降沿中断没有被及时响应,可以理解为第⼀次中断处理函数执⾏退出前清除中断标志位操作⼀次性清除了两次中断状态位的置起⾏为。