Android全栈11 分钟阅读5888 字
【全栈第17课】中断 + GPIO + workqueue
Android 全栈工程师进阶教程 第17课。中断 + GPIO + workqueue。基于公开 AOSP 知识整理,含原理、可编译示例、常见问题定位。
> 本文是《Android 全栈工程师进阶教程》系列第 17 课。完整 26 课见 GitHub 仓库。
【全栈第17课】中断 + GPIO + workqueue
0. 这节课你将做出什么
写一个能响应硬件中断的驱动:从 DTS 拿中断 GPIO、申请中断、中断来了用 workqueue 在下半部处理。 学完你能回答:触控 IC 有数据了怎么通知 CPU(而不是 CPU 一直轮询)?为什么中断处理函数里不能睡眠、不能做 I2C?"上半部/下半部"是什么?
1. 背景:轮询 vs 中断
CPU 怎么知道触控 IC 有新数据了?两种办法:
- 轮询:CPU 每隔几毫秒主动去问 IC"有数据吗"——浪费 CPU、费电、还有延迟。
- 中断:IC 有数据时,拉一根中断信号线(GPIO),CPU 被硬件打断,立刻去处理——高效、省电、低延迟。
几乎所有外设(触控、传感器、按键)都用中断上报数据。这是嵌入式/驱动的核心机制。
2. 全局图:中断上半部/下半部
硬件(IC 有数据)── 拉中断脚(GPIO)──▶ CPU 被打断
│
▼ 上半部(hardirq):极快!不能睡眠、不能久留
│ 只做:记个标记 + schedule_work() 调度下半部 → 立刻返回
│
▼ 下半部(workqueue,进程上下文):可以睡眠、可以做 I2C
regmap_read 读 IC 数据寄存器 → input_report 上报坐标/事件
为什么要分上半部/下半部? 中断期间通常关抢占(甚至关中断),如果在中断里慢慢做 I2C 读(I2C 传输要等、会睡眠),整个系统会卡死。所以上半部只能极快地"记一下、安排活",真正耗时的活丢到下半部(workqueue)在普通进程上下文里做。
3. 真实印证
真实触控驱动的中断流程就是这套:
- DTS 里
interrupts/interrupt-gpios指定中断脚 - probe 里
request_threaded_irq申请中断 - 中断来 → 下半部读 IC 触摸数据 →
input_report_abs上报坐标 → 用户层收到触摸事件
4. 前置准备
- 依赖 L15(platform driver/DTS)、L16(I2C 读数据)。
- 真编译:本机 Linux 6.17(已验证)。
- 文件:
hello_irq.c、hello-irq.dtsi、Makefile。
5. 动手:写中断驱动
5.1 DTS 中断脚 hello-irq.dtsi
&soc {
hello_irq_dev {
compatible = "xiaomi,hello-irq";
irq-gpios = <&tlmm 42 0>; // 中断脚:tlmm GPIO 控制器的 42 号脚
};
};
5.2 中断驱动 hello_irq.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h> // request_threaded_irq
#include <linux/gpio/consumer.h> // gpiod_*
#include <linux/workqueue.h>
#include <linux/of.h>
#include <linux/mod_devicetable.h>
struct hello_irq_data {
struct device *dev;
int irq;
struct gpio_desc *irq_gpio;
struct work_struct work; // 下半部 work
int irq_count;
};
// 下半部:进程上下文,可睡眠、可做 I2C、可上报事件
static void hello_work_handler(struct work_struct *work) {
struct hello_irq_data *d = container_of(work, struct hello_irq_data, work);
dev_info(d->dev, "下半部: 处理第 %d 次中断(这里可 regmap_read 读数据、input_report 上报)\n",
d->irq_count);
}
// 上半部:极快,只记标记 + 调度下半部,立刻返回
static irqreturn_t hello_irq_handler(int irq, void *id) {
struct hello_irq_data *d = id;
d->irq_count++;
schedule_work(&d->work); // 把耗时活丢给 workqueue
return IRQ_HANDLED; // 告诉内核这个中断处理了
}
static int hello_probe(struct platform_device *pdev) {
struct hello_irq_data *d = devm_kzalloc(&pdev->dev, sizeof(*d), GFP_KERNEL);
int ret;
if (!d) return -ENOMEM;
d->dev = &pdev->dev;
INIT_WORK(&d->work, hello_work_handler); // 初始化 work,绑下半部函数
// ① 从 DTS 拿中断 GPIO,设为输入
d->irq_gpio = devm_gpiod_get(&pdev->dev, "irq", GPIOD_IN);
if (IS_ERR(d->irq_gpio)) return PTR_ERR(d->irq_gpio);
// ② GPIO 转中断号
d->irq = gpiod_to_irq(d->irq_gpio);
if (d->irq < 0) return d->irq;
// ③ 申请中断:下降沿触发、threaded(ONESHOT 配合 threaded)
ret = devm_request_threaded_irq(&pdev->dev, d->irq,
hello_irq_handler, // 上半部
NULL, // 线程下半部(这里用 workqueue 代替,传 NULL)
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"hello-irq", d);
if (ret) { dev_err(&pdev->dev, "request_irq 失败 %d\n", ret); return ret; }
platform_set_drvdata(pdev, d);
dev_info(&pdev->dev, "probe ok, irq=%d\n", d->irq);
return 0;
}
static void hello_remove(struct platform_device *pdev) { // ★6.11+ void
struct hello_irq_data *d = platform_get_drvdata(pdev);
cancel_work_sync(&d->work); // ★卸载前确保 work 跑完,防 use-after-free
dev_info(&pdev->dev, "remove, 共 %d 次中断\n", d->irq_count);
}
static const struct of_device_id m[] = { { .compatible = "xiaomi,hello-irq" }, { } };
MODULE_DEVICE_TABLE(of, m);
static struct platform_driver drv = {
.probe = hello_probe, .remove = hello_remove,
.driver = { .name = "hello-irq", .of_match_table = m },
};
module_platform_driver(drv);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Lesson17 irq gpio workqueue");
逐块讲为什么:
- 上半部
hello_irq_handler只做两件事:irq_count++(记一下)和schedule_work(安排下半部),然后立刻return IRQ_HANDLED。绝不在这里做 I2C/sleep/锁。 - 下半部
hello_work_handler在 workqueue 线程跑(普通进程上下文),这里才能放心做regmap_read(I2C 会睡眠)、input_report(上报数据)。 cancel_work_sync在 remove 里必须有:卸载驱动时,可能还有 work 排队没跑,如果直接释放d,work 跑起来访问已释放的d→ use-after-free → panic(L24 常见根因)。cancel_work_sync等 work 跑完再走。IRQF_TRIGGER_FALLING:下降沿触发,要和硬件中断极性一致。错了 → 不触发或狂触发。
6. 看底层:一次中断的完整流程
IC 有数据 → 拉低中断脚(下降沿)
→ GIC(中断控制器)→ CPU 跳到中断向量
→ 内核中断框架 → 调你注册的 hello_irq_handler(上半部,关抢占)
schedule_work(&d->work) → work 入队 workqueue
→ 上半部返回,恢复抢占
→ 某个时刻 workqueue 的内核线程调度到 → 执行 hello_work_handler(下半部,可睡眠)
regmap_read 读数据 → input_report 上报
7. 编译 & 运行(✅ 真编译过)
make # 真编译出 hello_irq.ko
adb shell dmesg | grep -i hello # probe + 中断触发时的下半部日志
真编译结果(本机 6.17):
CC [M] hello_irq.o
LD [M] hello_irq.ko ← void remove,编译通过
> 验证状态(诚实):本机 6.17 真编译通过(remove 用 void,符合 6.11+)。中断真触发需真实硬件拉中断脚。
8. 踩坑提醒
- 上半部里做 I2C/sleep/mutex:中断上下文不能睡眠,这么干直接
BUG/scheduling while atomicpanic(L24 常见)。耗时活必须丢下半部。 - 触发极性写错:
IRQF_TRIGGER_FALLING/RISING/LEVEL和硬件不符 → 中断不来或狂来(中断风暴,系统卡死)。 - remove 没 cancel_work_sync:use-after-free,卸载时 panic。
- remove 返回 int(6.11+):platform 的 remove 要 void(同 L15)。
- LEVEL 触发不清中断标志:电平触发的中断,下半部要清 IC 的中断状态寄存器,否则中断一直拉着 → 中断风暴。
9. 常见问题分析与定位(中断实战)
9.1 常见问题清单
- 中断不触发(下半部从没跑)
- 中断风暴(中断疯狂触发,系统卡死/发烫)
scheduling while atomic/BUG: sleeping in atomicpanic- use-after-free panic(卸载时)
- 中断丢失(偶尔漏数据)
9.2 分析与定位
① 中断不触发
- 原因:触发极性错 / GPIO 配错 / IC 没真拉中断 / request_irq 失败。
- 定位:
adb shell cat /proc/interrupts | grep hello # 中断注册了吗?计数涨吗?
# 计数不涨 → 硬件没拉中断 或 极性错(用示波器看中断脚电平变化)
adb shell dmesg | grep -i "hello\|irq"
/proc/interrupts看中断计数:不涨 = 硬件层没触发(查极性/GPIO/IC);涨了但下半部没动 = schedule_work/下半部逻辑问题。
② 中断风暴
- 现象:
/proc/interrupts计数飞涨,CPU 占用高、发烫。 - 原因:电平触发没清中断标志 / 极性配错 / 硬件抖动。
- 定位:
watch -n1 'cat /proc/interrupts | grep hello'看涨多快;LEVEL 触发的检查下半部有没有清 IC 中断寄存器。
③ sleeping in atomic panic
- 原因:在上半部(中断上下文)里 sleep / 做 I2C / 拿 mutex。
- 定位:
dmesg看 panic 栈,在你的上半部函数里。把耗时活移到下半部。
④ use-after-free
- 原因:remove 没 cancel_work_sync,work 访问已释放数据。
- 修复:remove 里 cancel_work_sync。
⑤ 中断丢失
- 原因:下半部处理太慢,中断来得比处理快;或 ONESHOT 配置/清标志时机问题。
- 定位:对比中断计数和实际处理次数;优化下半部速度。
9.3 定位决策树
中断出问题
├─ 不触发 → cat /proc/interrupts 计数涨吗
│ ├─ 不涨 → 硬件没拉/极性错/GPIO → 示波器看中断脚
│ └─ 涨但下半部没动 → schedule_work/下半部逻辑
├─ 中断风暴 → /proc/interrupts 飞涨 → 电平触发没清标志/极性/抖动
├─ sleeping in atomic panic → dmesg 栈在上半部 → 耗时活移下半部
├─ use-after-free → remove 加 cancel_work_sync
└─ 丢中断 → 下半部太慢/ONESHOT/清标志时机 → 优化下半部
10. 小结
- 中断 > 轮询:IC 有数据拉中断脚通知 CPU,高效省电低延迟。
- 上半部快、下半部慢:上半部只记标记+schedule_work 立刻返回;耗时活(I2C/上报)在 workqueue 下半部做。
- 上半部绝不睡眠/做 I2C,否则 panic。
- remove 必须 cancel_work_sync 防 use-after-free;触发极性要和硬件一致。
- 排查口诀:不触发看 /proc/interrupts 计数(分硬件层 vs 软件层);风暴查清标志;panic 看栈在不在上半部。
下节预告
L18:电源管理 runtime PM + suspend/resume —— 息屏了驱动怎么关外设省电,唤醒怎么恢复?