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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章