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 驱动注册流程
好,到了最核心的部分。驱动注册流程,我把它总结成四步走:
- 匹配设备:内核根据设备树的
compatible找到你的驱动 - probe函数执行:初始化硬件、申请中断、注册input设备
- 事件上报:中断触发后,读取坐标并上报
- 注销清理:驱动卸载时释放资源
我直接给一个精简的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_state和input_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来演示,保证你看完就能上手写驱动。