2. Linux 输入子系统:架构、注册流程与事件上报
做 Qt 触摸屏开发,绕不开 Linux 输入子系统。说白了,你屏幕上的每一次点击、滑动,最终都要通过它变成应用能读懂的事件。我刚开始接触这块时,也觉得很抽象——不就是按个键、点个屏吗?后来调一个电容屏驱动,折腾了三天才发现是事件上报顺序错了。嗯,从那以后,我老老实实把 input 子系统的底層逻辑啃了一遍。
2.1 三层架构:谁在干活?
Linux input 子系统分三层,各司其职。我习惯用一张图来理解:
这三层的关系,我举个例子你就明白了。你用手指在触摸屏上滑动——驱动层负责把硬件信号转成坐标数据,核心层负责把这些数据包装成标准事件,应用层(比如你的 Qt 程序)通过 read() 从设备节点拿到事件。每一层只做自己的事,互不干扰。
2.2 设备驱动注册流程
驱动怎么注册到 input 子系统?我拆成四个步骤来讲。你在写触摸屏驱动时,基本就是照着这个模板来。
| 步骤 | 函数 / 操作 | 说明 |
|---|---|---|
| 1. 分配 input_dev | input_allocate_device() |
在内存中创建一个 input 设备结构体 |
| 2. 设置设备属性 | set_bit() / input_set_abs_params() |
声明设备类型、支持的事件、坐标范围 |
| 3. 注册设备 | input_register_device() |
将设备挂入核心层,生成 /dev/input/eventX |
| 4. 注销设备 | input_unregister_device() |
卸载驱动时清理资源 |
看一个实际代码片段,这是我之前调过的电容屏驱动简化版:
static int my_touch_probe(struct i2c_client *client, ...)
{
struct input_dev *input_dev;
// 1. 分配
input_dev = input_allocate_device();
if (!input_dev)
return -ENOMEM;
// 2. 设置属性
input_dev->name = "My Capacitive Touch";
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, 600, 0, 0);
input_set_abs_params(input_dev, ABS_PRESSURE, 0, 255, 0, 0);
// 3. 注册
error = input_register_device(input_dev);
if (error) {
input_free_device(input_dev);
return error;
}
// 保存指针,中断处理时会用到
i2c_set_clientdata(client, input_dev);
return 0;
}
set_bit 是否完整。少设一个 EV_ABS,应用层就收不到坐标事件。我曾经漏了 BTN_TOUCH,结果 Qt 能读到坐标但识别不了触摸按下——排查了整整一个下午。
2.3 事件上报机制
驱动注册好了,接下来就是干活——上报事件。Linux input 子系统用 struct input_event 这个结构体来传递数据。你想想看,每次触摸、每次按键,最终都会变成这样一个结构体:
struct input_event {
struct timeval time; // 时间戳,精确到微秒
__u16 type; // 事件类型:EV_ABS, EV_KEY, EV_SYN...
__u16 code; // 事件码:ABS_X, BTN_TOUCH, KEY_A...
__s32 value; // 事件值:坐标值、按键状态(0/1)
};
上报流程其实就三个函数调用,我称之为「上报三板斧」:
- input_report_key() — 上报按键或触摸状态
- input_report_abs() — 上报绝对坐标(触摸屏用这个)
- input_sync() — 同步,告诉核心层「这一包事件发完了」
看一个中断处理中的上报示例:
static irqreturn_t touch_irq_handler(int irq, void *dev_id)
{
struct input_dev *input_dev = dev_id;
int x, y, pressure;
// 从硬件寄存器读取坐标
x = read_hw_register(REG_X);
y = read_hw_register(REG_Y);
pressure = read_hw_register(REG_PRESSURE);
// 上报触摸状态
input_report_key(input_dev, BTN_TOUCH, pressure ? 1 : 0);
// 上报坐标
input_report_abs(input_dev, ABS_X, x);
input_report_abs(input_dev, ABS_Y, y);
input_report_abs(input_dev, ABS_PRESSURE, pressure);
// 同步——这一步千万不能忘
input_sync(input_dev);
return IRQ_HANDLED;
}
input_sync()。后果是什么?应用层收不到任何事件,因为核心层认为数据还没准备好。另一个坑是上报顺序——必须先上报 BTN_TOUCH,再上报坐标。顺序反了,Qt 可能会把这次触摸当成无效事件丢掉。
2.4 事件类型速查表
做触摸屏适配,你主要跟下面几种事件类型打交道。我整理了一张表,方便你写驱动时对照:
| 事件类型 | 宏定义 | 常用事件码 | 说明 |
|---|---|---|---|
| 同步事件 | EV_SYN |
SYN_REPORT |
标记一组事件结束 |
| 按键事件 | EV_KEY |
BTN_TOUCH, KEY_A |
0=松开, 1=按下, 2=长按 |
| 绝对坐标 | EV_ABS |
ABS_X, ABS_Y, ABS_PRESSURE |
触摸屏、轨迹球 |
| 相对坐标 | EV_REL |
REL_X, REL_Y |
鼠标、触摸板 |
2.5 应用层怎么读事件?
驱动层搞定了,应用层怎么拿数据?Qt 内部其实封装了 libinput 或直接操作 /dev/input/eventX。但为了让你理解底层,我写个简单的读取程序:
#include <linux/input.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main() {
struct input_event ev;
int fd = open("/dev/input/event0", O_RDONLY);
if (fd < 0) {
perror("open");
return -1;
}
while (1) {
read(fd, &ev, sizeof(ev));
if (ev.type == EV_ABS) {
printf("ABS: code=%d value=%d\n", ev.code, ev.value);
} else if (ev.type == EV_KEY && ev.code == BTN_TOUCH) {
printf("TOUCH: %s\n", ev.value ? "DOWN" : "UP");
} else if (ev.type == EV_SYN) {
printf("--- SYNC ---\n");
}
}
close(fd);
return 0;
}
关键点:应用层读事件是阻塞的。没有事件时 read() 会一直等。Qt 内部用 QSocketNotifier 或 QEventLoop 来非阻塞处理,但底层原理就是上面这段代码。
2.6 调试小技巧
最后分享几个我常用的调试手段,遇到触摸屏不工作时可以快速定位:
- 查看设备节点:
ls /dev/input/— 看看有没有 eventX 生成 - 查看设备信息:
cat /proc/bus/input/devices— 确认驱动是否注册成功 - 裸读事件:
hexdump /dev/input/event0— 直接看原始数据流 - 用 evtest:
evtest /dev/input/event0— 最常用的工具,能解析事件类型和值