linux 内核 --- input 子系统
为什么要用INPUT子系统
不采用input子系统,如果自己实现按键字符驱动,需要自己申请设备号,注册字符设备,实现file_operations接口,创建设备节点,实现阻塞、非阻塞、复用IO、异步通知,INPUT 子系统还统一定义了不同输入设备上报数据的格式。
在输入设备中会有以下几个问题:
a. 何时上报?是在输入设备输入事件中断产生时上报。
b. 如何上报?输入设备在中断函数中调用input提供的input_report_key函数。
INPUT子系统框架
输入子系统主要由三部分构成: 核心层(input.c)、事件处理层(evdev.c, joydev.c, mousedev.c 等等)、设备驱动层(具体硬件设备相关的驱动,比如按键驱动,触摸屏驱动等等 )
一个输入事件从产生到应用捕获的流程是: 产生事件(鼠标移动,触摸点击,按键等) -> 设备驱动捕获事件(通过硬件中断响应) , 上报事件(input_evnet) -> 事件处理层获取到事件, 上报用户 -> 用户获取事件进行逻辑处理。
事件处理层和设备层通过中间核心层接口进行注册和匹配,一个设备驱动可以匹配到多个事件处理层,产生的事件会上报到匹配到的事件处理层。
一个 struct input_handler 实例称为一个事件处理,不同的输入设备有不同的事件处理,比如按键、鼠标,INPUT 子系统也提供了一个能匹配所有设备驱动的事件处理,所以一个设备驱动至少对应一个事件处理。
一个输入设备的驱动(platform总线设备和设备驱动匹配后执行probe(),创建并注册 struct input_dev 实例,实现中断代码调用INPUT子系统提供的上报函数上报事件)称为设备驱动;设备驱动匹配到对应的事件驱动后,调用此事件处理的代码创建设备节点等操作。
INPUT子系统初始化
subsys_initcall(input_init);
module_exit(input_exit);
由上可知,input子系统作为一个模块,在kernel启动的时候就运行了 input_init()
static int __init input_init(void) { int err; err = class_register(&input_class); if (err) { pr_err("unable to register input_dev class\n"); return err; } err = input_proc_init(); if (err) goto fail1; err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0), INPUT_MAX_CHAR_DEVICES, "input"); if (err) { pr_err("unable to register char major %d", INPUT_MAJOR); goto fail2; } ...... }
先注册一个 input 类型的 class
static int __init input_proc_init(void) { struct proc_dir_entry *entry; proc_bus_input_dir = proc_mkdir("bus/input", NULL); if (!proc_bus_input_dir) return -ENOMEM; entry = proc_create("devices", 0, proc_bus_input_dir, &input_devices_proc_ops); if (!entry) goto fail1; entry = proc_create("handlers", 0, proc_bus_input_dir, &input_handlers_proc_ops); ...... }
然后在 /proc/input 下创建两个文件,用于向应用层提供 input 类型的设备信息
最后申请了主设备号 INPUT_MAJOR,给 INPUT 类型设备使用。
核心层 - input.c
这一层是linux内核实现的的一些通用的接口,可以向事件处理层和设备驱动层提供一些公用函数
- input_register_handler /input_unregister_handler -> 向事件处理层提供注册/释放一个hander的接口
- input_register_device /input_unregister_device -> 向设备驱动层提供注册/释放设备的接口(设备驱动程序会用到这两个接口来注册input设备)
- input_register_handle/input_unregister_handle -> 设备和事件匹配成功后会注册一个handle,通过这个handle来绑定上面的handler和device
- input_event -> 上报一个输入事件,由device驱动层调用上报到应用端去处理(设备驱动程序会调用该接口来上报事件,比如按键,鼠标等等)
以上就是input 子系统常用的接口,更多接口可阅读源码(driver\input\input.c)
事件处理层 - 各种 struct input_handler 实例注册
内核一共定义了如下几个事件处理类型
#define EV_SYN 0x00 #define EV_KEY 0x01 #define EV_REL 0x02 #define EV_ABS 0x03 #define EV_MSC 0x04 #define EV_SW 0x05 #define EV_LED 0x11 #define EV_SND 0x12 #define EV_REP 0x14 #define EV_FF 0x15 #define EV_PWR 0x16 #define EV_FF_STATUS 0x17 #define EV_MAX 0x1f #define EV_CNT (EV_MAX+1) // 决定了事件类型的最大数量
事件处理 - evdev
内核在事件处理层中实现了一个输入设备通用的事件驱动,即evdev,对应代码driver/input/evdev.c
。无论是按键、触摸屏还是鼠标,都可以通过evdev进行输入事件的处理。
evdev作为一个模块,在kernel启动的时候执行
module_init(evdev_init);
module_exit(evdev_exit);
evdev_init()为链表 input_handler_list 填充了一个节点,设备驱动调用 input_register_device(),如果设备驱动和evdev这个事件处理匹配,执行函数 evdev_connect() 创建字符设备和设备节点
static struct input_handler evdev_handler = {
.event = evdev_event,
.events = evdev_events,
.connect = evdev_connect,
.disconnect = evdev_disconnect,
.legacy_minors = true,
.minor = EVDEV_MINOR_BASE,
.name = "evdev",
.id_table = evdev_ids,
};
如果用户空间读取的是evdev提供的设备节点eventtn,则上报的是一个未经处理的通用于所有输入设备的事件数据,格式如下:
事件处理 - mousedev - 对应的 event type 是 EV_MSC
static struct input_handler mousedev_handler = { .event = mousedev_event, .connect = mousedev_connect, .disconnect = mousedev_disconnect, .legacy_minors = true, .minor = MOUSEDEV_MINOR_BASE, .name = "mousedev", .id_table = mousedev_ids, };
mousedev会对输入事件数据进行处理从而上报的是鼠标特有的事件,格式如下:
事件处理 - event type 是 EV_SYN
同步事件用来表示什么时候数据读取完成。读取完成后应用层会收到一个同步事件数据,如下0代表同步事件,3代表EV_ABS事件
设备驱动层
这一层就是具体的驱动程序,这一层就是需要我们自己来实现的。我们先分配一个input_dev结构指针,然后可以调用input_register_device 函数来注册一个input_device,同时在注册的时候也会去扫描有没有匹配的handler, 如果有匹配的,handler.connect 就会被调用。注册一个input device的具体步骤如下:
- 分配一个input_dev -- input_allocate_device
- 初始化input_dev成员变量
- 做一些硬件相关的操作,比如中断申请等
- 注册一个input_dev -- input_register_device
注册 input_dev 实例到内核
input 设备类型结构体
struct input_dev { const char *name; const char *phys; const char *uniq; struct input_id id; unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)]; unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; // 位图,记录设备支持的事件类型, 1比特对应一个事件类型,EV_CNT为32,即最大32个
// 事件类型,只需要一个 unsigned long 类型变量,所以 BITS_TO_LONGS(EV_CNT) == 1
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; // 位图,记录设备支持的按键类型
......
ktime_t timestamp[INPUT_CLK_MAX];
bool inhibited;
};
1、动态申请了一个 struct input_dev 类型变量dev,并初始化dev->dev.kobj.name = "input0",这个就是设备节点的名字;dev->dev的class name为 "input"
struct input_dev *input_allocate_device(void);
2、初始化 input_dev 的事件类型和事件值
__set_bit(EV_KEY, inputdev->evbit); /* 设置事件类型,对应input_event.type */ __set_bit(EV_REP, inputdev->evbit); /* 重复事件 */ __set_bit(KEY_0, inputdev->keybit); /* 设置哪个按键产生事件,对应input_event.code */
3、向内核注册输入设备
int __must_check input_register_device(struct input_dev *);
从 input_register_device()--->device_add() 可知,如果 input_dev.dev没有设置parent,/sys/devices/virtual/input 作为 input_dev.dev的parent。如果 input_dev.dev 没有设置 init_name,inpu_dev.dev的名字默认为 inputX(X表示数字,每添加一个,数字加1)
从input_register_device()--->input_attach_handler()--->evdev_connect()--->cdev_device_add()可知,INPUT子系统为我们做了设备号的申请、cdev创建并添加到内核、设备的创建并添加到内核
上报输入事件
当我们向 Linux 内核注册好 input_dev 以后还不能使用 input 设备, input 设备都是具有输入功能的,但是具体是什么样的输入值 Linux 内核是不知道的,我们需要获取到具体的输入值,或者说是输入事件,然后将输入事件上报给 Linux 内核。比如按键,我们需要在按键中断处理函数,或者消抖定时器中断函数中将按键值上报给 Linux 内核,这样 Linux 内核才能获取到正确的输入值。不同的事件,其上报事件的 API 函数不同,我们依次来看一下一些常用的事件上报 API 函数。
首先是 input_event 函数,此函数用于上报指定的事件以及对应的值,函数原型如下
void input_event(struct input_dev *dev,
unsigned int type,
unsigned int code,
int value);
input_event 函数可以上报所有的事件类型和事件值, Linux 内核也提供了其他的针对具体事件的上报函数,这些函数其实都用到了 input_event 函数。比如上报按键所使用的input_report_key 函数,此函数内容如下:
static inline void input_report_key(struct input_dev *dev, unsigned int code, int value) { input_event(dev, EV_KEY, code, !!value); }
当我们上报事件以后还需要使用 input_sync 函数来告诉 Linux 内核 input 子系统上报结束,input_sync 函数本质是上报一个同步事件,此函数原型如下所示:
void input_sync(struct input_dev *dev)
驱动实例
#include <linux/device.h> #include <linux/input.h> #include <linux/interrupt.h> #include <linux/module.h> #include <linux/timer.h> #include <linux/poll.h> #include <linux/fs.h> #include <linux/errno.h> #include <linux/miscdevice.h> #include <linux/kernel.h> #include <linux/major.h> #include <linux/mutex.h> #include <linux/proc_fs.h> #include <linux/seq_file.h> #include <linux/stat.h> #include <linux/init.h> #include <linux/device.h> #include <linux/tty.h> #include <linux/kmod.h> #include <linux/gfp.h> #include <linux/platform_device.h> #include <linux/of_gpio.h> #include <linux/of_irq.h> #include <linux/interrupt.h> #include <linux/irq.h> #include <linux/slab.h> #include <linux/fcntl.h> #include <asm/current.h> #include <asm/io.h> static struct input_dev *input_key_dev; struct input_gpio_key{ int m_gpio; //引脚号 struct gpio_desc * m_gpiod; int m_irq; int m_flag; int m_code; struct timer_list m_timer; } ; static struct input_gpio_key * input_keys_gpio; static irqreturn_t input_key_irq(int irq, void *dev_id) { struct input_gpio_key * key_desc = (struct input_gpio_key *)dev_id; printk(KERN_INFO "irq:key_desc->m_code=%d\n", key_desc->m_code); mod_timer(&key_desc->m_timer, jiffies+HZ/100); return IRQ_RETVAL(IRQ_HANDLED); } void input_key_timer_fn(unsigned long arg) { int val; struct input_gpio_key * key_desc = (struct input_gpio_key *)arg; val = gpiod_get_value(key_desc->m_gpiod); /* 上报按键值 0: 按下 1: 抬起 */ input_report_key(input_key_dev, key_desc->m_code, val); input_sync(input_key_dev); } static int input_key_probe(struct platform_device *pdev) { int retval = 0; int err, i; int count; enum of_gpio_flags flags; struct device_node * node = pdev->dev.of_node; count = of_gpio_count(node); //获取设备节点下 gpios属性有几个gpio if(!count){ retval = -1; goto exit_entry; } input_keys_gpio = kmalloc(sizeof(struct input_gpio_key) * count, GFP_KERNEL); if(!input_keys_gpio){ retval = -1; goto exit_entry; } for(i = 0; i < count; i++){ input_keys_gpio[i].m_gpio = of_get_gpio_flags(node, i, &flags); input_keys_gpio[i].m_gpiod = gpio_to_desc(input_keys_gpio[i].m_gpio); //根据gpio引脚转换成gpio desc input_keys_gpio[i].m_irq = gpiod_to_irq(input_keys_gpio[i].m_gpiod); //获取gpio中断号 input_keys_gpio[i].m_flag = flags & OF_GPIO_ACTIVE_LOW; //只获取低电平触发中断的标志 input_keys_gpio[i].m_code = KEY_1+i; //user key1,2 setup_timer(&input_keys_gpio[i].m_timer, input_key_timer_fn, (unsigned long)&input_keys_gpio[i]); add_timer(&input_keys_gpio[i].m_timer); } //申请中断 for(i = 0; i < count; i++){ err = request_irq(input_keys_gpio[i].m_irq, input_key_irq, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "input_gpio_key", &input_keys_gpio[i]); } input_key_dev = input_allocate_device(); //分配一个 input_dev 结构体 if(input_key_dev == NULL){ printk(KERN_ERR "input_allocate_device filed"); retval = -1; goto exit_entry; } input_key_dev->name = "input_key_dev"; //设置支持的输入事件类型 set_bit(EV_KEY, input_key_dev->evbit); //set_bit(EV_LED, input_key_dev->evbit); //设置key支持的事件码(支持哪些按键) set_bit(KEY_1, input_key_dev->keybit); set_bit(KEY_2, input_key_dev->keybit); //注册一个输入设备 retval = input_register_device(input_key_dev); exit_entry: return retval; } static int input_key_remove(struct platform_device *pdev) { int retval = 0; int count, i; count = of_gpio_count(pdev->dev.of_node); for(i = 0 ; i < count; i++){ free_irq(input_keys_gpio[i].m_irq, &input_keys_gpio[i]); del_timer(&input_keys_gpio[i].m_timer); } kfree(input_keys_gpio); if(input_key_dev != NULL){ input_unregister_device(input_key_dev); input_free_device(input_key_dev); input_key_dev = NULL; } return retval; } static const struct of_device_id input_key_table[] = { {.compatible = "100ask,gpio_key"}, { }, }; static struct platform_driver input_key_driver = { .probe = input_key_probe, .remove = input_key_remove, .driver = { .name = "InputKey", //会根据这个名字去匹配device .of_match_table = input_key_table, }, }; static int __init input_key_init(void) { platform_driver_register(&input_key_driver); //注册platfrom 驱动 return 0; } static void __exit input_key_exit(void) { platform_driver_unregister(&input_key_driver); } module_init(input_key_init); module_exit(input_key_exit); MODULE_LICENSE("GPL");
input_dev、input_handler 以及 input_handle之间的关系
注册input_handler的时候会去扫描input_dev_list 链表中有没有匹配的input_dev。同时在注册input_dev的时候也会去扫描input_handler_list 链表查找是否有与之匹配的input_handler 。如果input_device 和 input_handler有匹配成功的,input_handler ->connect函数就会调用。
connect 函数里面会为input_device 分配一个minor(次设备号),并且注册一个handle,将input_dev 和 input_handler 地址分别赋值给 handle.dev 和 handle.handler,通过一个handle将input_dev和input_handler 绑定起来。
input_register_handle 调用时,将handle->d_node 添加到 input_dev->h_list, 将handle->h_node 添加到input_handler->h_list中,这样在input_dev中就能通过input_dev->h_list 找到input_handle, 然后根据input_handle->handler成员找到对应的事件处理层。同理也可以通过input_handler ->h_list 找到input_handle, 然后再通过input_handle->dev 成员找到对应的input_dev。至此input_dev和input_handler就能通过一个input_handle 一一对应起来。设备和事件处理层就连同通了,事件处理层上面的就是用户层。
应用层
应用层代码会通过如下结构体读取事件信息
struct input_event {
struct timeval time;
unsigned short type;
unsigned short code;
unsigned int value;
};
time:事件产生的时间
type:event type 事件类型,如果是按键type == EV_KEY,如下图:
#define EV_SYN 0x00 #define EV_KEY 0x01 #define EV_REL 0x02 #define EV_ABS 0x03 #define EV_MSC 0x04 #define EV_SW 0x05 #define EV_LED 0x11 #define EV_SND 0x12 #define EV_REP 0x14 #define EV_FF 0x15 #define EV_PWR 0x16 #define EV_FF_STATUS 0x17 #define EV_MAX 0x1f #define EV_CNT (EV_MAX+1) // 决定了事件类型的最大数量
code:哪个设备产生的事件,对于键盘,code 的取值小于 BTN_MISC,每一个code值对应键盘的一个按键,如下图:
value:对于按键,可以设置产生事件value等于1、没产生事件value等于0
应用代码实例:
static struct input_event inputevent;
err = read(fd, &inputevent, sizeof(inputevent));
查看 input 设备信息
读取 input 设备输入信息
hexdump /dev/input/event0
查看 input 设备信息
cat /proc/bus/input/devices
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
2020-03-16 AZ ------ 打印标签