概述

按键输入、键盘、鼠标、触摸屏等等这些都属于输入设备,按键和键盘就是代表按键信息,鼠标和触摸屏代表坐标信息,因此在应用层的处理就不同。为此 input 子系统分为 input 驱动层、 input 核心层、 input 事件处理层,最终给用户空间提供可访问的设备节点,input 子系统框架如图 所示:

  • 驱动层:输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容。
  • 核心层:承上启下,为驱动层提供输入设备注册和操作接口。通知事件层对输入事件进行处理。
  • 事件层:主要和用户空间进行交互。

数据结构

Input 子系统实现的数据结构如下:原图

  • input_dev:
    input_dev 结构体用于描述一个 input 设备,例如按键、鼠标、键盘等,我们编写设备驱动主要就是构造这个结构体。该结构体包含设备类型,消息类型,事件 code 等信息。该数据结构在内核里会挂载到一个全局链表 input_dev_list,所有注册的 input 设备都会挂载到该链表中

  • input_handler:
    input_handler 用于表示一个 input 事件,用于上报 input 事件,系统中所有注册的 input_handler 也会挂载到一个全局链表 input_handler_list 中。input_handler 可以类比设备驱动模型中 driver 方法,用于驱动 device,这里 input_handler 用于处理 input_dev 设备产生的事件。input_dev 和 input_handler 之间也是通过一种匹配规则来进行匹配建立联系,当 input_dev 注册的时候会遍历 input_handler_list 链表,通过 id_table 或 input_handler 的 match 方法进行匹配,匹配成功后两者之间会建立联系。同样对于 input_handler 的注册也是一样,当一个 input_handler 注册时会遍历 input_dev_list 链表,当两个结构体满足匹配关系时则建立联系

  • input_handle:
    在上面的介绍中当 input_handler 和 input_dev 匹配成功后会建立联系,这个联系就是通过该结构体来建立的。当 input_dev 和 input_handler 匹配成功后会创建一个 evdev 结构体,evdev 结构体下有一个 input_handle 结构体,input_handle 结构体会指向匹配成功的 input_dev 和 input_handler,而且将自己挂载到 input_dev 和 input_handler 结构体中

  • evdev_client:
    evdev_client 结构体表示一个 input 客户,也就是 app 用户会使用 input_dev,那么这个 app 用户就用 evdev_client 结构体来描述,可以看到,该结构体挂载在 evdev 结构体下,且是以链表的形式组织,也就说明一个 input 设备可以被多个 client 打开使用

  • evdev:
    通过对 input_handler 和 evdev_client 的介绍其实也就已经将 evdev 介绍完了,evdev_client 使用设备,那么需要知道使用的是哪个设备 input_dev 以及如何驱动这个设备 input_handler,这都是通过 input_handler 结构体完成的。evdev 是将 input 设备,input 设备的驱动方法和用户 client 联系起来的一个结构体

通过对 input 结构体的介绍,其实我们就已经大致了解了驱动的整体框架。其实跟设备模型中 device 和 device_driver 之间的关系很像,device 和 driver 之间有一个匹配规则,当匹配成功后,两者之间建立联系,完成驱动设备的功能。但是为什么这里不直接使用设备驱动模型呢,不理解。

实现

input 驱动的实现无非就是 input_dev、input_handler 的注册等流程,下面一个个看下具体相关流程吧:

input_dev 注册流程

input_register_device 函数用于注册一个 input_dev 设备:

1
int input_register_device(struct input_dev *dev)

该函数主要完成以下工作:

