Linux 输入子系统框架:input_dev 结构体、事件上报机制、与用户空间的交互
好,咱们进入第二章。这一章我打算把 Linux 输入子系统的骨架给你拆开看看。说白了,就是搞明白「触摸屏按下去之后,数据是怎么一路跑到用户空间的应用程序手里的」。
我自己刚接触驱动时,最头疼的就是这层抽象。明明硬件寄存器读到了坐标,为什么还要绕这么大一圈?后来踩过几次坑才明白——这套框架就是为了让你不用重复造轮子。你想想看,鼠标、键盘、触摸屏、游戏手柄,上报的都是「事件」,那为什么不统一一套接口呢?
2.1 输入子系统的三层架构
Linux 输入子系统分三层,我习惯这么记:
- 核心层(Input Core):承上启下,管理设备注册和事件分发。
- 驱动层(Driver Layer):你写的触摸驱动就在这层,负责跟硬件打交道。
- 事件处理层(Event Handler):把内核事件转换成用户空间能读的格式,比如 evdev、joydev。
这三层的关系,我画了张图,你一看就明白:
数据从下往上走:硬件触发中断 → 驱动层读取寄存器 → 核心层封装事件 → 事件处理层写入设备节点 → 用户空间读到数据。反过来,应用下发配置也是这条路径,只是方向相反。
2.2 input_dev 结构体深度解析
每个输入设备在内核里都对应一个 struct input_dev。这个结构体很大,但常用的字段其实就那几块。我挑重点说:
struct input_dev {
const char *name; // 设备名称,比如 "Goodix Touchscreen"
const char *phys; // 物理路径,比如 "i2c-0/0-005d"
struct input_id id; // 总线类型、厂商ID、产品ID、版本
unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; // 支持的事件类型位图
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; // 支持的按键位图
unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; // 相对坐标位图
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; // 绝对坐标位图
struct input_absinfo absinfo[ABS_CNT]; // 绝对坐标参数(最大值、最小值、分辨率等)
int (*open)(struct input_dev *dev);
void (*close)(struct input_dev *dev);
int (*flush)(struct input_dev *dev, struct file *file);
struct list_head node; // 用于挂接到全局 input_dev_list
struct device dev; // 内核设备模型
// ... 还有很多,但上面这些是驱动开发最常用的
};
核心要点:evbit 是总开关。你必须在注册设备之前设置好它,告诉核心层「我这个设备能上报什么类型的事件」。比如触摸屏设置 EV_ABS 和 EV_KEY,鼠标设置 EV_REL 和 EV_KEY。
我个人习惯在 probe 函数里这样初始化:
struct input_dev *input_dev;
int ret;
input_dev = devm_input_allocate_device(&client->dev);
if (!input_dev)
return -ENOMEM;
input_dev->name = "My Touchscreen";
input_dev->id.bustype = BUS_I2C;
// 设置支持的事件类型
__set_bit(EV_ABS, input_dev->evbit);
__set_bit(EV_KEY, input_dev->evbit);
// 设置支持的按键(触摸按下/抬起)
__set_bit(BTN_TOUCH, input_dev->keybit);
// 设置绝对坐标参数
input_set_abs_params(input_dev, ABS_X, 0, 1024, 0, 0);
input_set_abs_params(input_dev, ABS_Y, 0, 768, 0, 0);
input_set_abs_params(input_dev, ABS_PRESSURE, 0, 255, 0, 0);
ret = input_register_device(input_dev);
if (ret) {
dev_err(&client->dev, "Failed to register input device\n");
return ret;
}
经验之谈:input_set_abs_params 的第五个参数是 fuzz(噪声容限),第六个参数是 flat(死区)。触摸屏一般设 0,但如果是摇杆类设备,适当设一点 fuzz 可以滤掉微小抖动。我曾经在一个项目里忘了设 fuzz,结果坐标一直在跳,排查了半天才发现是硬件噪声。
2.3 事件上报机制
设备注册好了,接下来就是上报事件。触摸驱动里最常用的上报函数就三个:
| 函数 | 作用 | 参数说明 |
|---|---|---|
input_report_key(dev, code, value) |
上报按键事件 | code: BTN_TOUCH 等;value: 1按下/0抬起 |
input_report_abs(dev, axis, value) |
上报绝对坐标 | axis: ABS_X/ABS_Y/ABS_PRESSURE 等 |
input_sync(dev) |
同步事件,告诉核心层「这批事件发完了」 | 无额外参数 |
标准的上报流程是这样的:
// 假设在中断处理或工作队列中
void touch_report(struct input_dev *input, int x, int y, int pressure, int touch_down)
{
// 1. 上报触摸状态
input_report_key(input, BTN_TOUCH, touch_down);
// 2. 上报坐标
input_report_abs(input, ABS_X, x);
input_report_abs(input, ABS_Y, y);
input_report_abs(input, ABS_PRESSURE, pressure);
// 3. 同步——这一步绝对不能漏!
input_sync(input);
}
注意:input_sync() 是必须调用的。如果你忘了调,事件会一直积压在缓冲区里,用户空间永远读不到数据。我刚开始写驱动时就犯过这个错,触摸屏点了半天没反应,还以为是硬件坏了。
为什么会这样?因为内核为了效率,不会每上报一个值就立刻通知用户空间。它把多个事件打包成一个「帧」,input_sync 就是帧结束标记。用户空间的 read() 只有收到这个标记才会返回数据。
2.4 与用户空间的交互
驱动上报的事件,最终通过 evdev 模块呈现给用户空间。每个注册的 input_dev 都会在 /dev/input/ 下生成一个 eventX 节点。
用户空间读到的数据结构是 struct input_event:
struct input_event {
struct timeval time; // 事件发生的时间戳
__u16 type; // 事件类型,比如 EV_ABS、EV_KEY、EV_SYN
__u16 code; // 事件码,比如 ABS_X、BTN_TOUCH
__s32 value; // 事件值,坐标值或按键状态
};
每个 input_event 固定 16 字节。用户空间程序通过 read() 读取,一次可能读到多个事件。比如一次触摸按下,典型的事件序列是:
// 用户空间读到的原始数据(十六进制 + 解析)
type=EV_ABS (3), code=ABS_X (0), value=520
type=EV_ABS (3), code=ABS_Y (1), value=340
type=EV_ABS (3), code=ABS_PRESSURE (24), value=128
type=EV_KEY (1), code=BTN_TOUCH (330), value=1
type=EV_SYN (0), code=SYN_REPORT (0), value=0 // 帧结束
我常用的用户空间测试命令是 evtest:
# 查看所有输入设备
cat /proc/bus/input/devices
# 监听某个设备的事件
evtest /dev/input/event2
调试技巧:如果你没有 evtest,可以用 hexdump /dev/input/event2 直接看原始字节。16 字节一组,对照 input_event 结构体就能解析出来。我在现场调试时经常这么干,比装工具快多了。
2.5 避坑指南
最后分享几个我实际项目中踩过的坑:
- 事件顺序问题:我建议先上报坐标,再上报按键,最后 sync。有些用户空间程序依赖这个顺序,比如先读到坐标变化再读到触摸按下,逻辑上更合理。
- 重复上报问题:如果触摸屏没有按下,不要反复上报
BTN_TOUCH=0。内核会过滤掉重复值,但浪费 CPU。我见过一个驱动每秒上报 100 次空事件,白白消耗电量。 - 坐标范围匹配:
input_set_abs_params设置的 min/max 必须和触摸屏实际分辨率匹配。不匹配的话,用户空间的校准程序会算错。我曾经把 X 轴范围设成 0-4095,但触摸屏实际只有 0-1024,结果坐标全跑到屏幕外面去了。 - 多指触摸:如果支持多点触控,需要用
MT protocol B,涉及ABS_MT_SLOT、ABS_MT_TRACKING_ID等。这个后面章节会详细讲,这里先提个醒。
嗯,这一章的内容就到这。框架搭好了,后面写具体驱动时你就能理解每一步在干什么了。