Input子系统-Touch Screen

0、Input 子系统介绍

Linux内核为了更好的统一的管理输入型设备:键盘、鼠标、触摸屏、摇杆等,建立起来的一套内核框架,分为四个部分:应用层、input event 、input core、硬件驱动。

代码结构:

input event: /kernel/drivers/input/evdev.c (通用模型)

                       /kernel/drivers/input/mousedev.c

                       /kernel/drivers/input/joydev.c

input core: /kernel/drivers/input/input*.c

 

硬件驱动: /kernel/drivers/input/目录下的文件夹(各种类型的input设备)

Input子系统-Touch Screen

 

 

硬件驱动: 将底层的硬件输入转化为统一事件形式,向输入核心(input core)汇报

Input core:承上启下,为驱动层提供 输入设备注册:input_register_device;为input event层 提供 handler注册接口:input_register_handler;通知事件处理层对事件进行处理,在/proc目录下产生相应的设备信息

Input event: 主要是和用户空间交互。(linux中在用户空间将所有的设备都当作文件来处理,所以在一般的驱动程序中都有提供fops接口,以及在/dev下会生成相应的设备文件node,这些操作在输入子系统中由处理层完成)

 

Input 子系统解决了什么问题?

1、在GUI界面中,用户的*度十分大,可以做的事情非常多,Input子系统可以响应不同的输入类设备,而且还能够对不同的输入类设备的输入做出不同的动作。

2、Input子系统还解决了不同的输入类设备的输入事件与应用层之间的数据传输,使得应用层能够获取到不同的输入设备的输入事件,input输入子系统能够包括所有不同种类的输入设备,在应用层能够感知到所有发生的输入事件。

Input子系统的工作流程:

以一次触摸屏触摸事件作为例子来说明input子系统的工作过程:

当我们在触摸屏上按下的时候,就会触发中断(中断是早就注册好的),之后就会去执行这个中断所绑定的中断处理函数,在函数中就会去读取硬件寄存器来判断触摸的区域和状态--------> 将触摸区域信息上报给input core层 ---------->Input core层处理好了之后就会上报给input event层,在这里就会将我们的输入事件封装成一个input event结构体放入一个缓冲区 ------> 应用层read将会将缓冲区中的数据读取出来,获取输入事件的信息。

综上,我们可以发现,input子系统被划分成三个层次是很有必要的,其中input core层的存在主要是将驱动层上报的输入事件整合成标准模型上报给 input event层,而input event层和驱动层都属于并行层以实现对不同种类的输入设备的支持。

 

1、Input core层代码分析

static int __init input_init(void) //subsys_initcall(input_init); 模块入口

err = class_register(&input_class); //创建设备类 /sys/class/input

err = input_proc_init(); //proc文件系统相关的初始化

proc_bus_input_dir = proc_mkdir("bus/input", NULL); //在/proc/bus目录下创建input目录

