4、GPIO中断编程:中断控制器GIC简介、GPIO中断触发方式、中断服务函数编写、中断优先级配置

好,咱们进入第四章。这一章讲中断,算是驱动开发里比较核心的一块了。我刚开始做嵌入式那会儿,总觉得轮询也能凑合用,直到项目里需要处理高速数据流,CPU被占得死死的,才意识到中断有多重要。说白了,中断就是让外设在有事的时候主动通知CPU,而不是让CPU一直傻等着。

4.1 中断控制器GIC简介

ARM架构里,中断管理靠的是GIC(Generic Interrupt Controller)。GIC负责接收各个外设的中断信号,然后统一分发给CPU核心。你想想看,如果没有GIC,每个外设都直接连CPU,那CPU得有多少个引脚才够用?

GIC主要分两代:GICv2和GICv3。Cortex-A系列处理器大多用GICv2,Cortex-A53/A72这些64位处理器开始支持GICv3。咱们课程基于Cortex-A7,所以重点讲GICv2。

特性 GICv2 GICv3
支持CPU数量 最多8个 无限制
中断号范围 0-1019 0-65535
寄存器接口 内存映射 内存映射 + 系统寄存器
中断分组 Group 0/1 Group 0/1/S

GIC内部有两个关键模块:分发器(Distributor)和CPU接口(CPU Interface)。分发器负责中断的使能、优先级配置、触发方式设置;CPU接口则负责把中断信号送给具体的CPU核心。

核心概念:GIC把中断分为三类——SPI(共享外设中断)、PPI(私有外设中断)、SGI(软件触发中断)。GPIO中断通常属于SPI,中断号在32-1019之间。

GICv2 中断处理流程 GPIO控制器 定时器 UART 分发器 使能/优先级/触发 中断号映射 CPU接口 中断屏蔽 中断应答 CPU核心 外设中断 → 分发器(配置/路由) → CPU接口(屏蔽/应答) → CPU核心(执行ISR) GIC负责中断的收集、优先级仲裁和分发,CPU只需处理最终到达的中断

4.2 GPIO中断触发方式

GPIO中断的触发方式,说白了就是告诉GIC:什么情况下算一次中断事件。ARM芯片的GPIO模块通常支持以下几种触发方式:

  • 上升沿触发:信号从低到高跳变时触发
  • 下降沿触发:信号从高到低跳变时触发
  • 双边沿触发:上升沿和下降沿都触发
  • 高电平触发:信号保持高电平时持续触发
  • 低电平触发:信号保持低电平时持续触发

我的经验:按键检测一般用下降沿触发,配合消抖处理。如果是电平触发,记得在中断服务函数里清除中断标志,否则会一直触发,CPU就卡死了。我曾经在一个项目里忘了清标志,结果系统启动后直接死循环,查了半天才发现问题。

配置触发方式,通常操作GPIO控制器的几个寄存器。以某款Cortex-A7芯片为例:

/* GPIO中断触发方式配置 */
#define GPIO_BASE      0x11000000
#define GPIO_ISR       0x00    /* 中断触发类型寄存器 */
#define GPIO_IBE       0x04    /* 双边沿使能寄存器 */
#define GPIO_IEV       0x08    /* 中断触发极性寄存器 */

void gpio_irq_set_trigger(int pin, int trigger_mode)
{
    uint32_t reg_val;
    
    switch(trigger_mode) {
        case TRIGGER_RISING_EDGE:
            /* 设置电平触发,上升沿 */
            reg_val = readl(GPIO_BASE + GPIO_ISR);
            reg_val &= ~(1 << pin);   /* 边沿触发 */
            writel(reg_val, GPIO_BASE + GPIO_ISR);
            
            reg_val = readl(GPIO_BASE + GPIO_IEV);
            reg_val |= (1 << pin);    /* 上升沿 */
            writel(reg_val, GPIO_BASE + GPIO_IEV);
            break;
            
        case TRIGGER_FALLING_EDGE:
            reg_val = readl(GPIO_BASE + GPIO_ISR);
            reg_val &= ~(1 << pin);
            writel(reg_val, GPIO_BASE + GPIO_ISR);
            
            reg_val = readl(GPIO_BASE + GPIO_IEV);
            reg_val &= ~(1 << pin);   /* 下降沿 */
            writel(reg_val, GPIO_BASE + GPIO_IEV);
            break;
            
        case TRIGGER_HIGH_LEVEL:
            reg_val = readl(GPIO_BASE + GPIO_ISR);
            reg_val |= (1 << pin);    /* 电平触发 */
            writel(reg_val, GPIO_BASE + GPIO_ISR);
            
            reg_val = readl(GPIO_BASE + GPIO_IEV);
            reg_val |= (1 << pin);    /* 高电平 */
            writel(reg_val, GPIO_BASE + GPIO_IEV);
            break;
    }
}