1、为 input_dev 创建 struct device 类型的变量,并设置其所属的 class 为 input_class,并调用 device_add 完成 device 类型变量的注册,至此完成 strcut device 类型变量注册至 input_class 中,且完成了 input_dev 对应的 sysfs 属性文件的创建。 
2、将该 input_device 注册至 input_dev_list。
3、遍历 input_handler_list 链表,针对 input_handler_list 上每一个 input_handler,均与注册的 input_device 进行匹配检测,若匹配检测成功,则调用  input_handler->connect 接口,进行 input_handler 与 input_device 的绑定(通过调用 input_register_handle 实现,也可以理解为在 input_handler、input_device 上 完成 input_handler、input_device 的事件消息的订阅操作)。因 input_handler 和 input_device 可实现多对多的绑定,因此,会遍历 input_handler_list 链表上所有已注册的 input_handler,而不是检测到一个 input_handler 的匹配即返回。

input_handler 的注册流程

input_handler 的注册函数是 input_register_handler:

1
int input_register_handler(struct input_handler *handler)

该函数主要完成以下工作:

1、将该 input_device 注册至 input_handler_list      
2、遍历 input_dev_list 链表,针对 input_dev_list 上每一个 input_dev,均与注册的 input_device 进行匹配检测,若匹配检测成功,则调用 input_handler->connect 接口,进行 input_handler 与 input_device 的绑定(通过调用 input_register_handle 实现,也可以理解为在 input_handler、input_device 上完成 input_handler、input_device 的事件消息的订阅操作)。因 input_handler 和 input_device 可实现多对多的绑定,因此,会遍历 input_dev_list 链表上所有已注册的 input_dev,而不是检测到一个 input_dev 的匹配即返回

input_handle 的注册流程

input_handle 的注册函数是 input_register_handle:

1
int input_register_handle(struct input_handle *handle)

该函数主要完成以下工作:

1、 完成 input_handle 与 input_handler 的关联
2、完成 input_handle 与 input_dev 的关联

一个完整的事件上报流程

应用打开并读设备

当应用打开一个 input 设备时会通过 vfs 调用到驱动程序的 file_operations, 对于 input 设备就是 evdev.c 下的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static const struct file_operations evdev_fops = {
.owner = THIS_MODULE,
.read = evdev_read,
.write = evdev_write,
.poll = evdev_poll,
.open = evdev_open,
.release = evdev_release,
.unlocked_ioctl = evdev_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = evdev_ioctl_compat,
#endif
.fasync = evdev_fasync,
.llseek = no_llseek,
};

这个就是 input_dev 注册的时候会向系统注册的字符设备,该字符设备的注册时机是什么时候呢,先来看下 input_handle 的初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static struct input_handler evdev_handler = {
.event = evdev_event,
.events = evdev_events,
.connect = evdev_connect,
.disconnect = evdev_disconnect,
.legacy_minors = true,
.minor = EVDEV_MINOR_BASE,
.name = "evdev",
.id_table = evdev_ids,
};

static int __init evdev_init(void)
{
return input_register_handler(&evdev_handler);
}

static void __exit evdev_exit(void)
{
input_unregister_handler(&evdev_handler);
}

evdev_handler 里面的成员 connect 的指针指向 evdev_connect,当 input_dev 和 input_handler 匹配成功后就会调用 connect,函数,而 evdev_connect 里面会调用

1
2
3
4
5
...
cdev_init(&evdev->cdev, &evdev_fops);

cdev_device_add(&evdev->cdev, &evdev->dev);
...

evdev_fops 就是上面那个 file_operations。然后应用调用 open 就会调用驱动的 evdev_open 函数,应用调用 read 就会调用驱动的 evdev_read 函数,下面说明下 open 和 read 函数主要的工作: evdev_open 的工作:

1、 创建并分配一个 evdev_client 结构体   
2、 将 evdev_client 放入 evdev 的链表中   

evdev_read 的工作:

如果没有数据等待休眠,调用 wait_event_interruptible 函数,将当前工作挂载在 evdev_client 的 wait 等待队列中

事件产生上报数据

