1. 从一次按键失灵说起:input 子系统到底管什么
很多人第一次接触 Linux 驱动,都是从 GPIO、LED、蜂鸣器这些"看得见摸得着"的东西入手的——点个灯、响个蜂鸣器,成就感来得快。但一旦碰到"按键按下没反应""触摸屏坐标乱跳""遥控器红外码收不到"这类问题,就会突然发现:底层寄存器明明配置对了,中断也进了,可上层应用就是拿不到数据。这时候你遇到的,就是input 子系统。
我在早期做一块工控板的时候,就栽过这个跟头。当时用 GPIO 中断读一个机械按键,自己在中断里做了个消抖,然后通过copy_to_user把键值传给应用层。单按键测试没问题,可一旦同时接入按键、旋转编码器、触摸屏三路输入,应用层就开始丢事件、错乱。后来才明白:Linux 早就为"输入类设备"设计了一整套标准框架,也就是 input 子系统,我做的那些事情,框架里全都有,而且做得比我好得多。
input 子系统是 Linux 内核中专门负责输入设备的框架层。它统一了按键、键盘、鼠标、触摸屏、摇杆、加速度计、旋转编码器、红外接收等所有"产生输入事件"的设备,向上对应用层暴露统一的字符设备接口(/dev/input/eventX),向下为驱动开发者提供一套注册、上报、管理的 API。它的核心价值在于三点:
- 统一接口:不管你是电容触摸屏还是机械按键,应用层看到的都是
struct input_event事件流,不用为每种设备写一套解析逻辑。 - 事件抽象:把物理动作抽象成"事件类型 + 事件码 + 事件值"三元组,比如按键按下就是
EV_KEY / KEY_ENTER / 1。 - 解耦分层:驱动只管"发生了什么",框架负责"怎么传给用户",中间还夹着事件过滤、重复上报、同步等一堆细节。
这篇文章适合谁看?如果你正在写第一个 input 驱动、正在调试触摸屏坐标、正在被evdev的事件流搞晕,或者面试被问到"input 子系统怎么工作的"答不上来,那这篇就是给你准备的。我会从框架分层讲到核心数据结构,从注册流程讲到事件上报,再把我踩过的坑一个个摊开讲。全程按"为什么这么设计"的思路走,不堆 API 手册。
提示:input 子系统的代码主要位于
drivers/input/目录,核心文件是input.c、evdev.c、input-polldev.c,头文件是include/linux/input.h和include/uapi/linux/input.h。建议边看文章边对照源码,理解会快很多。
2. input 子系统的三层架构:为什么要有 input_dev、input_handler、input_handle
要真正搞懂 input 子系统,绕不开它的三层结构。很多人看源码时被input_dev、input_handler、input_handle这三个结构体绕晕,其实只要抓住一句话:这是典型的"设备-驱动"匹配模型,只不过方向反过来了——是设备去找能处理它的 handler。
2.1 三个核心结构体各自的职责
先看这三个结构体分别代表什么:
| 结构体 | 代表角色 | 谁创建 | 典型实例 |
|---|---|---|---|
input_dev | 一个具体的输入设备 | 设备驱动 | 按键设备、触摸屏 |
input_handler | 一类事件的处理者 | 框架或子系统 | evdev、joydev、mousedev |
input_handle | 设备与处理者的绑定关系 | 匹配时动态创建 | 每个匹配对对应一个 |
input_dev是驱动开发者最常打交道的。你写一个按键驱动,第一步就是devm_input_allocate_device()分配一个input_dev,然后设置它支持哪些事件类型、哪些事件码,最后input_register_device()注册进内核。这个结构体里最关键的两个字段是:
evbit[]:位图,标记这个设备能产生哪些事件类型(EV_KEY、EV_ABS、EV_REL 等)。keybit[] / absbit[] / relbit[]:位图,标记具体支持哪些事件码(比如 KEY_ENTER、ABS_X)。
input_handler是"消费者"。内核自带的 handler 有好几个,最常用的就是evdev,它负责把事件通过/dev/input/eventX暴露给用户空间。还有joydev(游戏杆)、mousedev(老式鼠标)、sysrq(系统请求键)等。每个 handler 通过input_register_handler()注册自己,并声明一个id_table,表示"我能处理哪些设备"。
input_handle则是匹配成功后的产物。当一个新的input_dev注册进来,内核会遍历所有已注册的 handler,用handler->id_table去匹配设备的id。匹配上了,就创建一个input_handle,把两者连起来,并调用 handler 的connect()回调。
2.2 匹配过程到底发生了什么
我用一个具体场景把流程串起来。假设你写了一个按键驱动,注册了一个input_dev,它的id.bustype = BUS_HOST,id.vendor = 0x1234。此时内核里已经注册了 evdev handler,它的id_table里有一条{ .bustype = BUS_HOST }(实际上 evdev 的匹配更宽松,这里简化说明)。
匹配流程大致是:
input_register_device()被调用,设备加入input_dev_list。- 内核遍历
input_handler_list,对每个 handler 调用input_match_device()。 - 匹配规则是:handler 的
id_table中任意一项与设备的id匹配(bustype、vendor、product、version 逐字段比较,INPUT_DEVICE_ID_MATCH_*位决定哪些字段参与比较)。 - 匹配成功,调用
handler->connect(handler, dev, id),evdev 的 connect 会创建一个input_handle,并注册一个字符设备节点/dev/input/eventX。 - 之后设备每次
input_event()上报,都会通过 handle 找到对应的 handler,最终写入 evdev 的缓冲区,唤醒等待读取的用户进程。
反过来,如果先注册的是 handler,后注册设备,流程一样,只是遍历方向相反。这种"双向匹配"设计的好处是:设备和 handler 的注册顺序无关,谁先谁后都能正确绑定。
2.3 为什么不用简单的字符设备直接搞定
看到这里你可能会问:我直接写个字符设备,自己实现 read/write/ioctl,不也能传按键数据吗?为什么要搞这么复杂的三层结构?
我当初也这么想过,直到遇到几个现实问题:
- 多设备复用:一块板子上可能有 5 个按键、1 个触摸屏、1 个旋转编码器。如果每个都写一套字符设备,应用层要打开 7 个设备节点,还要分别解析不同格式的数据。用 input 子系统,它们可以各自是
/dev/input/event0~6,但数据格式完全一致。 - 事件语义标准化:
KEY_ENTER、ABS_X、REL_WHEEL这些码是内核统一定义的,应用层用同一套常量就能解析所有设备。你自己定义的私有格式,换个项目就得重写。 - 现成的用户态生态:
libinput、evtest、X.Org、Wayland、Android 的 InputReader,全都基于 evdev 接口。你自己造一套,这些工具一个都用不了。 - 内核统一管理:电源管理、事件去重、时间戳、同步事件(SYN_REPORT)这些细节,框架都帮你处理了。
所以三层架构不是"为了复杂而复杂",而是用一次性的学习成本,换取长期的复用性和生态兼容性。这笔账怎么算都划算。
3. 写一个 input 驱动:从分配设备到上报第一个按键事件
理论讲完,该动手了。这一节我用一个虚拟按键驱动作为例子,把 input 驱动的完整骨架走一遍。之所以用虚拟设备而不是真实 GPIO,是因为它不依赖具体硬件,你在任何 Linux 机器上都能编译加载测试,非常适合入门。
3.1 分配与初始化 input_dev
第一步是分配设备。现代内核推荐用devm_系列函数,它能自动管理资源释放,驱动卸载时不用手动 free:
#include <linux/module.h> #include <linux/input.h> #include <linux/slab.h> struct my_button { struct input_dev *input; struct timer_list timer; }; static struct my_button *btn; static int __init my_button_init(void) { int ret; btn = devm_kzalloc(NULL, sizeof(*btn), GFP_KERNEL); if (!btn) return -ENOMEM; btn->input = devm_input_allocate_device(NULL); if (!btn->input) return -ENOMEM; btn->input->name = "my-virtual-button"; btn->input->phys = "mybutton/input0"; btn->input->id.bustype = BUS_HOST; btn->input->id.vendor = 0x0001; btn->input->id.product = 0x0001; btn->input->id.version = 0x0100; /* 声明支持按键事件,并指定具体按键码 */ input_set_capability(btn->input, EV_KEY, KEY_ENTER); input_set_capability(btn->input, EV_KEY, KEY_ESC); ret = input_register_device(btn->input); if (ret) { pr_err("register input device failed: %d\n", ret); return ret; } pr_info("my-virtual-button registered\n"); return 0; }这里有几个细节值得展开:
devm_input_allocate_device(NULL)的第一个参数是父设备指针。真实驱动里应该传&pdev->dev或&client->dev,这样设备树和 sysfs 里的层级关系才正确。我这里传 NULL 是为了简化。input_set_capability()是设置能力的推荐方式,它内部会根据事件类型自动设置对应的位图。你也可以直接__set_bit(KEY_ENTER, btn->input->keybit),但前者更安全,因为它会检查事件类型是否合法。id字段很重要,它决定了设备能否被正确的 handler 匹配。bustype一般填BUS_HOST(板载)、BUS_I2C、BUS_SPI、BUS_USB等,取决于你的物理总线。
3.2 上报事件的正确姿势
设备注册好了,接下来就是上报事件。input 子系统提供了一组input_report_*函数,最常用的是:
void input_report_key(struct input_dev *dev, unsigned int code, int value); void input_report_abs(struct input_dev *dev, unsigned int code, int value); void input_report_rel(struct input_dev *dev, unsigned int code, int value); void input_sync(struct input_dev *dev);一个完整的按键上报序列是这样的:
static void report_key_press(struct input_dev *input, unsigned int code) { input_report_key(input, code, 1); /* 按下 */ input_sync(input); /* 同步,标志一次完整事件结束 */ input_report_key(input, code, 0); /* 抬起 */ input_sync(input); }input_sync()绝对不能省。它对应的是EV_SYN / SYN_REPORT事件,作用是告诉用户空间"这一批事件到此为止,可以处理了"。如果你连续上报了 X、Y 坐标却不 sync,用户空间会一直等,直到超时或缓冲区满,表现为"触摸屏卡顿""坐标延迟"。我见过太多新手在这里翻车。
input_report_key的 value 取值有讲究:
0:按键抬起1:按键按下2:自动重复(键盘长按场景,一般由框架处理,驱动不用管)
对于绝对值设备(触摸屏、摇杆),input_report_abs上报的是坐标值,需要配合input_set_abs_params()设置范围:
input_set_abs_params(input, ABS_X, 0, 1023, 0, 0); input_set_abs_params(input, ABS_Y, 0, 599, 0, 0);后两个参数是fuzz和flat。fuzz是噪声容差,小于这个变化量的值会被忽略;flat是中心平坦区,主要用于摇杆。触摸屏一般设 0 就行,但如果是电阻屏抖动厉害,可以设个几像素的 fuzz 来过滤。
3.3 用定时器模拟按键做实测
为了让你能真正跑起来,我用一个内核定时器周期性模拟按键:
static void button_timer_cb(struct timer_list *t) { struct my_button *b = from_timer(b, t, timer); static int pressed = 0; if (!pressed) { input_report_key(b->input, KEY_ENTER, 1); input_sync(b->input); pressed = 1; } else { input_report_key(b->input, KEY_ENTER, 0); input_sync(b->input); pressed = 0; } mod_timer(&b->timer, jiffies + msecs_to_jiffies(2000)); }在init里初始化定时器并启动,在exit里del_timer_sync()。编译加载后,用evtest工具就能看到每 2 秒一次的按键事件:
Event: time 1700000000.123456, type 1 (EV_KEY), code 28 (KEY_ENTER), value 1 Event: time 1700000000.123456, -------------- SYN_REPORT ------------ Event: time 1700000002.123456, type 1 (EV_KEY), code 28 (KEY_ENTER), value 0 Event: time 1700000002.123456, -------------- SYN_REPORT ------------看到这个输出,说明你的第一个 input 驱动已经跑通了。从这一刻起,你写的任何输入设备,只要遵循这套上报规范,就能被 evtest、libinput、桌面环境直接识别。
注意:真实硬件驱动里,上报动作通常发生在中断上下文或工作队列中。中断上下文里不能睡眠,所以
input_report_*系列函数内部用的是自旋锁,可以在中断里安全调用。但如果你要做复杂的消抖逻辑,建议放到工作队列或定时器里做,别在中断里死等。
4. evdev 与用户空间:事件是怎么从内核流到应用的
驱动把事件上报了,但应用层怎么拿到?这中间的桥梁就是evdev。理解 evdev 的工作机制,对调试输入问题至关重要——因为 90% 的"输入异常"最终都要落到 evdev 的缓冲区行为上。
4.1 evdev 的缓冲区与读取语义
evdev 为每个 input 设备创建一个字符设备节点/dev/input/eventX,并维护一个环形缓冲区(struct evdev_client里的buffer[])。驱动每次input_event()上报,最终会调用evdev_event(),把事件写入所有绑定了该设备的 client 的缓冲区。
用户空间通过read()读取,每次读到的是一串struct input_event:
struct input_event { struct timeval time; /* 时间戳 */ __u16 type; /* 事件类型,如 EV_KEY */ __u16 code; /* 事件码,如 KEY_ENTER */ __s32 value; /* 事件值,如 1/0 */ };这里有个非常容易踩的坑:read()返回的数据长度不一定是sizeof(struct input_event)的整数倍吗?其实是的,但一次 read 可能返回多个事件,也可能只返回一个。很多新手写的解析代码假设"一次 read 就是一个事件",结果在事件密集时(比如触摸屏滑动)就错位了。正确做法是:
struct input_event ev; ssize_t n; while ((n = read(fd, &ev, sizeof(ev))) == sizeof(ev)) { /* 处理单个事件 */ }或者一次读一大块,然后按sizeof(struct input_event)切分。永远不要假设 read 的边界和事件的边界对齐。
4.2 用 evtest 和 hexdump 排查问题
调试输入设备,我强烈推荐两个工具:
- evtest:交互式查看事件流,还能列出设备支持的所有事件类型和码。用法
evtest /dev/input/eventX,不带参数会列出所有设备让你选。 - hexdump:当 evtest 显示异常时,直接
hexdump -C /dev/input/eventX看原始字节,能排除解析层的干扰。
有一次我调试一个 I2C 触摸屏,evtest 显示坐标一直是 (0,0)。用 hexdump 一看,发现事件流里只有SYN_REPORT,根本没有ABS_X/ABS_Y。顺着这个线索查驱动,发现是 I2C 读函数返回了错误码但没检查,导致坐标数据全是 0。如果没有 hexdump 这层"裸数据"视角,我可能会一直在应用层找问题。
4.3 事件类型与常用事件码速查
input 子系统定义的事件类型不多,但事件码成百上千。下面这张表是我平时最常查的:
| 事件类型 | 值 | 含义 | 典型事件码 |
|---|---|---|---|
| EV_SYN | 0x00 | 同步事件 | SYN_REPORT、SYN_DROPPED |
| EV_KEY | 0x01 | 按键/按钮 | KEY_ENTER、KEY_A、BTN_LEFT |
| EV_REL | 0x02 | 相对位移 | REL_X、REL_Y、REL_WHEEL |
| EV_ABS | 0x03 | 绝对坐标 | ABS_X、ABS_Y、ABS_MT_POSITION_X |
| EV_MSC | 0x04 | 杂项 | MSC_SCAN |
| EV_SW | 0x05 | 开关 | SW_LID、SW_HEADPHONE_INSERT |
| EV_LED | 0x11 | LED | LED_NUML、LED_CAPSL |
| EV_REP | 0x14 | 自动重复 | REP_DELAY、REP_PERIOD |
其中EV_SYN的SYN_DROPPED值得单独说:当 evdev 缓冲区溢出(用户空间读得太慢),内核会丢弃事件并插入一个SYN_DROPPED。应用层收到这个事件时,应该丢弃当前正在处理的部分状态,重新同步。触摸屏驱动里如果没处理这个,会出现"手指抬起后坐标还停在原地"的诡异现象。
5. 触摸屏与多点触控:input 子系统里最复杂的场景
如果说按键是 input 子系统的"Hello World",那多点触控就是"毕业设计"。它涉及 slot 管理、协议 A/B 的选择、坐标跟踪,是 input 子系统里最容易出问题的部分。我专门用一节来讲,因为这块的坑实在太多。
5.1 协议 A 与协议 B 的本质区别
Linux 多点触控有两套协议:
- 协议 A(Protocol A):每个触摸点独立上报,用
ABS_MT_POSITION_X/Y加SYN_MT_REPORT分隔,最后SYN_REPORT结束。无状态,每次都要上报所有活跃点。 - 协议 B(Protocol B):用 slot 机制,每个触摸点绑定一个 slot,通过
ABS_MT_SLOT切换,只上报变化。有状态,效率更高。
协议 B 是现在的主流,内核文档明确推荐新驱动用协议 B。它的核心 API 是:
input_mt_slot(input, slot); /* 切换到某个 slot */ input_mt_report_slot_state(input, MT_TOOL_FINGER, active); /* 上报该 slot 状态 */ input_report_abs(input, ABS_MT_POSITION_X, x); input_report_abs(input, ABS_MT_POSITION_Y, y); input_sync(input);input_mt_report_slot_state()的active参数是关键:传true表示这个 slot 有手指,传false表示手指离开。当手指离开时,你不需要再上报坐标,框架会自动把该 slot 标记为无效。我见过有人手指离开后还继续上报旧坐标,导致"幽灵触摸"。
5.2 slot 数量与坐标跟踪的坑
slot 数量通过input_mt_init_slots(input, num_slots, flags)初始化。num_slots要大于等于你的硬件实际支持的最大触摸点数。设小了会丢点,设大了浪费内存但无害。
flags里最常用的是INPUT_MT_DIRECT(直接输入设备,如触摸屏)和INPUT_MT_POINTER(间接设备,如触摸板)。这个标志影响框架对坐标的跟踪策略。
坐标跟踪是另一个大坑。硬件上报的原始坐标往往需要经过坐标变换(旋转、缩放、镜像)才能对应到屏幕。这个变换可以在驱动里做,也可以在设备树里通过touchscreen-swapped-x-y、touchscreen-inverted-x等属性配置。我的建议是:优先用设备树属性,驱动里保持原始坐标上报。这样换一块屏幕只需要改设备树,不用重新编译驱动。
5.3 一个真实的多点触控调试案例
之前调一块 5 点电容屏,现象是:单指正常,双指时第二个点的坐标总是第一个点的镜像。查了半天驱动,最后发现是input_mt_init_slots()的 flags 传了INPUT_MT_POINTER而不是INPUT_MT_DIRECT。框架按触摸板的逻辑去跟踪坐标,把第二个点当成了"相对第一个点的偏移",于是出现了镜像。
改成INPUT_MT_DIRECT后问题消失。这个坑的教训是:flags 不是随便填的,它直接决定了框架的坐标跟踪算法。触摸屏用 DIRECT,触摸板用 POINTER,别搞混。
另外,多点触控调试一定要用支持多点的工具。evtest能看原始事件,但看 slot 变化不直观。我推荐用libinput debug-events或者自己写个小程序解析ABS_MT_SLOT和ABS_MT_TRACKING_ID,把每个 slot 的状态打印出来,一目了然。
6. 那些年我踩过的 input 子系统坑
前面讲的都是"应该怎么做",这一节专门讲"我怎么做错的"。这些坑在官方文档里基本找不到,但每一个都让我调了大半天。
6.1 忘记 input_sync 导致的"事件延迟"
这是新手第一大坑。表现是:按键按下去,应用层要等好几秒才收到,或者干脆收不到。原因就是驱动上报了EV_KEY却没调input_sync(),evdev 缓冲区里的事件一直不"封包",用户空间读不到完整的一批。
记住一个原则:每次逻辑上完整的一次输入动作,必须以input_sync()结尾。按键按下是一次,抬起是另一次;触摸屏一次采样周期是一次。宁可多 sync,不可少 sync。
6.2 在中断里做耗时操作
input 驱动的上报函数可以在中断里调用,但中断处理函数本身要尽量短。我见过有人在 GPIO 中断里做 20ms 的软件消抖(mdelay(20)),结果系统直接卡死。正确做法是:中断里只做标记,把消抖和上报放到定时器或工作队列里。
static irqreturn_t button_irq(int irq, void *data) { struct my_button *b = data; mod_timer(&b->debounce_timer, jiffies + msecs_to_jiffies(20)); return IRQ_HANDLED; }定时器回调里再读 GPIO 电平、判断、上报。这样中断上下文只花了几个时钟周期。
6.3 事件码用错导致的"按键无响应"
KEY_ENTER和BTN_LEFT都是按键类事件,但语义完全不同。前者是键盘按键,后者是鼠标按钮。如果你把触摸屏的"按下"上报成BTN_LEFT,桌面环境会把它当成鼠标点击,行为完全不对。
选事件码的原则是:看这个设备在用户空间应该被当成什么。键盘类用KEY_*,鼠标/触摸板按钮用BTN_*,游戏手柄用BTN_*加ABS_*。拿不准就查include/uapi/linux/input-event-codes.h,里面有详细注释。
6.4 缓冲区溢出与 SYN_DROPPED 处理
evdev 的缓冲区大小有限(默认 64 个事件)。如果用户空间读得慢,缓冲区满了,内核会丢弃最旧的事件并插入SYN_DROPPED。应用层必须处理这个事件,否则状态会错乱。
处理逻辑是:收到SYN_DROPPED后,丢弃当前所有未完成的状态,重新从设备读取当前状态(通过EVIOCGKEY、EVIOCGABS等 ioctl),然后继续。libinput 和 X.Org 都实现了这套逻辑,自己写应用的话别忘了。
6.5 设备树属性与驱动逻辑冲突
设备树里的linux,code、linux,axis、touchscreen-*等属性,会和驱动里的硬编码逻辑冲突。我遇到过设备树配了linux,code = <KEY_POWER>,驱动里又硬编码上报KEY_ENTER,结果两个事件都发出去了,应用层收到两个按键。
原则是:能用设备树配置的,就别在驱动里硬编码。驱动只负责"读硬件、上报原始值",语义映射交给设备树。这样同一份驱动能适配不同板子,符合 Linux 的设计哲学。
7. 从 input 子系统延伸出去:面试与进阶方向
input 子系统看起来只是"输入设备驱动",但它牵扯的知识面其实很广。如果你能把这块吃透,很多相关领域都会豁然开朗。
7.1 面试常问的几个问题
我面过不少人,input 子系统相关的题目出现频率很高。典型的有:
- input 子系统的三层结构是什么?答
input_dev、input_handler、input_handle,并说清匹配流程。 - evdev 和 input 核心层的关系?evdev 是一个 handler,通过 handle 绑定到设备,负责字符设备接口和缓冲区。
- 多点触控协议 A 和 B 的区别?无状态 vs 有状态,slot 机制。
- input_sync 的作用?标志一批事件结束,对应
EV_SYN/SYN_REPORT。 - 如何在中断上下文安全上报事件?
input_report_*内部用自旋锁,可在中断调用,但中断处理本身要短。
这些问题看似简单,但要答得让面试官满意,必须理解背后的"为什么"。比如问input_sync,你只答"同步事件"是不够的,要说出"evdev 缓冲区按批处理,没有 sync 用户空间读不到完整事件"。
7.2 和 SPI、I2C 子系统的关系
input 子系统经常和 SPI、I2C 子系统配合使用。触摸屏控制器、键盘控制器大多挂在 I2C 或 SPI 总线上。典型的驱动结构是:
- I2C/SPI 驱动负责总线通信,读寄存器拿原始数据。
- input 驱动负责把原始数据转成事件上报。
这两个角色可以在一个驱动里,也可以拆成 MFD(多功能设备)+ 子设备。比如很多 PMIC 芯片,MFD 驱动负责 I2C 通信,input 子设备负责按键。理解这种分层,对写复杂驱动很有帮助。
7.3 用户态输入栈:libinput 与 Wayland
内核的 input 子系统只是"把事件送到用户空间",真正决定"这个按键干什么"的是用户态输入栈。现代 Linux 桌面用libinput统一处理输入设备,它从 evdev 读事件,做手势识别、加速度处理、设备分类,然后分发给 Wayland compositor 或 X.Org。
如果你做的是嵌入式设备,可能不需要 libinput,直接用 evdev 读事件就行。但如果你做的是桌面或平板类产品,理解 libinput 的行为很重要——因为很多"输入不符合预期"的问题,根源在 libinput 的配置而不是驱动。
7.4 后续可以深入的方向
input 子系统往下挖,还有不少值得研究的东西:
- input-polldev:用于不支持中断的设备,框架帮你轮询。适合低速设备如加速度计。
- ff-memless:力反馈(震动)支持,游戏手柄常用。
- uinput:用户空间模拟输入设备,测试和自动化神器。你可以用 uinput 写个程序模拟触摸屏,测试上层应用。
- input 子系统的电源管理:
input_dev的 suspend/resume 回调,以及EVIOCGRAB独占设备。
我个人最推荐先玩透uinput,因为它能让你在不碰硬件的情况下,快速验证上层逻辑。写个脚本模拟按键、触摸、摇杆,比反复烧录驱动高效得多。
最后分享一个我自己的习惯:每次调新的输入设备,我都会先用evtest确认原始事件流正确,再用libinput debug-events确认用户态解析正确,最后才在应用里处理。分层验证,逐层排除,比一上来就在应用层 debug 快十倍。这个习惯帮我省下的时间,比我学任何单个 API 都多。