3、触摸屏驱动架构:Linux/RTOS下的触摸屏驱动框架、中断与轮询模式选择

各位工程师朋友,咱们接着聊触摸屏驱动。这一节我打算把驱动架构这块掰开揉碎了讲清楚。说实话,很多刚入行的朋友觉得驱动就是配配寄存器、调调参数,其实没那么简单。驱动架构选得好,后面抗干扰、校准这些工作才能事半功倍。

3.1 Linux下的触摸屏驱动框架

Linux下的触摸屏驱动,核心是Input子系统。我习惯把它理解成三层结构:设备驱动层、核心层、事件处理层。说白了,驱动层负责跟硬件打交道,核心层做中转,事件处理层把数据送给用户空间。

先看一个典型的驱动入口代码:

static const struct of_device_id my_touch_of_match[] = {
    { .compatible = "mycompany,my-touch", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_touch_of_match);

static struct platform_driver my_touch_driver = {
    .probe  = my_touch_probe,
    .remove = my_touch_remove,
    .driver = {
        .name = "my_touch",
        .of_match_table = my_touch_of_match,
        .pm = &my_touch_pm_ops,
    },
};
module_platform_driver(my_touch_driver);

嗯,这里要注意,of_match_table是设备树匹配的关键。我在项目中遇到过有人忘了配这个,结果驱动死活加载不上,查了半天才发现是设备树节点名跟驱动不匹配。

probe函数里,我们要做几件事:

  • 分配input_dev结构体
  • 设置触摸屏的坐标范围、压力值范围
  • 注册中断或启动轮询定时器
  • 注册input设备

看个简化版的probe:

static int my_touch_probe(struct platform_device *pdev)
{
    struct input_dev *input;
    int ret;

    input = devm_input_allocate_device(&pdev->dev);
    if (!input)
        return -ENOMEM;

    input->name = "My Touch Screen";
    input->id.bustype = BUS_I2C;

    __set_bit(EV_ABS, input->evbit);
    input_set_abs_params(input, ABS_X, 0, 4095, 0, 0);
    input_set_abs_params(input, ABS_Y, 0, 4095, 0, 0);
    input_set_abs_params(input, ABS_PRESSURE, 0, 255, 0, 0);

    ret = input_register_device(input);
    if (ret) {
        dev_err(&pdev->dev, "Failed to register input device\n");
        return ret;
    }

    platform_set_drvdata(pdev, input);
    return 0;
}

你想想看,这里input_set_abs_params的四个参数分别是:轴类型、最小值、最大值、模糊值、平坦值。模糊值我一般设0,平坦值看具体芯片,有些芯片在零点附近有抖动,设个4-8能过滤掉。

3.2 RTOS下的触摸屏驱动框架

RTOS下就没Linux那么复杂了。说白了,RTOS的驱动就是裸机编程的升级版。我建议的做法是:

  • 封装一个触摸屏设备结构体
  • 提供初始化、读取、校准三个接口
  • 用信号量或消息队列做数据同步

看个FreeRTOS下的示例:

typedef struct {
    uint16_t x;
    uint16_t y;
    uint16_t pressure;
    uint8_t  touch_event;  // 0: release, 1: press, 2: move
} touch_point_t;

typedef struct {
    I2C_HandleTypeDef *i2c;
    touch_point_t      point;
    SemaphoreHandle_t  data_sem;
    TaskHandle_t       task_handle;
} touch_device_t;

static void touch_read_task(void *arg)
{
    touch_device_t *dev = (touch_device_t *)arg;
    uint8_t buf[8];

    while (1) {
        // 读取触摸数据
        HAL_I2C_Master_Receive(dev->i2c, TOUCH_ADDR, buf, 8, 100);
        dev->point.x = (buf[0] << 8) | buf[1];
        dev->point.y = (buf[2] << 8) | buf[3];
        dev->point.pressure = buf[4];

        // 通知其他任务
        xSemaphoreGive(dev->data_sem);
        vTaskDelay(pdMS_TO_TICKS(10));  // 100Hz采样
    }
}

我个人习惯在RTOS下用独立任务做触摸读取,而不是在中断里处理。为什么?因为I2C通信本身就有延迟,在中断里做I2C读写会阻塞其他中断,得不偿失。

3.3 中断模式 vs 轮询模式

这是个老生常谈的问题,但每次选型都有人翻车。我直接给结论:

对比项 中断模式 轮询模式
CPU占用率 低(事件触发) 高(持续占用)
响应延迟 快(微秒级) 取决于轮询周期
抗干扰能力 弱(易误触发) 强(可软件滤波)
实现复杂度 中等 简单
典型场景 消费电子、快速响应 工业HMI、高可靠性

我曾经在一个工业平板项目里选了中断模式,结果现场电磁干扰严重,触摸屏频繁误触发。后来改成轮询模式,配合软件去抖,问题就解决了。所以我的建议是:

工业HMI场景,优先选轮询模式。 除非你的触摸控制器有硬件滤波且中断信号干净,否则别冒险。

3.4 中断模式的设计要点

如果你非要用中断模式,那有几个坑必须避开:

  • 中断触发方式:用双边沿触发,别用电平触发。电平触发在干扰下会一直触发中断,把CPU拖死。
  • 中断处理要快:中断服务函数里只做标志位设置,真正的数据读取放到任务或工作队列里。
  • 去抖处理:硬件上加RC滤波,软件上做延时确认。

看个Linux下的中断处理示例:

static irqreturn_t my_touch_irq_handler(int irq, void *dev_id)
{
    struct my_touch_data *ts = dev_id;

    // 只设置标志,不读数据
    ts->irq_pending = 1;

    // 唤醒工作队列
    schedule_work(&ts->work);

    return IRQ_HANDLED;
}

static void my_touch_work_handler(struct work_struct *work)
{
    struct my_touch_data *ts = container_of(work, struct my_touch_data, work);
    uint8_t buf[8];

    // 在这里读I2C数据
    i2c_master_recv(ts->client, buf, 8);
    // 处理并上报
    input_report_abs(ts->input, ABS_X, (buf[0] << 8) | buf[1]);
    input_report_abs(ts->input, ABS_Y, (buf[2] << 8) | buf[3]);
    input_sync(ts->input);
}

嗯,这里要注意,工作队列的优先级要设高一点,否则触摸响应会感觉卡顿。我一般设成HIGH_PRIORITY_WORK

3.5 轮询模式的设计要点

轮询模式在工业HMI里更常见。核心就是定时器或任务循环,周期性地读取触摸数据。关键参数是轮询频率:

  • 触摸屏:50-100Hz就够了,人手指的移动速度没那么快
  • 电阻屏:100-200Hz,因为电阻屏需要检测压力变化
  • 电容屏:50-80Hz,太高了反而增加功耗

小技巧:轮询周期不要用固定的delay,用定时器或系统tick。这样能保证采样间隔均匀,不会因为其他任务阻塞导致采样抖动。

看个Linux下用hrtimer做轮询的例子:

static enum hrtimer_restart my_touch_timer_callback(struct hrtimer *timer)
{
    struct my_touch_data *ts = container_of(timer, struct my_touch_data, timer);
    uint8_t buf[8];

    // 读取触摸数据
    i2c_master_recv(ts->client, buf, 8);

    // 软件去抖:连续两次采样一致才上报
    if (abs(ts->last_x - ((buf[0] << 8) | buf[1])) < 5 &&
        abs(ts->last_y - ((buf[2] << 8) | buf[3])) < 5) {
        input_report_abs(ts->input, ABS_X, ts->last_x);
        input_report_abs(ts->input, ABS_Y, ts->last_y);
        input_sync(ts->input);
    }

    ts->last_x = (buf[0] << 8) | buf[1];
    ts->last_y = (buf[2] << 8) | buf[3];

    hrtimer_forward_now(timer, ms_to_ktime(10));  // 100Hz
    return HRTIMER_RESTART;
}

这里我加了个简单的去抖逻辑。为什么这么做?因为工业现场电磁干扰会导致单次采样异常,连续两次采样一致能过滤掉大部分毛刺。我曾经在电机驱动器旁边测试,不加去抖时坐标跳动有几十个像素,加上去抖后基本稳定在1-2个像素内。

3.6 混合模式:中断唤醒 + 轮询读取

这是我个人比较推荐的一种方案,尤其适合低功耗场景。思路是:

  • 触摸屏休眠时,用中断唤醒系统
  • 唤醒后切换到轮询模式,持续读取数据
  • 检测到触摸释放后,再切回中断等待模式

这样做的好处很明显:平时功耗低,触摸时响应快,而且轮询阶段可以加各种滤波算法。我在一个电池供电的HMI项目里用了这个方案,待机功耗从50mA降到了5mA,客户非常满意。

注意:切换模式时要处理好状态机,避免出现中断和轮询同时操作I2C总线的情况。我建议用互斥锁保护总线访问。

3.7 驱动调试的实用技巧

最后分享几个调试技巧,都是血泪教训换来的:

  • 先确认硬件通信正常:用i2c-tools或逻辑分析仪抓一下,确保I2C读写没问题
  • 打印原始数据:在驱动里加printk或串口输出,看触摸坐标是否合理
  • 测试边界条件:四个角落、边缘区域、快速滑动,这些地方最容易出问题
  • 压力测试:连续点击1000次,看有没有丢数据或卡死

我曾经遇到一个坑:触摸屏在低温环境下I2C通信会偶尔失败。后来在驱动里加了重试机制,失败时重新初始化I2C控制器,问题就解决了。所以,驱动里一定要有错误处理和恢复机制,别指望硬件永远不出问题。

好了,这一节就讲到这里。下一节我们聊聊触摸校准算法,这是HMI手感好坏的关键,敬请期待。