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 中断注册与处理
中断注册,我一般分三步走:
- 获取中断号:通过
gpiod_to_irq()把GPIO描述符转成中断号。 - 注册中断:调用
request_threaded_irq()或devm_request_threaded_irq()。 - 实现中断处理函数:分顶半部和底半部。
代码示例:
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和中断管理的核心流程串起来了:
4.6 常见故障排查
最后,分享几个我在RK平台上遇到过的GPIO和中断问题:
| 现象 | 原因 | 解决方法 |
|---|---|---|
| 中断不触发 | 设备树GPIO配置错误,或中断号获取失败 | 检查/sys/kernel/debug/gpio确认GPIO状态 |
| 中断风暴 | 缺少IRQF_ONESHOT,或中断处理未清除标志 | 加上IRQF_ONESHOT,确认硬件中断状态寄存器已清除 |
| 系统卡死 | 中断上下文调用了睡眠函数 | 检查顶半部代码,把耗时操作移到工作队列 |
| 工作队列不执行 | 工作队列被频繁调度,或系统负载过高 | 使用mod_delayed_work加最小间隔 |
嗯,GPIO和中断管理这块,说白了就是「规矩」二字。只要遵守中断上下文不睡眠、顶半部只做最少处理、耗时操作扔工作队列这几个原则,大部分问题都能避免。我当初踩过的坑,希望你别再踩一遍。