2. Linux 输入子系统:架构、注册流程与事件上报

做 Qt 触摸屏开发,绕不开 Linux 输入子系统。说白了,你屏幕上的每一次点击、滑动,最终都要通过它变成应用能读懂的事件。我刚开始接触这块时,也觉得很抽象——不就是按个键、点个屏吗?后来调一个电容屏驱动,折腾了三天才发现是事件上报顺序错了。嗯,从那以后,我老老实实把 input 子系统的底層逻辑啃了一遍。

2.1 三层架构:谁在干活?

Linux input 子系统分三层,各司其职。我习惯用一张图来理解:

Linux Input 子系统三层架构 应用层(用户空间) Qt / 窗口管理器 / 游戏 / 输入法 核心层(Input Core) 事件分发 / 设备管理 / 协议解析 驱动层(设备驱动) 触摸屏 / 键盘 / 鼠标 / 游戏手柄 数据流向:硬件 → 驱动层 → 核心层 → 应用层 事件从下往上传递,每一层只做自己的事

这三层的关系,我举个例子你就明白了。你用手指在触摸屏上滑动——驱动层负责把硬件信号转成坐标数据,核心层负责把这些数据包装成标准事件,应用层(比如你的 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)
};

上报流程其实就三个函数调用,我称之为「上报三板斧」:

  1. input_report_key() — 上报按键或触摸状态
  2. input_report_abs() — 上报绝对坐标(触摸屏用这个)
  3. 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 — 最常用的工具,能解析事件类型和值