驱动框架入门:Linux input子系统架构、设备树节点配置、probe函数执行流程

好,咱们进入第二章。说实话,很多做触摸驱动的朋友,一开始就被 input 子系统绕晕了。其实没那么复杂。你把它想成一个「快递中转站」就行——硬件产生数据,驱动打包,input 核心负责派送给对应的应用程序。

我刚开始接触这块时,也花了不少时间理清架构。今天我就把核心脉络给你捋一遍,再结合设备树和 probe 流程,让你看完就能上手写代码。

Linux input 子系统整体架构

input 子系统分三层,说白了就是三个角色:

  • 设备驱动层:你写的触摸驱动就在这里。它负责和硬件打交道,读取坐标、按键值。
  • 核心层:input core。它提供注册接口、事件分发、设备管理。你不需要改它。
  • 事件处理层:比如 evdev、joydev。它们把内核事件转换成用户空间能读的 /dev/input/eventX 节点。

我画了张图,你看一眼就明白了:

Linux input 子系统三层架构图 Linux input 子系统三层架构 设备驱动层(触摸驱动、按键驱动等) 实现 read/write/irq_handler,调用 input_report_abs / input_sync 上报数据 核心层(input core) input_register_device / input_register_handler / 事件分发 事件处理层(evdev / joydev / mousedev) 创建 /dev/input/eventX,用户空间通过 read/poll 获取事件 用户空间:应用层直接读 eventX

你看,数据流是从下往上的。触摸屏产生中断,驱动在中断里拿到坐标,调用 input_report_abs 和 input_sync,核心层就把事件塞到 evdev 的缓冲区里。用户空间的应用程序通过 read(/dev/input/eventX) 就能拿到。

我的习惯:调试时先用 hexdump /dev/input/eventX 或 evtest 看事件是否到达用户空间。如果 eventX 有数据但应用没反应,问题多半在应用层;如果 eventX 没数据,那就要查驱动或硬件了。

设备树节点配置

现在大部分 ARM 平台都用设备树来描述硬件。触摸屏的设备树节点怎么写?我直接给你一个典型例子:

/* 设备树中 I2C 触摸屏节点示例 */
&i2c3 {
    status = "okay";
    clock-frequency = <400000>;

    touchscreen@38 {
        compatible = "goodix,gt911";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
        irq-gpios = <&gpio1 13 GPIO_ACTIVE_HIGH>;
        reset-gpios = <&gpio1 12 GPIO_ACTIVE_HIGH>;
        touchscreen-size-x = <1024>;
        touchscreen-size-y = <600>;
        touchscreen-max-id = <5>;
    };
};

这里有几个关键点,我挨个说一下:

  • compatible:驱动匹配的关键字。驱动里 of_match_table 必须和这个一致。
  • reg:I2C 设备地址。注意 7 位地址和 8 位地址的区别,我踩过这个坑。
  • interrupts:中断号加触发方式。触摸屏一般用下降沿或低电平触发。
  • gpio 属性:irq-gpios 和 reset-gpios 是自定义属性,驱动里通过 of_get_named_gpio 解析。
  • touchscreen-xxx:这些是 input 子系统定义的通用属性,驱动里用 touchscreen_parse_properties 一键解析。
我曾经踩过的坑:设备树里 interrupt 和 irq-gpios 都写了,结果中断注册了两次。后来我统一只用 interrupts 属性,gpio 只用于复位和唤醒。建议你二选一,别混用。

probe 函数执行流程

驱动加载时,内核会调用 probe 函数。这是整个驱动的入口,也是你写代码的主战场。我把它拆成 6 个步骤:

步骤 操作 关键函数 / 宏
1 解析设备树,获取 GPIO、中断、分辨率等参数 of_get_named_gpio / device_property_read_u32
2 初始化硬件:复位、上电、读取芯片 ID 确认通信正常 gpiod_set_value / i2c_master_send
3 申请中断,注册中断处理函数 devm_request_threaded_irq
4 分配并初始化 input_dev 结构体 devm_input_allocate_device
5 设置 input_dev 支持的事件类型和按键/坐标范围 set_bit / input_set_abs_params
6 注册 input 设备到核心层 input_register_device

我给你看一段精简的 probe 代码,这是我实际项目里提炼出来的骨架:

static int my_touch_probe(struct i2c_client *client,
                          const struct i2c_device_id *id)
{
    struct device *dev = &client->dev;
    struct input_dev *input;
    int ret;

    /* 1. 解析设备树 */
    ret = device_property_read_u32(dev, "touchscreen-size-x", &x_max);
    if (ret)
        return dev_err_probe(dev, ret, "missing touchscreen-size-x\n");

    /* 2. 硬件初始化 */
    gpiod_set_value(client->irq_gpio, 1);
    msleep(20);
    /* 读取芯片 ID 确认通信正常 */
    ret = i2c_smbus_read_byte_data(client, REG_CHIP_ID);
    if (ret < 0)
        return dev_err_probe(dev, ret, "chip not found\n");

    /* 3. 申请中断 */
    ret = devm_request_threaded_irq(dev, client->irq, NULL,
                                    my_touch_irq_handler,
                                    IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                                    "my_touch", client);
    if (ret)
        return dev_err_probe(dev, ret, "failed to request irq\n");

    /* 4. 分配 input_dev */
    input = devm_input_allocate_device(dev);
    if (!input)
        return -ENOMEM;

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

    /* 5. 设置事件类型 */
    set_bit(EV_ABS, input->evbit);
    set_bit(EV_KEY, input->evbit);
    set_bit(BTN_TOUCH, input->keybit);

    input_set_abs_params(input, ABS_X, 0, x_max, 0, 0);
    input_set_abs_params(input, ABS_Y, 0, y_max, 0, 0);
    input_set_abs_params(input, ABS_PRESSURE, 0, 255, 0, 0);

    /* 6. 注册 input 设备 */
    ret = input_register_device(input);
    if (ret)
        return dev_err_probe(dev, ret, "failed to register input device\n");

    return 0;
}

你看,probe 函数的逻辑其实很固定。我个人的习惯是:先确保硬件通信正常,再注册 input 设备。如果芯片 ID 读不到,后面就不用做了,直接返回错误。

核心要点:

  • probe 失败时一定要调用 dev_err_probe,它会在失败时打印错误信息,还能抑制 EPROBE_DEFER 的重复打印。
  • 中断处理函数里只做最轻量的事:读坐标、上报事件。千万别在里面做耗时操作。
  • input_set_abs_params 的第三个参数是最大值,第四个是最小值。触摸屏一般最小值是 0。

嗯,到这里你应该对 input 子系统的框架、设备树配置、probe 流程有了整体认识。下一块内容我们会深入中断处理函数怎么写,以及如何做多点触控的上报。不过那是后面的事了,先把今天这些消化掉。