2. Linux输入子系统:架构、核心结构与事件流

做触摸屏驱动,绕不开Linux输入子系统。说白了,它就是内核里一套标准化的“输入事件处理管道”。我最早接触它是在一个工控平板项目上,当时想把一个串口触摸屏接入系统,翻了不少资料才搞明白这套机制。今天咱们就把这块掰开揉碎了讲清楚。

2.1 输入子系统整体架构

输入子系统分三层:设备驱动层、核心层和事件处理层。你可以把它想象成一个工厂流水线——驱动层负责“采集原料”,核心层负责“分拣传送”,事件处理层负责“包装出货”。

Linux 输入子系统三层架构 设备驱动层 (Device Drivers) 触摸屏驱动 / 键盘驱动 / 鼠标驱动 / 游戏手柄驱动 职责:硬件初始化、中断处理、数据采集、调用 input_event() 上报 核心层 (Input Core) input_dev 注册/注销 / 事件分发 / 设备匹配 职责:维护输入设备链表、将事件路由到对应的 handler 事件处理层 (Event Handlers) evdev / joydev / mousedev / keyboard 职责:将核心层事件转换为用户空间可读的 /dev/input/eventX 节点 用户空间通过 open/read/select/poll 操作 /dev/input/eventX 获取输入事件

嗯,这张图我建议你多看两眼。很多新手写驱动时只盯着硬件操作,忽略了核心层和事件处理层的配合,结果设备注册成功了,用户空间就是读不到事件——我当年就踩过这个坑。

2.2 input_dev 结构体深度解析

每个输入设备在内核里都对应一个 struct input_dev。它定义在 linux/input.h 里,字段很多,但常用的就这几类:

字段 类型 说明
name const char * 设备名称,用户空间通过它识别设备
phys const char * 物理路径,比如 USB 的 bus/port 信息
id struct input_id 总线类型、厂商 ID、产品 ID、版本号
evbit unsigned long [] 支持的事件类型位图(EV_KEY、EV_ABS 等)
keybit unsigned long [] 支持的按键码位图(BTN_TOUCH、KEY_A 等)
absbit unsigned long [] 支持的绝对轴位图(ABS_X、ABS_Y、ABS_PRESSURE)
absinfo struct input_absinfo [] 每个绝对轴的最小值、最大值、分辨率等参数
我的习惯:注册触摸屏时,evbit 一定要设置 EV_ABS 和 EV_KEY,keybit 要包含 BTN_TOUCH,absbit 要包含 ABS_X、ABS_Y、ABS_PRESSURE。少一个,用户空间的触摸框架就可能识别不到设备。

来看一个典型的触摸屏 input_dev 初始化代码:

struct input_dev *touch_dev;

touch_dev = input_allocate_device();
if (!touch_dev) {
    pr_err("Failed to allocate input device\n");
    return -ENOMEM;
}

touch_dev->name = "My Touch Screen";
touch_dev->phys = "i2c/touch@1a";
touch_dev->id.bustype = BUS_I2C;

/* 设置支持的事件类型 */
__set_bit(EV_ABS, touch_dev->evbit);
__set_bit(EV_KEY, touch_dev->evbit);

/* 设置支持按键 */
__set_bit(BTN_TOUCH, touch_dev->keybit);

/* 设置绝对轴参数 */
input_set_abs_params(touch_dev, ABS_X, 0, 1024, 0, 0);
input_set_abs_params(touch_dev, ABS_Y, 0, 768, 0, 0);
input_set_abs_params(touch_dev, ABS_PRESSURE, 0, 255, 0, 0);

这里要注意 input_set_abs_params 的第五个参数是 fuzz(抖动容忍值),第六个是 flat(平坦区)。我做过一个项目,触摸屏坐标总在跳动,后来把 fuzz 设成 4,问题就解决了。

2.3 事件上报机制

驱动采集到硬件数据后,通过 input_event() 上报。核心函数就三个:

  • input_report_key(dev, code, value) — 上报按键事件
  • input_report_abs(dev, axis, value) — 上报绝对轴事件
  • input_sync(dev) — 同步事件,告诉核心层“这一批数据发完了”
我曾经踩过的坑:上报完坐标和触摸状态后,忘记调用 input_sync()。结果用户空间收不到任何事件,因为核心层认为数据还没准备好。这个 sync 就像快递的“确认发货”按钮——不点它,包裹永远在仓库里。

触摸屏典型的上报流程:

/* 触摸按下 */
input_report_key(dev, BTN_TOUCH, 1);
input_report_abs(dev, ABS_X, x);
input_report_abs(dev, ABS_Y, y);
input_report_abs(dev, ABS_PRESSURE, pressure);
input_sync(dev);

/* 触摸移动 */
input_report_abs(dev, ABS_X, new_x);
input_report_abs(dev, ABS_Y, new_y);
input_report_abs(dev, ABS_PRESSURE, new_pressure);
input_sync(dev);

/* 触摸抬起 */
input_report_key(dev, BTN_TOUCH, 0);
input_report_abs(dev, ABS_PRESSURE, 0);
input_sync(dev);

你想想看,每次触摸操作至少产生三个事件包:按下、移动(可能多个)、抬起。每个包都必须以 input_sync() 结尾。这是协议的一部分,不能省。

2.4 与用户空间的交互

事件经过核心层分发后,由 evdev 处理器写入 /dev/input/eventX 设备节点。用户空间程序通过标准的文件操作接口读取:

#include <linux/input.h>
#include <fcntl.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("Axis %d = %d\n", ev.code, ev.value);
        } else if (ev.type == EV_KEY) {
            printf("Key %d = %d\n", ev.code, ev.value);
        } else if (ev.type == EV_SYN) {
            printf("--- Sync ---\n");
        }
    }
    close(fd);
    return 0;
}

struct input_event 的结构很简单:

字段 类型 说明
time struct timeval 事件发生的时间戳,精度微秒
type __u16 事件类型(EV_KEY、EV_ABS、EV_SYN 等)
code __u16 事件码(BTN_TOUCH、ABS_X、SYN_REPORT 等)
value __s32 事件值(按键 0/1、坐标值、压力值)

关键点:用户空间每次 read() 返回一个 struct input_event,大小固定为 24 字节(32位系统)或 32 字节(64位系统)。触摸屏的每次 sync 之间可能包含多个事件,用户空间需要循环读取直到收到 EV_SYN。

在实际的图形框架中(比如 Qt 的 libinput 或 Android 的 EventHub),它们会通过 select() 或 epoll() 监听多个 event 节点,一旦有数据可读就批量处理。嗯,这里有个性能细节:如果驱动上报频率太高,用户空间来不及消费,内核的输入缓冲区会溢出——我遇到过触摸卡顿,最后发现是中断处理里上报太频繁,加了限速就好了。

2.5 小结

输入子系统的设计思路很清晰:驱动只管采集和上报,核心层负责路由,事件处理器负责和用户空间对接。你写触摸屏驱动时,核心工作就是:

  1. 分配并初始化 input_dev,设置好 evbit、keybit、absbit 和 absinfo
  2. 在中断或轮询中采集硬件数据,调用 input_report_xxx() 上报
  3. 每次上报完一组数据,记得 input_sync()
  4. 注册设备,让核心层把它挂到输入设备链表里