AI 技术博客
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.chello-irq.dtsiMakefile

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. 踩坑提醒

  1. 上半部里做 I2C/sleep/mutex:中断上下文不能睡眠,这么干直接 BUG/scheduling while atomic panic(L24 常见)。耗时活必须丢下半部。
  2. 触发极性写错:IRQF_TRIGGER_FALLING/RISING/LEVEL 和硬件不符 → 中断不来或狂来(中断风暴,系统卡死)。
  3. remove 没 cancel_work_sync:use-after-free,卸载时 panic。
  4. remove 返回 int(6.11+):platform 的 remove 要 void(同 L15)。
  5. LEVEL 触发不清中断标志:电平触发的中断,下半部要清 IC 的中断状态寄存器,否则中断一直拉着 → 中断风暴。

9. 常见问题分析与定位(中断实战)

9.1 常见问题清单

  1. 中断不触发(下半部从没跑)
  2. 中断风暴(中断疯狂触发,系统卡死/发烫)
  3. scheduling while atomic / BUG: sleeping in atomic panic
  4. use-after-free panic(卸载时)
  5. 中断丢失(偶尔漏数据)

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. 小结

  1. 中断 > 轮询:IC 有数据拉中断脚通知 CPU,高效省电低延迟。
  2. 上半部快、下半部慢:上半部只记标记+schedule_work 立刻返回;耗时活(I2C/上报)在 workqueue 下半部做。
  3. 上半部绝不睡眠/做 I2C,否则 panic。
  4. remove 必须 cancel_work_sync 防 use-after-free;触发极性要和硬件一致。
  5. 排查口诀:不触发看 /proc/interrupts 计数(分硬件层 vs 软件层);风暴查清标志;panic 看栈在不在上半部。

下节预告

L18:电源管理 runtime PM + suspend/resume —— 息屏了驱动怎么关外设省电,唤醒怎么恢复?

评论