linux中input子系统与I2C子系统类似,也被主观分成三部分:输入驱动、输入设备和输入核心。
- 输入驱动 :由linux抽象出通用的几个输入事件代码(evdev.c、keyboard.c、mousedev.c)。
- 输入设备 :需要用户自己实现具体输入设备的代码。
- 输入核心 :搭建输入子系统,并提供输入设备与输入驱动需要的注册API。
在不采用input子系统,而是自己实现的按键字符驱动中,会自己注册驱动,提供file_operations接口,并在读接口中,读取按键的电平值上传给应用。在linux系统中(linux4.9.88),构建了input子系统,所有采用input子系统的设备,在有输入事件后都会主动上报输入事件。
在输入设备中会有以下几个问题:a. 何时上报?是在输入设备输入事件中断产生时上报。b. 如何上报?输入设备在中断函数中调用input提供的input_report_key函数。c. 上报什么东西?(猜测) 由输入设备设定,再经由input.c代码实现与设定值相对应的输入事件驱动匹配。然后经由输入驱动决定上报的具体格式数据。
输入核心(driver/input/input.c):
此代码主要负责搭建linux中input子系统架构,从入口函数进行分析:
/* include/uapi/linux/major.h */#define INPUT_MAJOR 13
/* driver/input/input.c */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; }
return 0;
fail2: input_proc_exit(); fail1: class_unregister(&input_class); return err;}
从input.c 入口函数,可以发现其实现了字符驱动函数都需要实现的通用操作。其中主设备号被固定为13,不同的输入设备主设备号相同,分配的次设备号不同。
输入设备:
以之前写的一个按键驱动代码为例,详细分析此代码如何采用input子系统的。
代码路径:https://github.com/LinuxTaoist/Linux_drivers/blob/master/KEY/input_key/1.0/input_key.c
其入口函数就不再介绍,都是platform_driver通用操作,主要关注一下其probe函数和中断服务函数:
static irqreturn_t gpio_key_handler(int irq, void *dev_id){ /* 上报输入事件 */ input_report_key(dev->inputdev, dev->irq_keydesc->value, 0); input_sync(dev->inputdev);}
static int input_key_probe(struct platform_device *pdev){
/* 申请中断 */ for (i = 0; i < input_key_dev.count; i++) { ret = request_irq(input_key_dev.irq_keydesc[i].irqnum, gpio_key_handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, input_key_dev.irq_keydesc[i].name, &input_key_dev);
} /* 申请input分配input_dev设备 */ input_key_dev.inputdev = input_allocate_device(); input_key_dev.inputdev->name = INPUT_KEY_NAME; /* 初始化input_dev,设置产生哪些事件 */ __set_bit(EV_KEY, input_key_dev.inputdev->evbit); /* 设置产生按键事件 */ __set_bit(EV_REP, input_key_dev.inputdev->evbit); /* 重复事件,比如按下去不放开,就会一直输出信息 */
/* 初始化input_dev,设置产生哪些按键 */ __set_bit(KEY_0, input_key_dev.inputdev->keybit); /* 注册input_dev设备 */ ret = input_register_device(input_key_dev.inputdev); return 0;}
输入设备代码是需要用户自己完成的,其步骤大概包括:
- 获取设备树硬件属性
- 注册事件中断
- 向input子系统申请input_dev设备
- 设置input_dev设备相关参数
- 注册input_dev设备
- 中断服务函数上报输入事件
实现以上步骤,基本上input输入设备代码就完成了。这里分析输入设备是如何加入到input子系统的:
a. 首先需要向input.c申请一个input_dev设备,用到 input_allocate_device来申请:
/* driver/input/input.c */struct input_dev *input_allocate_device(void){ static atomic_t input_no = ATOMIC_INIT(-1); struct input_dev *dev;
dev = kzalloc(sizeof(struct input_dev), GFP_KERNEL); if (dev) { dev->dev.type = &input_dev_type; dev->dev.class = &input_class; device_initialize(&dev->dev); mutex_init(&dev->mutex); spin_lock_init(&dev->event_lock); init_timer(&dev->timer); INIT_LIST_HEAD(&dev->h_list); INIT_LIST_HEAD(&dev->node);
dev_set_name(&dev->dev, "input%lu", (unsigned long)atomic_inc_return(&input_no));
__module_get(THIS_MODULE); }
return dev;}
可以看到input.c函数input_allocate_device内部初始化一个input_dev(包括初始化了input_dev的h_list成员),然后初始化内部其他成员,将input_dev返回给调用者。
b. 根据当前设备需要设置input_dev其他成员,并调用input_register_device注册到input子系统:
int input_register_device(struct input_dev *dev){ /* 插入链表中 */ list_add_tail(&dev->node, &input_dev_list); /* 匹配input_dev对应的handler */ list_for_each_entry(handler, &input_handler_list, node) input_attach_handler(dev, handler);
}
input_dev与handler匹配函数:input_attach_handler
static int input_attach_handler(struct input_dev *dev, struct input_handler *handler){ /* 匹配测试 */ id = input_match_device(handler, dev);
error = handler->connect(handler, dev, id);}
先匹配测试,当匹配成功以后会进入 handler->connect()函数进行handler(输入驱动)中的connect函数,放在驱动输入分析。
输入驱动:
在概述中介绍过,输入驱动就是对通用的输入事件进行抽象的代码。这里以evdev.c为例。
代码路径:driver/input/evdev.c
static int __init evdev_init(void){ return input_register_handler(&evdev_handler);}
int input_register_handler(struct input_handler *handler){ INIT_LIST_HEAD(&handler->h_list);
list_add_tail(&handler->node, &input_handler_list);
list_for_each_entry(dev, &input_dev_list, node) input_attach_handler(dev, handler);}
这里输入驱动注册函数input_register_handler与输入设备代码注册函数input_register_device对应,都是在注册时寻找有没有对应的handler/input_dev,然后进入匹配函数。
static int input_attach_handler(struct input_dev *dev, struct input_handler *handler){ /* 匹配测试 */ id = input_match_device(handler, dev);
error = handler->connect(handler, dev, id);}
这里匹配成功后,进入handler->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_connect:
static int evdev_connect(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id){
/* 获取新的次设备号 */ minor = input_get_new_minor(EVDEV_MINOR_BASE, EVDEV_MINORS, true);
dev_set_name(&evdev->dev, "event%d", dev_no);
/* 将handler和input_dev绑定,及通过handle可以获取到inpu_dev和对应handler */ evdev->handle.dev = input_get_device(dev); evdev->handle.name = dev_name(&evdev->dev); evdev->handle.handler = handler; evdev->handle.private = evdev; /* 将handle作为字符驱动注册到内核 */ evdev->dev.devt = MKDEV(INPUT_MAJOR, minor); evdev->dev.class = &input_class; evdev->dev.parent = &dev->dev; evdev->dev.release = evdev_free; device_initialize(&evdev->dev);
/* 将新建的handle注册到input子系统 */ error = input_register_handle(&evdev->handle);
/* 增加file_operations接口 */ cdev_init(&evdev->cdev, &evdev_fops); evdev->cdev.kobj.parent = &evdev->dev.kobj; error = cdev_add(&evdev->cdev, evdev->dev.devt, 1);
error = device_add(&evdev->dev);
}
通过这里的分析,就明白了最开始input.c中为什么只调用register_chrdev_region单纯申请一下主设备号,而没有cdev_init和cdev_add来注册file_operations。
原因是只有在input_dev与handler匹配成功,才会在connect函数中注册对应的输入驱动的file_operations。而且还会生成handle结构体,其成员
evdev->handle.dev = input_get_device(dev); evdev->handle.handler = handler;
如此便能通过handler/input_dev,找到对应的彼此。
接着分析input_register_handle:
int input_register_handle(struct input_handle *handle){ struct input_handler *handler = handle->handler; struct input_dev *dev = handle->dev;
list_add_tail_rcu(&handle->d_node, &dev->h_list);
list_add_tail_rcu(&handle->h_node, &handler->h_list);
return 0;}
发现是将handler与对应的input_dev的hlist成对插入handle->d_node、handle->h_node中,方便以后查找。
以上便是input子系统的大概流程了,一切都看似铺垫完毕了。
那么问题来了。如何读取这个按键呢,准确实时的获取键值?
在linux中是这样的,当有按键被按下时,输入设备代码中的中断就会被触发,此时我们只需要在中断中获取按键的状态并调用input子系统提供的上报函数直接上报即可。
static irqreturn_t gpio_key_handler(int irq, void *dev_id){ /* 上报输入事件 */ input_report_key(dev->inputdev, dev->irq_keydesc->value, 0); input_sync(dev->inputdev);}
static inline void input_report_key(struct input_dev *dev, unsigned int code, int value){ input_event(dev, EV_KEY, code, !!value);}
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value){ unsigned long flags;
if (is_event_supported(type, dev->evbit, EV_MAX)) {
spin_lock_irqsave(&dev->event_lock, flags); input_handle_event(dev, type, code, value); spin_unlock_irqrestore(&dev->event_lock, flags); }}
static void input_handle_event(struct input_dev *dev, unsigned int type, unsigned int code, int value){ input_pass_values(dev, dev->vals, dev->num_vals);}
static void input_pass_values(struct input_dev *dev, struct input_value *vals, unsigned int count){
handle = rcu_dereference(dev->grab); if (handle) { count = input_to_handler(handle, vals, count); } else { list_for_each_entry_rcu(handle, &dev->h_list, d_node) if (handle->open) { count = input_to_handler(handle, vals, count); if (!count) break; } }
/* trigger auto repeat for key events */ if (test_bit(EV_REP, dev->evbit) && test_bit(EV_KEY, dev->evbit)) { for (v = vals; v != vals + count; v++) { if (v->type == EV_KEY && v->value != 2) { if (v->value) input_start_autorepeat(dev, v->code); else input_stop_autorepeat(dev); } } }}
static unsigned int input_to_handler(struct input_handle *handle, struct input_value *vals, unsigned int count){ handler->events(handle, vals, count);}
最终会进入输入驱动的events函数,
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,};
static void evdev_events(struct input_handle *handle, const struct input_value *vals, unsigned int count){
client = rcu_dereference(evdev->grab);
if (client) evdev_pass_values(client, vals, count, ev_time); else list_for_each_entry_rcu(client, &evdev->client_list, node) evdev_pass_values(client, vals, count, ev_time);}
static void evdev_pass_values(struct evdev_client *client, const struct input_value *vals, unsigned int count, ktime_t *ev_time){ if (wakeup) wake_up_interruptible(&evdev->wait);}
可以看到这里是唤醒evdev->wait事件,那么就在当前文件找对应的函数wait_event_interruptible(&evdev->wait):
static ssize_t evdev_read(struct file *file, char __user *buffer, size_t count, loff_t *ppos){
for (;;) { if (client->packet_head == client->tail && (file->f_flags & O_NONBLOCK)) return -EAGAIN;
while (read + input_event_size() <= count && evdev_fetch_next_event(client, &event)) {
if (input_event_to_user(buffer + read, &event)) return -EFAULT;
read += input_event_size(); }
if (read) break;
if (!(file->f_flags & O_NONBLOCK)) { error = wait_event_interruptible(evdev->wait, client->packet_head != client->tail || !evdev->exist || client->revoked); } }
return read;}
分析到这里基本就清楚了!首先应用会先调用open输入驱动文件,然后调用read函数进入evdev_read。此函数的设计是在死循环中一直上报按键的状态。
然后会根据应用传入的flag是阻塞还是非阻塞决定是否休眠此函数,如果被休眠,就不会再运行输入事件上报。
最后在其他API中使用唤醒接口就能实时控制输入事件的上报。在输入驱动中的中断函数实时发生,并调用input_report_key最终会调用wait_event_interruptible实时上传输入事件给应用层。
4.总结本次的文章主要是对input子系统进行个人的系统分析。input子系统其实就是linux针对不同的输入设备搭建的一个架构,抽象出相同的代码,然后提供用户通用的接口,按linux的标准来就能上报标准的输入数据。
其大致运行流程是:
- 输入驱动注册进去后,系统会在/dev/input/节点下生成对应的eventN
- APP应用程序阻塞式open("/dev/input/eventN",O_RDWR),并执行read。内核进入evdev_read中,(也可以是其他匹配的驱动read),进入死循环并在其中上报输入事件,但是会设计等待队列休眠。
- 在设备驱动中断中调用input_report,最终会调用等待队列唤醒,从而使evdev_read循环执行一次输入事件上报,然后再次休眠。从而实现输入事件实时上报的功能。
如需技术交流,可关注公众号 “开源519” 。
开源519.jpg