20、TSP中断处理:中断上半部、中断下半部、线程化中断

触摸屏的中断处理,说白了就是当手指碰到屏幕时,系统怎么快速响应,又不会把CPU卡死。我刚开始接触TSP驱动时,总觉得中断处理不就是个回调函数吗?后来踩了坑才明白,这里面的门道深着呢。

今天咱们就聊聊中断处理的三种方式:上半部、下半部,还有线程化中断。嗯,这部分内容很关键,搞懂了它,你写出来的驱动才够“稳”。

为什么需要分上半部和下半部?

你想想看,手指在屏幕上滑动时,中断信号一个接一个地来。如果每个中断都在中断上下文里处理完所有事情——读取数据、解析协议、上报坐标、唤醒等待队列——那CPU就啥也别干了。

中断上下文有个特点:它不能睡眠,不能调度。这意味着你在中断里不能调用 msleep(),不能拿信号量,不能做任何可能阻塞的操作。但触摸屏驱动偏偏需要做很多事,比如通过I2C读取数据——I2C传输本身就可能需要等待。

所以,内核把中断处理拆成了两段:

  • 上半部(Top Half):在真正的硬件中断上下文里执行,只做最紧急的事。比如读取硬件状态寄存器、清除中断标志位。
  • 下半部(Bottom Half):在稍后、更宽松的上下文里执行。可以做耗时操作,比如解析数据、上报事件。

核心原则:上半部越快越好,下半部能干的事绝不放上半部。

上半部:只做“必须立刻做”的事

上半部里,我个人的习惯是只做三件事:

  1. 读取中断状态寄存器,确认是哪个中断源触发了。
  2. 清除硬件中断标志位,防止重复触发。
  3. 触发下半部(比如调度一个tasklet或工作队列)。

举个例子,一个典型的TSP中断上半部长这样:

static irqreturn_t tsp_irq_handler(int irq, void *dev_id)
{
    struct tsp_data *ts = dev_id;
    int status;

    // 1. 读取中断状态
    status = i2c_smbus_read_byte(ts->client, REG_INT_STATUS);
    if (status < 0)
        return IRQ_NONE;

    // 2. 清除中断标志
    i2c_smbus_write_byte(ts->client, REG_INT_CLEAR, 0xFF);

    // 3. 调度下半部
    schedule_work(&ts->work);

    return IRQ_HANDLED;
}

注意看,这里我用了 schedule_work(),它会把下半部放到一个工作队列里,稍后由内核线程执行。这样上半部就结束了,耗时很短。

我曾经踩过的坑:有一次我在上半部里直接调用了 input_report_key() 上报触摸事件。结果发现系统在高频触摸时偶尔会丢数据。后来排查发现,input_report_key() 内部可能会触发调度点,而中断上下文不允许调度。从那以后,我坚持上半部只做“读状态+清标志+调度下半部”这三步。

下半部:三种实现方式

下半部的实现方式有好几种,我挑最常用的三种说说:

方式 特点 适用场景
tasklet 软中断上下文,不可睡眠,执行快 简单数据处理,不涉及I2C/SPI传输
工作队列(workqueue) 进程上下文,可睡眠,可阻塞 需要I2C/SPI通信、需要等待资源
线程化中断(threaded IRQ) 直接在内核线程中处理,无需显式调度 驱动逻辑较复杂,希望简化代码

对于TSP驱动来说,工作队列线程化中断是最常用的。因为触摸屏驱动几乎都要通过I2C读取坐标数据,而I2C传输是可能阻塞的。

下面是一个工作队列下半部的例子:

static void tsp_work_handler(struct work_struct *work)
{
    struct tsp_data *ts = container_of(work, struct tsp_data, work);
    u8 buf[64];
    int ret;

    // 通过I2C读取触摸数据
    ret = i2c_master_recv(ts->client, buf, sizeof(buf));
    if (ret < 0) {
        dev_err(&ts->client->dev, "I2C read failed\n");
        return;
    }

    // 解析数据并上报
    tsp_parse_and_report(ts, buf, ret);
}

