4. GPIO与中断管理:GPIO子系统框架、设备树中GPIO配置、中断注册与处理、中断上下文与工作队列

GPIO和中断,是传感器驱动里最基础也最容易出问题的两个环节。我刚开始做RK平台驱动时,就因为在中断上下文里调了个msleep(),导致系统直接卡死。从那以后,我对中断上下文和工作队列的敬畏心就特别强。

这一章,咱们把GPIO子系统框架、设备树配置、中断注册流程、以及中断上下文切换这几个硬骨头啃下来。说白了,就是让你知道:GPIO怎么配、中断怎么注册、中断来了以后代码该在哪儿跑。

4.1 GPIO子系统框架

Linux内核的GPIO子系统,分三层:

  • GPIO硬件层:芯片厂商实现的底层寄存器操作,RK平台这部分在drivers/pinctrl/rockchip/里。
  • GPIO核心层:提供统一的gpio_request()、gpio_direction_input()等API。
  • GPIO消费层:我们驱动开发者用的地方,通过gpiod_get()获取描述符,然后操作。

我个人习惯,在RK平台上尽量用gpiod_系列API,而不是老的gpio_系列。因为前者能自动处理设备树里的GPIO_ACTIVE_LOW标志,省去手动取反的麻烦。

核心API对照表

老API新API (推荐)说明
gpio_request()gpiod_get()获取GPIO描述符
gpio_direction_input()gpiod_direction_input()设置输入方向
gpio_to_irq()gpiod_to_irq()获取中断号
gpio_set_value()gpiod_set_value()设置输出电平

4.2 设备树中GPIO配置

设备树里配GPIO,是驱动能跑起来的第一步。RK平台的设备树GPIO描述格式是这样的:

/* 设备树节点示例 */
sensor@0 {
    compatible = "rockchip,sensor-example";
    reg = <0x00>;
    
    /* GPIO描述:<&gpio1 RK_PB2 GPIO_ACTIVE_LOW> */
    irq-gpios = <&gpio1 RK_PB2 GPIO_ACTIVE_LOW>;
    power-gpios = <&gpio0 RK_PA5 GPIO_ACTIVE_HIGH>;
    
    interrupt-parent = <&gpio1>;
    interrupts = <RK_PB2 IRQ_TYPE_EDGE_FALLING>;
};

这里有个坑,我踩过好几次:interrupt-parent和interrupts属性,跟irq-gpios是两套不同的中断描述方式。前者走的是中断控制器层级,后者走的是GPIO子系统。在RK平台上,传感器驱动建议用irq-gpios方式,因为更直观,而且能自动处理电平反转。

注意:设备树里GPIO_ACTIVE_LOW和GPIO_ACTIVE_HIGH只影响gpiod_set_value()的行为,不影响中断触发电平。中断触发电平由IRQ_TYPE_EDGE_FALLING这类标志单独控制。

4.3 中断注册与处理

中断注册,我一般分三步走:

  1. 获取中断号:通过gpiod_to_irq()把GPIO描述符转成中断号。
  2. 注册中断:调用request_threaded_irq()或devm_request_threaded_irq()。
  3. 实现中断处理函数:分顶半部和底半部。

代码示例:

static irqreturn_t sensor_irq_handler(int irq, void *dev_id)
{
    struct sensor_data *data = dev_id;
    
    /* 顶半部:只做最少的处理,比如清除中断标志 */
    /* 注意:这里不能调用可能导致睡眠的函数 */
    
    /* 调度工作队列处理实际业务 */
    schedule_work(&data->work);
    
    return IRQ_HANDLED;
}

static int sensor_probe(struct platform_device *pdev)
{
    struct sensor_data *data;
    int irq, ret;
    
    /* ... 分配data结构体,获取GPIO描述符 ... */
    
    irq = gpiod_to_irq(data->irq_gpio);
    if (irq < 0) {
        dev_err(&pdev->dev, "failed to get irq\n");
        return irq;
    }
    
    ret = devm_request_threaded_irq(&pdev->dev, irq,
                    sensor_irq_handler,   /* 顶半部 */
                    NULL,                 /* 线程化处理函数,这里用工作队列替代 */
                    IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                    "sensor_irq", data);
    if (ret) {
        dev_err(&pdev->dev, "failed to request irq\n");
        return ret;
    }
    
    return 0;
}