entry = proc_create("devices", 0, proc_bus_input_dir,//在proc/bus/input目录下创建devices文件(有对应的fileops操作)

entry = proc_create("handlers", 0, proc_bus_input_dir//在proc/bus/input目录下创建handlers文件(有对应的fileops操作)

err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0) //注册input字符设备,主设备号为13,次设备号未分配 fileops需要手动调用cdev_init添加

总结:input_init首先完成了input设备类的创建,而后完成了对proc文件系统的支持,最后调用register_chrdev_region向内核注册了主设备号为13的input字符设备类型驱动。(疑问:这里并没有调用cdev init添加fileops)

 

input core层提供给设备驱动层的接口函数主要有以下三个:

             input_allocate_device //分配一块input_dev结构体类型大小的内存

             input_set_capability //设置输入设备可以上报哪些输入事件

             input_register_device //向input core层注册设备

 

input_allocate_device

 struct input_dev *dev; //声明定义一个input_dev 指针

dev = kzalloc(sizeof(struct input_dev), GFP_KERNEL); //申请分配内存

dev->dev.type = &input_dev_type; //确定input设备的类型

dev->dev.class = &input_class; //确定input设备所属的设备类,input_init中创建

device_initialize(&dev->dev); //input 设备初始化

mutex_init(&dev->mutex); //互斥锁初始化

spin_lock_init(&dev->event_lock); //自旋锁初始化

init_timer(&dev->timer); //timer初始化

INIT_LIST_HEAD(&dev->h_list); //h_list 链表初始化

INIT_LIST_HEAD(&dev->node); //node 链表初始化

总结:当驱动层需要添加一个新的input设备时,调用input_allocate_device分配内存并完成其与input core层联系的初始化

 

 

input_set_capability

函数原型:input_set_capability(struct input_dev *dev, unsigned int type, unsigned int code)

参数: dev 就是input设备

            type 表示设备可以上报的事件类型

            code 表示上报这类事件中的那个事件

 

input_register_device

struct input_handler *handler; //定义了一个input_handler 型指针

__set_bit(EV_SYN, dev->evbit); //每一个input设备都会发生SYN事件,所以统一开启

__clear_bit(KEY_RESERVED, dev->keybit); //清除KEY_RESERVED事件对应的bit位 。也就是不传输这种类型的事件

input_cleanse_bitmasks(dev); //确保input_dev中用来记录事件的变量中没有提到的位掩码是干净的

error = device_add(&dev->dev); //添加设备

path = kobject_get_path(&dev->dev.kobj, GFP_KERNEL); //获取input设备对象所在的路径

list_add_tail(&dev->node, &input_dev_list); //链表挂接。将input_dev->node作为节点挂载到 input_dev_list 链表上去。

list_for_each_entry(handler, &input_handler_list, node) //遍历input_handler_list链表中的所有handler

input_attach_handler(dev, handler); //将handler 与 input设备进行匹配

id = input_match_device(handler, dev); //实际完成match的函数

for (id = handler->id_table; id->flags || id->driver_info; id++) //遍历handler->id_table 所指向的input_device_id数组中各个元素,进行下面的匹配流程

if (id->flags & INPUT_DEVICE_ID_MATCH_BUS) //匹配总线

if (id->flags & INPUT_DEVICE_ID_MATCH_VENDOR)//匹配供应商

if (id->flags & INPUT_DEVICE_ID_MATCH_PRODUCT)//匹配产品

if (id->flags & INPUT_DEVICE_ID_MATCH_VERSION) //匹配版本

if (!bitmap_subset(id->evbit, dev->evbit, EV_MAX))//匹配上传事件是否属实

。。。

if (!handler->match || handler->match(handler, dev)) //如果数组中某个匹配成功了就返回它的地址

error = handler->connect(handler, dev, id); //如果上述的匹配完成的画,就调用handler中的connect函数进行连接

input_wakeup_procfs_readers(); //更新proc文件系统

总结:input_register_device函数 主要完成了input 设备的链表挂接,并遍历了handler链表将input设备与handler进行了匹配

 

input core层提供给input_event层的接口函数主要有两个:

    input_register_handler //input_event层向input_core层注册handler

    input_register_handle //input_event层向input_core层注册handle ,和上面的handler是不一样的概念

 

 

input_register_handler

INIT_LIST_HEAD(&handler->h_list); //初始化handler->h_list链表

list_add_tail(&handler->node, &input_handler_list); //将handler加入这个链表中

list_for_each_entry(dev, &input_dev_list, node) //遍历input_dev_list链表

input_attach_handler(dev, handler); //匹配handler和input设备

input_wakeup_procfs_readers(); //更新文件系统

总结:input_register_handler 初始化了handler链表,并将handler添加到这个链表中,最后通过遍历input 设备链表 试图将handler和input设备进行匹配

综上:注册设备的时候,会将设备挂载到设备链表中(input_dev_list),然后通过遍历handler链表(input_handler_list)去完成设备和handler的匹配;

注册handler的时候,会将handler挂载到handler链表中去,然后通过遍历设备链表完成设备和handler的匹配。

注意:一个input 设备可能与多个handler匹配成功

input_register_handle

功能:注册一个handle

参数:struct handle ,调用input_register_handle时需要先将handle 结构体填充好

Input子系统-Touch Screen

从图中可以看出,一个handle就是用来记录系统中一对匹配成功的handler和device的,我们可以从这个handle中得到handler的信息也可得到device的信息。正是这个功能,我们可以从handler通过handle得到device的信息,也可以从device通过handle得到handler的信息。

input core 层总结:

input core层其实就是input子系统的框架,它提供了以下功能:

1、创建设备类、注册字符设备(init_input)

2、向设备驱动层提供注册接口(input_register_device )

3、提供上册handler和下层device之间的匹配函数(input_attach_handler)

4、向input event层提供注册handler的接口(input_register_handler)

 

2、input event层代码分析

input event层是input子系统的上层,它是由各个handler组成的,各个handler之间是平行关系,不存在相互调用的情况,在实际使用中可能出现多个handler对应一个输入设备的情况,例如:event handler和mouse handler可能都对应一个鼠标输入设备。event 是后期发展而来的带有一定普适性的handler。现在的内核通常都是调用这个来出来输入事件。

input event层的代码实现一般在/driver/input这个目录下,下面我们重点分析evdev.c这个文件。

evdev.c

static int __init evdev_init(void) //入口函数

return input_register_handler(&evdev_handler); //调用input core层提供的接口函数,注册一个handler。(注意:注册handler前需要先将handler结构体填充完成)

static struct input_handler evdev_handler = {

.event = evdev_event, //上报event事件 调用events

.events = evdev_events,

.connect = evdev_connect, //建立链接,在register 时候会被调用

.disconnect = evdev_disconnect,

.legacy_minors = true,

.minor = EVDEV_MINOR_BASE,

.name = "evdev",

.id_table = evdev_ids,

};

如上,handler结构体就是一系列函数指针的集合外加上match时需要的信息。

 

evdev_connect

minor = input_get_new_minor(EVDEV_MINOR_BASE, EVDEV_MINORS, true);//在evdev_table数组中找到一个没有被使用的最小的数组项

evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL); //给evdev结构体申请内存

