天天看点

Linux输入子系统浅析

Input子系统与TP驱动

对于众多的输入设备的驱动问题,linux提供了一套非常灵活的机制:input子系统。通过它我们只需要调用一些简单的函数,就可以将一个输入设备的功能呈现给应用程序。input输入子系统由输入子系统驱动层,核心层(Input Core),和事件处理层(Event Handler)三部分组成。

驱动层:负责和具体的硬件设备交互,采集输入设备的数据信息,通过核心层提供的API上报数据;

核心层:为事件处理层和设备驱动层提供接口API,起到一个中间层的作用;

事件处理层:通过核心层的API获取输入事件上报的数据,定义API与应用层交互。

主要是三大结构体所建立的联系,沟通了input子系统,他们在

kernel-4.4/include/linux/input.h中有定义:

struct input_dev:会在具体设备驱动层中被填充

struct input_handle:会在事件处理层和设备驱动层注册设备时通过input_dev或input_handler间接调用

struct input_handler:会在事件处理层如evdev.c中被实例化

浅析三大结构体关系

input_handle是连接input_dev和input_handler的桥梁,input_dev可以通过input_handle找到input_handler,同样的input_handler可以通过input_handle找到input_dev

一个device可能对应多个handler,而一个handler也不能只处理一个device,比如说一个鼠标,它可以对应evdev_handler,也可以对应mouse_handler,因此当其注册时与系统中的handler进行匹配,

就有可能产生两个实例,一个是evdev,另一个是mousedev,而任何一个实例中都只有一个handle,至于以何种方式来传递事件,就由用户程序打开哪个实例来决定

后面一个情况很容易理解,一个事件驱动不能只为一个甚至一种设备服务,系统中可能有多种设备都能使用这类handler,比如event handler就可以匹配所有的设备

在input子系统中,有8种事件驱动,每种事件驱动最多可以对应32个设备,因此dev实例总数最多可以达到256个。

以MTK的TP驱动为例贯穿讲解输入子系统:

MTK平台的TP驱动是分为两个部分组合在一起的,全平台的共享驱动mtk_tpd.c(抽象),以及各个型号TP的独立驱动(真实),mtk_tpd.c负责将TP注册到platform总线,以及利用input子系统核心层提供的API向事件处理层上报键值,各个型号的独立驱动负责I2C总线挂接,读取键值提交给mtk_tpd.c。mtk_tpd.c做的重要的一件事就是注册platform平台总线,对设备的申请tpd->dev = input_allocate_device();

==> input_register_device(tpd->dev)注册输入设备,一些事件的属性设置,以及对各型号TP的兼容遍历,都是在其probe函数(mtk_touch_driver函数的.of_match_table = touch_of_match的compatible = "mediatek,mt6739-touch"与在mt6739.dts注册的设备device touch: touch compatible = “mediatek,mt6739-touch”;相同,就执行tpd_probe函数)中完成的。

注册input device的过程就是为input device设置默认值,

==> list_add_tail(&dev->node, &input_dev_list)将新分配的input设备连接到input_dev_list链表上并与挂在input_handler_list链表中的handler相匹配

==> 调用input_attach_handler(dev, handler);去匹配

==> 调用input_match_device匹配(关于input_match_device函数,它是通过匹配id来确认匹配的,看handler的id是否支持),

所有的input_dev挂载到input_dev_list 链表上,所有的handler挂载到input_handler_list上),如果匹配成功就会调用handler的connnect函数,

==> connnect函数是在事件处理层定义并实现的,

以evdev.c为例,则connect函数就是 ==> evdev_connect。evdev_connect()函数主要用来连接input_dev和input_handler,这样事件的流通链才能建立,流通链建立后,事件才知道被谁处理,或者处理后将向谁返回结果。

总结:

TP的操作就是底层将信息储存在 /sys/class/input/eventn 中,然后上层对其进行读取识别,然后根据其中的信息进行事件处理。

--------------------------------------------- kernel层 --------------------------------------------------

驱动层:(在具体的设备驱动文件中注册driver/input/touchscreen)

1、注册input_dev,进入input_register_device()

(1)把input_dev添加到input_dev_list链表中

list_add_tail(&dev->node, &input_dev_list);

(2)判断input_handler的id,是否有支持这个设备的驱动

list_for_each_entry(handler, &input_handler_list, node); //遍历查找input_handler_list链表里所有input_handler

input_attach_handler(dev, handler); //判断两者id,若两者支持便进行连接。

事件处理层:(./kernel-3.18/drivers/input/evdev.c)

2、注册input_handler,进入input_register_handler()

(1)把input_handler添加到input_handler_list链表中

list_add_tail(&handler->node, &input_handler_list);

(2)判断input_dev的id,是否有支持这个驱动的设备