为什么我用IRQF_ONESHOT?因为传感器中断通常是电平触发,如果不加这个标志,中断处理期间如果电平没恢复,会反复触发中断,导致系统卡死。我当初在调试一个气压传感器时,就因为这个标志没加,中断风暴直接把CPU干到100%。

4.4 中断上下文与工作队列

中断上下文,说白了就是「不能睡觉」的上下文。在中断处理函数里,你不能:

  • 调用msleep()、usleep_range()等睡眠函数
  • 获取信号量(mutex)
  • 执行可能导致调度的操作(比如kmalloc(GFP_KERNEL))

那传感器驱动里,读取数据、处理数据这些耗时操作怎么办?答案是:扔到工作队列里去。

工作队列,是内核提供的一种机制,让你把任务推迟到进程上下文执行。在进程上下文里,你可以随便睡眠、随便分配内存。

我的经验:传感器驱动里,中断顶半部只做两件事:读取中断状态寄存器确认中断源,然后调度工作队列。所有I2C/SPI读写操作,全部放在工作队列里做。这样既保证了中断响应速度,又避免了在中断上下文里做耗时操作。

工作队列的使用示例:

/* 在probe函数里初始化工作队列 */
INIT_WORK(&data->work, sensor_work_handler);

/* 工作队列处理函数 */
static void sensor_work_handler(struct work_struct *work)
{
    struct sensor_data *data = container_of(work, struct sensor_data, work);
    
    /* 这里可以安全地调用i2c_read()、msleep()等 */
    sensor_read_data(data);
    sensor_process_data(data);
    
    /* 上报数据到输入子系统或IIO框架 */
    sensor_report_data(data);
}

/* 中断顶半部 */
static irqreturn_t sensor_irq_handler(int irq, void *dev_id)
{
    struct sensor_data *data = dev_id;
    
    /* 只做最少的处理 */
    schedule_work(&data->work);
    
    return IRQ_HANDLED;
}

这里有个细节:如果中断频率很高,工作队列可能来不及处理。我建议用mod_delayed_work()加一个最小间隔,比如10ms,避免工作队列被频繁调度。

4.5 知识结构总览

下面这张图,把GPIO和中断管理的核心流程串起来了:

GPIO与中断管理核心流程 GPIO与中断管理核心流程 设备树GPIO配置 GPIO子系统框架 中断注册与处理 中断上下文(顶半部) 工作队列(底半部) 进程上下文(I2C/SPI读写) 关键约束: • 中断上下文不能睡眠、不能获取mutex • 工作队列可以睡眠、可以分配内存 • 传感器I2C/SPI读写必须在进程上下文 • 顶半部只做最少处理,调度工作队列 • 使用IRQF_ONESHOT防止中断风暴 • 高频中断建议用mod_delayed_work限流

4.6 常见故障排查

最后,分享几个我在RK平台上遇到过的GPIO和中断问题:

现象原因解决方法
中断不触发设备树GPIO配置错误,或中断号获取失败检查/sys/kernel/debug/gpio确认GPIO状态
中断风暴缺少IRQF_ONESHOT,或中断处理未清除标志加上IRQF_ONESHOT,确认硬件中断状态寄存器已清除
系统卡死中断上下文调用了睡眠函数检查顶半部代码,把耗时操作移到工作队列
工作队列不执行工作队列被频繁调度,或系统负载过高使用mod_delayed_work加最小间隔

嗯,GPIO和中断管理这块,说白了就是「规矩」二字。只要遵守中断上下文不睡眠、顶半部只做最少处理、耗时操作扔工作队列这几个原则,大部分问题都能避免。我当初踩过的坑,希望你别再踩一遍。