input_report_key + input_sync:按键事件的正确报告姿势
这个仓库已经开源!所有教程,主线内核移植,跑新版本imx-linux/uboot都在这里,或者一起来尝试跑7.1的Linux!欢迎各位大佬观摩!喜欢的话点个⭐!(昨天刚更新)
仓库地址:https://github.com/Awesome-Embedded-Learning-Studio/imx-forge
静态网页:https://awesome-embedded-learning-studio.github.io/imx-forge/
上一章我们讲了 Input 子系统的架构,知道驱动需要通过input_event()报告事件。这一章我们深入看一下事件报告的具体实现。说实话,这部分代码虽然不多,但细节还挺多的,用错了可能导致用户空间收不到事件,或者收到错误的事件。
input_event:底层事件报告函数
input_event()是事件报告的核心函数,内核定义如下:
voidinput_event(structinput_dev*dev,unsignedinttype,unsignedintcode,intvalue);三个参数的含义:
type:事件类型,如EV_KEY(按键)、EV_REL(相对坐标)code:事件代码,如KEY_ENTER、BTN_LEFTvalue:事件值,对于按键来说 1=按下,0=松开
补充:按键的 value 只有 0 和 1,但其他事件类型可能有更多值。比如
EV_REL的 value 是移动量(可以是正负),EV_ABS的 value 是绝对坐标值。
调用input_event()前要确保设备支持该事件类型,否则会被忽略:
/* 确保设备支持 EV_KEY */set_bit(EV_KEY,dev->input_dev->evbit);set_bit(KEY_ENTER,dev->input_dev->keybit);/* 现在可以安全报告事件 */input_event(dev->input_dev,EV_KEY,KEY_ENTER,1);如果你报告了一个设备不支持的事件,Input Core 会默默忽略。这个设计有点坑——你不会收到任何错误提示,只是事件不生效。调试的时候如果发现事件没有传递到用户空间,第一件事就是检查设备能力是否正确设置。
input_report_key:便利宏
对于按键事件,Input 子系统提供了便利宏input_report_key():
voidinput_report_key(structinput_dev*dev,unsignedintcode,intvalue){input_event(dev,EV_KEY,code,value);}这个宏其实就是input_event()的封装,省去了写EV_KEY的麻烦。实际使用:
/* 报告 Enter 键按下 */input_report_key(dev->input_dev,KEY_ENTER,1);/* 报告 Enter 键松开 */input_report_key(dev->input_dev,KEY_ENTER,0);提示:类似的便利宏还有
input_report_rel()(相对坐标)、input_report_abs()(绝对坐标)等,对应不同的事件类型。
input_sync:事件同步点
报告事件后,必须调用input_sync()标记同步点:
voidinput_sync(structinput_dev*dev){input_event(dev,EV_SYN,SYN_REPORT,0);}EV_SYN是同步事件类型,SYN_REPORT是"一批事件结束"的标记。为什么需要这个?因为一次硬件动作可能产生多个事件,比如移动鼠标会同时报告 X 和 Y 的相对移动量。input_sync()告诉子系统"这一批事件结束了",确保用户空间原子地收到所有相关事件。
⚠️ 注意:忘记调用
input_sync()是常见的错误。没有同步点,事件可能会堆积在缓冲区里,用户空间 read() 可能阻塞或者收到延迟的事件。
完整的按键事件报告流程:
/* 按键按下 */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);按键代码:KEY_ENTER 从哪来的
我们一直在用KEY_ENTER,这个常量定义在哪里?它在内核头文件include/uapi/linux/input-event-codes.h:
#defineKEY_RESERVED0#defineKEY_ESC1#defineKEY_12#defineKEY_23/* ... 数百个按键定义 ... */#defineKEY_ENTER28#defineKEY_LEFTCTRL29/* ... */这个文件定义了所有标准的按键代码,包括键盘键、鼠标按钮、游戏手柄按钮等。常用的有:
KEY_ENTER/* 回车键,代码 28 */KEY_ESC/* ESC 键,代码 1 */KEY_1~KEY_9/* 数字键 1-9 */KEY_LEFTSHIFT/* 左 Shift,代码 42 */BTN_LEFT/* 鼠标左键,代码 272 */BTN_RIGHT/* 鼠标右键,代码 273 */提示:按键代码是跨平台的标准。你在 ARM 平台上报告
KEY_ENTER,x86 上的用户空间程序能正确识别。这就是 Input 子系统的好处——统一的按键代码映射。
实际驱动中的事件报告
让我们看一下实际驱动中的消抖工作函数:
staticvoiddebounce_work_handler(structwork_struct*work){structdelayed_work*dwork=to_delayed_work(work);structinput_key_dev*dev=container_of(dwork,structinput_key_dev,debounce_work);intcurrent_state;unsignedlongflags;/* 读取 GPIO 逻辑状态 */current_state=key_hw_get_raw_state(dev->gpio);spin_lock_irqsave(&dev->lock,flags);/* 只有状态变化才报告事件 */if(current_state!=dev->last_state){dev->last_state=current_state;/* input_report_key 会自动处理 GPIO_ACTIVE_LOW */input_report_key(dev->input_dev,KEY_ENTER,current_state);input_sync(dev->input_dev);}spin_unlock_irqrestore(&dev->lock,flags);}这里有几个重要的细节。首先是key_hw_get_raw_state()返回的是逻辑值(考虑了 GPIO_ACTIVE_LOW),所以current_state=1表示按键被按下,current_state=0表示按键被松开。我们直接把这个值传给input_report_key(),不需要再反转。
其次是用自旋锁保护last_state。中断处理函数和工作队列可能同时访问这个变量,不加锁会有竞态条件。虽然对于单个按键这种竞态可能不会造成致命问题,但正确做法还是加锁。
补充:为什么
current_state可以直接传给input_report_key()?因为gpiod_get_value()已经应用了 GPIO_ACTIVE_LOW 标志。如果设备树中指定了GPIO_ACTIVE_LOW,按下按键时物理电平是 0,但gpiod_get_value()返回 1(逻辑值"按下")。
事件的传递路径
报告事件后,事件是如何到达用户空间的?让我们追踪一下路径:
- 驱动调用
input_report_key()→ 调用input_event() - Input Core 处理→
input_handle_event()把事件放入缓冲区 - 唤醒 Handler→ evdev Handler 被唤醒
- Handler 唤醒用户空间→ 等待在 read() 的进程被唤醒
整个过程是同步的,input_report_key()返回时,事件已经在内核缓冲区中了。但用户空间 read() 不一定会立即返回,这取决于 read() 的调用方式(阻塞还是非阻塞)。
常见错误:忘记设置 keybit
我们说过,如果设备不支持某个事件类型,报告事件会被忽略。一个常见的错误是:
/* 只设置了 EV_KEY */set_bit(EV_KEY,dev->input_dev->evbit);/* 但忘记设置 KEY_ENTER!*//* set_bit(KEY_ENTER, dev->input_dev->keybit); *//* 报告事件 */input_report_key(dev->input_dev,KEY_ENTER,1);input_sync(dev->input_dev);/* 这个事件会被默默忽略!*/⚠️ 注意:
evbit声明支持的事件类型,keybit声明支持的按键代码。两个都要设置,事件才能正确传递。
正确做法:
/* 设置支持按键事件 */set_bit(EV_KEY,dev->input_dev->evbit);set_bit(KEY_ENTER,dev->input_dev->keybit);/* 现在可以报告事件 */input_report_key(dev->input_dev,KEY_ENTER,1);input_sync(dev->input_dev);报告多个按键
如果你的设备有多个按键,可以这样报告:
/* 设置设备能力 */set_bit(EV_KEY,dev->input_dev->evbit);set_bit(KEY_ENTER,dev->input_dev->keybit);set_bit(KEY_ESC,dev->input_dev->keybit);set_bit(KEY_1,dev->input_dev->keybit);/* 报告不同的按键 */input_report_key(dev->input_dev,KEY_ENTER,1);input_sync(dev->input_dev);input_report_key(dev->input_dev,KEY_ESC,1);input_sync(dev->input_dev);input_report_key(dev->input_dev,KEY_1,1);input_sync(dev->input_dev);每次报告一个按键后调用一次input_sync(),这样用户空间能区分不同的按键事件。