天天看點

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 ----------

繼續閱讀