一般采用中断方式处理外部事件,当按键或其他输入事件发生时,产生中断,执行中断服务程序,中断服务程序一般主要就是干两件事,读取数据和上报数据,对于读取数据没什么好说的,下面看下上报数据流程:上报数据通过 input core 层提供的 input_event 函数完成,函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
void input_event(struct input_dev *dev,
unsigned int type, unsigned int code, int value)
{
unsigned long flags;

if (is_event_supported(type, dev->evbit, EV_MAX)) {

spin_lock_irqsave(&dev->event_lock, flags);
input_handle_event(dev, type, code, value);
spin_unlock_irqrestore(&dev->event_lock, flags);
}
}

函数调用流程如下:

1
2
3
4
input_event()
-->input_handle_event()
-->input_pass_values()
-->input_to_handler()

input_to_handler 函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static unsigned int input_to_handler(struct input_handle *handle,
struct input_value *vals, unsigned int count)
{
struct input_handler *handler = handle->handler;
struct input_value *end = vals;
struct input_value *v;

/* 1、 使用 filter 过滤事件 */
if (handler->filter) {
for (v = vals; v != vals + count; v++) {
if (handler->filter(handle, v->type, v->code, v->value))
continue;
if (end != v)
*end = *v;
end++;
}
count = end - vals;
}

if (!count)
return 0;

/* 2、调用 handler 的 events 处理多个事件或者调用 event 一个一个处理事件 */
if (handler->events)
handler->events(handle, vals, count);
else if (handler->event)
for (v = vals; v != vals + count; v++)
handler->event(handle, v->type, v->code, v->value);

return count;
}

以 evdev 为例,在上面的 evdev_handler 中提供了 events 函数 evdev_handler,该函数调用流程如下:

1
2
3
4
5
evdev_events()
-->evdev_pass_values()
-->__pass_event()
-->wake_up_interruptible_poll(&client->wait,
EPOLLIN | EPOLLOUT | EPOLLRDNORM | EPOLLWRNORM);

__pass_event 函数将读取的事件传递给 client 结构体,wake_up_interruptible_poll 将应用程序唤醒(前面提到读取数据的时候如果没有数据就休眠)。

Input Tools

getevent 命令

getevent 指令用于获取 input 输入事件,比如获取按键上报信息、获取触摸屏上报信息等。 getevent -h:查看 getevent 帮助信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@rk3288:/ # getevent -h
getevent -h
Usage: getevent [-t] [-n] [-s switchmask] [-S] [-v [mask]] [-d] [-p] [-i] [-l] [-q] [-c count] [-r] [device]
-t: show time stamps
-n: don't print newlines
-s: print switch states for given bits
-S: print all switch states
-v: verbosity mask (errs=1, dev=2, name=4, info=8, vers=16, pos. events=32, props=64)
-d: show HID descriptor, if available
-p: show possible events (errs, dev, name, pos. events)
-i: show all device info and possible events
-l: label event types and names in plain text
-q: quiet (clear verbosity mask)
-c: print given number of events then exit
-r: print rate events are received
  • -t:显示时间戳
  • -n:不换行打印
  • -s:显示指定位的开关状态
  • -v:根据 mask 值显示相关信息,执行后会一直显示上报数据
  • -d:如果设备可用,显示设备隐藏的描述信息
  • -p:显示设备支持的事件类型和编码方式
  • -i:显示设备的所有信息和支持的事件,比 -p 显示更多信息
  • -l:以文本形式输出事件类型和名称,比 -t 更清楚直观
  • -c:打印固定数量的事件并退出
  • -r:显示事件上报速率

参数可以组合使用,一次性查看需要的触摸屏信息:

