在Linux系统中,输入子系统(Input Subsystem)负责处理所有输入设备的原始数据,并将其转换为统一的事件格式供上层应用使用。这个抽象层让键盘、鼠标、触摸屏等不同类型的输入设备能够以标准化的方式与系统交互。
我最早接触这个子系统是在调试一个工业触摸屏项目时。当时设备能识别但无法正确响应触摸事件,通过深入分析输入子系统的工作机制,最终定位到是坐标转换的问题。这种经历让我深刻理解了这个看似简单的子系统在实际开发中的重要性。
输入子系统主要由三部分组成:
输入子系统的核心是struct input_dev结构体,每个输入设备在内核中都对应这样一个实例。主要字段包括:
c复制struct input_dev {
const char *name; // 设备名称
unsigned long evbit[NBITS(EV_MAX)]; // 支持的事件类型位图
unsigned long keybit[NBITS(KEY_MAX)]; // 支持的按键码位图
// ...其他能力位图
int (*open)(struct input_dev *dev);
void (*close)(struct input_dev *dev);
// ...其他操作函数
};
在驱动开发中,我们需要:
input_allocate_device()set_bit(EV_KEY, dev->evbit)input_register_device(dev)典型的事件处理流程如下:
input_event()报告事件/dev/input/eventX读取事件事件结构体定义:
c复制struct input_event {
struct timeval time;
__u16 type; // 事件类型(EV_KEY, EV_ABS等)
__u16 code; // 事件代码(KEY_ENTER, ABS_X等)
__s32 value; // 事件值(0/1表示按键释放/按下)
};
以下是一个简单按键驱动的核心代码框架:
c复制#include <linux/input.h>
static struct input_dev *btn_dev;
static irqreturn_t btn_interrupt(int irq, void *dev_id)
{
int state = read_gpio_state();
input_report_key(btn_dev, BTN_0, state);
input_sync(btn_dev);
return IRQ_HANDLED;
}
static int __init btn_init(void)
{
// 1. 分配输入设备
btn_dev = input_allocate_device();
// 2. 设置设备能力
set_bit(EV_KEY, btn_dev->evbit);
set_bit(BTN_0, btn_dev->keybit);
// 3. 注册设备
input_register_device(btn_dev);
// 4. 申请中断
request_irq(GPIO_IRQ, btn_interrupt, IRQF_TRIGGER_RISING, "gpio_btn", NULL);
return 0;
}
触摸屏驱动更复杂,需要处理绝对坐标(ABS_X/ABS_Y)和多点触控协议(Type A/B)。关键设置包括:
c复制// 设置单点触摸能力
input_set_abs_params(dev, ABS_X, 0, MAX_X, 0, 0);
input_set_abs_params(dev, ABS_Y, 0, MAX_Y, 0, 0);
// 设置多点触摸能力(Type B)
input_mt_init_slots(dev, MAX_CONTACTS, 0);
input_set_abs_params(dev, ABS_MT_SLOT, 0, MAX_CONTACTS-1, 0, 0);
input_set_abs_params(dev, ABS_MT_POSITION_X, 0, MAX_X, 0, 0);
input_set_abs_params(dev, ABS_MT_POSITION_Y, 0, MAX_Y, 0, 0);
事件上报时需要先选择slot再上报坐标:
c复制input_mt_slot(dev, slot_id);
input_report_abs(dev, ABS_MT_TRACKING_ID, id);
input_report_abs(dev, ABS_MT_POSITION_X, x);
input_report_abs(dev, ABS_MT_POSITION_Y, y);
输入设备在用户空间表现为/dev/input/eventX字符设备,可以使用evtest工具调试:
bash复制sudo evtest /dev/input/event2
常用工具:
evtest:实时显示输入事件input-event-codes:查看输入设备支持的事件类型xinput:X11环境下的输入设备管理应用程序可以直接读取设备文件获取事件:
c复制struct input_event ev;
int fd = open("/dev/input/event2", O_RDONLY);
while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {
if (ev.type == EV_KEY && ev.code == KEY_ENTER) {
printf("Enter key %s\n",
ev.value ? "pressed" : "released");
}
}
现代桌面环境通常使用libinput库:
c复制struct libinput *li = libinput_path_create_context(&interface);
libinput_dispatch(li);
while ((event = libinput_get_event(li))) {
switch (libinput_event_get_type(event)) {
case LIBINPUT_EVENT_TOUCH_DOWN:
// 处理触摸按下事件
break;
}
libinput_event_destroy(event);
}
bash复制cat /proc/bus/input/devices
bash复制sudo input-events <device-id>
bash复制dmesg | grep input
问题1:设备注册成功但无事件上报
strace跟踪驱动系统调用问题2:触摸坐标不准确
问题3:多点触摸识别错误
IRQF_ONESHOT标志注册中断input_mt_report_pointer_emulation()减少事件数量CONFIG_INPUT相关配置通过input_filter机制可以修改或过滤输入事件:
c复制static bool my_filter(struct input_handle *handle,
unsigned int type, unsigned int code, int value)
{
if (type == EV_KEY && code == KEY_ESC) {
return false; // 过滤ESC键
}
return true;
}
static struct input_filter my_filter = {
.filter = my_filter,
};
// 注册过滤器
input_register_filter(&my_filter);
可以创建虚拟输入设备用于测试或特殊用途:
c复制struct input_dev *vdev = input_allocate_device();
// ...设置设备能力
input_register_device(vdev);
// 发送虚拟事件
input_report_key(vdev, KEY_A, 1);
input_sync(vdev);
现代Wayland合成器通过libinput处理输入:
关键数据结构:
c复制struct wl_seat {
struct wl_pointer *pointer;
struct wl_keyboard *keyboard;
struct wl_touch *touch;
};
在实际项目中,我曾遇到一个触摸屏在X11下工作正常但在Wayland下失效的问题。最终发现是Wayland合成器对多点触控协议的要求更严格,需要正确设置ABS_MT_SLOT和ABS_MT_TRACKING_ID。