INIT_LIST_HEAD(&evdev->client_list); //初始化evdev->client_list链表

dev_set_name(&evdev->dev, "event%d", dev_no); //设置input设备名字

evdev->handle.dev = input_get_device(dev);

evdev->handle.name = dev_name(&evdev->dev);

evdev->handle.handler = handler;

evdev->handle.private = evdev; 填充handle ,准备注册

device_initialize(&evdev->dev); //设备初始化

error = input_register_handle(&evdev->handle); //注册handle,完成链接。这样设备和handler就可以相互找到对方了(能找到对方是为了当发生一个输入事件可以调用对应的handler中的event去上报事件)

cdev_init(&evdev->cdev, &evdev_fops); //初始化cdev 添加fops

error = device_add(&evdev->dev); //添加设备到系统中

 

evdev_event

struct input_value vals[] = { { type, code, value } }; //将事件封装成input_value,input_value包含了一个输入事件的所有信息

evdev_events(handle, vals, 1); //调用evdev_events完成真正的事件上报

evdev_pass_values(client, vals, count, ev_time);//将input 事件的数据信息放入evdev_client结构体的缓冲中去(struct input_event buffer[];)

总结:input event层 完成了handler 的注册,在注册handler的过程中会调用匹配函数,当handler和input 设备匹配上时会调用input regiester handle 完成handler 和 input 设备的连接(handler 和input 设备的节点同时指向handle 结构体中的节点,如上面图示,这样做的目的就是保证input 设备能找到自己对应的handler),完成链接的input 设备在发生输入事件时会触发其中断函数,在中断函数中会调用input_event 函数上报事件,input_event->input_handle_event->input_pass_event->handler.event->evdev_event->evdev_events->evedv_pass_values完成从驱动层到应用层的信息传递。

 

3、硬件驱动层

rk开发板用的触摸屏是汇顶科技的gt1x型电容式触摸屏,驱动代码位于/driver/input/touchscreen/gt1x/gt1x.c,电容触摸屏通过IIC总线与SOC进行通信,利用其自带的触摸IC完成坐标计算后通过IIC将坐标信息传输给SOC,坐标的计算过程不需要SOC的参与,从这个角度上来说,电容触摸屏就是一个挂载到SOC上的IIC slave设备,与通常所说的Sensor是一样的性质。我们完成电容触摸屏的驱动就是在IIC总线模型下完成其驱动。

static int __init gt1x_ts_init(void) //驱动入口

return i2c_add_driver(&gt1x_ts_driver); //添加一个iic设备驱动(触摸屏其实就是一个iic设备)

driver->driver.bus = &i2c_bus_type; //指定触摸屏驱动的总线类型是 IIC

INIT_LIST_HEAD(&driver->clients); //初始化链表

res = driver_register(&driver->driver); //注册设备驱动,需要先确定驱动使用的总线

当IIC 设备与IIC 驱动匹配后,会调用gt1x_ts_driver 中的probe函数

 

gt1x_ts_probe

if (client->dev.of_node) //是否支持设备树

ret = gt1x_parse_dt(&client->dev); //解析设备树

gt1x_int_gpio = of_get_named_gpio(np, "goodix,irq-gpio", 0); //根据NAME获取GPIO

gt1x_rst_gpio = of_get_named_gpio(np, "goodix,rst-gpio", 0); //同上

if (!gpio_is_valid(gt1x_int_gpio) || !gpio_is_valid(gt1x_rst_gpio)) //判断上述两个GPIO口是否可用

ret = gt1x_request_io_port(); //申请GPIO端口

ret = gt1x_init(); //触摸IC 初始化

gt1x_power_switch(SWITCH_ON); //上电

ret = gt1x_reset_guitar(); //reset IC

gt1x_select_addr(); //确定从机地址,有一些时序操作

ret = gt1x_i2c_read_dbl_check(GTP_REG_FW_CHK_MAINSYS, reg_val, 1); //IIC读写测试,检查IC是否初始化成功

