4. 触摸驱动初始化:注册input设备、设置设备属性、中断申请与配置

触摸驱动的初始化,说白了就是让内核知道“有个触摸屏在这里,它长这样,它用这根中断线通知我”。

我刚开始写触摸驱动时,总觉得初始化就是填几个结构体,后来踩了坑才明白——顺序错了,整个设备可能都起不来。

这一节,我们一步步拆解初始化的三个核心动作:注册 input 设备、设置设备属性、中断申请与配置。

核心原则:先注册设备,再配置中断。中断处理函数里要用到的设备结构体指针,必须在申请中断前就准备好。

4.1 注册 input 设备——让内核认识你

触摸屏在 Linux 里属于 input 子系统。你要做的第一件事,就是分配并注册一个 input_dev 结构体。

我个人习惯在 probe 函数里这样写:

struct input_dev *input_dev;
int ret;

// 1. 分配 input 设备
input_dev = devm_input_allocate_device(&client->dev);
if (!input_dev) {
    dev_err(&client->dev, "Failed to allocate input device\n");
    return -ENOMEM;
}

// 2. 设置设备名称和物理路径
input_dev->name = "ft5x06_touch";
input_dev->phys = "i2c/ft5x06/input0";
input_dev->id.bustype = BUS_I2C;

// 3. 设置支持的事件类型
set_bit(EV_ABS, input_dev->evbit);      // 绝对坐标事件
set_bit(EV_KEY, input_dev->evbit);      // 按键事件(触摸按下/抬起)

// 4. 设置支持的按键类型
set_bit(BTN_TOUCH, input_dev->keybit);  // 触摸按键

// 5. 设置绝对坐标参数
input_set_abs_params(input_dev, ABS_MT_POSITION_X, 0, 800, 0, 0);
input_set_abs_params(input_dev, ABS_MT_POSITION_Y, 0, 480, 0, 0);
input_set_abs_params(input_dev, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);

// 6. 注册 input 设备
ret = input_register_device(input_dev);
if (ret) {
    dev_err(&client->dev, "Failed to register input device: %d\n", ret);
    return ret;
}

这里有个细节——devm_input_allocate_device 是带资源管理的版本。设备移除时,内核会自动帮你释放。我早期用手动分配,漏了一次 input_free_device,内存泄漏查了一整天。

小技巧:如果你支持多点触摸,记得设置 ABS_MT_SLOT 和 input_mt_init_slots()。单点触摸可以跳过,但多点触摸必须初始化 slot。

4.2 设置设备属性——告诉上层怎么用

注册完 input 设备,你还要告诉上层:

  • 你的触摸屏分辨率是多少?
  • 支持几个手指?
  • 上报的数据格式是什么?

这些信息通过 input_set_abs_params 和 input_mt_init_slots 来配置。

来看一个实际项目中我常用的配置:

// 设置多点触摸协议
input_mt_init_slots(input_dev, MAX_TOUCH_POINTS, 0);

// 设置 X/Y 轴范围
input_set_abs_params(input_dev, ABS_MT_POSITION_X, 0, SCREEN_WIDTH - 1, 0, 0);
input_set_abs_params(input_dev, ABS_MT_POSITION_Y, 0, SCREEN_HEIGHT - 1, 0, 0);

// 设置触摸压力范围(如果硬件支持)
input_set_abs_params(input_dev, ABS_MT_PRESSURE, 0, 255, 0, 0);

