1. 为什么需要输入子系统:从零散的驱动到统一的事件流
1.1 没有输入子系统时,驱动各自为政会带来哪些问题
早年的内核里,键盘、鼠标、触摸屏这些输入设备驱动都是各写各的,按键驱动直接调用应用层的回调,触摸屏驱动直接往某个自定义的字符设备里塞坐标。看起来简单直接,但一旦设备多起来,问题就原形毕露:
第一,每个驱动都要自己维护一套设备节点、读函数、写函数和中断处理,代码大量重复,一个按键扫描逻辑在键盘驱动里写一遍,在GPIO按键驱动里又写一遍。第二,应用层没法统一处理,鼠标要开/dev/mouse,键盘要开/dev/keyboard,触摸屏可能叫/dev/touch,每个设备的协议还不一样,写个支持所有输入设备的中间层比登天还难。第三,事件语义混乱,有的驱动上报的是坐标原始值,有的是经过校准的值,有的用整型、有的用浮点,应用层拿到数据根本不知道该怎么解释。
输入子系统就是为了解决这三个问题而生的。它把“设备驱动”和“事件处理”拆开,驱动层只负责上报“发生了什么事件”,比如“按下了一个键”“手指移到了某个坐标”;处理层负责把这些事件组织成统一的格式,再通过字符设备提供给用户空间。从此以后,写驱动的只管上报,写应用的只管读event节点,两边都不需要关心对方的细节。
1.2 输入子系统的分层结构与数据流转
整个输入子系统在Linux内核里分四层,理解这四层基本就掌握了全局。
最底层是设备驱动层,也就是drivers/input/下面那些具体的驱动,比如键盘驱动、鼠标驱动、触摸屏驱动。这一层和具体硬件打交道,负责读取寄存器、处理中断、把硬件信号转换成标准的输入事件。注意,标准事件并不一定是最终用户空间看到的那个数据,它只是“内核标准的中间表示”。
往上一层是输入核心层,就是input.c这个文件。它负责设备注册、事件分发、处理器的匹配。你可以把input core看成是一个邮局:设备驱动把“信”(事件)丢进邮局,邮局根据收件人列表把信投递给不同的handler,至于收件人拿到信之后怎么解读,邮局不管。
再往上是事件处理层,也就是evdev.c、joydev.c这些文件。这里最关键的是evdev,它把输入事件转换成struct input_event这种带时间戳、类型、编码、值的结构体,映射到/dev/input/eventX字符设备,用户空间读的就是它。
最顶层就是用户空间了,包括Qt的输入插件、Android的EventHub、X11/Wayland的输入驱动,以及你自己写的getevent、hexdump这一类调试程序。它们通过read、ioctl、poll这些系统调用和内核交互。
数据流大概是这样的:硬件触发中断 -> 驱动在中断或底半部中读取状态 -> 调用input_report_key、input_report_abs等接口 -> input core把事件投递给所有匹配的handler -> evdev把事件放进自己的环形缓冲区 -> 应用层通过read/select读到数据。
1.3 源码阅读路径与关键文件
看代码是理解输入子系统最直接的方式。建议先看这几个文件,按顺序来:
- include/uapi/linux/input.h:定义了所有的输入事件类型常量、按键编码、轴编码,这是整个系统的“字典”。
- drivers/input/input.c:核心层。注册、注销、事件派发,重点看input_register_device和input_event这两个函数。
- drivers/input/evdev.c:事件处理层。重点看evdev_read、evdev_event和evdev_connect,搞明白事件缓冲区是怎么工作。
- drivers/input/keyboard/gpio_keys.c:一个最经典的按键驱动,代码量不大,适合第一遍练手。
- drivers/input/touchscreen/下随便挑一个触摸屏驱动:用来理解复杂的EV_ABS和多点触摸协议。
我建议读代码的时候不要从头读到尾,太容易晕。先跑一个最简单的gpio_keys,配合getevent看事件的产生,再反过头来读代码,会发现每个结构体和函数都特别具体。
2. 核心数据结构与注册机制:读懂输入设备的内核身份证
2.1 input_dev:设备的能力描述,而不是硬件寄存器描述
如果你写过驱动,一定见过struct input_dev。很多初学者把它当成一个“硬件信息结构体”,其实不对,它描述的是这个设备具备的输入能力,而不是硬件长什么样。
比如你有一个GPIO按键,硬件上就是一个引脚、一个上拉电阻、一个按键。但input_dev里记录的是:支持哪些事件类型(EV_KEY)、支持哪些按键编码(假定是KEY_POWER)、按键事件值的语义(1按下、0松开)。至于按键引脚是GPIO3_12还是GPIO5_07,input core不关心,那是驱动自己的事。
注册一个输入设备的常规流程是这样的:
struct input_dev *dev; dev = devm_input_allocate_device(&pdev->dev); dev->name = "gpio_power_key"; // 设备名,用户空间靠它识别 dev->id.bustype = BUS_HOST; set_bit(EV_KEY, dev->evbit); set_bit(KEY_POWER, dev->keybit); input_register_device(dev);千万别漏了set_bit这一步。input core分发事件时会检查事件的类型和编码是否在设备的能力位图里。如果你注册的时候没有set EV_KEY,后面调用input_report_key时,事件会被静默丢弃,内核不会报错,应用层等了半天什么都没有。我第一次写驱动就踩过这个坑,排查了两天才发现是能力位图没设置完整。
除了事件类型位图,还有几个标志位值得注意:dev->propbit用于指示设备的属性,比如INPUT_PROP_DIRECT表示这是一个直接触控设备(手指落点就是光标位置),INPUT_PROP_POINTER表示这是一个指针设备。这些属性影响用户空间的输入处理策略,比如X11会根据这个决定是否启用指针加速。
2.2 input_handler与input_handle:谁在读、怎么配对
input_dev是设备侧,那谁来接收事件?是input_handler。每个handler代表一种事件处理方式:evdev_handler把事件发到字符设备,joydev_handler把事件转成摇杆协议,mousedev_handler模拟旧的鼠标设备。
配对的过程很有意思。input_register_device时,内核会遍历所有已注册的handler,检查两者的id_table是否匹配。反过来注册handler时,也会遍历所有已注册的device。匹配成功后,内核建立一个input_handle来维持这个连接关系。也就是说同一时刻一个设备可以被多个handler连接,比如一个鼠标设备可以同时被evdev_handler、mousedev_handler连接,于是系统里既有event节点,又有mouse节点。
但要注意,不同的handler对事件的处理是互相独立的,有的会复制事件,有的会过滤事件,有的甚至会把多个设备事件合并成一个(mousedev就是这么干的)。如果发现数据被“吃掉了”,优先怀疑是匹配到了某个不想要的handler。后来内核引入了input_handler的filter函数指针,可以主动拦截事件,在evdev里还能区分“拿到事件后是否让事件继续向下传播”,非常实用。
2.3 从驱动注册到事件上送的完整调用链
这部分建议直接对照源代码看,我帮大家理一条最核心的调用路径。
驱动通过input_register_device -> input_register_device->input_attach_handler完成handler匹配,建立连接。然后每当驱动上报事件时,假设调用input_report_key(实际会走input_event),流程是这样的:
input_report_key(dev, KEY_POWER, 1); -> input_event(dev, EV_KEY, KEY_POWER, 1); -> input_handle_event(dev, type, code, value); // 核心层进行事件有效性判断、过滤消抖等 -> input_pass_values(dev, vals, nvals); // 把事件派发给所有已连接的input_handler -> handler->event(handle, ...); // 对evdev来说,就是evdev_event -> evdev_pass_values(...); // 写入evdev客户端环形缓冲区,唤醒等待进程这段链路的完整理解对排查问题非常有帮助。比如按键事件没有上报,你就得逐层确认:是驱动压根没调用input_report_key,还是调用了但事件被过滤,还是进了evdev缓冲区但应用没读出来。每一层都有对应的检查手段,后面我会讲到具体怎么查。
3. 事件类型、上报API与坐标语义:一次击键是如何变成数据的
3.1 三类核心事件:EV_KEY、EV_REL、EV_ABS
用户空间最终看到的struct input_event只有四个有效字段:timeval时间戳、type事件类型、code事件编码、value事件值。
- EV_KEY是按键类事件,value只有三个合法值:0表示松开,1表示按下,2表示长按/重复。很多新手觉得2没用,其实键盘驱动在按键持续按住时会周期性上报2,用户空间根据它实现“按住连发”,比如方向键持续移动。
- EV_REL是相对位移事件,鼠标移动就是典型的EV_REL:code为REL_X表示水平位移,REL_Y表示垂直位移,value表示移动的增量。注意这个增量是“相对上一次位置的差值”,所以应用层需要自己维护当前坐标。
- EV_ABS是绝对坐标事件,触摸屏单点经典协议就是它:ABS_X表示X轴绝对坐标,ABS_Y表示Y轴绝对坐标,value表示当前位置的具体值。
很多输入设备会混合上报多种类型,比如触摸板既支持EV_REL(手指滑动时)又支持EV_ABS(手指位置)。使用之前建议读一下事件位图,不要假设设备只发某一种事件。
3.2 上报流程与input_sync的作用
写驱动的时候,最容易被忽略的是input_sync,很多第一次上手的朋友都会问:为什么按键明明按下了,应用层收不到?
看下面这个例子:
input_report_key(dev, KEY_VOLUMEUP, 1); input_report_key(dev, KEY_VOLUMEUP, 0); input_sync(dev);input_sync的作用是告诉内核“这一批事件已经完整了,可以派发给用户空间了”。input子系统用EV_SYN/SYN_REPORT来标记事件包边界,内核并不会在你每次调用input_report_key时立刻把事件发出去,而是在收到input_sync之后,把从上一个sync到当前的所有事件作为一个整体派发。
为什么要这样设计?因为很多设备在中断里会读取多个硬件状态,比如按下一个键的同时还伴随着一个编码值变化。如果事件报了一半就发给用户空间,应用层看到的会是一个“不完整状态”,可能在两个不同的事件之间读到自相矛盾的数据。把事件收集齐了再统一派发,应用层每次读到的都是完整的状态快照。
所以drive一定要保证每个中断处理流程都以input_sync结尾。漏掉sync的危害很大,轻则事件长时间不出现,重则缓冲区里堆积完整事件时时间戳错乱。调试时我习惯先用getevent看一眼,如果没有事件输出,但设备节点存在而且有中断在产生,十有八九就是sync丢了。
3.3 触摸屏与EV_ABS的absinfo细节
EV_ABS事件并不是只上报一个坐标值就行,还得告诉用户空间这个坐标轴的范围、分辨率、漂移容忍度等,这些信息存放在struct input_absinfo里。
struct input_absinfo { __u32 value; __u32 min; __u32 max; __u32 fuzz; __u32 flat; __u32 res; };- min、max是坐标轴有效范围。触摸屏的ADC一般是12位,范围0~4095,或者屏幕分辨率范围内直接映射。
- fuzz是噪声容限,通常用来过滤ADC的微小抖动。如果fuzz设得太大,细微滑动会被吃掉,显得触摸“发涩”;设得太小,静止时坐标会一直抖动。
- flat是按键类绝对轴的“死区”设置,用于方向摇杆这类设备。
- res是每毫米的分辨率,可以用于计算物理尺寸,内核开发指南建议尽量补充。
在驱动里注册绝对轴的方式是这样的:
set_bit(EV_ABS, dev->evbit); input_set_abs_params(dev, ABS_X, 0, 4095, 5, 0); input_abs_set_res(dev, ABS_X, 100); // 100 units/mm如果要用多点触摸协议,则要额外注意type B协议,涉及ABS_MT_SLOT、ABS_MT_POSITION_X等特殊编码,还要实现input_mt_init_slots、input_mt_slot等一系列接口。这里先不展开,但我要提醒一句:量产项目里触摸屏多采用type B协议,而不少驱动只把单点坐标给做对了,槽位跟踪没做,导致多指手势时坐标乱跳。调试触摸屏的时候,先确定使用的是type A还是type B,再决定怎么解析。
4. 用户态读取与调试:从/dev/input/eventX到分析工具
4.1 如何快速找到设备节点
注册成功的输入设备会在/dev/input/下生成eventX节点。问题是系统里可能有十几个event节点,哪个才是目标设备?
先别急着挨个试,最稳妥的方式是看内核维护的设备列表:
cat /proc/bus/input/devices这个文件会列出每个输入设备的名称、物理地址、总线类型、支持的事件能力位图,以及对应的event节点。比如你看到Handlers=sysrq kbd event3,说明这个设备被sysrq和kbd这些handler连接了,event节点是event3。
另一个更直观的方式是查看符号链接:
ls -l /dev/input/by-path/这里会根据物理连接路径生成带设备名的链接,比如platform-按键对应的链接,直接指向真实的eventX,比死记event节点的数字编号省心得多。
我个人的习惯是先用cat /proc/bus/input/devices把目标设备的“设备名”和“event节点”对应关系记下来,再在代码里用open("/dev/input/eventX")打开。如果设备数量不固定,可以考虑用libevdev或者直接读by-path,避免在代码里硬编码event编号。
4.2 从hexdump到getevent:两代调试工具的对比
最简单的调试方式,其实不用装任何工具,直接读节点:
hexdump -C /dev/input/event3然后按一下键盘上的按键,你会看到类似这样的输出:
00000000 67 65 0b 5f 00 00 00 00 74 08 32 0a 00 00 00 00 |ge._....t.2.....| 00000010 01 00 00 00 01 1e 00 00 01 00 00 00 00 00 00 00 |................|第一行是时间戳(秒和微秒),第二行的16个字节可以拆成四段:type(4字节) + code(4字节) + value(4字节) + 填充(4字节)。比如type=0x01表示EV_KEY,code=0x011e是KEY_W,value=1表示按下。看着十六进制解码太痛苦,所以我又爱用Android的getevent工具:
getevent -lt /dev/input/event3输出可读性就强多了:
[ 123.456] EV_KEY KEY_W DOWN [ 123.456] EV_SYN SYN_REPORT 00000000 [ 123.890] EV_KEY KEY_W UP [ 123.890] EV_SYN SYN_REPORT 00000000getevent的-l参数会显示可读名称,-t显示时间戳,-s打印设备能力位图。如果设备能力跟你预期不符,用-s一眼就能看出来。
除了这两个工具,还有一个思路是直接写个小程序来读,这样你能完全控制数据处理的逻辑,方便做后续的自动化验证。一个最小可用的C程序我放在第4.3节,用Python也可以。
4.3 用select/poll避免阻塞:一个最小可用的读取程序
读取输入设备时,最简单的办法是阻塞式read。但实际项目里你得同时处理网络、界面、其他IO,不能因为读输入设备把主线程卡死。这时候习惯用select或者poll来等待事件。
#include <linux/input.h> #include <fcntl.h> #include <poll.h> #include <stdio.h> #include <unistd.h> int main(int argc, char **argv) { const char *path = argv[1] ? argv[1] : "/dev/input/event3"; int fd = open(path, O_RDONLY); struct pollfd pfd = { .fd = fd, .events = POLLIN }; struct input_event ev; if (fd < 0) { perror("open"); return 1; } while (1) { int ret = poll(&pfd, 1, 5000); if (ret == 0) { printf("timeout\n"); continue; } if (ret < 0) { perror("poll"); break; } ssize_t n = read(fd, &ev, sizeof(ev)); if (n == (ssize_t)sizeof(ev)) { printf("type=%u code=%u value=%d\n", ev.type, ev.code, ev.value); } else if (n < 0) { perror("read"); break; } } close(fd); return 0; }编译运行:
gcc -o read_input read_input.c ./read_input /dev/input/event3使用这个程序加一个关键经验:注册设备之后有时候节点权限是root:root 0600,普通用户读不了。调试时最省事的办法是把用户加入input组,或者用udev规则设置权限,而不是每次sudo去跑,否则后面集成到应用里会因为权限问题突然找不到设备。
4.4 ioctl获取设备能力位图与绝对轴信息
读事件能拿到的是数据流,但如果你想了解这个设备怎么用,还得通过ioctl查能力。对内核对用户空间来说,能力位图是判断设备类型的关键。
unsigned long evbit = 0; ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit); // 通过位运算检查是否支持EV_KEY、EV_ABS等这里有个很容易搞错的点:EVIOCGBIT宏要根据要查询的“事件类型”填入参数。查询设备支持哪些事件类型时,第一个参数固定是0;拿到evbit之后,如果想进一步查设备支持哪些按键,需要先知道设备确实支持EV_KEY,然后再执行:
unsigned long keybit[KEY_MAX / BITS_PER_LONG + 1] = {0}; ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keybit)), keybit);后面那个sizeof参数,必须跟你传入缓冲区大小相等,否则内核不知道要拷贝多少字节,读出来的可能是残值。我之前写过一次,传入sizeof(unsigned long)结果缓冲区明明更大,读出来的keybit全是乱的,查了好久才发现是这个坑。
绝对轴的信息可以用EVIOCGABS读取:
struct input_absinfo abs; ioctl(fd, EVIOCGABS(ABS_X), &abs); printf("min=%d max=%d fuzz=%d flat=%d res=%d\n", abs.min, abs.max, abs.fuzz, abs.flat, abs.res);读到了abs范围后,你就能把驱动上报的原始值映射到实际屏幕坐标。比如触摸屏ABS_X范围是0~4095,屏幕分辨率是1080,那在无缩放的情况下应用层就得自己算比例。分辨率res字段的引入就是为了让用户空间不用猜物理尺寸,直接根据物理单位换算,能用就尽量用。
5. 常见故障与排查实录:为什么按键不灵、坐标漂了、节点丢了
5.1 按键没有事件上报:三种常见的“假死”
按键没反应是输入相关故障里最高频的,遇到的次数多了,现在基本按套路排查。
第一种情况,设备节点都看不到。打开/proc/bus/input/devices,如果根本没有这个设备,说明问题在驱动注册阶段。先检查dmesg里有没有input_register_device报错,再看module_init有没有执行,再确认设备树里的GPIO、中断配置是否正确。很多驱动在probe阶段因为中断请求失败就直接返回了错误码,设备根本来不及注册。
第二种情况,设备节点存在,但在getevent里按按键没输出。这种问题通常在驱动的事件上报路径。我会在中断函数里临时加一条printk,确认按键按下时中断确实产生。如果中断来了但没上报,重点看是不是漏了input_sync,或者上报的type/code没有被能力位图覆盖。
第三种情况,getevent有输出,但应用层读不到。这时候怀疑evdev客户端的缓冲区有问题。evdev的环形缓冲默认比较浅,如果缓冲区溢出,内核会丢弃最旧的事件并给read返回EAGAIN或者标记事件丢失。应用层如果是阻塞式读取,溢出之后可能出现“永远等不到新事件”的情况。解决方法是应用层改用一个独立的读取线程,或者调大缓冲区(通过input_evdev的buffer size配置),但最根本的还是确保驱动的上报节流合理,别在中断里高频重复上报相同值。
5.2 触摸坐标错乱或漂移:先查absinfo,再查MT协议
触摸屏坐标乱,最常见的原因是absinfo的范围和实际不匹配。比如屏幕只有1280x720,驱动却把ABS_X的max设成了4095,坐标系没有归一化,应用层按自己的分辨率去缩放,坐标自然对不上。排查手段很简单,用第4.4节那段代码,把ABS_X、ABS_Y的min、max全部读出来,跟设备真实分辨率对比。如果min不是从0开始,应用层还得减去偏移,这里也容易出问题。
第二个常见原因是x轴和y轴反了。有的屏幕接线比较特殊,驱动里没有做坐标交换。验证方法更直观:触摸屏上从左往右滑,看ABS_X是变大还是变小。如果变大说明X方向正常,如果变小,需要通过驱动里的坐标变换逻辑翻转。实际项目中,这种顺滑的操作往往在驱动里就能解决,不用在应用层做。
第三个隐蔽问题是多点触摸时的slot错乱。如果你用type B协议上报坐标,而驱动在input_mt_slot里设置槽位的顺序跟硬件报点顺序不一致,用户空间看到的可能是两个手指的坐标在互相交换,持续滑动时尤其明显。调试时先按一个手指,再按第二个,观察ABS_MT_SLOT变化,确认每个触摸点是否稳定对应同一个slot。如果出现slot频繁切换,“伪指针漂移”的体感就出来了。
5.3 卡键、重复触发与事件丢失
卡键的表现是按键按一下,应用层一直认为按住不放。问题常常出在驱动上报流程里按键的释放事件没有正确发送。检查一下按键的触发逻辑:GPIO中断触发方式用上升沿和下降沿都要处理,只处理了一个沿,就会出现“按下有事件,松开没事件”。如果是轮询方式,必须在循环里同时判断当前值和上一次值,检测状态变化,而不是只上报“当前是按下”。
重复触发跟卡键还不太一样。按住一个键,系统每隔几十毫秒上报一次2(EV_KEY value=2)。如果你不想要重复事件,驱动层面可以在上报前过滤value=2的周期上报,只在状态变化时上报。用户空间也可以设置键盘重复延迟,但最干净的方式是驱动只报状态变化。
事件丢失则是缓冲区问题。evdev的每个客户端(每个打开event节点的进程)都有自己的环形缓冲区,如果一个进程读取速度慢,缓冲满了之后内核会丢弃后续事件。如果怀疑这里,我建议用两个终端同时getevent同一个节点,一个保持不读,另一个看是否出现事件丢失标记。
5.4 设备节点混乱、权限与udev规则
默认eventX数字编号不是固定的,越插越乱是常见现象。如果你的应用在/etc/init.d里写死了/dev/input/event2,下次开机多了一个设备,你的程序就可能读到完全无关的设备上去了。
解决方案就是用by-path或者udev自定义稳定名称。具体做法是在/etc/udev/rules.d/下新建规则文件,比如:
SUBSYSTEM=="input", ATTRS{name}=="gpio_power_key", MODE="0666" SUBSYSTEM=="input", ATTRS{name}=="gpio_power_key", SYMLINK+="input/power_btn"有了这条规则,设备不仅权限可读,而且固定生成/dev/input/power_btn,应用层再也不用关心event数字编号。名字匹配建议尽量精确,避免多个设备共用一套name导致软链冲突。实际工作中,在产品没有屏幕和命令行的设备上,这一条尤其重要:系统升级后event节点的编号一旦变化,应用就会打不开设备,用udev固定名称可以从根上解决。
5.5 中断里不要乱printk:性能与实时性的博弈
排查故障最粗暴的方法就是加printk,但是千万记得中断里的printk是带锁的,可能会触发调度和长时间中断屏蔽。如果你的驱动在中断里高强度上报,比如触摸屏,在中断里加printk可能导致中断延时、触摸坐标卡顿、应用层读取超时。正确的做法是临时在中断函数里把一个计数器递加,通过proc或者debugfs提供出去,看完再删掉。实在要打印,建议限频,比如每上报100次打印一次。printk属于排查工具,不属于产品代码,发布前一定要清干净。
6. 进阶扩展:uinput模块与真实调试心得
6.1 uinput:不需要写内核驱动,也能制造输入事件
uinput是Linux提供的一个特殊输入设备。通过它,用户空间程序可以模拟一个输入设备,往内核里注入任何你想要的事件。执行/dev/uinput设备的写入,本质上就是从一个虚构的输入设备上报真实事件。
它的使用流程是这样的:打开/dev/uinput,通过ioctl设置设备能力和名称,创建input设备;然后用write写入struct input_event来上报事件。
在什么场景下用uinput很值得?最常见的两个场景:一是外设映射,比如把触摸板上的某个手势转换成按键;二是自动化测试,模拟键盘和鼠标的输入事件,省去实际操作硬件的麻烦。还有不少蓝牙手柄驱动也借助uinput把蓝牙传来的数据封装成内核输入事件。
使用uinput的注意事项和写真实驱动几乎完全一致:必须上报EV_SYN,能力位图必须先设置完整再创建设备。还有一个容易困惑的点:uinput创建的设备也会出现在/proc/bus/input/devices里,占一个event节点。如果系统里出现了一些奇怪的输入设备,可以通过设备名快速识别是不是某个程序创建的虚拟设备。
6.2 我总结的几条输入子系统调试习惯
调试输入子系统这几年,我自己总结了一套固定的工作流,写在这里供参考。
第一件事永远是先确认设备是不是真的注册了。打开/proc/bus/input/devices,确认设备名、事件类型、handlers这些信息对不对。设备都没注册成功,后面怎么查都是白费力气。
第二件事是确认硬件信号有没有到软件。用中断计数、GPIO状态读取或者简单的示波器抓一下,区分是硬件问题还是软件问题。这一步能节省大量排查时间。按键抖动造成的误触发,在硬件层面解决比在软件层面加防抖逻辑方案要省心得多。
第三件事是确认事件有没有到用户空间。用getevent是最快的,至少能确认从驱动到内核到节点这一段是通的。如果getevent也看不到,再回到驱动加调试打印;如果getevent能看到但应用读不到,聚焦在evdev缓冲区和应用层的读逻辑上。
第四件事是保留一份输入事件的基准测试。拿一个刚买来的标准键盘鼠标,用getevent录一段它的事件流,作为后续测试的自参照基准。很多怪异问题,比如按键值偏移、滑动方向反了,对比基准数据一眼就看出来。
最后再说一个小技巧:排查多设备干扰时,把其他输入设备都拔掉,只保留目标设备,再用getevent观察。如果拔掉其他设备之后问题消失了,说明问题大概率是设备之间的资源冲突或者中断争用,而不是单独某个设备的驱动写错了。这个方法在嵌入式设备上尤其管用,因为嵌入式设备里的输入设备经常挤在同一组GPIO和中断源上。