线程化中断:让事情更简单

线程化中断是内核后来引入的一种机制。说白了,就是你在注册中断时直接告诉内核:“我这个中断处理函数需要在进程上下文里跑,你帮我创建一个内核线程来执行它。”

这样做的好处很明显:你不用再手动调度工作队列了,代码更简洁。而且线程化中断有自己的优先级,可以设置实时优先级,保证触摸响应及时。

注册方式如下:

ret = request_threaded_irq(ts->irq, NULL,
                           tsp_threaded_irq_handler,
                           IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                           "tsp_irq", ts);

注意这里:第一个回调函数传了 NULL,表示上半部用默认的快速处理(只做唤醒线程的工作)。第二个回调函数 tsp_threaded_irq_handler 就是在线程上下文里执行的。

线程化中断处理函数长这样:

static irqreturn_t tsp_threaded_irq_handler(int irq, void *dev_id)
{
    struct tsp_data *ts = dev_id;
    u8 buf[64];
    int ret;

    // 读取中断状态
    ret = i2c_smbus_read_byte(ts->client, REG_INT_STATUS);
    if (ret < 0)
        return IRQ_NONE;

    // 清除中断标志
    i2c_smbus_write_byte(ts->client, REG_INT_CLEAR, 0xFF);

    // 读取触摸数据(可以阻塞,因为在线程上下文)
    ret = i2c_master_recv(ts->client, buf, sizeof(buf));
    if (ret < 0)
        return IRQ_HANDLED;

    // 解析并上报
    tsp_parse_and_report(ts, buf, ret);

    return IRQ_HANDLED;
}

你看,整个处理逻辑都放在一个函数里了,不用分上半部和下半部。内核自动帮你管理线程的创建和调度。

我的建议:新写的TSP驱动,优先考虑线程化中断。它让代码更清晰,也避免了手动管理工作队列的麻烦。但如果你对中断响应时间有极苛刻的要求(比如低于100微秒),那还是用传统的上半部+tasklet方式。

三种方式的对比总结

我画了一张图,帮你理清这三种方式的关系:

TSP中断处理三种方式对比 硬件中断触发 上半部:读状态、清标志、调度下半部 tasklet(软中断) 不可睡眠,执行快 工作队列(workqueue) 可睡眠,适合I2C传输 线程化中断 自动线程管理,代码简洁 解析数据、上报触摸事件

实际项目中的选择策略

我在实际项目中遇到过各种情况,总结下来就这么几条经验:

  • 简单触摸屏(单点、电阻屏):用线程化中断就够了,代码量少,维护方便。
  • 多点电容屏(主流手机):推荐工作队列方式。因为数据量大,解析逻辑复杂,工作队列可以灵活控制优先级。
  • 低功耗场景(穿戴设备):用线程化中断配合 IRQF_ONESHOT 标志,可以保证中断处理完成前不会再次触发,避免中断风暴。
  • 实时性要求极高(游戏手机):上半部+tasklet,把触摸数据读取放到tasklet里,减少调度延迟。

避坑指南:我曾经在一个项目里用了线程化中断,但忘了加 IRQF_ONESHOT 标志。结果在I2C传输失败时,中断反复触发,导致系统响应变慢。加了 IRQF_ONESHOT 后,问题解决。这个标志的意思是:在中断线程处理完之前,硬件中断线保持屏蔽状态。

小结

中断处理是TSP驱动的核心。上半部负责快速响应,下半部负责实际工作。线程化中断让这一切变得更简单。你写驱动时,先想清楚你的场景:需要多快的响应?数据量有多大?然后选择合适的方式。

嗯,这部分内容就聊到这儿。记住一句话:中断上下文里别睡觉,下半部里别着急


公众号:蓝海资料掘金营,微信deep3321