1
2
3
4
5
6
7
8
9
10
11
12
root@rk3288:/ # getevent -tlr /dev/input/event3
getevent -tlr /dev/input/event3
[ 2514.550104] EV_ABS ABS_MT_TRACKING_ID 0000001c
[ 2514.550104] EV_ABS ABS_MT_POSITION_X 00002dac
[ 2514.550104] EV_ABS ABS_MT_POSITION_Y 000018ca
[ 2514.550104] EV_KEY BTN_TOUCH DOWN
[ 2514.550104] EV_ABS ABS_X 00002dac
[ 2514.550104] EV_ABS ABS_Y 000018ca
[ 2514.550104] EV_SYN SYN_REPORT 00000000 rate 0
[ 2514.638845] EV_ABS ABS_MT_TRACKING_ID ffffffff
[ 2514.638845] EV_KEY BTN_TOUCH UP
[ 2514.638845] EV_SYN SYN_REPORT 00000000 rate 11

tslib 调试工具

配置 tslib

配置文件/etc/ts.conf。触摸模块从上到下加载。每一行指定一个模块及其参数,模块按顺序处理。在顶部使用一个 module_raw,访问您的设备,然后是过滤器模块的任意组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Uncomment if you wish to use the one-wire linux input layer S70/A70...
# module_raw one_wire_ts_input

# Uncomment if you wish to use the linux input layer event interface
module_raw input

# Uncomment if you're using a Sharp Zaurus SL-5500/SL-5000d
# module_raw collie

# Uncomment if you're using a Sharp Zaurus SL-C700/C750/C760/C860
# module_raw corgi

# Uncomment if you're using a device with a UCB1200/1300/1400 TS interface
# module_raw ucb1x00

# Uncomment if you're using an HP iPaq h3600 or similar
# module_raw h3600

# Uncomment if you're using a Hitachi Webpad
# module_raw mk712

# Uncomment if you're using an IBM Arctic II
# module_raw arctic2

module pthres pmin=1
module variance delta=30
module dejitter delta=100
module linea

插件

  • pthres 压力阈值过滤器
  • variance 方差过滤器。尽最大努力滤除来自触摸屏 ADC 的随机噪声
  • dejitter 消除 X 和 Y 坐标上的抖动
  • debounce 简单的去抖动机制,在触摸手势停止后的指定时间内丢弃输入事件
  • skip nhead 按下后跳过样品,ntail 释放前跳过样品
  • lowpass 简单的低通指数平均滤波模块
  • linear 线性缩放 - 校准 - 模块,主要用于将触摸屏坐标转换为屏幕坐标
  • invert 围绕给定值在 X /Y 方向反转值
  • median 中值滤波器减少样本坐标值中的噪声。它能够过滤信号中不需要的单个大跳跃
  • iir 无限脉冲响应滤波器
  • tslib 从触摸屏驱动采样到的设备坐标进行处理再提供给应用端的过程大体如下

raw device --> variance --> dejitter --> linear --> application

ts_calibrate 触摸屏校准工具

校准由 linear 插件完成,它使用自己的配置文件 /etc/pointercal。不要手动编辑此文件。它是由 ts_calibrate 程序创建的 :

测试过滤后的输入行为。

您可以使用以下命令快速测试由配置的过滤器产生的触摸行为 ts_test_mt: ts_test_mt:

总结

  • 应用 open 一个 input 设备,通过 vfs 调用到驱动字符设备的 open 函数,该函数创建构造一个 evdev_client 结构体并将该结构体放入打开的 input 设备的 evdev 结构体链表中
  • 应用 read 读取一个 input 设备,通过 vfs 调用到驱动的字符设备 read 函数,如果没有数据就休眠
  • 事件发生产生中断,在中断函数里读取数据并上报事件,上报事件流程是通过 input_dev 结构体找到驱动该结构体的 input_handler 结构体,然后调用 input_handler 结构体的 events 函数,在 events 函数里,主要干两件事,一是将外部事件传递给 evdev_client 结构体,二是将应用程序唤醒

参考文献

https://ld246.com/article/1598093237235
https://www.cnblogs.com/deng-tao/p/6094049.html
https://blog.csdn.net/lickylin/article/details/106449104?spm=1001.2014.3001.5501
《韦东山老师相关课程和文档》