驱动框架入门:Linux input子系统架构、设备树节点配置、probe函数执行流程
好,咱们进入第二章。说实话,很多做触摸驱动的朋友,一开始就被 input 子系统绕晕了。其实没那么复杂。你把它想成一个「快递中转站」就行——硬件产生数据,驱动打包,input 核心负责派送给对应的应用程序。
我刚开始接触这块时,也花了不少时间理清架构。今天我就把核心脉络给你捋一遍,再结合设备树和 probe 流程,让你看完就能上手写代码。
Linux input 子系统整体架构
input 子系统分三层,说白了就是三个角色:
- 设备驱动层:你写的触摸驱动就在这里。它负责和硬件打交道,读取坐标、按键值。
- 核心层:input core。它提供注册接口、事件分发、设备管理。你不需要改它。
- 事件处理层:比如 evdev、joydev。它们把内核事件转换成用户空间能读的 /dev/input/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 流程有了整体认识。下一块内容我们会深入中断处理函数怎么写,以及如何做多点触控的上报。不过那是后面的事了,先把今天这些消化掉。