// 设置触摸面积
input_set_abs_params(input_dev, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
input_set_abs_params(input_dev, ABS_MT_WIDTH_MAJOR, 0, 255, 0, 0);

input_set_abs_params 的五个参数分别是:

参数 含义 示例值
axis 坐标轴类型 ABS_MT_POSITION_X
min 最小值 0
max 最大值 799
fuzz 抖动容忍值 0(不滤波)
flat 平坦区(死区) 0
曾经踩过的坑:有一次我把 X 轴最大值设成了 799,但硬件实际分辨率是 1024。结果触摸点永远点不到屏幕右边。后来我加了一行调试打印,才发现是参数配错了。建议你在 probe 里把分辨率参数打印出来确认。

4.3 中断申请与配置——让硬件能叫你

触摸屏是中断驱动的。手指一碰,硬件拉低中断引脚,驱动就要去读数据。

申请中断的代码通常长这样:

struct device *dev = &client->dev;
struct touch_data *ts = dev_get_drvdata(dev);

// 获取中断号
int irq = client->irq;
if (!irq) {
    dev_err(dev, "No IRQ specified\n");
    return -EINVAL;
}

// 申请中断
ret = devm_request_threaded_irq(dev, irq,
                                NULL,           // 顶半部,设为 NULL 表示全部在底半部处理
                                touch_irq_handler, // 底半部处理函数
                                IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                                "ft5x06_touch", ts);
if (ret) {
    dev_err(dev, "Failed to request IRQ %d: %d\n", irq, ret);
    return ret;
}

这里我用了 devm_request_threaded_irq,而不是传统的 request_irq。为什么?

  • 顶半部(hardirq):只做最少的处理,比如清中断标志。我直接传 NULL,全部交给底半部。
  • 底半部(threaded handler):可以睡眠,可以调用 i2c 读写。触摸屏的坐标读取就在这里做。
  • IRQF_ONESHOT:保证中断处理完成前,硬件不会再次触发中断。这对电平触发的中断特别重要。
为什么用 threaded IRQ? 触摸屏通过 I2C 读取数据,I2C 传输可以睡眠。普通中断上下文不能睡眠,所以必须用线程化中断。

4.4 中断处理函数的实现

底半部处理函数是触摸驱动的核心。它负责:

  1. 读取触摸点坐标
  2. 上报给 input 子系统
  3. 通知上层应用

一个典型的中断处理函数:

static irqreturn_t touch_irq_handler(int irq, void *dev_id)
{
    struct touch_data *ts = dev_id;
    int i, touch_count;
    u8 buf[TOUCH_DATA_LEN];

    // 1. 读取触摸数据
    if (i2c_master_recv(ts->client, buf, TOUCH_DATA_LEN) != TOUCH_DATA_LEN) {
        dev_err_ratelimited(&ts->client->dev, "I2C read failed\n");
        return IRQ_HANDLED;
    }

    // 2. 解析触摸点数
    touch_count = buf[2] & 0x0F;

    // 3. 上报每个触摸点
    for (i = 0; i < touch_count; i++) {
        int x = (buf[3 + i * 6] << 8) | buf[4 + i * 6];
        int y = (buf[5 + i * 6] << 8) | buf[6 + i * 6];

        input_mt_slot(ts->input_dev, i);
        input_mt_report_slot_state(ts->input_dev, MT_TOOL_FINGER, true);
        input_report_abs(ts->input_dev, ABS_MT_POSITION_X, x);
        input_report_abs(ts->input_dev, ABS_MT_POSITION_Y, y);
    }

    // 4. 上报同步事件
    input_mt_sync_frame(ts->input_dev);
    input_sync(ts->input_dev);

    return IRQ_HANDLED;
}

注意这里我用了 dev_err_ratelimited 而不是 dev_err。为什么?

我曾经在一个项目里,I2C 总线偶尔出错,结果 dev_err 每秒打印几千条日志,直接把内核 log 缓冲区撑爆了。从那以后,凡是中断里可能高频触发的错误打印,我都用 ratelimited 版本。

4.5 初始化流程总览

把上面三步串起来,完整的初始化流程是这样的:

static int touch_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    struct touch_data *ts;
    int ret;

    // 1. 分配私有数据结构
    ts = devm_kzalloc(&client->dev, sizeof(*ts), GFP_KERNEL);
    if (!ts)
        return -ENOMEM;
    ts->client = client;
    i2c_set_clientdata(client, ts);

    // 2. 硬件初始化(复位、配置寄存器等)
    ret = touch_hardware_init(client);
    if (ret)
        return ret;

    // 3. 注册 input 设备
    ret = touch_input_register(ts);
    if (ret)
        return ret;

    // 4. 申请中断
    ret = touch_request_irq(ts);
    if (ret) {
        input_unregister_device(ts->input_dev);
        return ret;
    }

    dev_info(&client->dev, "Touch driver initialized successfully\n");
    return 0;
}

这个顺序是固定的:硬件初始化 → input 注册 → 中断申请。调换了,中断来了但 input 设备还没注册好,数据就丢了。

我的习惯:在 probe 函数末尾加一行 dev_info 打印。调试时一眼就能看出驱动是否加载成功,不用去翻 dmesg 找错误码。

4.6 本章核心流程图

下面这张图展示了触摸驱动初始化的完整数据流和依赖关系:

触摸驱动初始化流程 probe 入口 硬件初始化(复位/配置) 分配 input_dev 结构体 设置设备属性(分辨率/协议) 注册 input 设备 申请中断(threaded IRQ) 关键顺序约束 硬件初始化 → input 注册 → 中断申请 顺序不可调换 中断处理流程 I2C 读取触摸数据 解析坐标 → 上报事件 input_sync() 通知上层

这张图把初始化的六个步骤串起来了。你写驱动时,可以对着这个流程检查自己的 probe 函数——少一步都不行。

曾经踩过的坑:有一次我把中断申请放在了 input 注册之前。结果中断触发时,input 设备还没注册好,上报的事件全部丢失。从那以后,我每次写 probe 都会默念一遍顺序:硬件 → input → 中断。

好了,这一节的内容就到这里。初始化是触摸驱动的基石,写扎实了,后面的数据上报和事件处理才能稳。