首先说明一下,本文是基于Linux-2.6.38版本内核来分析Linux输入子系统架构和原理的。这阵子本来没有打算花时间来分析Linux input system的,然而当在研究S3C6410触摸屏驱动的时候悲剧不期而至,内核中并没有实现6410的触摸屏驱动,不过有关于S3C2410触摸屏的驱动,往s3c2410_ts.c文件里面一看,居然实现过程中用到了输入子系统这一神马机制。瞄了下代码,流程基本知道了,但是关于输入子系统的原理是怎样的呢?全部知其然,好吧,就去了解这一神秘的东东吧!Now。。。
1、为何引入input system?
以前我们写一些输入设备(键盘、鼠标等)的驱动都是采用字符设备、混杂设备处理的。问题由此而来,Linux开源社区的大神们看到了这大量输入设备如此分散不堪,有木有可以实现一种机制,可以对分散的、不同类别的输入设备进行统一的驱动,所以才出现了输入子系统。
输入子系统引入的好处:
(1)统一了物理形态各异的相似的输入设备的处理功能。例如,各种鼠标,不论PS/2、USB、还是蓝牙,都被同样处理。
(2)提供了用于分发输入报告给用户应用程序的简单的事件(event)接口。你的驱动不必创建、管理/dev节点以及相关的访问方法。因此它能够很方便的调用输入API以发送鼠标移动、键盘按键,或触摸事件给用户空间。X windows这样的应用程序能够无缝地运行于输入子系统提供的event接口之上。
(3)抽取出了输入驱动的通用部分,简化了驱动,并提供了一致性。例如,输入子系统提供了一个底层驱动(成为serio)的集合,支持对串口和键盘控制器等硬件输入的访问。
注:更多详细描述可参见《精通Linux设备驱动程序开发》这本书。
2、输入子系统架构
上图展示了输入子系统的操作。此子系统包括一前一后运行的两类驱动:输入事件(event)驱动和输入设备(device)驱动。
输入事件驱动负责和应用程序的接口;
而输入设备驱动负责和底层输入设备的通信。
输入事件驱动和输入设备驱动都可以利用输入子系统的高效、可重用的核心提供的服务。
Now,我们看到输入子系统中有两个类型的驱动,当我们要为一个输入设备(如触摸屏)的编写驱动的时候,我们是要编写两个驱动:输入设备驱动和输入事件驱动??
答案是否定的。在子系统中,事件驱动是标准的,对所有的输入类都是可以用的,所以你更可能的是实现输入设备驱动而不是输入事件驱动。你的设备可以利用一个已经存在的,合适的输入事件驱动通过输入核心和用户应用程序接口。
3、主要数据结构和内核编程接口
input_event
include/linux/input.h
evdev产生的每个事件包都采用此格式。
input_dev
include/linux/input.h
代表一个输入设备。
input_handler
include/linux/serial_core.h
事件驱动支持的入口函数。
psmouse_protocol
drivers/input/mouse/psmouse-base.c
所支持的PS/2鼠标协议驱动相关的信息。
psmouse
drivers/input/mouse/psmouse.h
PS/2鼠标驱动支持的方法。
内核编程接口概述 内核接口位置描述
input_register_device()
drivers/input/input.c
向input核心注册一个设备。
input_unregister_device()
drivers/input/input.c
从input核心移除一个设备。
input_report_rel()
include/linux/input.h
在某个方向产生相对移动。
input_report_abs()
include/linux/input.h
在某个方向产生绝对移动。
input_report_key()
include/linux/input.h
产生一个按键或按钮按击。
input_sync()
include/linux/input.h
表明输入子系统能收集以前产生的事件,将这些事件组成一个evdev包,并通过/dev/input/ inputX发送给用户空间。
input_register_handler()
drivers/input/input.c
注册一个用户事件驱动。
sysfs_create_group()
fs/sysfs/group.c
用特定属性创建sysfs节点组。
sysfs_remove_group()
fs/sysfs/group.c
移除用sysfs_create_group()创建的sysfs组。
tty_insert_flip_char()
include/linux/tty_flip.h
发送一个字符给线路规程层。
platform_device_register_simple()
drivers/base/platform.c
创建一个简单平台设备。
platform_device_unregister()
drivers/base/platform.c
卸载一个平台设备。
4、输入设备驱动(注意,以下是结构详细描述)
在输入子系统的设备驱动中,最重要的数据结构是struct input_dev,需要完成的大部分工作都是围绕着它来的,它是驱动的主体。每个struct input_dev代表一个输入设备。/* include/linux/input.h */
struct input_dev {
const char *name; /*设备名 */
const char *phys;
const char *uniq;
struct input_id id; /*用于匹配事件处理层handler */
unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; /*用于记录支持的事件类型的位图*/
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; /*记录支持的按键值的位图 */
unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; /*记录支持的相对坐标的位图 */
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; /*记录支持的绝对坐标的位图 */
unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];
unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; /*led */
unsigned long sndbit[BITS_TO_LONGS(SND_CNT)]; /*beep */
unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];
unsigned long swbit[BITS_TO_LONGS(SW_CNT)];
unsigned int keycodemax; /*支持的按键值的个数 */
unsigned int keycodesize; /*每个键值的字节数 */
void *keycode; /*存储按键值的数组首地址 */
int(*setkeycode)(struct input_dev *dev, int scancode, int keycode); /* 修改键值的函数,可选 */
int(*getkeycode)(struct input_dev *dev, int scancode, int *keycode); /* 获取扫描码的键值,可选 */
structff_device *ff;
unsigned int repeat_key; /*最近一次按键值,用于连击 */
struct timer_list timer; /*自动连击计时器 */
int sync; /*最后一次同步后没有新的事件置1*/
int abs[ABS_MAX + 1]; /* 当前各个坐标的值 */
int rep[REP_MAX + 1]; /*自动连击的参数 */
unsigned longkey[BITS_TO_LONGS(KEY_CNT)]; /*反映当前按键状态的位图 */
unsigned long led[BITS_TO_LONGS(LED_CNT)]; /*反映当前led状态的位图 */
unsigned long snd[BITS_TO_LONGS(SND_CNT)]; /*反映当前beep状态的位图 */
unsigned long sw[BITS_TO_LONGS(SW_CNT)];
int absmax[ABS_MAX + 1]; /*记录各个坐标的最大值 */
int absmin[ABS_MAX + 1]; /*记录各个坐标的最小值 */
int absfuzz[ABS_MAX + 1]; /*记录各个坐标的分辨率 */
int absflat[ABS_MAX + 1]; /*记录各个坐标的基准值 */
int(*open)(struct input_dev *dev); /*打开函数 */
void(*close)(struct input_dev *dev); /*关闭函数 */
int(*flush)(struct input_dev *dev, struct file *file); /* 断开连接时冲洗数据 */
int(*event)(struct input_dev *dev, unsigned int type, unsigned int code, intvalue); /* 回调函数,可选 */
struct input_handle *grab;
spinlock_t event_lock;
struct mutex mutex;
unsigned int users;
int going_away;
struct device dev;
struct list_head h_list; /*handle链表 */
struct list_head node; /*input_dev链表 */
};
struct input_event是事件传送的载体,输入子系统的事件都是包装成struct input_event传给用户空间。 struct input_event成员介绍
/* include/linux/input.h */
struct input_event {
struct timeval time; /*时间戳 */
__u16 type; /*事件类型 */
__u16 code; /*事件代码 */
__s32 value; /*事件值,如坐标的偏移值 */
};
struct input_dev注册的时候需要跟匹配的hanlder建立连接,匹配的依据就是struct input_dev所包含的struct input_id。
struct input_id成员描述
/* include/linux/input.h */
struct input_id {
__u16bustype; /*总线类型 */
__u16vendor; /*生产商编号 */
__u16product; /*产品编号 */
__u16version; /*版本号 */
};
5、input_dev成员详解
(1) 打开和关闭函数
struct input_dev中有open和close两个函数指针。在与handler第一次连接之后会调用open函数,断开连接会调用close。open中应该完成硬件初始化的相关工作,并且申请用到的其他资源,如中断号。close函数做相反的工作。
(2) 事件类型
Linux输入子系统支持的事件类型如下:
/* include/linux/input.h */
#define EV_SYN 0x00 /* 同步事件 */
#define EV_KEY 0x01 /*按键事件 */
#define EV_REL 0x02 /*相对坐标 */
#define EV_ABS 0x03 /*绝对坐标 */
#define EV_MSC 0x04
#define EV_SW 0x05
#define EV_LED 0x11 /*led */
#define EV_SND 0x12 /*beep */
#define EV_REP 0x14 /*连击事件 */
#define EV_FF 0x15
#define EV_PWR 0x16
#define EV_FF_STATUS 0x17
#define EV_MAX 0x1f
#define EV_CNT (EV_MAX+1)
按键事件(EV_KEY)是最简单的事件类型,用来描述按键或者按钮。报告按键事件使用以下函数:
input_report_key(struct input_dev *dev, int code, int value)
code的值在<内核>include/linux/input.h中定义,大小从0到KEY_MAX。value为0时表示按键抬起,非0时代表按键按下。对同一个按键来说,只有当value的值和上次不同才会产生一次事件。
除了按键事件,相对坐标事件(EV_REL)和绝对坐标事件(EV_ABS)也是常用的事件。为了使设备支持这两类事件,需要在初始化时对struct input_dev的evbit相应比特置1,并且还要分别在relbit和absbit位图中为支持的坐标轴置1。
相对坐标事件用来描述类似鼠标移动的消息。报告相对坐标的函数如下:
input_report_rel(structinput_dev *dev, int code, int value)
code描述坐标轴,value代表相对移动(可正可负)。只有当value的值非零时才能产生一个有效的事件。
绝对坐标需要额外的工作。初始化时需要填充input_dev中的一些数据域。对于支持的每个坐标轴调用如下函数:
input_set_abs_params(struct input_dev *dev, int axis,int min, int max, int fuzz, int flat);
函数参数从右往左依次代表输入设备指针、坐标轴、最小值、最大值、分辨率、基准值。最后两个参数也可以填为0,代表设备非常精确并且总能精确的回到中心位置。
input_set_abs_params函数的代码
static inline void input_set_abs_params(structinput_dev *dev, int axis, int min, int max, int fuzz, int flat)
{
dev->absmin[axis]= min;
dev->absmax[axis]= max;
dev->absfuzz[axis]= fuzz;
dev->absflat[axis]= flat;
dev->absbit[BIT_WORD(axis)]|= BIT_MASK(axis);
/* 这一行已经注册了坐标 */
}
另外输入设备驱动中经常用到同步事件(EV_SYN),输入子系统会默认支持此事件,驱动无需注册。鼠标、触摸板之类的设备需要用它来提示上层已经发送完了一个完整的事件报告。同步事件的报告形式如下:
input_sync(zlgkpd->input);
(3) keycode、 keycodemax和keycodesize
首先说明扫描码和键值的区别。如程序清单 1.4<!--[if gte mso 9]><![endif]-->所示,例子中包含四个按键的值,那么扫描码的范围是0~3,键值就是keypad_keycode中的四个值。
keycode、 keycodemax和keycodesize这三个数据域保存的值分别是键值数组的首地址、键值的个数和每个键值的字节大小。有了这三个变量,就可以在运行是改变键盘的键值映射。比如原来数组中的第四个键值为KEY_Z,可以根据需要改为KEY_D或者其他的值。这样同样的扫描码就对应的不同的键值。
这三个域正确填充之后,内核可以使用input_dev的成员函数来修改键值映射。修改映射的函数就是input_dev中的setkeycode、getkeycode这两个函数指针对应的函数,如果注册之前不初始化它们,则初始化函数把系统默认的函数赋给它们。
EVIOCGKEYCODE和EVIOCSKEYCODE这两个ioctl命令分别用来查看和修改键值。
(4) 按键的自动连击
我们都有这样的经历:按住方向键不松开,一直把文档往某个方向拉。这个功能就是自动连击,也就是在按键抬起之前连续发送按键事件。
按键连击事件(EV_REP)的开启非常简单,在input_devd的evbit相应比特置1即可。dev->rep[REP_DELAY]和dev->rep[REP_PERIOD]分别存储连击的延时和周期,如果驱动不对它们赋值,则系统为他们分别赋为250和33。按键延时即按键到第一次连击的间隔,按键周期即两次连击之间的间隔。它们的单位都是毫秒。
(5) 总线类型
Input_dev中input_id用到了总线类型。输入子系统支持的总线类型如下所示。
/* include/linux/input.h */
#define BUS_PCI 0x01
#define BUS_ISAPNP 0x02
#define BUS_USB 0x03
#define BUS_HIL 0x04
#define BUS_BLUETOOTH 0x05
#define BUS_VIRTUAL 0x06
#define BUS_ISA 0x10
#define BUS_I8042 0x11
#define BUS_XTKBD 0x12
#define BUS_RS232 0x13
#define BUS_GAMEPORT 0x14
#define BUS_PARPORT 0x15
#define BUS_AMIGA 0x16
#define BUS_ADB 0x17
#define BUS_I2C 0x18
#define BUS_HOST 0x19
#define BUS_GSC 0x1A
#define BUS_ATARI 0x1B
(6) 其他的事件类型
EV_LED和EV_SND事件是针对键盘上的led和蜂鸣器。这两类事件是由输入子系统内核发送给驱动的。
如果驱动支持这两类事件的话,应该填充input_dev中的event函数指针,其实这是一个回调函数。另外需要填充input_dev中evbit对应的比特位。
这个回调函数可能在中断或者中断的底半部调用,因此event函数不能睡眠且必须尽快结束。