list_for_each_entry(dev, &input_dev_list, node); //遍历查找input_dev_list链表里所有input_dev

input_attach_handler(dev, handler);//判断两者id,若两者支持便进行连接。

3、判断input_handler和input_dev的id,进入input_attach_handler()

(1)匹配两者id

id = input_match_device(handler, dev); //匹配input_handler和dev的id,匹配不成功退出函数

(2)匹配成功调用input_handler ->connect

error = handler->connect(handler, dev, id); //匹配成功建立连接

核心层:(kernel-4.4/drivers/input/input.c)

4、建立input_handler和input_dev的连接,进入input_handler->connect()

(1)input_register_handle函数,将handle通过d_node挂到input device的h_list,通过h_node挂到handler的h_list上,两者的.h_list都指向了同一个handle结构体,然后通过.h_list 来找到handle的成员.dev和handler,便能找到对方,便建立了连接connect

list_add_tail_rcu(&handle->d_node, &dev->h_list); //连接input_dev->h_list

list_add_tail_rcu(&handle->h_node, &handler->h_list); //连接input_handler->h_list

5、有事件发生时,比如触摸中断,在中断函数中需要进入input_event()上报事件,TP上报流程如下

驱动中调用input_report_abs上报绝对坐标

->input_event ------ input.h
  ->input_handle_event ------ input.c
   ->dev->event(dev, type, code, value); ------input.c
    ->evdev.c/evdev_event() ------ evdev.c
     ->evdev_events ------ evdev.c
      -> evdev_pass_values ------ evdev.c
           

然后将 type、value、code 存储在 evdev_client 的 struct input_event buffer[] 中,input_event buffer存放在一个设备节点文件,在evdev_connect中注册生成了 /sys/class/input/event%d ,这个字符设备文件就是连接kernel与framework的桥梁了。

----------------------------------------接下来到framework层---------------------------------------------

6、再看 framework 上层怎么读取这个文件中的 buffer 的,我们从 InputReader.cpp 来分析

在frameworks/native/services/inputflinger/InputReader.cpp中

