20、TSP中断处理:中断上半部、中断下半部、线程化中断
触摸屏的中断处理,说白了就是当手指碰到屏幕时,系统怎么快速响应,又不会把CPU卡死。我刚开始接触TSP驱动时,总觉得中断处理不就是个回调函数吗?后来踩了坑才明白,这里面的门道深着呢。
今天咱们就聊聊中断处理的三种方式:上半部、下半部,还有线程化中断。嗯,这部分内容很关键,搞懂了它,你写出来的驱动才够“稳”。
为什么需要分上半部和下半部?
你想想看,手指在屏幕上滑动时,中断信号一个接一个地来。如果每个中断都在中断上下文里处理完所有事情——读取数据、解析协议、上报坐标、唤醒等待队列——那CPU就啥也别干了。
中断上下文有个特点:它不能睡眠,不能调度。这意味着你在中断里不能调用 msleep(),不能拿信号量,不能做任何可能阻塞的操作。但触摸屏驱动偏偏需要做很多事,比如通过I2C读取数据——I2C传输本身就可能需要等待。
所以,内核把中断处理拆成了两段:
- 上半部(Top Half):在真正的硬件中断上下文里执行,只做最紧急的事。比如读取硬件状态寄存器、清除中断标志位。
- 下半部(Bottom Half):在稍后、更宽松的上下文里执行。可以做耗时操作,比如解析数据、上报事件。
核心原则:上半部越快越好,下半部能干的事绝不放上半部。
上半部:只做“必须立刻做”的事
上半部里,我个人的习惯是只做三件事:
- 读取中断状态寄存器,确认是哪个中断源触发了。
- 清除硬件中断标志位,防止重复触发。
- 触发下半部(比如调度一个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方式。
三种方式的对比总结
我画了一张图,帮你理清这三种方式的关系:
实际项目中的选择策略
我在实际项目中遇到过各种情况,总结下来就这么几条经验:
- 简单触摸屏(单点、电阻屏):用线程化中断就够了,代码量少,维护方便。
- 多点电容屏(主流手机):推荐工作队列方式。因为数据量大,解析逻辑复杂,工作队列可以灵活控制优先级。
- 低功耗场景(穿戴设备):用线程化中断配合
IRQF_ONESHOT标志,可以保证中断处理完成前不会再次触发,避免中断风暴。 - 实时性要求极高(游戏手机):上半部+tasklet,把触摸数据读取放到tasklet里,减少调度延迟。
避坑指南:我曾经在一个项目里用了线程化中断,但忘了加 IRQF_ONESHOT 标志。结果在I2C传输失败时,中断反复触发,导致系统响应变慢。加了 IRQF_ONESHOT 后,问题解决。这个标志的意思是:在中断线程处理完之前,硬件中断线保持屏蔽状态。
小结
中断处理是TSP驱动的核心。上半部负责快速响应,下半部负责实际工作。线程化中断让这一切变得更简单。你写驱动时,先想清楚你的场景:需要多快的响应?数据量有多大?然后选择合适的方式。
嗯,这部分内容就聊到这儿。记住一句话:中断上下文里别睡觉,下半部里别着急。