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