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