4. R-Car触摸驱动架构

中断处理,说白了就是触摸屏向主控芯片“喊报告”的方式。你想想看,手指点下去,信号从触摸屏传到驱动,再传到应用层,这中间每一步都有延迟。而中断处理,往往是整个链路里最容易出问题的一环。

我个人习惯,拿到一个新平台的触摸屏驱动,第一件事就是看中断处理函数。为什么?因为这里藏着最多的“坑”。

4.1 中断处理的基本模型

触摸屏的中断处理,通常有两种模型:

模型类型 触发方式 典型延迟 适用场景
边缘触发(Edge-triggered) 电平跳变沿 低(微秒级) 高精度触摸、游戏场景
电平触发(Level-triggered) 电平持续有效 中(毫秒级) 普通滑动、点击操作
共享中断(Shared IRQ) 多个设备共用 高(不确定) 低端硬件、引脚受限

我在项目中遇到过,某款车机用了共享中断,结果触摸屏和蓝牙模块抢中断。手指滑动时,蓝牙刚好在传输数据,触摸响应直接卡顿。嗯,后来改成独立中断,问题就解决了。

4.2 中断处理函数的“快与慢”

中断处理函数,一定要快。这是铁律。

为什么?因为中断上下文里,你不能做耗时操作。比如 I2C 读写、内存分配、打印日志,这些都不该出现在中断处理函数里。

警告: 千万不要在中断处理函数里做 I2C 读取!我曾经见过一个驱动,在中断里直接调用 i2c_transfer(),结果每次中断都要等几百微秒,系统响应直接崩了。

正确的做法是:中断处理函数只做最轻量的事——读取状态寄存器、清除中断标志、然后唤醒一个工作队列或 tasklet。

// 错误示例:中断里做 I2C 读取
static irqreturn_t touch_irq_handler_bad(int irq, void *dev_id)
{
    struct touch_device *ts = dev_id;
    // 这里调用了 I2C 读取,耗时!
    ts->read_data(ts);
    input_report_key(ts->input, BTN_TOUCH, 1);
    input_sync(ts->input);
    return IRQ_HANDLED;
}

// 正确示例:中断只做标记,工作队列处理
static irqreturn_t touch_irq_handler_good(int irq, void *dev_id)
{
    struct touch_device *ts = dev_id;
    // 只清除中断标志
    ts->clear_irq(ts);
    // 唤醒工作队列
    schedule_work(&ts->work);
    return IRQ_HANDLED;
}

4.3 工作队列与 tasklet 的选择

中断下半部,有两种主流方式:工作队列(workqueue)和 tasklet。

我个人的经验是:

  • tasklet:适合非常短的操作,比如更新一个状态变量。它在软中断上下文执行,不能睡眠。
  • 工作队列:适合需要睡眠的操作,比如 I2C 读写、内存分配。它在进程上下文执行。

触摸屏驱动,我建议用工作队列。因为触摸数据读取,几乎必然涉及 I2C 或 SPI 通信,这些都需要睡眠等待。

小技巧: 如果触摸屏的采样率很高(比如 120Hz 以上),可以考虑用 threaded IRQ。它把中断处理直接放到内核线程里,既避免了中断上下文限制,又减少了上下文切换开销。

4.4 中断延迟的测量方法

怎么知道你的中断处理到底慢不慢?别靠感觉,要测。

我常用的方法有两种:

  1. ftrace 跟踪:直接看中断处理函数的耗时分布。
  2. GPIO 示波器法:在中断处理函数入口和出口各拉一次 GPIO,用示波器看脉冲宽度。

第二种方法虽然土,但最直观。我在调试一款车机时,就是用示波器发现中断处理函数耗时 3.2ms,远超预期的 500μs。后来发现是 I2C 总线频率太低,改成 400kHz 后,耗时降到 800μs。

// 用 GPIO 测量中断处理耗时
static irqreturn_t touch_irq_handler(int irq, void *dev_id)
{
    struct touch_device *ts = dev_id;
    
    // 拉高 GPIO,开始计时
    gpio_set_value(DEBUG_GPIO, 1);
    
    // 实际处理
    ts->clear_irq(ts);
    schedule_work(&ts->work);
    
    // 拉低 GPIO,结束计时
    gpio_set_value(DEBUG_GPIO, 0);
    
    return IRQ_HANDLED;
}

4.5 中断优先级与 CPU 亲和性

在多核系统里,中断可以绑定到特定 CPU 核心。这能避免中断在不同核心间迁移,减少缓存失效。

我建议:

  • 把触摸屏中断绑定到一个独立核心(比如 CPU2 或 CPU3)。
  • 避免和音频、显示等实时性要求高的中断抢核心。
# 设置中断亲和性,绑定到 CPU2
echo 4 > /proc/irq/123/smp_affinity

注意:这里的 4 是二进制 100,表示 CPU2。具体数值要看你的核心编号。

关键指标: 触摸屏中断响应延迟,建议控制在 500μs 以内。超过 1ms,用户就能感知到卡顿。

4.6 中断合并与防抖

触摸屏的物理特性决定了,一次触摸可能产生多次中断。比如手指按下时,信号会有抖动,导致中断反复触发。

处理方式有两种:

  1. 硬件防抖:触摸屏 IC 内部自带防抖,一般可以配置防抖时间(比如 10ms)。
  2. 软件防抖:在驱动里做时间戳过滤,两次中断间隔小于阈值就忽略。

我个人更倾向硬件防抖。软件防抖虽然灵活,但会增加中断处理开销。而且,如果防抖时间设置不当,反而会引入额外延迟。

// 软件防抖示例
static irqreturn_t touch_irq_handler(int irq, void *dev_id)
{
    struct touch_device *ts = dev_id;
    unsigned long now = jiffies;
    
    // 如果距离上次中断不到 5ms,忽略
    if (time_before(now, ts->last_irq + msecs_to_jiffies(5)))
        return IRQ_HANDLED;
    
    ts->last_irq = now;
    ts->clear_irq(ts);
    schedule_work(&ts->work);
    
    return IRQ_HANDLED;
}

4.7 中断处理流程全景图

下面这张图,展示了触摸屏中断从触发到上报的完整流程。你可以对照自己的驱动,看看每一步是否都做到了最优。

触摸屏中断处理流程 触摸事件发生 硬件中断触发 中断处理函数(清标志) 工作队列(读取数据) 数据上报到 input 子系统 应用层响应 关键优化点 • 中断函数要快 • 避免 I2C 读写 • 使用工作队列 测量方法 • ftrace 跟踪 • GPIO 示波器法 • 目标:<500μs

4.8 常见问题与避坑指南

避坑: 我曾经遇到一个 case,触摸屏中断处理函数里调用了 printk() 打印调试信息。结果 printk 在中断上下文里导致死锁,系统直接挂掉。记住:中断里不要打印日志!
  • 中断丢失:如果中断处理时间太长,后续中断会被丢弃。解决办法:缩短处理时间,或者使用 threaded IRQ。
  • 中断风暴:触摸屏硬件故障,导致中断持续触发。解决办法:在驱动里加中断次数限制,超过阈值就禁用中断。
  • 优先级反转:低优先级中断占用了 CPU,导致高优先级中断被延迟。解决办法:合理设置中断优先级,或者绑定独立核心。
经验之谈: 调试中断问题时,先用示波器看 GPIO 波形,确认中断是否正常触发。很多时候,问题不在驱动,而在硬件。