2、驱动框架入门:Linux input子系统架构、设备树节点配置、驱动注册流程

好,咱们进入第二章。这一章我打算聊聊Linux input子系统。说实话,这是触摸屏驱动的核心骨架。你想想看,没有这个框架,每个触摸屏厂商都得自己搞一套接口,那上层应用得疯掉。

2.1 为什么需要input子系统?

说白了,Linux内核里设备类型太多了。键盘、鼠标、触摸屏、游戏手柄……它们虽然长得不一样,但本质都是输入设备。内核大佬们就想:能不能搞一套统一的模型?

于是input子系统诞生了。它做了三件事:

  • 统一设备抽象:不管你是USB触摸屏还是I2C触摸屏,最后都注册成input设备
  • 标准事件上报:触摸坐标、按键值、相对位移,都有固定的事件类型
  • 用户空间接口:通过/dev/input/eventX,应用层直接读就行

核心思想:驱动只负责「检测到触摸→上报事件」,至于事件怎么处理,那是应用层的事。各司其职,清爽得很。

2.2 input子系统三层架构

我习惯把input子系统分成三层,像三明治一样:

层次 角色 典型文件
核心层 注册管理、事件分发 drivers/input/input.c
驱动层 硬件操作、事件上报 你的触摸屏驱动
事件层 与用户空间交互 evdev.c/dev/input/eventX

嗯,这里要注意:驱动层是你需要写的。核心层和事件层内核已经帮你搞定了。你只需要调用几个API就行。

2.3 设备树节点配置

现在主流平台都用设备树(Device Tree)来描述硬件。我见过不少新手直接在驱动里硬编码I2C地址和中断引脚,结果换个板子就得改代码。设备树就是来解决这个问题的。

一个典型的触摸屏设备树节点长这样:

&i2c2 {
    status = "okay";
    clock-frequency = <400000>;

    touchscreen@38 {
        compatible = "goodix,gt911";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <8 IRQ_TYPE_EDGE_FALLING>;
        irq-gpios = <&gpio1 8 GPIO_ACTIVE_LOW>;
        reset-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
        touchscreen-size-x = <1024>;
        touchscreen-size-y = <600>;
    };
};

我来拆解一下关键字段:

  • compatible:驱动匹配的关键字,驱动里用of_match_table来匹配
  • reg:I2C设备地址,0x38是GT911的常用地址
  • interrupts:中断配置,这里用了下降沿触发
  • touchscreen-size-x/y:触摸屏分辨率,驱动初始化时读这个值

我的习惯:我会把触摸屏的物理参数(分辨率、翻转方向、最大触点数)都放在设备树里。这样同一个驱动可以适配不同规格的屏幕,只需要改dts文件就行。

2.4 驱动注册流程

好,到了最核心的部分。驱动注册流程,我把它总结成四步走:

  1. 匹配设备:内核根据设备树的compatible找到你的驱动
  2. probe函数执行:初始化硬件、申请中断、注册input设备
  3. 事件上报:中断触发后,读取坐标并上报
  4. 注销清理:驱动卸载时释放资源

我直接给一个精简的probe函数示例,你看一眼就明白了:

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

    // 1. 分配input设备
    input = devm_input_allocate_device(&client->dev);
    if (!input)
        return -ENOMEM;

    // 2. 设置设备信息
    input->name = "GT911 Touchscreen";
    input->id.bustype = BUS_I2C;

    // 3. 设置支持的事件类型
    __set_bit(EV_ABS, input->evbit);      // 支持绝对坐标
    __set_bit(EV_KEY, input->evbit);      // 支持按键事件
    
    // 4. 设置触摸坐标范围
    input_set_abs_params(input, ABS_MT_POSITION_X, 
                         0, 1024, 0, 0);
    input_set_abs_params(input, ABS_MT_POSITION_Y, 
                         0, 600, 0, 0);
    
    // 5. 注册input设备
    ret = input_register_device(input);
    if (ret) {
        dev_err(&client->dev, "注册input设备失败\n");
        return ret;
    }

    // 6. 申请中断
    ret = devm_request_threaded_irq(&client->dev, 
                                    client->irq,
                                    NULL, touch_irq_handler,
                                    IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                                    "touch", input);
    if (ret) {
        dev_err(&client->dev, "申请中断失败\n");
        return ret;
    }

    dev_info(&client->dev, "触摸屏驱动加载成功\n");
    return 0;
}

这段代码里,我用了devm_系列函数。这是内核提供的资源管理机制,好处是:probe失败或者驱动卸载时,内核会自动帮你释放资源。我曾经在早期项目里手动管理资源,漏了一个free_irq,结果系统重启都关不掉中断……从那以后我就只用devm_了。

2.5 事件上报:触摸数据怎么送到用户空间?

中断来了之后,驱动要做的就是把触摸坐标报上去。标准做法是用input_mt_report_slot_stateinput_report_abs

static irqreturn_t touch_irq_handler(int irq, void *dev_id)
{
    struct input_dev *input = dev_id;
    int x, y, id;

    // 读取硬件寄存器获取坐标
    x = read_touch_x();
    y = read_touch_y();
    id = read_touch_id();  // 触摸点ID

    // 上报触摸事件
    input_mt_slot(input, id);
    input_mt_report_slot_state(input, MT_TOOL_FINGER, true);
    input_report_abs(input, ABS_MT_POSITION_X, x);
    input_report_abs(input, ABS_MT_POSITION_Y, y);

    // 同步事件,告诉内核这批数据发完了
    input_sync(input);

    return IRQ_HANDLED;
}

这里有个关键点:input_sync必须调用。不调用的话,应用层收不到完整的事件包。我调试第一个触摸驱动时,坐标死活读不到,查了两天才发现是忘了加input_sync。嗯,这种坑踩过一次就记住了。

2.6 避坑指南

我曾经踩过的坑:

  • 中断触发方式搞错:设备树配了下降沿触发,但硬件实际是低电平触发。结果中断风暴,系统直接卡死。建议先用示波器确认波形。
  • 坐标范围不匹配:设备树配了1024x600,但触摸屏IC实际输出是2048x1200。应用层拿到坐标直接偏了一半。解决办法:驱动里做一次线性映射。
  • 多点触摸协议选错:Linux支持Type A和Type B两种多点协议。Type B需要跟踪触摸点ID,Type A不需要。我建议新项目直接用Type B,兼容性更好。

2.7 小结

这一章我们走通了input子系统的全流程。从设备树配置,到probe函数,再到中断上报。说白了,触摸屏驱动就是个「检测→上报」的循环。框架帮你搭好了,你只需要填硬件相关的部分。

下一章我会讲具体的触摸屏协议解析,包括GT911和FT5x06这两种常见芯片的数据格式。到时候我会拿实际项目里的log来演示,保证你看完就能上手写驱动。