bool InputReaderThread::threadLoop() {  
mReader->loopOnce();  
->void InputReader::loopOnce() {  
     int32_t oldGeneration;  
     int32_t timeoutMillis;  
     ...  
     size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE); 
           

跟踪到在构造函数里,mEventHub 是 eventHub 的实例,那么就是调用 eventHub 的 getEvents 方法。

在frameworks/native/services/inputflinger/EventHub.cpp中

size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {  
  ...  
  for (;;) {  
  ...  
  scanDevicesLocked(); //这个往里走就是通过EventHub::openDeviceLocked  
  //打开*DEVICE_PATH = "/dev/input" 这个设备 ,
  //最终用的open,实际到kernel层就是input设备注册的open  
...  
  int32_t readSize = read(device->fd, readBuffer, sizeof(struct input_event) * capacity); //这里的device->fd就是/dev/input/eventn这个设备文件,就是从这里读取出event的buffer  
           

再往上就是对这些数据的处理了。

下图就是对应的流程框架图:

事件上报流程:

一旦上层打开设备文件就会调用==> evdev_open函数,evdev_open分配并初始化一个client结构体,并将它和evdev关联起来,关联的内容是,将client->evdev指向它所表示的evdev,

==> 调用evdev_attach_client()将client挂到evdev->client_list上,我们驱动层上报的输入事件

的键值,就是存放在evdev->buffer中的。

==> 调用evdev_open_device()函数,通过调用核心层input.c中的input_open_device函数实现的,打开输入设备使设备准备好接收或者发送数据。

==> 调用evdev_read函数实现事件处理层对上层的读操作,我们的事件处理层对上层的读操作,用一个等待队列wake_up_interruptible(&evdev->wait)实现阻塞,这样就能保证,我们只有在触摸按键事件发生,中断到来,我们才去上报按键事件,并唤醒阻塞,让事件处理层的evdev_read将键值最终通过copy_to_user送到用户空间。

mtk的mtk_tpd.c怎么做兼容多个TP:

关键代码:遍历mtk的tpd_driver_list里面的所有的驱动,判断名字是否为NULL,每一个module touch IC驱动都会添加到这个静态数组里面对于if (tpd_load_status == 1)这个条件,

会判断我们所遍历的每一个module IC驱动的初始化函数,probe成功即i2c通信成功的话就会将tpd_load_status置1(具体驱动的probe函数中),所以我们就是通过这个值判断哪一个驱动的。

具体TP ic驱动里面,主要进行IC的上电、申请中断号注册中断处理函数、Update FW等动作。

重点有两个函数:事件处理线程、中断处理函数

中断处理函数:

一旦触摸事件发生,则触发中断,此中断处理函数唤醒等待队列。

static irqreturn_t tpd_eint_interrupt_handler(void)
{
    TPD_DEBUG_PRINT_INT;
    tpd_flag = 1;
    wake_up_interruptible(&waiter);//唤醒等待队列
    return IRQ_HANDLED;
}

           

事件处理线程:

static int touch_event_handler(void *unused)
{
    ...

   sched_setscheduler(current, SCHED_RR, &param);
    do {
        set_current_state(TASK_INTERRUPTIBLE);//设置Task 的状态为可中断的等待状态
        if (tpd_eint_mode) {
            wait_event_interruptible(waiter, tpd_flag != 0);//满足tpd_flag!=0 就唤醒队列
            tpd_flag = 0;//改变条件
        } else {
            msleep(tpd_polling_time);
        }

   set_current_state(TASK_RUNNING);//设置Task 的状态为执行态
        ...

#if defined(CONFIG_GTP_SLIDE_WAKEUP)
        if (DOZE_ENABLED == doze_status) {
            ret = gtp_i2c_read(i2c_client_point, doze_buf, 3);
            GTP_DEBUG("0x814B = 0x%02X", doze_buf[2]);
            if (ret > 0) {
                if (0xAA == doze_buf[2]) {
                    GTP_INFO("Forward slide up screen!");
                    doze_status = DOZE_WAKEUP;
                    input_report_key(tpd->dev,
                             KEY_POWER, 1);
                    input_sync(tpd->dev);
                    input_report_key(tpd->dev,
                             KEY_POWER, 0);
                    input_sync(tpd->dev);
                    /* clear 0x814B */
                    doze_buf[2] = 0x00;
                    gtp_i2c_write(i2c_client_point,
                              doze_buf, 3);
                } else if (0xBB == doze_buf[2]) {
                    GTP_INFO("Back slide up screen!");
                    doze_status = DOZE_WAKEUP;
                    input_report_key(tpd->dev,
                             KEY_POWER, 1);
                    input_sync(tpd->dev);

   ...

}
           

开启这个线程的目的是读取坐标,上报坐位给上层使用;它会在循环内轮询触摸事件,但触摸事件是随机的,所以用等待队列实现阻塞。

只有当触摸事件的中断到来,才唤醒队列,通过I2C通信gtp_i2c_read读取数据,之后通过input_report_xx和input_sync函数上报坐标。

补充:TP input_report_xx上报的内容一般有(从TP驱动ft6336s/focaltech_core.c截取部分代码):

static void tpd_down(int x, int y,int press, int id)
{
        if ((!press) && (!id))
    {
        input_report_abs(tpd->dev, ABS_MT_PRESSURE, 100);
        input_report_abs(tpd->dev, ABS_MT_TOUCH_MAJOR, 100);
    }
    else
    {
        input_report_abs(tpd->dev, ABS_MT_PRESSURE, press); //上报手指按下还是抬起的状态
        input_report_abs(tpd->dev, ABS_MT_TOUCH_MAJOR, press);
        /* track id Start 0 */
        input_report_abs(tpd->dev, ABS_MT_TRACKING_ID, id);//id可以不用上报,上层可以自动匹配
    }

   input_report_key(tpd->dev, BTN_TOUCH, 1); //上报按键状态

   input_report_abs(tpd->dev, ABS_MT_POSITION_X, x); //上报x轴坐标
        input_report_abs(tpd->dev, ABS_MT_POSITION_Y, y);//上报y轴坐标

   input_mt_sync(tpd->dev);//同步上报事件,通知上层完成一次上报
        TPD_DEBUG_SET_TIME;
        TPD_EM_PRINT(x, y, x, y, id, 1);
         tpd_history_x=x;
         tpd_history_y=y;
#ifdef TPD_HAVE_BUTTON
        if (FACTORY_BOOT == get_boot_mode()|| RECOVERY_BOOT == get_boot_mode())
        {
        tpd_button(x, y, 1);//虚拟按键的处理,x和y的数据还有按键状态:按下和释放
        }
#endif
        TPD_DOWN_DEBUG_TRACK(x,y);
 }
           

窗体顶端

1.tp driver的tpd_down()和tpd_up()函数中不需要上报id号,上层会自动进行匹配;

2.tpd_up()函数中只需要上报BTN_TOUCH和mt_sync信息,其他信息不用上报,如下:

窗体顶端

static  void tpd_up(int x, int y,int *count) 
{
     input_report_key(tpd->dev, BTN_TOUCH, 0);
     //printk("U[%4d %4d %4d] ", x, y, 0);
     input_mt_sync(tpd->dev);
     TPD_EM_PRINT(x, y, x, y, 0, 0);
     if (FACTORY_BOOT == get_boot_mode()|| RECOVERY_BOOT == get_boot_mode())
     {   
        tpd_button(x, y, 0); 
     }      
}
           

---------- 爱生活,爱安卓,爱Linux ----------

继续阅读