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, ¶m);
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 ----------