4.3 中断服务函数编写

中断服务函数(ISR)是中断处理的灵魂。写ISR有几个铁律,我刚开始写驱动时踩过不少坑,这里给你总结一下:

ISR编写铁律:

  • ISR要短小精悍,不要在中断里做复杂运算
  • ISR里不能调用可能导致睡眠的函数(如mutex_lock、kmalloc)
  • 必须清除中断标志,否则会反复触发
  • 如果需要处理大量数据,用中断底半部(tasklet/workqueue)

在Linux内核里注册GPIO中断的流程是这样的:

/* GPIO中断服务函数示例 */
static irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
    struct gpio_dev *dev = (struct gpio_dev *)dev_id;
    
    /* 1. 读取中断状态,确认中断源 */
    uint32_t status = readl(dev->base + GPIO_MIS);
    if (!(status & (1 << dev->pin)))
        return IRQ_NONE;
    
    /* 2. 清除中断标志 */
    writel((1 << dev->pin), dev->base + GPIO_IC);
    
    /* 3. 处理中断事件(这里只做标记,实际处理放到底半部) */
    dev->irq_count++;
    schedule_work(&dev->work);
    
    /* 4. 返回IRQ_HANDLED表示已处理 */
    return IRQ_HANDLED;
}

/* 中断注册 */
static int gpio_irq_init(struct platform_device *pdev)
{
    struct gpio_dev *dev = platform_get_drvdata(pdev);
    int ret;
    
    /* 获取中断号 */
    dev->irq = platform_get_irq(pdev, 0);
    if (dev->irq < 0)
        return dev->irq;
    
    /* 注册中断 */
    ret = request_irq(dev->irq, gpio_irq_handler,
                      IRQF_TRIGGER_FALLING, "gpio_key", dev);
    if (ret) {
        dev_err(&pdev->dev, "request_irq failed\n");
        return ret;
    }
    
    return 0;
}

这里有个细节:request_irq的第三个参数是中断标志,可以指定触发方式。我个人习惯在设备树里配好触发方式,代码里用IRQF_TRIGGER_NONE,让内核自动解析设备树配置。

4.4 中断优先级配置

中断优先级决定了多个中断同时到达时,CPU先处理哪个。GICv2支持256级优先级(0-255),数值越小优先级越高。嗯,这里要注意:不同芯片厂商的实现可能不一样,有的只支持16级或32级。

优先级配置主要在GIC的分发器里操作:

/* GIC优先级配置 */
#define GIC_DIST_BASE   0x10400000
#define GICD_IPRIORITYR 0x400   /* 优先级寄存器基址 */

void gic_set_irq_priority(int irq, int priority)
{
    uint32_t reg_offset;
    int byte_offset;
    uint32_t reg_val;
    
    /* 每个中断占用8位,4个中断占一个32位寄存器 */
    reg_offset = GICD_IPRIORITYR + (irq / 4) * 4;
    byte_offset = (irq % 4) * 8;
    
    reg_val = readl(GIC_DIST_BASE + reg_offset);
    reg_val &= ~(0xFF << byte_offset);
    reg_val |= (priority & 0xFF) << byte_offset;
    writel(reg_val, GIC_DIST_BASE + reg_offset);
}
优先级值 含义 典型用途
0-31 最高优先级 系统时钟、紧急事件
32-95 高优先级 高速外设(DMA、以太网)
96-159 中优先级 GPIO、UART、I2C
160-255 低优先级 按键、温度传感器

避坑指南:我曾经在一个项目里把GPIO中断优先级设得比系统定时器还高,结果导致系统调度紊乱,任务无法按时切换。记住:系统定时器中断优先级一定要高于普通外设中断,否则系统会变得不稳定。

配置完优先级后,还要设置中断目标CPU。GICv2里每个中断可以路由到特定的CPU核心:

/* 设置中断目标CPU */
#define GICD_ITARGETSR  0x800

void gic_set_irq_cpu(int irq, int cpu_mask)
{
    uint32_t reg_offset;
    int byte_offset;
    uint32_t reg_val;
    
    reg_offset = GICD_ITARGETSR + (irq / 4) * 4;
    byte_offset = (irq % 4) * 8;
    
    reg_val = readl(GIC_DIST_BASE + reg_offset);
    reg_val &= ~(0xFF << byte_offset);
    reg_val |= (cpu_mask & 0xFF) << byte_offset;
    writel(reg_val, GIC_DIST_BASE + reg_offset);
}

多核处理器里,一般把中断分散到不同核心上,避免某个核心负载过高。比如网卡中断给CPU0,GPIO中断给CPU1,这样能提升整体性能。

好了,这一章的内容就这些。中断编程的核心就是理解GIC的工作机制,正确配置触发方式,写好ISR,再合理分配优先级。把这些搞明白了,大部分外设中断驱动你都能拿下来。