全部博文(13)
分类: LINUX
2012-11-29 15:15:53
在Linux系统中,用户大多数的输入信息都是通过input子系统向上层报告的,包括:键盘输入、鼠标输入、触摸屏输入等。另外,Linux系统的用户层的应用程序也是通过input子系统提供的统一接口获得用户大多数的输入。
input子系统是分层结构的。当用户需要开发按键、鼠标、触摸屏等驱动时,仅需驱动硬件,获得硬件输入数据,然后向上层报告从硬件获得的输入事件即可。不需要处理与用户层的通信接口。
在input子系统中,input_dev描述的是输入的硬件设备。所以每驱动一个硬件输入设备,都要为之初始化一个input_dev实例。同时,input_dev实例也描述了硬件设备的可以产生的输入类型、输入键值。input_dev定义在include/linux/input.h文件下,如程序清单 1.1 所示。
程序清单 input_dev的定义
点击(此处)折叠或打开
下面对input_dev的重要成员进行介绍:
private:该成员是input_dev的私有数据,其具体内容由各个输入设备定义。
name: 该成员是input_dev的名字。需要注意的是:虽然每注册一个input_dev实例都会生成一个设备文件节点,但是该设备文件节点的名字与name没有联系。
id: 该成员是input_id类型,描述了设备的总线类型、厂商号、产品号、版本的信息。
evbit: 该成员表示设备可以产生的事件类型的集合,比较典型的是:EV_KEY(按键事件)、EV_ABS(触摸屏事件)。evbit的值是各个事件宏的组成的掩码。
keybit:若设备可以产生按键事件,那么keybit成员则记录设备可以输入键值的集合。
accept: 若该成员有被初始化,那么该设备对应的设备文件节点被打开时,该成员指向的函数将被调用。
close: 若该成员有被初始化,那么该设备对应的设备文件节点关闭时,该成员指向的函数将调用。
grab: 该成员指向负责驱动该设备的input_handler实例。
2. input_handlerinput_dev描述的是设备,那么input_handler则驱动同一类型的设备。input_handler也是定义在include/linux/input.h文件中,如程序清单 1.2所示。
程序清单 input_handler的定义
点击(此处)折叠或打开
下面对input_handler的各成员进行介绍:
private:驱动的私有数据。
event: 当设备要产生输入事件时,则需要找到负责驱动该设备的input_handler实例,并用该实例的event成员指向的函数把事件向上层报告。
connect:把互相匹配的input_handler和input_dev实例绑定起来。
disconnect:解除input_handler和input_dev实例的绑定。
fops: 每成功地注册一个输入设备input_dev实例都会生成一个设备文件节点,那么负责驱动这些设备的input_handler实例的fops成员则为这些设备文件节点实现标准的系统调用。
minor: 每个input_handler实例都负责驱动一些输入设备。那么这些输入设备对应的设备节点文件的次设备号都是以该input_handler实例的minor成员的值为起点。另外,minor成员的值必须是32的整倍数,也是说保证minor的值右移五位后大于零。
name: handler的名字。
id_table: 该成员是储存input_handler可以驱动的input_dev实例的类型的列表。
blacklist: 该成员是储存input_handler可以不理会的input_dev实例的类型的列表。
input子系统中维护着一个input_dev_list链表。每成功注册一个inut_dev实例后,都会把该实例添加到这个链表。
3.input_handle每当成功注册一个input_dev实例时,input子系统也会生成一个input_handle实例。把刚注册成功的input_dev实例和负责为之驱动的input_handler实例关联起来。input_handle是定义在include/linux/input.h,如程序清单 1.3所示。
程序清单 1 3 input_handle的定义
在上述代码中,dev成员是指一个input_dev实例,handler成员指向负责驱动该input_dev实例的input_handler实例。
在input子系统中,维护着一个 input_handler类型的指针数组input_table。该数组的每一个元素都可以驱动一类的输入设备,如按键类、事件类、触摸屏类等。每成功注册一个input_handler实例,都会把该实例添加到input_table中。同样每当成功注册一个input_dev实例时,也需要该实例与可以驱动它的input_handler实例绑定起来。而这个绑定是由生成的一个input_handle实例完成。所以input_handler、input_dev、input_handle的三者关系如 图 1.1所示。
1.1 这三个数据结构的关系
子系统分析input子系统是分层结构的。我们可以把input子系统分成三层:用户接口层、驱动层、硬件驱动层。下面对input子系统的各层结构进行分析。
用户接口层是input子系统的最上一层,负责与用户空间的通信。该层为用户空间访问input子系统实现了各种接口。实现该层的驱动代码文件是drivers/input/input.c。其模块的初始化入口函数是input_init,如程序清单 1.1 所示。
1.1 input_init函数的实现
点击(此处)折叠或打开
我们再看看这个函数做了什么工作。(1)行代码是生成一个名字为“input”设备类。(2)行代码是为input子系统在/proc目录下初始化一些文件。(3)行代码是为input子系统注册一个主设备号为INPUT_MAJOR(其值为13)的字符设备,其文件操作列表是input_fops。以后,input子系统所有生成的设备文件节点尽管其次设备号不一样,但其主设备号都是INPUT_MAJOR(13)。当用户层的应用程序访问这一类设备文件节点时,其文件操作列表都是input_fops。(4)行代码是在/dev/目录新建一个input目录。以后,input子系统所有生成的设备文件节点都在/dev/input/目录下。
由于在用户空间的应用程序访问input生成的所有设备文件节点都是通过文件操作列表input_fops进行的,我们再看看input_fops为我们提供了什么。input_fops的实现如 程序清单 1.5所示。
1.5 input_fops的实现
点击(此处)折叠或打开
input_fops的实现极其简单,仅仅实现了open成员,其指向的函数是input_open_file。再看看input_open_file函数的代码,如程序清单 1.6 所示。
1.6 input_open_file函数的实现
点击(此处)折叠或打开
在上述函数中,(1)行代码是通过设备文件节点的次设备号从input_table获得驱动节点对应的设备的input_handler对象。至于为什么能这样获得,在后面的章节再述。(2)—(3)行代码是检查input_handler实例的f_op成员是否已经初始化。如果是,则用input_handler实例的f_op成员指向的文件操作列表替换掉被打开的设备文件节点的文件对象指向的文件操作列表。(4)行代码是执行新的文件操作列表的open成员指向的函数,重新执行打开文件操作。
在这里可以看出,其实在用户接口层所做的工作并不多。其主要工作是当用户应用程序打开input的设备文件节点时,为该节点的文件对象找到合适的文件操作列表。该层的工作如图 1.2所示。
1.2 用户接口层工作示意图
在前面的描述知道,在input子系统对于每一个输入设备都要为之初始化一个input_dev实例。对于同一类输入设备的input_dev实例,都需要一个input_handler实例为之驱动。在驱动层,其主要作用是维护着一个input_handler类型的数组input_table。这些input_handler实例为上面的用户接口层实现文件操作,同时驱动下面的硬件驱动层的input_dev实例。
驱动层并没有统一的模块初始化入口。驱动层是由多个模块组成的。每个一模块仅是注册一个input_handler实例,以驱动一类的输入设备。如drivers/input/evdev.c文件所生成的模块是用于驱动事件类输入设备;drivers/input/mousedev.c文件所生成的模块用于驱动鼠标类输入设备;drivers/input/tslibdev.c文件所生成的模块用于驱动触摸屏类输入设备。
这里仅介绍evdev,其它的可以触类旁通。
1.注册一个input_handler实例evdev的模块的初始化入口函数是evdev_init,代码如程序清单 1.7 所示。
1.7 evdev_init函数的实现
点击(此处)折叠或打开
evdev_init函数的工作仅是注册evdev_handler。再看看evdev_handler的初始化,如程序清单 1.8所示。
1.8 evdev_handler的实现
点击(此处)折叠或打开
evdev_handler的minor成员的值是EVDEV_MINOR_BASE(64)。需要注意的是所有input_handler实例的minor成员的值必须是32的整倍数。
input_register_handler的函数实现如程序清单 1.9所示。
1.9 input_register_handler函数的实现
点击(此处)折叠或打开
在上述函数中,(1)—(2)行代码是先检查handler的fops成员是否已经初始化,如果已经初始化就把handler加入到input_table数组中。加入到数据组后,它在input_table数组的索引号就是它的minor成员的值右移5位之后的值,也就是整除以32之后的值。这就是input_handler实例的minor成员的值必须是32的整倍数的原因。(3)行代码是把handler加入到input_handler_list链表中。(4)—(6)行代码是扫描系统中所有已注册的input_dev实例,每找到一个handler能为之驱动的input_dev实例,就把两者绑定起来。
这里我们先不管是如果找到input_dev实例以及如何判断input_handler实例和input_dev实例是否匹配。我们在这里,只关注找到一个能与之匹配的input_dev实例后,(5)行的代码如何把实例和handler绑定起来。
再从程序清单 1.8看看evdev_handler的connect成员指向的evdev_connect函数。该函数的实现如程序清单 1.10所示。
1.10 evdev_connect函数的实现
点击(此处)折叠或打开
该函数需要三个参数:第一个是input_handler类型handler;第二个input_dev类型的dev;第三个我们不用管。注意,handler和dev必须是匹配的。
在这里我们需要关注一个数据结构evdev,其定义如程序清单 1.11所示。
1.11 evdev的定义
点击(此处)折叠或打开
在evdev模块,对于一个输入设备都看成是evdev设备。evdev的handle成员负责组织输入设备的input_dev实例和相应的input_handler实例。在evdev模块,维护着一个evdev类型的数组evdev_table,用于储存所有的evdev设备。
再看看evdev_connect函数。在该函数的(1)行代码中,是找出evdev_table数组第一个为空的元素的索引号minor。(2)—(5)行代码是生成一个evdev实例,并初始化。其中(3)行代码是使evdev的handle成员的input_dev类型的dev成员指向传入的dev;(4)行代码是使evdev的handle成员的handler成员指向传入的handler。至此,dev和handler就实现了关联。(6)行代码是把新生成evdev实例加入到evdev_table数组中。(7)—(8)行代码是为输入设备注册一个字符文件节点。该节点的文件名为input/eventN,其中N就minor的值;次设备号是EVDEV_MINOR_BASE + minor,EVDEV_MINOR_BASE的值为64,也是evdev_handler的minor的值。
由此我们可以看出:一个特定的input_handler实例驱动一类输入设备,该实例的minor成员的值是32的整倍数;由这一类的输入设备而产生的设备文件节点的次设备号是以该实例的minor成员的值为基础的;同时由这一类的输入设备而产生的设备文件节点的文件名也是统一命名,如evdev类的命令为event0、event1、event2……
当evdev_handler成功注册后,产生的效果如图 1.3所示。
1.3 evdev设备的结构图
在上文说过,在用户接口层为用户空间提供的文件操作列表是由驱动层的input_handler实例提供的。在这里,我们看看evdev_handler实现的文件操作列表。evdev_handler的f_op成员指向evdev_fops。evdev_fops的实现如程序清单 1.1所示。
程序清单 1.12 evdev_fops的实现
点击(此处)折叠或打开
l 打开操作
打开操作的实现函数是evdev_open。该函数的代码如程序清单 1.13所示。
1.13 evdev_open函数的实现
点击(此处)折叠或打开
在上述函数中,(1)行代码是通过设备文件节点的次设备号,计算出该节点对应的evdev对象在evdev_table的索引号。(2)行代码,是调用该节点对应的input_dev的accept成员指向的函数,传入参数是evdev_handler和文件对象。(3)—(4)行代码是生成并初始化一个evdev_list实例,然后把该evdev_list实例作为文件对象的私有数据。
这里需要重点关注的是evdev_list结构。evdev_list的定义如程序清单 1.14所示。
1.14 evdev_list的定义
点击(此处)折叠或打开
该结构是用于管理与用户空间互交的数据。其中数据缓冲区是buffer成员,数据区的头和尾分别用head和tail成员指向。buffer是input_event类型的数组。也这是说,与用户空间交互的数据类型是input_event类型的。input_event的定义如程序清单 1.15所示。
点击(此处)折叠或打开
1.15 input_event的定义
input_event结构中,type表示输入的数据的事件类型可以是EV_KEY(按键)、EV_ABS(绝对座标)等;code是输入的键值;value是根不同的事件类型取值。
l 读取操作
用户空间的应用程序从input子系统的获得的所有输入数据都是通过标准的read调用实现的。evdev_handler提供的读取操作函数是evdev_read。该函数的实现如程序清单 1.16所示。
1.16 evdev_read函数的实现
点击(此处)折叠或打开
在上述函数中,(1)行代码是从文件对象的私有数据中,获得设备的evdev_list实例list。这是在open函数中设置好的。list不但是输入设备与用户空间通信的数据缓冲区,也和对相关的input_dev实例和input_handler实例关联。需要注意的是list是环形缓冲区。(2)行代码是检查缓冲区中是否有数据,以及相关的合法性。(3)行代码是查看list缓冲区中是否有数据,如果没有数据则一直等待到缓冲区中有数据。(4)—(5)行代码是把缓冲中的数据复制用户空间的缓冲区,直到环形缓冲区中的数据为空或者返回到用户空间的数据大于或等于用户空间要求的数据长度为此。
由此可以知道,当用户应用程序向input子系统获得输入设备的输入数据时,仅是在缓冲区中直接读取数据。那么输入设备是如何把数据存放到相应的缓冲区中呢?这需要依靠evdev_event函数。
l evdev_event函数
在程序清单 1.8中,evdev_handler的event成员是指向evdev_event函数。当evdev类的输入设备有数据输入时,则通过evdev_event函数把输入数据存放到相应的evdev_list缓冲区中,等待应用程序来读取。evdev_event函数的实现如程序清单 1.17所示。
1.17 evdev_event函数的实现
点击(此处)折叠或打开
该函数有四个输入参数:handle是与输入设备联系的input_handle实例;type是输入的类型;code是输入的键值;value是输入值。在evdev_event函数,(1)行代码是在传入的handle的私有数据中获得与输入设备相关的evdev实例。这是在open中设置的。在(2)—(4)行代码是把传入type、code、value和当前时间值组成一个input_event对象,存放到缓冲区中。当然要看evdev是到底关联了多少个缓冲区:如果是关联了一个缓冲区,则使(2)—(3)行的代码;如果是关联了多个缓冲区,则使用(3)—(4)行的代码把evevt对象存放到所有关联的缓冲区中。(5)行代码是唤醒等待输入数据的相关进程。
在驱动层,数据的流图如图 1.4所示。
1.4 缓冲区读、写的关系
当然,evdev_handler的f_op成员还实现了write方法。该方法最终还是调用evdev_event函数把数据写入到相关的evdev_list缓冲区。write方法的实现主要是让应用程序能产生输入事件。
这里仅说明设备驱动层的工作机理,至于具体的编程实现,请参考下一编文档。
1. 注册一个input_dev实例注册一个input_dev实例是使用input_register_device函数。
该函数的实现代码如程序清单 1.18所示。
1.18 input_register_device函数的实现
点击(此处)折叠或打开
该函数只有一个输入参数是要注册的input_dev实例dev。(1)行代码是为dev添加EV_SYN事件的支持。这是所有的输入设备都需要的,因为在每次向上层提交输入数据时都会用到。(2)—(3)行代码是初始化dev的定时器,如果dev->rep[REP_DELAY]和dev->rep[REP_PERIOD]没有设值,则将其赋默认值。这主要是处理重复按键的。(4)—(5)行代码是把dev添加到input_dev_list链表中。(6)—(7)行代码是扫描input_handler_list——这是所有已注册的input_handler实例的集合,把dev与input_handler_list的每个元素进行匹配;如果能匹配,则把找到的input_handler与dev绑定起来。绑定操作是input_handler的connect成员,如果evdev_handler对象,connect就指向evdev_connect函数,前面分析过。绑定完成后,将为该输入设备产生一个设备文件节点。
现在只需要我们关注的是如何判断一个input_dev实例和一个input_handler实例是否匹配。判断匹配的函数是input_match_device,该函数的代码如程序清单 1.19所示。
1.19 input_device_device函数的实现
点击(此处)折叠或打开
在input_match_device函数中,有两个输入参数:id和input_dev类型的实例dev。id是存放输入设备特征信息的列表。input_handler的实例id_table成员指向它可以驱动的设备的特征的列表;而blacklist成员则指向它可以忽略的设备的特征的列表。
在上述函数中,将扫描id列表的每个元素,并对比各个匹配项。在(1)行—(2)行代码中,如果id的当前元素的flags有设置INPUT_DEVICE_ID_MATCH_BUS,则需要匹配总线类型;如果不匹配,进入下一次循环,id移动到下一个元素,再进行对比;如果匹配,则匹配下一项。同样在(2)—(3)行代码分别匹配INPUT_DEVICE_ID_MATCH_VENDOR、INPUT_DEVICE_ID_MATCH_PRODUCT、INPUT_DEVICE_ID_MATCH_VERSION项。如果这项都匹配成功或者不需要匹配,则进入(4)—(5)行的代码进一步的匹配工作。
看看(4)行的代码中的MATCH_BIT宏,其定义如程序清单 1.20所示。
1.20 MATCH_BIT的定义
点击(此处)折叠或打开
如果把(4)行代码展开来则如下所示:
点击(此处)折叠或打开
NBITS(EV_MAX)是dev的evbit成员或id的evbit成员的数组长度。上面代码的整体意思是id有设置的事件类型,dev是否都设置了。如果是,则该项匹配成功,进入下一项的匹配;否则,进入下一轮的匹配。(4)—(5)行代码以同样的方法匹配evbit、keybit、relbit、absbit、mscbit、ledbit、sndbit和ffbit项。
在input_register_device中,会把系统中注册的每个input_handler实例的id列表与dev进行匹配。而系统中已注册的input_handler一般是有事件handler(evdev_handler)、按键handler、绝对座标handler和相对座标handler。这里我们有必要看它们的id成员。
事件类型的,定义在drivers/input/evdev.c:
点击(此处)折叠或打开
上面好像什么没有设置,其实根据input_match_device函数,反而能匹配所有设备。
相对座标(鼠标)类型的,定义在driver/input/mousedev.c:
点击(此处)折叠或打开
这里的匹配比较严格。如果要获得鼠标驱动的支持,input_dev实例的id项,必须参考上面的内容进行设置。但是要获得事件驱动的支持,input_dev实例的id项的设置则比较自由。
2. 提交输入数据当设备驱动层的驱动模块从硬件获得输入数据时,需要把数据向上层提交,使用户空间的应用程序能获得这些数据。提交输入数据使用input_event函数实现的。该函数的实现如程序清单 1.21 所示。
1.21 input_event函数的实现
点击(此处)折叠或打开
input_event有四个输入参数:dev是提交数据的input_dev实例;type是输入的类型;code是输入的键值;value是输入值。(1)行—(2)行的代码比较长,但其工作仅是根据不同输入数据类型作出调整。最后,真正把数据向上层提交是(3)—(4)行代码中的event指向的函数。这请参考上一节1.3.22.的内容。
第2章 子系统在input子系统中,也可能会用到其它的总线驱动程序。如触摸屏除了有ADS数/模接口之外,还有的是使用串口接口、USB接口和PS/2接口。那么触摸屏驱动程序要从这些接口获得触摸屏的数据,就必须借助这些接口的驱动程序。同样,键盘和鼠标驱动也会使用PS/2接口或USB接口。我们可以统称这一类设备为总线型输入设备。
那么在input子系统的硬件驱动层,是如何使用其它驱动程序而获得总线型输入设备的输入数据?这就需要使用到serio子系统。serio子系统分了三层,最上一层是驱动层,也是就是input子系统中的硬件驱动层;中间层为上、下两层提供了各种操作接口;最下一层是设备层,设备层封装了总线型输入设备,为驱动层访问硬件实现了统一的接口。
由此可知,input子系统是借助了serio子系统实现了自己的硬件驱动层。那么input子系统和serio子系统的关系如图 2.1所示。
2.1 input子系统和serio子系统的关系
从上图可以知道,input子系统与serio子系统出现了交集。在上图的最中间一层,在input子系统中是硬件驱动层,而在serio子系统中是驱动层。在本章将以串口触摸屏为例,详细介绍serio子系统。
子系统的数据结构serio结构描述是对硬件设备的封装,它把硬件设备封装成端口。该结构定义在include/linux/serio.h,如程序清单 2.1所示。
2.1 serio结构的定义
点击(此处)折叠或打开
下面对serio的重要成员进行介绍:
private:指向私有数据。
name: 设备的名字。
type: 总线类型。
write: 驱动程序向输入硬件设备写入数据的方法。
注意serio没有read的方法。也是说上层驱动不能主动读取输入设备的数据。
2. serio_driver结构serio_driver结构是对上层驱动输入设备驱动的封装。该结构定义在include/linux/serio.h,如 程序清单 2.2所示。
2.2 serio_driver结构的定义
点击(此处)折叠或打开
下面对serio_driver的重要成员进行介绍:
private: 这是私有数据的指针。
interrupt:这是对中断处理函数的模仿。也是输入设备驱动获得设备输入数据的方法。
connect:该方法serio_driver实例在注册时被调用。
disconnect:该方法serio_driver实例在注销时被调用。
串口触摸屏驱动工作在input子系统的硬件驱动层,所以在该驱动模块要实现并注册一个input_dev实例;同时该驱动也工作在serio子系统的驱动层,所以在该驱动模块也要实现并注册一个serio_driver实例。
串口触摸屏驱动模块的实现有很多个,都是在driver/input/touchscreen/目录下。这里介绍touchit213.c文件。
实例该模块的初始化入口函数是touchit213_init,如程序清单 2.3 所示。
2.3 touchit213_init函数的实现
点击(此处)折叠或打开
该函数比较简单,仅是使用serio_register_driver函数注册一个serio_driver类型的对象touchit213_drv。serio_register_drivrer函数的实现在drivers/input/serio/serio.c,也就是serio子系统的中间层驱动模块的实现文件。该函数的实现如程序清单 2.4 所示。
2.4 serio_register_driver函数的定义
点击(此处)折叠或打开
serio_register_driver函数只有一个输入参数drv,这是需要注册的serio_drivers对象。(1)行代码是获得serio_sem的信号量。该信号量由中间层维护。(2)行代码是把drv加入到serio_driver_list链表中。(3)—(4)行代码是把drv的driver成员加入到serio_bus中,并注册。(5)—(6)行代码是判断drv是否需要和系统中已注册的serio对象绑定,如果需要进入下一步;否则跳转函数结束处。(7)—(8)行代码是扫描serio_list列表——该列表保存系统中所有已注册的serio对象,每找到一个serio对象,如果该对象的drv成员为空,也就是还没有serio_driver对象为之驱动,则调用serio_connect函数,并以找到serio对象和drv作为参数。
serio_connect函数的实现如程序清单 2.5所示。
2.5 serio_connect_port函数的实现
点击(此处)折叠或打开
在上述代码中,(1)行代码是最重要的一句代码。也是这一段代码把serio和drv绑定起来。看看serio_bind_drive函数的实现如程序清单 2.6所示。
2.6 serio_bind_driver函数的实现
点击(此处)折叠或打开
在上述函数的(1)行代码是使用drv的connect方法绑定serio和drv。同时,drv的connect方法也起来着判断serio和drv是否匹配的作用。至于如何判断serio和drv是否匹配,以及如何绑定serio和drv,则由驱动层的serio_driver实例的初始化部份代码决定。
从这里也可以看出serio子系统的设计也不是那么严谨。判断设备(serio实例)和驱动(serio_driver实例)是否匹配以及绑定匹配的设备和驱动本应是一个子系统中最重要一环,但在serio子系统却没有统一的判断标准和绑定方法,还要靠二次开发的程序员自行处理。这大大增加了驱动层与设备层耦合性。
touchit213驱动模块的初始化入口函数是注册一个serio_driver实例。我们看看这serio_driver实例的初始化代码,如程序清单 2.7所示。
2.7 serio_driver_touchit21的实现
点击(此处)折叠或打开
serio_driver_touchit213的connect成员是指向touchit213_connect函数。该函数在driver_touchit213被注册时调用,用于判断要注册的serio_driver实例和系统中已有serio的实例是否匹配,如果匹配,实施两者的绑定;在这里同时负责input_dev设备的初始化工作。touchit213_connect函数的代码如程序清单 2.8所示。
2.8 touchit213_connect函数的实现
点击(此处)折叠或打开
touchit213_connect函数有两个参数:一个是serio_driver类型的drv,这是要的注册的serio_driver实例,在这里就是touchit213_drv;另一个是系统中已注册的serio实例。
touchit213.c定义了一个自己的数据结构touchit213,如程序清单 2.9所示。
2.9 touchit213_drv的定义
点击(此处)折叠或打开
再看看touchit213_connect函数,(1)行和(2)行的代码分为touchit213类型的指针touchit213和input_dev类型的指针input_dev申请内存空间。(3)行和(4)行代码是对input_dev作为基本的初始化工作。(5)行和(6)行代码是把touchit21关联传入的serio和drv。这也是说touchit213根据没有判断serio和drv是否匹配,它认为系统中已有的serio实例都能和touchit213_drv匹配。(10)—(13)行代码是初始化input_dev的id成员。注意这样的id设置只能匹配evdev驱动,也是说input_dev注册后是生成event类设备。(14)行代码是设置input_dev产生EV_SYN事件、EV_ABS(触摸屏)事件、EV_KEY(按键)事件。(15)行代码是设置input_dev可以报告的键值是BTN_TOUCH,这表示触摸屏接触。(16)—(17)行代码是设置触摸屏的X、Y座标的最大和最小值。(18)行代码是设置触摸的压力最大和最小值分别是300和0。(19)行代码是设置touchit213为serio的dev成员私有数据。(21)行代码是注册input_dev。
作为input子系统的硬件驱动层的一个重要的任务是当硬件有数据输入时,能读出输入数据,并向上层报告。在这里,当下层有数据输入时,已注册的serio_driver的interrupt指向函数就调用,并传入硬件输入的数据。这是模拟了中断处理函数。至于该函数触发机理请看下一节的内容。
touchit213_drv的interrupt成员指向的是touchit213_interrupt函数。该函数的定义如程序清单 2.10所示。
2.10 touchit213_interrupt函数的实现
点击(此处)折叠或打开
touchit213_interrupt有多个传入参数:第一个是产生数据的设备serio;第二个是硬件输入的数据data。其它的参数我们不用管,这都模拟中断处理函数的。这里的输入数据仅一个字节的data。这也是说明该函数每一次调用仅传入一个字节,那么并不是每次调用都能产生一个事件。只有累计传入的输入数据足够时,才能产生一个事件。
在touchit213_interrupt的(1)行代码是从serio的dev成员的私有数据获得touchit213对象;(2)行代码是在touchit213中获得input_dev对象。这都是在上一小节所述的connect函数中设置好的。(3)行代码是把data保存到touchit213维护的缓冲区,touchit213的idx成员是记录缓冲区中最后一个数据的索引。下面的switch语句表明只有传入的数据累计达到5个时,才能产生一次事件。
(5)和(6)行代码是分别从缓冲区中获得X、Y的座标值。(7)行代码其实是清空缓冲区。(8)—(12)行的代码表明,如果用户输入的是触摸屏按下事件,就向上层提交用户按下的X、Y座标值和按下的压力值为299;如果用户输入的是触摸屏提起事件,就向上层提交按下的压力值为0。(13)行代码向上层报告事件提交完毕。上述的input_report_abs和input_sync最终都是调用input_event函数向上层提交输入数据。
通过设备层,驱动层的就可以访问通过串口、USB、PS/2连接的输入硬件设备。这一节,我们通过实现串口触摸屏驱动为例,说明设备层的工作原理。
实例注册一个serio实例所用的函数是serio_register_port。该函数的实现如程序清单 2.11所示。
2.11 serio_register_port函数的实现
点击(此处)折叠或打开
在上述函数中,(1)行和(4)代码分别获得和释放serio_sem信号量,用于保护(2)行和(3)行代码的安全。(2)行代码是设置serio的一部份成员,然后把serio添加到serio_list链表中,最后把serio的dev成员注册到seio_bus总线中。serio_connect_port在程序清单 2.5中描述过。不过,这里serio_driver类型参数为NULL。那么在该函数,对serio几乎什么都没做。
注册一个serio实例的主要操作就是把serio添加到serio_bus总线以及把 serio加入到serio_list链表中。
子系统由于我们要使用串口总线和串口触摸屏通信,为了使用现有的串口驱动,这少不得和tty子系统打交道。对于一个串口控制器即串口硬件端口,都有一个uart_port实例来管理。那么这些描述串口硬件端口的uart_port实例,都一个tty_driver为之驱动。当用户应用程序打开某串口端口对应的设备文件节点时,会对这个端口生成一个tty_struct实例,负责tty与用户程序的通信。tty_struct实例与tty_driver实例之间还有一个tty_ldisc。这是线路规程。tty子系统的大概结构如图 2.2所示。
2.2 tty结构简图
在上图中,我们需要找出serio子系统与和tty子系统通信的结合点。uart_port是描述硬件结构,本身没有通信能力。tty_driver有通信能力,但只有和tty_struct实例结合才能找到相应uart_port实例进而同硬件通信。tty_struct实例是在应用程序打开文件节点时动态生成的,不能被我们的serio子系统静态掌握。剩下的只有tty_ldisc。
当用户空间的应用程序打开某串口端口对应的文件节点时,就会针对这个端口生成一个tty_struct实例。同时会为这个tty_struct实例在tty_ldiscs数组中选择一个tty_ldisc实例(在默认情况是tty_ldiscs[N_TTY]),作为它的线路规程。所有关于这个tty_struct实例的通信数据都必须经过这个线路规程,实施数据调整。由于tty_ldiscs数组是静态存在的,里面的元素都具有完整数据通信能力。这为我们的通信打开了方便之门。
具体的做法是:在serio子系统的设备层模块生成一个自己的tty_ldisc实例并注册到tty_ldiscs数组中。这时我们还要需一个与具体uart_port实例关联的tty_struct实例;那么我们就使用用户应用程序打开对某个串口端口的文件节点;同时我们在tty子系统中添加一个ioctl命令用于把该文件节点的tty_struct实例关联上我们指定的线路规程。这时serio子系统与tty子系统的通信通道就打开了。操作如 REF 图 2.3所示。
2.3 更改线路规程示意图
的初始化针对串口通信的serio子系统的设备层的驱动模块源码文件是drivers/input/serio/serport.c。该模块的初始化入口函数是serport_init。该函数的实现如程序清单 2.12所示。
2.12 serport_init函数的实现
点击(此处)折叠或打开
serport_init函数的工作很简单,仅是注册一个tty的线路规程实例serport_ldisc。该函数会把serport_ldisc注册到tty子系统的tty_ldiscs数组的第N_MOUSE(2)个元素里。 serport_ldisc的初始化如程序清单 2.13所示。
2.13 serport_ldisc的初始化
点击(此处)折叠或打开
l open的实现
serport_ldisc的open成员指向的是serport_ldisc_open函数。由于我们需要修改tty子系统的用户接口层的代码。在修改代码中,将显式地调用此函数,执行一部份的针对串口通信设备层的初始化工作。该函数的实现如程序清单 2.14所示。
2.14 serport_ldisc_open函数的实现
点击(此处)折叠或打开
在上述函数中提及到一个数据结构serport。这是该驱动模块自己定义的,如程序清单 2.15所示。
2.15 serport结构的定义
点击(此处)折叠或打开
该结构描述了模块所用的资源,包括tty通道、serio设备。
再看看serport_ldisc_open函数。该函数的(1)、(2)行代码是为serport类型的serport和serio类型的serio分配内容空间。(6)行代码是把serport作为tty的私有数据。(7)—(13)行代码是初始化serio的各成员。(14)行代码是初始化serio的信号量。注意,直至该函数的结束都没有注册serio。
l read的实现
当用户应用程序对tty的设备文件的文件对象执行read调用时,文件对象掌握的tty_struct实例所属的线路规程实例的read的方法将被调用,用于把缓冲区的数据读入到用户空间中。但这里实现的线路规程的read方法显然不是这种用法。看看它的实现函数serport_ldisc_read,如程序清单 2.16所示。
2.16 serport_ldisc_read函数的实现
点击(此处)折叠或打开
(1)行代码是从tty的私有数据中获得serport实例。(2)行是注册serio,彻底完成该层的初始化工作。然后在(3)行代码休眠,直到serio->type为0为止。最后在(4)行注销serio。好像整个函数到底什么都没有做。其实,(3)行代码是不会返回的。因为在该层的其它代码,没有它的唤醒代码。
也是说,这里的线路规程的read方法是用于启动serio。当用户应用程序对掌握这线路规程文件节点执行read调用时,read调用是不会返回而一直地休眠下去。
的实现串口通信是异步通信,也是说程序无法判断串口接口在何时有数据到达。所以实现方法一般是,当串口接口有数据到达时,触发中断处理程序,把到达的数据保存在缓冲区;当用户应用程序需要数据时,直接在缓冲区中读取。在tty子系统中,当串口接口有数据到达时,中断处理函数会通过该串口接口对应的线路规程的receive_buf方法把到达的数据保存在缓冲区中。这里的线路规程的receive_buf方法的实现是为了通知serio子系统的驱动层有数据到达。其现实函数是serport_ldisc_receive,如程序清单 2.17所示。
2.17 serport_ldisc_receive函数的实现
点击(此处)折叠或打开
该函数的输入参数有:tty是串口接口对应的tty_struct对象;cp是到达数据的指针;count是到达数据的长度。(1)行代码是从tty的私有数据中获得serport实例。(2)—(3)行代码是针对到达数据的每一个字节都调用serio_interrupt函数来处理,直到处理完所有的到达数据为止。再看看serio_interrupt函数,如程序清单 2.18所示。
2.18 serio_interrupt函数的实现
点击(此处)折叠或打开
在上述函数中,(1)行—(5)行代码是用于保护它们中间的代码。在(2)—(4)的代码中,在实际中会执行的只有(3)行的代码。(3)行代码是调用serio对应的serio_driver驱动的interrupt方法。
需要注意的是,上述所讲函数都运行在中断执行路径中。
在2.3.3小节中,我们说serio_drivr的interrupt成员是对中断处理的模拟。那么该中断处理模拟函数是如何触发的,在这里就可以得到答案了。那么整体的数据流程如图 2.4所示。
2.4 整体数据流程图
按理来说,驱动都是模块化的,只要按要求正确加载驱动模块即可完成驱动的初始化工作。但这里的串口触摸屏的一部份初始化操作是更改tty的线路规程和执行线路规程的read方法。这都依靠用户应用程序执行。
1.更改tty子系统的代码在tty子系统的用户接口层代码(driver/char/io_tty.c),我们需要添加一个ioctl命令用于切换线路规程。添加的代码如程序清单 2.19所示。
2.19 添加ioctl命令
点击(此处)折叠或打开
在上述代码中,完成线路规程的切换后,然后执行新线路规程的open方法。这是执行serport的初始化。
2.应用程序的代码为了执行serport的初始化工作,我们执行一部应用程序的代码。这段代码如程序清单 2.20所示。
2.20 启动serport的部分代码
点击(此处)折叠或打开
在上述代码,openSerial是自己实现的函数,主要工作是打开串口设备文件获得文件描述符,然后设置串口波特率、数据位、停止位等属性。这请参考串口编程的相关手册。然后调用ioctl执行线路规程的切换和调用read作serport的初始化工作。需要注意的是,read调用传入的数据大小是什么都不要紧,因为这不会通信的。上述代码在执行到read时,将在read调用中永远休眠。