ret |= gt1x_init_failed; if (ret) //判断IC是否初始化成功,如果没有使用默认配置

GTP_ERROR("Init failed, use default setting");

gt1x_abs_x_max = GTP_MAX_WIDTH;

gt1x_abs_y_max = GTP_MAX_HEIGHT;

gt1x_int_type = GTP_INT_TRIGGER;

gt1x_wakeup_level = GTP_WAKEUP_LEVEL;

ret = gt1x_get_chip_type(); //获取触摸IC的芯片类型:GT1X 、GT2X,对于不同的芯片类型会有不同的同步方式

ret = gt1x_init_panel(); //配置驱动IC,初始化完成后不可以再调用该函数

ret = gt1x_send_cfg(gt1x_config, gt1x_cfg_length);

gt1x_i2c_write(GTP_REG_CONFIG_DATA, config, cfg_len); // 通过IIC 将CONFIG 数组写到驱动IC中去,类似于IIC配置Sensor的过程

gt1x_wq = create_singlethread_workqueue("gt1x_wq"); //创建一个专用的内核线程来执行提交到工作队列中的函数

INIT_WORK(&gt1x_work, gt1x_ts_work_func); //初始化工作队列,当发生中断的时候,在中断处理函数中会调用这个工作队列。

ret = gt1x_request_input_dev(); //对Input子系统的支持

input_dev = input_allocate_device(); //申请分配Input_dev 结构体内存空间

input_set_abs_params(input_dev, ABS_MT_PRESSURE, 0, 255, 0, 0); //设置触摸事件参数

。。。

input_dev->name = gt1x_ts_name; //填充input_dev结构体

。。。

ret = input_register_device(input_dev); //注册input 设备

ret = gt1x_request_irq(); //申请中断

ret = request_irq(gt1x_i2c_client->irq,gt1x_ts_irq_handler

request_threaded_irq(irq, handler, NULL, flags, name, dev); //分配中断号

 

 

gt1x_ts_irq_handler

queue_work(gt1x_wq, &gt1x_work); //提交任务到工作队列

 

gt1x_ts_work_func

gt1x_touch_event_handler(point_data, input_dev, NULL);//处理touch 事件

u8 touch_data[1 + 8 * GTP_MAX_TOUCH + 2] = { 0 }; //touch 事件数据组成:1个字节的touch status + (8个字节的坐标信息)*touch num + 一个字节的keycode + 一个字节的checksum

memcpy(touch_data, data, 11); //先读十一个字节的数据(默认只有一个touch num)

if (touch_num > 1) //如果touch num 大于1 再去读取剩余的数据

gt1x_i2c_read((GTP_READ_COOR_ADDR + 11), &touch_data[11], 1 + 8 * touch_num + 2 - 11);

check_sum += touch_data[i]; 计算校验和

key_value = touch_data[1 + 8 * touch_num]; //keycode

if (CHK_BIT(cur_event, BIT_TOUCH)) //处理touch event

input_x = coor_data[1] | (coor_data[2] << 8);

input_y = coor_data[3] | (coor_data[4] << 8);

input_w = coor_data[5] | (coor_data[6] << 8);

input_x = GTP_WARP_X(gt1x_abs_x_max, input_x);

input_y = GTP_WARP_Y(gt1x_abs_y_max, input_y); //读取坐标

gt1x_touch_down(input_x, input_y, input_w, i); //上报

input_report_abs(input_dev, ABS_MT_POSITION_X, x);

input_report_abs(input_dev, ABS_MT_POSITION_Y, y);

input_mt_sync(input_dev);

 

总结:gt1x_ts_probe 函数完成了触摸IC的初始化、以及中断事件的初始化,当中断发生时,调用中断处理函数(中断处理函数的上半部分,触摸中断事件需要处理的信息较多,不能全部放到中断处理函数当中去,这里通过工作队列实现了将中断处理分为上下两个部分,在中断处理函数的上半部分调用了queue_work 将中断需要处理的信息作为任务提交到了工作队列),显然这个工作队列也是在probe中完成的初始化以及创建一个专用的内核线程来执行提交到工作队列中的函数gt1x_ts_work_func。

分析到这里,我们可以完整的总结出一次input 事件上报的链路了:

触摸屏IC初始化完成->手指点击触摸屏->引发一次中断->gt1x_ts_irq_handler->queue_work(gt1x_wq, &gt1x_work)->gt1x_ts_work_func->gt1x_touch_event_handler->gt1x_touch_down->input_report_abs->input_event->input_handle_event->input_pass_event->handler.event->evdev_event->evdev_events->evedv_pass_values