Input 子系统按键 - 别重复造轮子了
说实话,写到这里我们已经实现过两种按键驱动了:第一种用字符设备接口,用户空间自己 read/poll;第二种用 poll 改进,支持阻塞等待。这两套方案都能用,代码跑起来也没问题。但每次写完我都会想一个问题:Linux 系统里这么多输入设备——键盘、鼠标、触摸屏、游戏手柄——难道每个驱动都自己搞一套接口?那应用层岂不是要为每个设备写专门的代码?
后来发现 Linux 早就为我们准备好了统一的解决方案——Input 子系统。这是处理输入设备的"正统"方式,所有输入设备都用同一套 API,应用层也用统一的方式访问。你写一个按键驱动,X11、Wayland、Qt、GTK 这些框架直接就能识别,不需要额外适配。
为什么最后才学 Input 子系统
你可能会问,既然 Input 子系统这么好,为什么不一开始就讲?说实话,这是一个循序渐进的考虑。Input 子系统抽象程度高,直接上手的话你很难理解它到底在干什么。先学字符设备,你会理解设备节点的概念、中断处理、工作队列这些基础;再学 poll,你知道应用层怎么等待事件;最后学 Input 子系统,你才能 appreciate 这种抽象带来的便利。
就像学编程,先学底层原理,再学上层框架。Input 子系统是框架,但如果你不懂底层原理,用框架就会变成"调包侠",遇到问题不知道怎么排查。
Input 子系统到底解决了什么问题
我们之前自己写的字符设备驱动,本质上是一个"私有协议"。设备节点 /dev/beep 是我们自定义的,read() 返回的数据格式也是我们自己定义的。这样的驱动有几个明显的问题:
第一个问题是应用层兼容性差。你想在 Qt 应用里用这个按键?对不起,Qt 不认识你自定义的设备节点。你想在浏览器里捕获按键事件?浏览器只支持标准输入设备。你得为每个应用写适配层,或者自己写一个中间层把自定义事件转换成标准事件。说实话,这种重复造轮子的事情真的很没意思。
第二个问题是设备节点管理麻烦。每次加载驱动都要记得创建设备节点,忘记这步的话用户空间根本访问不了。虽然有 udev 规则可以自动创建,但你得维护额外的配置文件。Input 子系统会自动在 /dev/input/eventX 创建设备节点,不需要任何额外配置。
第三个问题是事件格式不统一。我们的驱动返回的是简单的结构体,但标准输入设备支持的事件类型多得多——按键、相对坐标、绝对坐标、LED 状态、自动重复等等。如果以后要扩展功能,自己实现的协议就要不断改,而 Input 子系统早就定义好了标准格式。
Input 子系统的分层架构
Input 子系统采用了经典的分层设计,理解这个架构很重要,不然你写驱动的时候会不知道该调用哪个接口:
┌─────────────────────────────────────────┐
│ 用户空间 (X11/Wayland/Qt/应用程序) │
└───────────────────┬─────────────────────┘
│ /dev/input/eventX
┌───────────────────▼─────────────────────┐
│ Input Core (input.c) │
│ - 事件分发 │
│ - Handler 管理 │
└───────────────────┬─────────────────────┘
┌───────────┴───────────┐
│ │
┌────────▼────────┐ ┌────────▼─────────┐
│ Evdev Handler │ │ 其他 Handler │
│ (evdev.c) │ │ (kbd/mouse/...) │
└─────────────────┘ └──────────────────┘
▲
┌─────────┴───────────┐
│ Input 驱动层 │
│ (我们的驱动) │
└─────────────────────┘最底层是驱动层,也就是我们要写的代码。驱动层的工作很简单:检测硬件变化(比如按键按下),然后调用 Input 子系统提供的事件报告函数。
中间是 Input Core,它负责事件分发。你调用 input_event() 报告一个事件,Input Core 会把这个事件分发给所有注册的 Handler。
最上层是各种 Handler,它们把事件传递给用户空间。最常见的 Handler 是 evdev,它创建 /dev/input/eventX 设备节点,用户空间通过 read() 就能读到事件。还有其他 Handler 比如 kbd(把键盘事件转发到控制台)、mouse(处理鼠标事件)、joystick(游戏手柄)等。
TIP
驱动只需要调用 input_event() 报告事件,剩下的分发、用户空间接口、设备管理都由子系统处理。这就是分层的好处,每层只做自己的事情。
Input 子系统驱动的核心结构
我们来看一下 Input 子系统驱动的核心数据结构。首先是 input_dev,它代表一个输入设备:
struct input_dev {
const char *name; // 设备名称
const char *phys; // 物理路径
struct input_id id; // 设备 ID
unsigned long evbit[NBITS(EV_MAX)]; // 支持的事件类型
unsigned long keybit[NBITS(KEY_MAX)]; // 支持的按键代码
/* ... 更多能力位图 ... */
};input_dev 包含设备的各种信息:名字、物理路径、设备 ID,最重要的就是能力位图(bit field)。通过这些位图,驱动声明这个设备支持哪些事件类型、哪些按键代码。比如我们的按键驱动就设置 EV_KEY 和 KEY_ENTER 这两个位。
然后是驱动自己的设备结构体:
struct input_key_dev {
struct gpio_desc *gpio; // GPIO 描述符
struct input_dev *input_dev; // Input 设备
struct delayed_work debounce_work; // 消抖延时工作
spinlock_t lock; // 保护并发访问
int last_state; // 上次报告的状态
};和之前的字符设备驱动相比,这里没有 cdev、没有 class、没有设备节点管理。Input 子系统把这些都接管了,我们只需要关注硬件交互和事件报告。
工作流程:从按键到事件
整个流程其实很直观。用户按下按键,GPIO 电平变化,触发中断。中断处理函数里我们不直接报告事件,而是调度一个延时工作来消抖。延时工作到期后,我们读取 GPIO 状态,如果状态确实改变了,就调用 Input 子系统的函数报告事件:
/* 报告按键按下 */
input_report_key(dev->input_dev, KEY_ENTER, 1);
input_sync(dev->input_dev);
/* 报告按键松开 */
input_report_key(dev->input_dev, KEY_ENTER, 0);
input_sync(dev->input_dev);input_report_key() 是一个便利宏,它会调用底层的 input_event()。input_sync() 告诉子系统"这一批事件结束",确保事件原子地传递给用户空间。
INFO
input_sync() 的重要性:如果你报告多个相关事件(比如按键的按下 + 自定义扫描码),必须用 input_sync() 标记同步点,确保用户空间收到完整的一批事件。
Input 子系统 vs 字符设备:直观对比
| 特性 | 字符设备驱动 | Input 子系统 |
|---|---|---|
| 设备节点 | 自定义 /dev/beep | 标准 /dev/input/eventX |
| 用户空间支持 | 需要自己写应用 | X11、Qt、GTK 直接支持 |
| 事件格式 | 自定义协议 | 标准 input_event 结构 |
| 设备节点创建 | 手动 mknod 或 udev | 自动创建 |
| 多按键支持 | 需要自己扩展 | 天然支持 |
| 消抖处理 | 自己实现 | 可用子系统机制 |
设备树配置
Input 子系统驱动的设备树配置和之前的平台驱动类似,但有一些标准属性:
imxaes_key_input: key-input {
compatible = "imxaes-input-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;
label = "imxaes-key";
linux,code = <28>; /* KEY_ENTER */
linux,input-type = <1>; /* EV_KEY = 1 */
status = "okay";
};linux,code 和 linux,input-type 是通用的 GPIO 按键绑定的标准属性。虽然我们在驱动里硬编码了 KEY_ENTER,但设备树也可以指定按键代码,这样同一个驱动可以支持不同的按键映射。
接下来的内容
这个教程会按顺序讲解以下内容:
- Input 子系统架构 - 深入理解 Input Core 和 Handler 的工作机制
- 事件报告 -
input_report_key()和input_sync()的使用 - 延时消抖 - 用
delayed_work实现可重新调度的消抖 - 用户空间集成 - 应用程序开发
- 编译和测试 - 完整的构建流程和验证方法
学完这个教程,你就掌握了"正统"的 Linux 按键驱动写法。以后遇到任何输入设备需求,不管是键盘、触摸屏还是游戏手柄,你都知道该怎么做。
相关文档:
- GPIO 按键(轮询) - GPIO 输入基础
- 中断消抖按键 - 中断与工作队列
- Platform LED 驱动 - Platform 框架
下一步: 继续阅读 02_input_architecture.md 深入了解 Input 子系统的架构设计。