2011年8月21日星期日

usb video class


1 Video Class 基础概念

Usb协议中,除了通用的软硬件电气接口规范等,还包含了各种各样的Class协议,用来为不同的功能定义各自的标准接口和具体的总线上的数据交互格式和内容。这些Class协议的数量非常多,最常见的比如支持U盘功能的Mass Storage Class,以及通用的数据交换协议:CDC class。此外还包括Audio Class, Print Class等等。
理论上说,即使没有这些Class,通过专用驱动也能够实现各种各样的应用功能。但是,正如Mass Storage Class的使用,使得各个厂商生产的U盘都能通过操作系统自带的统一的驱动程序来使用,对U盘的普及使用起了极大的推动作用,制定其它这些Class也是为了同样的目的。
Video Class 协议的目的是给USB接口的视频设备提供一个统一的数据交换规范。最初版本是在2003年9月才添加到USB Class规范中的,1.1的版本更是在2005年才发布。相比之下,Mass Storage Class 早在1998年就发布了。支持Video Class协议的多媒体芯片也是在2005年才陆续发布。所以USB 视频设备目前的现状是,在设备一端,多数依旧还采用原先的各种包含通用USB功能的多媒体处理芯片,主机端需要安装专用的驱动程序,基本上各个产品之间不具备兼容性。甚至对于操作系统而言,也只有在XP的SP2以后,才包含了对通用的Video class协议的支持。所以即使是某些多媒体设备(比如Logitech最新的几款摄像头)包含了对Video Class的支持,在Win2000等操作系统上依然需要安装驱动程序。不过,应该说使用Video Class无疑会是一个趋势,在相应的多媒体芯片陆续投入市场后,支持Video Class的多媒体设备应该会在一两年内会迅速普及开来。
除了在硬件上通过相应的多媒体芯片支持Video Class的设备以外,对于包含了操作系统的智能手机,当然也可以在手机端通过驱动程序来实现对Video Class的支持,就好像原先支持任何一种专用的USB驱动一样。只不过数据交换的格式不是自己随意制订的,而是按照Video Class的规范来实现的。
由于目前支持Video Class的设备还很少,所以在Linux上还没有开源的Video Class的主机端驱动,设备端的Video Class驱动就更没有见到开源的代码了。本文在介绍USB Video Class架构的基础上,主要是探讨Linux操作系统下设备端Video Class驱动的实现。不过在其它平台下的实现思路应该也是类似的。

2 USB Video Class 协议结构

2.1 设备拓扑结构

在拓扑结构上Video Class 将视频设备抽象为几个主要的硬件功能模块:

Ø 输入端点 Input Terminal
Ø 输出端点 Output Terminal
Ø camera端点 Camera Terminal
Ø 选择单元 Selector Unit
Ø 处理单元 Processing Unit
Ø 拓展单元 Extension Unit

下图是一幅摘自USB_Video_Example 1.1.pdf (www.usb.org)的拓扑结构示例图:

图1 USB Video Camera Topology Example

从sensor和另一个复合视频设备得到的数据流由IT 和 CT输入,经SU选择送PU处理,再由OT绑定到指定的USB端点。最后由USB端点与主机交互将数据发送到host端。在实际设备中,可能没有其中的某些功能模块,也可能其中的几个模块都是由同一硬件来完成的。


2.2 协议层次结构

上图中,左半部的框架组成了Video Class中的控制接口界面,右半部的框架组成了视频流传输接口界面。这两部分购成了Video Class的主要协议框架。

2.2.1 Descriptor Layout

与Class相关的信息,当然是主机端通过向设备端获取描述符(Descriptor)来得到的, 下图摘自USB_Video_Class_1.1.pdf , 给出了一个Video Class协议描述符应用示例的Layout。

图2 Video Camera Descriptor Layout Example

可以看到,在Descriptor Layout中,在标准描述符里,除了Device Descriptor, Configuration Descriptor, Interface Descriptor, Endpoint Descriptor,String Descriptor以外,还有一个USB2.0 协议中后期才新加的IAD 即 Interface Association Descriptor,用来描述多个相关Interface之间的关系,在Video Class中,IAD用来描述VideoControl Interface和VideoStreaming Interface之间的关系。
图中深色的部分就是Video Class 协议相关的专用描述符(Class Specific Descriptor)了。主要就是对硬件图像采集和处理模块的物理拓扑结构和功能的描述,以及对视频传输格式(包括编码格式,码率等等视频图像相关参数)的描述。
通过从设备处获得这些描述符,主机可以得知视频设备端的结构及其所支持的功能。而控制这些功能模块,对数据源和数据流进行配置,则需要通过Request来完成。

2.3 Request

Request是由主机向设备端发起的功能请求,包括所有USB设备都需要支持的Standard Device Requests 和与Class相关的Class Specific Requests :

2.3.1 Standard Device Requests

下图列出了USB Spec中规定的标准Request

图3 Standard Device Requests

这其中,有一部分Request是由USB控制芯片在硬件一级就直接完成的应答工作,比如SET_ADDRESS,有些则需要由软件来做进一步的处理,比如Set_Configuration。软硬件的这种任务的划分还与具体的硬件芯片相关。因为这部分是标准协议相关,本文就不详述。

2.3.2 Class Specific Requests

Class Specific Requests的数据结构Layout与标准Request是一样的,只是内容不同而已。VideoClass的Class Specific Requests主要根据Interface分为两类,其下又根据具体功能模块做进一步的划分:

Ø VideoControl Requests
- Camera Terminal Control Requests
- Selector Unit Control Requests
- Processing Unit Control Requests
- Extension Unit Control Requests
Ø VideoStreaming Requests
- Interface Control Requests

这其中,Interface Control Requests因为是用来在主机和设备之间协商数据交互格式和分辨率,流量等信息的,所以一般来说是必须实现的。
而Camera Terminal Control Requests 和 Processing Unit Control Requests中的内容,则是目前常用的即时通讯软件如MSN / QQ 等在其视频控制界面上集成的控制参数。
其中,Camera Terminal Control Requests包含了对曝光时间,曝光模式,对焦,变焦,平移等sensor获取数据阶段时的参数控制。
而Processing Unit Control Requests中则包含了亮度,增益,色调,对比度,白平衡等等sensor在获取到图像数据后,图像处理阶段的相关参数。

图4 Win2000上MSN的视频控制界面之一

不过实际上,以上两者的划分在硬件层次并不是绝对的。很多参数的控制在sensor硬件级别上是同一层次的。不过,将这些功能抽象成这两类,正如在硬件的拓扑结构上将功能模块抽象出来一样,有利于通用化的程序设计。

3 USB Video Class Linux设备端的实现

3.1 驱动架构

3.1.1 平台及软件基础

本文讨论的是USB Video Class在Linux操作系统上的设备端实现。具体是在Omap平台上,基于USB Gadget的驱动架构来实现的。
USB Gadget驱动分为两层,底层是处理与USB控制芯片硬件相关的内容,对上层屏蔽了大部分硬件相关的设置,并处理了一部分标准Request和EP0相关的标准操作流程。上层则是Class相关的部分,官方的Gadget驱动中,已经包含了File Storage(U盘功能),RNDIS(USB网卡功能)等的支持。考虑到Video Class的数据交换过程与File Storage有很多相似的地方,所以本文在Video Class的实现中,在大的框架上仿照了File Storage驱动的架构。

3.1.2 基本框架和数据流程

在本文实现的Video Class驱动中,整体的框架基本上分为两大部分。
一部分是负责处理模块的初始化过程,并负责处理Usb总线上的Descriptor和Requests的交互过程。包括USB总线上的控制和查询包的接收,解释,分配和应答。
另一部分,是在初始化过程中启动的一个独立的内核线程。负责具体的控制指令的执行和图像数据的获取及传输工作。这其中的许多操作都有可能引起睡眠,或者需要对文件进行操作,因此必须有一个线程做为依托。
模块的流程基本上是这样的:
在init函数中向Gadget底层驱动注册VideoClass数据结构。所有的描述符都定义为全局结构变量,在模块初始化过程中,进一步完成对描述符的填充过程,启动独立的内核线程,并注册EP0的complete回调函数。
在启动的内核线程中打开并初始化camera设备。将camera设置为默认的参数配置。读取图像数据并将数据填充到BUF里而后提交Request到VideoStream Interface里的BULK IN端点中。而后睡眠等待由于数据传送完毕被唤醒或有(异常)Exception发生被唤醒。
如果是数据传送完毕,则继续读取,填充并发送图像数据,如果有异常发生,则转而处理异常。
另一方面,哪些情况会引发异常呢?主要是驱动程序与BUS总线交互信息的这部分模块中,如果发生主机端重新设置Configuration,改变USB设备配置,或者发生总线插拔等引起总线状态变化的时候,会产生一个相应的异常,等待内核线程被唤醒并处理这些异常。
此外,在处理Requests的时候,有时候需要与camera驱动模块交互控制信息,同样需要操作文件句柄,目前的做法是在ep0 request的回调函数(context)中启动一个bottom half task,在Bottom half中完成相应的控制。
从总体结构上说,这样做很难看,理想的话应该在上述独立的内核线程中统一处理与Camera模块相关的操作,但是目前的架构,要唤醒该线程,只有两个途径,一是数据传输完毕或被取消,二是有总线状态变化相关的异常发生。如果硬加一个异常用来处理Requests似乎也很难看,而且对架构需要有较大的调整。所以目前只好简化的采用了前面所说的方案。
然后关于什么时候打开设备,开始往USB总线上放置数据,目前的处理方式也不是非常理想。目前是在模块初始化后立即获取第一帧图像而后等待主机端读取,实际上,主机端可能并不马上安排图像数据的传输。但是如果在主机端需要的时候才去读取数据的话,因为sensor获取数据需要一段曝光时间再加上压缩数据所需的时间,不可能立刻响应,所以在时间上肯定不能满足开始一段的数据传输请求。也需要继续仔细分析在何时启动camera模块最为合适。

3.2 与Camera驱动和V4L2子系统的配合

在linux内核中,2002年12月起发布了V4L2 (Video For Linux Two ) 0.1版本的规范,V4l2试图为所有和视频相关的设备都提供统一的接口,这其中当然也就包括了Camera设备。
而USB Video Class这一部分内容恰恰与视频设备也是密切相关的,所以在某些平台产品的实现中,甚至是在VideoClass中直接包含了Camera的驱动程序。这样做对于单一产品来说,可以大大简化驱动的层次结构,应该说是处理Camera的最直接简洁的办法。
但是,在本文的实现中,考虑到Linux内核中合理的模块划分的原则,也是为了符合Gadget驱动的其他Class实现的一贯风格,所以还是尽量使用V4L2的接口来控制Camera模块。这样做也有利于代码的移植。减小不同功能模块之间的耦合性。
理想的方式自然是所有的与Camera相关的操作都通过V4L2接口来实现,不过目前的实现中还是有些例外,引发例外的主要因素是效率问题。
由于在USB总线上传输图像,受到总线速度的限制,特别是在非USB2.0接口的芯片中,所以势必要采用JPEG或MPEG编码对数据进行压缩。V4L2子系统的框架中,包含了对编码器的支持,但是,也许是笔者对V4L2子系统的学习还不够深入,其对编码器的支持应该是对硬件编码芯片的支持,即使如此,也没有见到相关的代码,而且,在实现中使用的手机主板上也没有硬件的编解码芯片,所以在实现中Camera驱动通过V4L2子系统对应用层提供的是原始的未经编码的数据流,由应用程序调用IJG ( Independent JPEG Group )JPEG库函数来实现jpeg的编解码工作。
所以如果通过V4L2的read接口来获取数据,势必只能得到原始的图像数据,而且在V4L2的实现中,通过Read方式获取的数据,需要通过多次内存拷贝才能到达调用者处。所以也会很大程度的影响图像处理的速度。
要对图像进行压缩,要不在用户空间调用IJG库,要不在内核中再实现一个JPEG压缩算法。按前者来实现的话,涉及到Video Class如何去启动一个用户程序来读取Camera数据并在压缩后再传送给内核,也不是完全没法实现,但是无疑是一个非常糟糕的实现办法。
后者的话,涉及到这个JPEG压缩算法应该在什么地方实现,以及由谁来调用的问题(Video Class 还是 V4L2)。考虑到在存在硬件编码芯片的情况下,该芯片的管理和使用应该会纳入V4L2子系统中,所以考虑到兼容性,目前实现的方式是将JPEG压缩算法作为一个独立的模块插入内核,由V4L2子系统调用相关函数对图像数据进行压缩,然后再在Camera驱动中Export一个额外的函数接口,USB Video Class通过该函数接口启动Camera图像的读取和压缩过程,以区别标准的V4L2子系统的数据处理流程。压缩后的图像数据直接写入通过指针参数传递进来的内存地址空间中,尽可能的减少内存拷贝的次数,加速图像的传递。
这样做带来的问题就是需要在V4L2架构中添加额外的接口,增加了USB Video Class和V4L2子系统之间的耦合性,不能完全将这两个模块隔离开来。应该还有更好的解决方案。

3.3 JPEG编码相关

Jpeg的编解码,在Linux操作系统中,基本上采用的都是IJG(www.ijg.org)的JPEG库函数Libjpeg,这是一个相当可靠稳定和高效的开源项目,支持JPEG标准(不包括JPEG2000)的绝大多数编码方式。如非无奈,确实没有必要另外再写一个编码程序。但是由于需要在内核中使用,所以只好自己再编一个了。
JPEG编码相关的代码除了IJG的源代码以外,在网上还可以搜索到若干,但是无疑IJG的代码是最完善的。其它我能搜到的代码,多多少少都有一些BUG,而且也只是实现了JPEG标准的最基本的功能(当然,对于Video Class的应用来说已经是足够了)。最重要的是,多数是用浮点数运算来实现的,撇开速度不说,在本文的实现中OMAP平台的CPU也不支持浮点数运算。。。所以,本文实现中,最终是参考了网上搜到的某个算法的流程(主要是IJG的架构太复杂,一来没有时间精力和能力进行完整的分析,二来也不适合在内核中使用如此复杂的架构),在快速离散余弦变化的整数算法上仿照了IJG库的算法两者综合起来完成的。最终的代码还有很多需要改进的地方,不过,对于VideoClass来说,应该勉强够用了。这其中的具体问题,打算在另外单独的文档中再说明。

3.4 操作系统相关

说操作系统相关,可能说大了一些,这里主要涉及的内容是在本文的实现中,在WIN2000和WINXP平台的MSN测试中,遇到的一些问题。
由于VideoClass的协议只是规定了数据传输的格式和内容,对具体实现中的一些细节并没有作硬性的规定,所以导致有些细节可能存在不兼容的实现方式。(当然,我想主要还是本文的实现,由于能力有限,没有充分考虑到各种情况下的容错性,如果驱动做得好应该可以避免出现问题)。所以在WIN2000和WINXP的MSN测试中,遇到了一些平台相关的问题,有些功能在2000下能正常工作在XP下存在Bug,有些却相反。有些已经解决,有些只是猜测了可能的原因,罗列如下:

3.4.1 视频窗口关闭再打开后,没有图像

开始是在XP的MSN上发现有这样的问题,2000下没有,分析BUS数据可以看到,XP在关闭视频窗口的时候,会执行一个Abort Pipe的操作,这个操作应该会中断BULK传输,但是在设备端,Gadget底层驱动接收不到这个事件(也有可能是Gadget底层驱动的BUG),所以在VideoClass中无从得知这个传输已经被取消了,这样睡眠在等待数据传送完毕或失败上的线程也就无法被唤醒,自然也就不会继续发送数据。造成主机端再度打开视频窗口时接收不到图像数据。而在2000下的MSN中,关闭视频窗口的动作系统不会发送这个Abort Pipe事件,所以也就没有问题。
考虑到每次打开视频窗口的时候,主机端都会设置Streaming Interface的图像分辨率,码率等参数。而这之后主机端才会读取图像数据,所以后来解决的办法是在主机端设置Streaming Interface的时候,将之前已经放入BULK IN传输节点的数据 Dequeue出来,这样会造成这个传输的失败,从而唤醒睡眠的线程。但是如果仅仅这样做,XP能够正常工作了,2000又显示不了图像了。分析认为由于部分数据丢失,所以造成第一帧图像的数据是不完整的,无法正常解压缩显示,但是XP下的MSN有较好的容错性,能够丢弃这一帧图像,继续读取之后的数据,而2000下的MSN容错能力较差,无法再正常解读后面的图像数据。所以最终的解决办法是在发现传输失败后,将当前这一帧的图像数据从头开始重新发送,这样在XP和2000下就都能正常工作了。
不知道这种解决方案是否仅仅是一种治标的方案,有待以后继续研究。

3.4.2 某些分辨率下图像无法正常显示

在Win2000中如果提供160*120分辨率的图像,图像非常容易就停止刷新了,而BUS上实际数据还是在发送的。而在160*112(两者都是16的整倍数)的分辨率的情况下,就几乎不会发生这种情况。如果说这有可能还是JPEG的压缩算法有点问题,那另外一种情况就一定是XP 和 2000的区别了:如果设备这端通过描述符和Streaming Interface申明只能支持160*120 或者 160*112 的分辨率,2000可以接受这种分辨率,而XP根本就不能接受,总线上的控制传输就停止了,在界面上则显示检测不到Camera或Camera正在被其它设备打开占用,只有在进一步提供更高的320*240的分辨率的情况下,XP才会承认Camera的存在!其它问题倒不大,就是在本文的实现平台上,受软件编码JPEG速度的限制,320*240的分辨率下,视频的帧频会低一些,影响图像的流畅性。

3.5 其它

3.5.1 特殊效果的控制

应该说,VideoClass的Control Request基本上涵盖了V4L2标准界面提供的大部分控制参数,但是,还是有一部分没有涵盖,至于特定驱动专有的控制就更无法体现了,尤其是在MSN等应用程序的界面上,更不可能提供这些参数的控制了。但是,我们还是可以想办法trick过这个问题。
比如手机上常见的图像效果的设定,虽然不是特别有意义,但是既然是很常见的,为什么不能把它也做到Web Cam中呢?所以,如果一定要做,我们可以利用MSN控制界面上的原有的控制界面,借用其中一两个控制参数来实现图像效果的设定。
本文的实现中选择采用色调来控制图像效果,因为实际上这个参数是很不常用的,甚至只能在XP的高级设定中找到,对于99.9%的用户我相信都不会去改变这个参数。而它的字面含义与我们实现的功能也不算一点关系都没有,毕竟有很多效果实际上就是改变一下图像的颜色(当然还有一部分例外了)。
类似的可以用一些我们认为常用的设置替换既有的参数。这样做的缺点就是控制参数的字面含义与实际功能不太吻合,优点当然就是可以提供给用户更多更常用的图像设置。比如设置一个黑白,素描之类的图像的效果,玩玩抽象派视频聊天。

图5 图像效果设置

2011年8月11日星期四

给模块传递参数

对于如何向模块传递参数,Linux kernel 提供了一个简单的框架。其允许驱动程序声明参数,并且用户在系统启动或模块装载时为参数指定相应值,在驱动程序里,参数的用法如同全局变量。
使用下面的宏时需要包含头文件

通过宏module_param()定义一个模块参数:
module_param(name, type, perm);
name既是用户看到的参数名,又是模块内接受参数的变量;
type表示参数的数据类型,是下列之一:byte, short, ushort, int, uint, long, ulong, charp, bool, invbool;
perm指定了在sysfs中相应文件的访问权限。访问权限与linux文件爱你访问权限相同的方式管理,如0644,或使用stat.h中的宏如S_IRUGO表示。
0表示完全关闭在sysfs中相对应的项。

这些宏不会声明变量,因此在使用宏之前,必须声明变量,典型地用法如下:
static unsigned int int_var = 0;
module_param(int_var, uint, S_IRUGO);
这些必须写在模块源文件的开头部分。即int_var是全局的。也可以使模块源文件内部的变量名与外部的参数名有不同的名字,通过module_param_named()定义。module_param_named(name, variable, type, perm);其中name是外部可见的参数名,variable是源文件内部的全局变量名,而module_param通过module_param_named实现,只不过name与variable相同。
例如:
static unsigned int max_test = 9;
module_param_name(maximum_line_test, max_test, int, 0);

如果模块参数是一个字符串时,通常使用charp类型定义这个模块参数。内核复制用户提供的字符串到内存,并且相对应的变量指向这个字符串。
例如:
static char *name;
module_param(name, charp, 0);
另一种方法是通过宏module_param_string()让内核把字符串直接复制到程序中的字符数组内。
module_param_string(name, string, len, perm);
这里,name是外部的参数名,string是内部的变量名,len是以string命名的buffer大小(可以小于buffer的大小,但是没有意义),perm表示sysfs的访问权限(或者perm是零,表示完全关闭相对应的sysfs项)。
例如:
static char species[BUF_LEN];
module_param_string(specifies, species, BUF_LEN, 0);

如果需要传递多个参数可以通过宏module_param_array()实现。
module_param_array(name, type, nump, perm);
其中,name既是外部模块的参数名又是程序内部的变量名,type是数据类型,perm是sysfs的访问权限。指针nump指向一个整数,其值表示有多少个参数存放在数组name中。值得注意是name数组必须静态分配。
例如:
static int finsh[MAX_FISH];
static int nr_fish;
module_param_array(fish, int, &nr_fish, 0444); //最终传递数组元素个数存在nr_fish中
通过宏module_param_array_named()使得内部的数组名与外部的参数名有不同的名字。
例如:
module_param_array_named(name, array, type, nump, perm);

通过宏MODULE_PARM_DESC()对参数进行说明:
static unsigned short size = 1;
module_param(size, ushort, 0644);
MODULE_PARM_DESC(size, “The size in inches of the fishing pole”
“connected to this computer.” );


说明:from http://blog.csdn.net/iczyh/archive/2008/10/26/3149727.aspx
module_param() 和 module_param_array() 的作用就是让那些全局变量对 insmod 可见,使模块装载时可重新赋值。
module_param_array() 宏的第三个参数用来记录用户 insmod 时提供的给这个数组的元素个数,NULL 表示不关心用户提供的个数
module_param() 和 module_param_array() 最后一个参数权限值不能包含让普通用户也有写权限,否则编译报错。这点可参考 linux/moduleparam.h 中 __module_param_call() 宏的定义。
字符串数组中的字符串似乎不能包含逗号,否则一个字符串会被解析成两个

一个测试用例:parm_hello.c

#include
#include
#include

#define MAX_ARRAY 6

static int int_var = 0;
static const char *str_var = "default";
static int int_array[6];
int narr;

module_param(int_var, int, 0644);
MODULE_PARM_DESC(int_var, "A integer variable");

module_param(str_var, charp, 0644);
MODULE_PARM_DESC(str_var, "A string variable");

module_param_array(int_array, int, &narr, 0644);
MODULE_PARM_DESC(int_array, "A integer array");


static int __init hello_init(void)
{
int i;
printk(KERN_ALERT "Hello, my LKM.\n");
printk(KERN_ALERT "int_var %d.\n", int_var);
printk(KERN_ALERT "str_var %s.\n", str_var);

for(i = 0; i < narr; i ++){
printk("int_array[%d] = %d\n", i, int_array[i]);
}
return 0;
}

static void __exit hello_exit(void)
{
printk(KERN_ALERT "Bye, my LKM.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ydzhang");
MODULE_DEION("This module is a example.");


测试:insmod parm_hello.ko int_var=100 str_var=hello int_array=100,200

2010年3月19日星期五

《Essential Linux Device Drivers》第8章

第 8 章 I2 C 协议

I2 C ( The Inter-Integrated Circuit )及其 子集 SMBus ( System Management Bus )均为同步串行接口,普遍存在于桌面电脑和嵌入式设备中。 本章通过实现访问 I2 C EEPROM 和 I2 C RTC 的驱动实例,让我们理解内核如何支持 I2 C /SMBus 主机适配器和客户设备。在结束本章之前,让我们也浏览一下内核支持的两种其它的串行接口:串行外围接口( SPI )总线和 1-wire 总线。

所有这些串行接口( I2 C , SMBus , SPI 和 1-wire ),都有两个共同的特性:

· 交换的数据总量较少。

· 数据传输率较低。
I2 C/SMBus 是什么?

I2 C 是广泛用于桌面和笔记本电脑中的串行总线,用于处理器和一些外设之间的接口,这些外设包括 EEPROM 、音频编解码器 以及监控温度、供电电压等参数的专用芯片。此外, I2 C 也在嵌入式设备中大行其道,用于和 RTC ,智能电池电路、多路复用器,端口扩展卡,光收发器,以及其它类似设备之间的通信。由于 I2 C 被大量的微控制器所支持,在当前的市场上可找到大量便宜的 I2 C 设备。

I2 C 和 SMBus 为主-从协议,其通信双方为主机适配器(主控制器)和客户设备(从设备)。主机控制器在桌面电脑上通常为南桥芯片组的一部分;而在嵌入式设备上,通常为微控制器的一部分。图 8.1 显示了在 PC 兼容硬件上 I2 C 总线的例子。
图 8.1. PC 兼容硬件上 的 I2 C/SMBus.

I2 C 及其子集 SMBus 最初分别为 Philips 和 Intel 所开发,均为 2 线接口 。这 2 根线为时钟线和双向数据线,分别被称为串行时钟( Serial Clock , SCL )和串行数据( Serial Data , SDA )。由于 I2 C 总线仅需要一对总线,因此在电路板上占用了更少的空间。因此带来的问题是带宽较窄。 I2 C 在标准模式下支持最高 100Kbps 的传输率,在快速模式下最高可达 400Kbps (然而, SMBus 最高仅支持 100Kbps )。因此它们仅适用于慢速设备。即使 I2 C 支持双向数据交换,因为仅有一根数据线,故通信是半双工的。

I2 C 和 SMBus 设备使用 7 位地址。协议也支持 10 位地址,但很多设备仅响应 7 位地址,故在总线上最多有 127 个设备。源于协议的主-从特性,设备地址也称为从地址。
I2 C 核心

I2 C 核心由主机适配器驱动和客户驱动可利用的函数和数据结构组成。核心中的公共代码简化了驱动开发者的工作。核心也间接使客户驱动独立于主机适配器,以使客户设备即使用于采用不同 I2 C 主机适配器的电路板上,亦可保持客户驱动运行如常。核心层的此机制及其好处也可在内核中其它的很多设备驱动类中发现,如 PCMCIA , PCI 和 USB 等。

除了核心,内核的 I2 C 底层由如下组成:

· I2 C 主机适配器的设备驱动。属于总线驱动,通常由适配器驱动和算法( algorithm )驱动组成。前者利用后者和 I2 C 总线交互。

· I2C 客户设备的设备驱动。

· i2c-dev ,允许在用户模式下实现 I2 C 客户驱动。

你更可能的是实现客户驱动,而不是适配器或 algorithm 驱动,因为相比于 I2 C 主机适配器,有多得多的 I2C 设备。因此在本章中,我们将主要讨论客户驱动。

图 8.2 展示了 Linux 的 I2 C 子系统。它显示了 I2 C 内核模块和 I2 C 总线上的主机适配器和客户设备的交互。
图 8.2. Linux I2 C 子系统

由于 SMBus 是 I2 C 的子集,因此仅使用 SMBus 指令和你的设备交互的驱动可工作于 SMBus 和 I2 C 适配器。表 8.1 列出了 I2 C 核心提供的和 SMBus 兼容的数据传输流程。
表 8.1. I2C 核心提供的和 SMBus 兼容的数据 访问函数

函数


作用

i2c_smbus_read_byte()


从设备读取一个字节(不定义位置偏移,使用以前发起的命令的偏移)

i2c_smbus_write_byte()


从设备写入一个字节(使用以前发起的命令的偏移)

i2c_smbus_write_quick()


向设备发送一个比特 ( 取代清单 8.1 中的 Rd/Wr 位 ).

i2c_smbus_read_byte_data()


从设备指定偏移处读取一个字节

i2c_smbus_write_byte_data()


向设备指定偏移处写入一个字节

i2c_smbus_read_word_data()


从设备指定偏移处读取二个字节

i2c_smbus_write_word_data()


向设备指定偏移处写入二个字节

i2c_smbus_read_block_data()


从设备指定偏移处读取一块数据 .

i2c_smbus_write_block_data()


向设备指定偏移处写入一块数据 . (<= 32 字节 )
总线事务

在实现驱动例子之前,通过放大镜观察导线 ,让我们来更好的理解 I2 C 协议。清单 8.1 展示了和 I2 C EEPROM 交互的代码片断,以及在总线上发生的相应的事务。这些事务是在运行代码片断时通过相连的 I2 C 总线分析仪捕获的。这些代码使用的时用户模式的 I2 C 函数(在第 19 章 “ 用户空间的驱动 ” 中,我们将讨论更多的用户模式的 I2 C 编程)。
清单 8.1. I2 C 总线上的事务

Code View:

/* ... */

/*

* Connect to the EEPROM. 0x50 is the device address.

* smbus_fp is a file pointer into the SMBus device.

*/

ioctl(smbus_fp, 0x50, slave);



/* Write a byte (0xAB) at memory offset 0 on the EEPROM */

i2c_smbus_write_byte_data(smbus_fp, 0, 0xAB);



/*

* This is the corresponding transaction observed

* on the bus after the write:

* S 0x50 Wr [A] 0 [A] 0xAB [A] P

*

* S is the start bit, 0x50 is the 7-bit slave address (0101000b),

* Wr is the write command (0b), A is the Accept bit (or

* acknowledgment) received by the host from the slave, 0 is the

* address offset on the slave device where the byte is to be

* written, 0xAB is the data to be written, and P is the stop bit.

* The data enclosed within [] is sent from the slave to the

* host, while the rest of the bits are sent by the host to the

* slave.

*/

/* Read a byte from offset 0 on the EEPROM */

res = i2c_smbus_read_byte_data(smbus_fp, 0);



/*

* This is the corresponding transaction observed

* on the bus after the read:

* S 0x50 Wr [A] 0 [A] S 0x50 Rd [A] [0xAB] NA P

*

* The explanation of the bits is the same as before, except that

* Rd stands for the Read command (1b), 0xAB is the data received

* from the slave, and NA is the Reverse Accept bit (or the

* acknowledgment sent by the host to the slave).

*/






设备例子: EEPROM

我们的第一个客户驱动例子时 I2 C 总线上的 EEPROM 设置,如图 8.1 所示。几乎所有的笔记本和桌面电脑都有类似的 EEPROM ,用于存储 BIOS 配置信息。例子中的 EEPROM 有两个 memory bank 。相对应于每个 memory bank ,驱动提供 /dev 接口: /dev/eep/0 和 /dev/eep/1 。应用程序在这些节点上操作,和 EEPROM 交换数据。

每个 I2 C/SMBus 客户设备都分配有一个从地址,作为设备标识。例子中的 EEPROM 有两个从地址, SLAVE_ADDR1 和 SLAVE_ADDR2 ,分别对应于两个 memory bank 。

例子中驱动所使用的 I2 C 指令和 SMBus 兼容,因此它可以工作于 I2 C 和 SMBus EEPROM 。
初始化

正如所有的驱动类那样, I2 C 客户驱动也有 init() 入口点。初始化用于分配数据结构,向 I2 C 核心注册驱动,将 sysfs 和 Linux 设备模型联系在一起。这些在清单 8.2 中完成。
清单 8.2. 初始化 EEPROM 驱动

Code View:

/* Driver entry points */

static struct file_operations eep_fops = {

.owner = THIS_MODULE,

.llseek = eep_llseek,

.read = eep_read,

.ioctl = eep_ioctl,

.open = eep_open,

.release = eep_release,

.write = eep_write,

};



static dev_t dev_number; /* Allotted Device Number */

static struct class *eep_class; /* Device class */



/* Per-device client data structure for each

* memory bank supported by the driver

*/



struct eep_bank {

struct i2c_client *client; /* I2C client for this bank */

unsigned int addr; /* Slave address of this bank */

unsigned short current_pointer; /* File pointer */

int bank_number; /* Actual memory bank number */

/* ... */ /* Spinlocks, data cache for

slow devices,.. */

};



#define NUM_BANKS 2 /* Two supported banks */

#define BANK_SIZE 2048 /* Size of each bank */



struct ee_bank *ee_bank_list; /* List of private data

structures, one per bank */





/*

* Device Initialization

*/

int __init

eep_init(void)

{



int err, i;



/* Allocate the per-device data structure, ee_bank */

ee_bank_list = kmalloc(sizeof(struct ee_bank)*NUM_BANKS,

GFP_KERNEL);

memset(ee_bank_list, 0, sizeof(struct ee_bank)*NUM_BANKS);

/* Register and create the /dev interfaces to access the EEPROM

banks. Refer back to Chapter 5, "Character Drivers" for

more details */

if (alloc_chrdev_region(&dev_number, 0,

NUM_BANKS, "eep") < 0) {

printk(KERN_DEBUG "Can't register device\n");

return -1;

}



eep_class = class_create(THIS_MODULE, DEVICE_NAME);

for (i=0; i < NUM_BANKS;i++) {



/* Connect the file operations with cdev */

cdev_init(&ee_bank[i].cdev, &ee_fops);



/* Connect the major/minor number to the cdev */

if (cdev_add(&ee_bank[i].cdev, (dev_number + i), 1)) {

printk("Bad kmalloc\n");

return 1;

}

class_device_create(eep_class, NULL, (dev_number + i),

NULL, "eeprom%d", i);

}



/* Inform the I2C core about our existence. See the section

"Probing the Device" for the definition of eep_driver */

err = i2c_add_driver(&eep_driver);



if (err) {

printk("Registering I2C driver failed, errno is %d\n", err);

return err;

}



printk("EEPROM Driver Initialized.\n");

return 0;

}







清单 8.2 发起了设备节点的创建,但为了完成此过程,需要添加如下内容至 /etc/udev/rules.d/ 目录下合适的规则文件中:

KERNEL:"eeprom[0-1]*", NAME="eep/%n"

作为从内核收到的 uevent 的响应,将创建 /dev/eep/0 和 /dev/eep/1 。需要从第 n 个 memory bank 读取数据的用户模式程序可以操作 /dev/eep/n 来达到其目的。

清单 8.3 实现了 EEPROM 驱动的 open() 函数。当应用程序打开 /dev/eep/X 时,内核将调用 eep_open() 。 eep_open() 在私有区域中存储了每个设备相关的数据结构,因此可以从驱动中的其它函数中直接访问。
清单 8.3. 打开 EEPROM 驱动

int

eep_open(struct inode *inode, struct file *file)

{



/* The EEPROM bank to be opened */

n = MINOR(file->f_dentry->d_inode->i_rdev);



file->private_data = (struct ee_bank *)ee_bank_list[n];



/* Initialize the fields in ee_bank_list[n] such as

size, slave address, and the current file pointer */

/* ... */

}


探测设备

I2 C 客户驱动,在主机控制器驱动和 I2 C 核心的合作下,使其自身成为从设备,其过程如下:

1. 在初始化过程中,注册 probe() 方法。当相连的主机控制器被检测出, I2 C 核心将调用此方法。在清单 8.2 中, eep_init() 通过调用 i2c_add_driver() 注册 eep_probe() 。

static struct i2c_driver eep_driver =

{

.driver = {

.name = "EEP", /* Name */

},

.id = I2C_DRIVERID_EEP, /* ID */

.attach_adapter = eep_probe, /* Probe Method */

.detach_client = eep_detach, /* Detach Method */

};



i2c_add_driver(&eep_driver); `

设备标识符 I2C_DRIVERID_EEP ,对于每个设备应该是唯一的,并在 include/linux/i2c-id.h 中定义之。

2. 当 I2 C 核心调用客户驱动的 probe() 方法时,表明主机适配器已经存在。在 probe() 里将调用 i2c_probe() ,其参数为驱动所关联的从设备的地址,以及具体的探测函数 attach() 。

清单 8.4 实现了 EEPROM 驱动的 probe() 方法: eep_probe() 。 normal_i2c 指明了 EEPROM bank 的地址,它是 i2c_client_address_data 结构体的成员。此结构体中其它的成员能被用于更多的地址控制。你可以通过设置 ignore 字段要求 I2 C 核心忽略一段地址范围。 如果你想绑定一个从地址到一个特殊的主机适配器上,你也可以使用 probe 成员指定(适配器、从地址)对。对于某些场合,这样做有其用处。例如,你的处理器支持两个 I2 C 主机适配器,在总线 1 上 有一个 EEPROM ,总线 2 上有一个温度传感器,两个设备从地址相同。

3. 主机控制器在总线上搜索步骤 2 中指定的从设备。为此,它产生一个总线事务,例如 S SLAVE_ADDR Wr , S 是起始位, SLAVE_ADDR 是设备的数据手册中指定的 7bit 的从地址, Wr 是 “ 总线事务 ” 一节中所描述过的写命令。如果某个运行中的从设备存在于总线上,它将发送确认比特 ([A]) 加以回应。

4. 在步骤 3 中如果主机适配器检测到从设备, I2 C 核心会调用步骤 2 中在 i2c_probe() 的第三个参数中指定的 attach() 。对于 EEPROM 驱动,此例程为 eep_attach() ,它将注册和设备关联的客户数据结构,如清单 8.5 所示。如果你的设备需要初始的编程序列(例如,在数字视频接口( Digital Visual Interface , DVI )传输芯片开始工作之前,必须对它的寄存器进行初始化 ),可在此例程中完成这些操作。
清单 8.4. 探测 EEPROM Bank 的存在

#include



/* The EEPROM has two memory banks having addresses SLAVE_ADDR1

* and SLAVE_ADDR2, respectively

*/

static unsigned short normal_i2c[] = {

SLAVE_ADDR1, SLAVE_ADDR2, I2C_CLIENT_END

};



static struct i2c_client_address_data addr_data = {

.normal_i2c = normal_i2c,

.probe = ignore,

.ignore = ignore,

.forces = ignore,

};



static int

eep_probe(struct i2c_adapter *adapter)

{

/* The callback function eep_attach(), is shown

* in Listing 8.5

*/

return i2c_probe(adapter, &addr_data, eep_attach);

}


清单 8.5. Attaching a Client

int

eep_attach(struct i2c_adapter *adapter, int address, int kind)

{

static struct i2c_client *eep_client;



eep_client = kmalloc(sizeof(*eep_client), GFP_KERNEL);



eep_client->driver = &eep_driver; /* Registered in Listing 8.2 */

eep_client->addr = address; /* Detected Address */

eep_client->adapter = adapter; /* Host Adapter */

eep_client->flags = 0;

strlcpy(eep_client->name, "eep", I2C_NAME_SIZE);



/* Populate fields in the associated per-device data structure */

/* ... */



/* Attach */

i2c_attach_client(new_client);

}


检查适配器的功能

每个主机适配器的功能都有限。一个适配器可能不支持表 8.1 中包含的所有命令。例如,它可能支持 SMBus read_word 命令,但不支持 read_block 命令。客户驱动在使用这些命令前必须检查适配器是否对其提供支持。

I2 C 核心提供两个函数以完成此功能:

1. i2c_check_functionality() 检查某个特定的功能 是否被支持。

2. i2c_get_functionality() 返回包含所有被支持功能 的掩码。

在 include/linux/i2c.h 可看到所有可能支持 功能 的列表。
访问设备

为了从 EEPROM 读取数据,首先需要从与此设备节点关联的私有数据域中收集调用线程的信息。其次,使用 I2 C 核心提供的 SMBus 兼容的数据访问例程(表 8.1 显示了可用的函数)读取数据。最后,发送数据至用户空间,并增加内部文件指针,以便下一次的 read()/write() 操作可以从上一次结束处开始。这些步骤在清单 8.6 中完成,此清单忽略了通常的完整性和错误检查。
清单 8.6. 从 EEPROM 读取数据

Code View:

ssize_t

eep_read(struct file *file, char *buf,

size_t count, loff_t *ppos)

{

int i, transferred, ret, my_buf[BANK_SIZE];



/* Get the private client data structure for this bank */

struct ee_bank *my_bank =

(struct ee_bank *)file->private_data;



/* Check whether the smbus_read_word() functionality is

supported */

if (i2c_check_functionality(my_bank->client,

I2C_FUNC_SMBUS_READ_WORD_DATA)) {



/* Read the data */

while (transferred < count) {

ret = i2c_smbus_read_word_data(my_bank->client,

my_bank->current_pointer+i);

my_buf[i++] = (u8)(ret & 0xFF);

my_buf[i++] = (u8)(ret >> 8);

transferred += 2;

}



/* Copy data to user space and increment the internal

file pointer. Sanity checks are omitted for simplicity */

copy_to_user(buffer, (void *)my_buf, transferred);

my_bank->current_pointer += transferred;

}



return transferred;

}







写数据至设备与此类似,使用的是 i2c_smbus_write_XXX() 函数。

一些 EEPROM 芯片有 RFID ( Radio Frequency Identification )发送器,用于无线发送存储的信息。用于自动化供应链处理,例如货物监控和资产跟踪。这些 EEPROM 通常通过一个访问保护 bank 来控制对数据 bank 的安全访问。对于此类情况,为了能够操作数据 bank ,驱动还不得不对访问保护 bank 中的相应位进行处理。

为了从用户空间访问 EEPROM 块,需要开发应用程序以操作 /dev/eep/n 。为了读出 EEPROM 块的内容,需要做如下操作:

bash> od –a /dev/eep/0

0000000 S E R # dc4 ff soh R P nul nul nul nul nul nul nul

0000020 @ 1 3 R 1 1 5 3 Z J 1 V 1 L 4 6

0000040 5 1 0 H sp 1 S 2 8 8 8 7 J U 9 9

0000060 H 0 0 6 6 nul nul nul bs 3 8 L 5 0 0 3

0000100 Z J 1 N U B 4 6 8 6 V 7 nul nul nul nul

0000120 nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul

*

0000400

作为练习,试着去修改 EEPROM 驱动,创建 EEPROM 块的 /sys 接口,而不是 /dev 接口。你可以重用第 5 章清单 5.7“ 使用 Sysfs 控制并口 LED 电路板 ” 中的代码,帮助完成此工作。
其它函数

为了获得全功能的驱动,你需要添加剩余的入口点。这些和第 5 章所讨论的普通字符驱动差别不大,因此未提供代码清单:

· 为了支持 lseek() 系统调用,用于给内部文件指针赋以新值,需要实现 llseek() 函数。内部文件指针存储了有关 EEPROM 访问的状态信息。

· 为了验证数据的完整性, EEPROM 驱动可实现 ioctl() 函数,用于校准并验证存储的数据的校验和。

· EEPROM 中不需要 poll() 和 fsync() 方法。

· 如果你选择将驱动编译为一个模块,需要提供 exit() 方法,以注销 设备,并清理客户设备特定的数据结构。从 i2c 核心卸载驱动只需执行如下操作:


i2c_del_driver(&eep_driver);




设备例子:实时时钟

让我们选取通过 I2 C 总线和嵌入式控制器相连的 RTC 芯片的例子。连接框图见图 8.3 。
图 8.3. 嵌入式系统上的 I2 C RTC

设定 RTC 的 I2 C 从地址为 0x60 ,其寄存器空间组织如表 8.2 。

表 8.2. I2 C RTC 的寄存器分布

寄存器名


描述


偏移

RTC_HOUR_REG


小时计数


0x0

RTC_MINUTE_REG


分钟计数


0x1

RTC_SECOND_REG


秒钟计数


0x2

RTC_STATUS_REG


标志记中断状态


0x3

RTC_CONTROL_REG


使能/禁止 RTC


0x4

让我们的驱动基于前面讨论过的 EEPROM 驱动。我们将假设 I2 C 客户驱动结构、从设备注册和 II2 C 核心函数已经完成 ,仅实现和 RTC 通信的代码。

当 I2 C 核心检测到从地址为 0x60 的设备在 I2 C 总线上时,将调用 myrtc_attach() 。其调用序列类似于清单 8.5 中的 eep_attach() 。假定在 myrtc_attach() 中你必须完成如下的芯片初始化:

1. 将 RTC 状态寄存器( RTC_STATUS_REG )清零。

2. 通过设置 RTC 控制寄存器( RTC_CONTROL_REG )中的相应位,启动 RTC (如果它还未开始运行)。

为了完成以上功能,让我们构建一个 i2c_msg 结构体,使用 i2c_transfer() 在总线上产生 I2 C 事务。此传输机制为 I2 C 所独有,和 SMBus 并不兼容 。为了向前面讨论过的两个 RTC 寄存器写入数据,你必须构建两个 i2c_msg 消息。第一个消息设置寄存器偏移。在我们的例子中, RTC_STATUS_REG 的值为 3 。第二个消息携带希望写入指定偏移的字节数。在此例中,共两个字节,一个字节写入 RTC_STATUS_REG ,另一个写入 RTC_CONTROL_REG 。

Code View:

#include /* For struct i2c_msg */

int

myrtc_attach(struct i2c_adapter *adapter, int addr, int kind)

{

u8 buf[2];

int offset = RTC_STATUS_REG; /* Status register lives here */

struct i2c_msg rtc_msg[2];



/* Write 1 byte of offset information to the RTC */

rtc_msg[0].addr = addr; /* Slave address. In our case,

this is 0x60 */

rtc_msg[0].flags = I2C_M_WR; /* Write Command */

rtc_msg[0].buf = &offset; /* Register offset for

the next transaction */

rtc_msg[0].len = 1; /* Offset is 1 byte long */



/* Write 2 bytes of data (the contents of the status and

control registers) at the offset programmed by the previous

i2c_msg */

rtc_msg[1].addr = addr; /* Slave address */

rtc_msg[1].flags = I2C_M_WR; /* Write command */

rtc_msg[1].buf = &buf[0]; /* Data to be written to control

and status registers */

rtc_msg[1].len = 2; /* Two register values */

buf[0] = 0; /* Zero out the status register */

buf[1] |= ENABLE_RTC; /* Turn on control register bits

that start the RTC */



/* Generate bus transactions corresponding to the two messages */

i2c_transfer(adapter, rtc_msg, 2);



/* ... */

printk("My RTC Initialized\n");

}





因为 RTC 已经被初始化,并开始计时了,你可以通过读取 RTC_HOUR_REG , RTC_MINUTE_REG 和 RTC_SECOND_REG 来获取当前的时间。其操作如下:

Code View:

#include /* For struct rtc_time */

int

myrtc_gettime(struct i2c_client *client, struct rtc_time *r_t)

{

u8 buf[3]; /* Space to carry hour/minute/second */

int offset = 0; /* Time-keeping registers start at offset 0 */

struct i2c_msg rtc_msg[2];



/* Write 1 byte of offset information to the RTC */

rtc_msg[0].addr = addr; /* Slave address */

rtc_msg[0].flags = 0; /* Write Command */

rtc_msg[0].buf = &offset; /* Register offset for

the next transaction */

rtc_msg[0].len = 1; /* Offset is 1 byte long */



/* Read current time by getting 3 bytes of data from offset 0

(i.e., from RTC_HOUR_REG, RTC_MINUTE_REG, and RTC_SECOND_REG) */

rtc_msg[1].addr = addr; /* Slave address */

rtc_msg[1].flags = I2C_M_RD; /* Read command */

rtc_msg[1].buf = &buf[0]; /* Data to be read from hour, minute

and second registers */

rtc_msg[1].len = 3; /* Three registers to read */



/* Generate bus transactions corresponding to the above

two messages */

i2c_transfer(adapter, rtc_msg, 2);

/* Read the time */

r_t->tm_hour = BCD2BIN(buf[0]); /* Hour */

r_t->tm_min = BCD2BIN(buf[1]); /* Minute */

r_t->tm_sec = BCD2BIN(buf[2]); /* Second */

return(0);

}







myrtc_gettime() 实现了总线相关的 RTC 驱动的底层部分。 RTC 驱动的顶层部分应该和内核的 RTC API 保持一致,如第 5 章的 “RTC 子系统 ” 一节中所讨论的。此机制的好处是不管你的 RTC 是位于 PC 的南桥内部,还是如本例一样,位于嵌入式控制器的外部,应用程序可以不加改变而运行。

RTC 通常用 BCD ( Binary Coded Decimal )格式存储时间,每组位元( 4 位)表示 0 ~ 9 之间的数,而不是 0 ~ 15 。内核提供了宏 BCD2BIN() 用于将 BCD 码变换成十进制数,以及宏 BIN2BCD() 用于相反的操作。当从 RTC 寄存器读取数据时, myrtc_gettime() 利用了宏 BCD2BIN() 用于转换。

drivers/rtc/rtc-ds1307.c 提供了 RTC 驱动的实例,用于处理 Dallas/Maxim DS13XX 系列 I2 C 芯片。

作为 2 线总线, I2 C 总线没有从设备用于中断请求的信号线,但一些 I2 C 主机适配器可以中断 CPU ,触发数据传输请求。然而,此中断驱动操作对于 I2C 客户驱动 是透明的,隐藏于 I2 C 核心提供的服务例程里。假设图 8.3 中 I2 C 主机控制器是嵌入式 SoC 的一部分,并有中断 CPU 的能力, myrtc_attach() 里对 i2c_transfer() 的调用 将完成如下操作:

· 构建对应于 rtc_msg[0] 的事务,并使用主机控制器驱动提供的服务例程写入总线。

· 等待直到主机控制器触发发送结束中断,表明 rtc_msg[0] 已经在信号线上。

· 在中断处理例程里,查看 I2 C 主机控制器状态寄存器,判断是否从 RTC 从设备里接收到确认信号。

· 如果主机控制器的状态和控制寄存器并非全部正确,则返回错误。

· 对于 rtc_msg[1] 重复同样过程。
I2C-dev

有时,当你需要支持大量慢速的 I2 C 设备时,从用户空间对所有这些设备进行驱动就很有必要了。 I2 C 层支持 i2c-dev 驱动以达到此目的。第 19 章的 “ 用户模式 I2C ” 中,有使用 i2c-dev 实现用户模式 I2 C 驱动的例子。
使用 LM-Sensors 监控硬件

LM-Sensors 项目,主页为 www.lm-sensors.org ,使 Linux 具有硬件监控能力。很多计算机系统使用传感器芯片来监控诸如温度、供电电压以及风扇转速等参数。周期性的检查这些参数是非常重要的。损坏了的 CPU 风扇可能会导致随机、异常的软件问题。如果是医疗设备系统出现故障,其后果将难以想象!

LM-Sensors 利用传感器芯片的设备驱动来排除故障。它利用 sensors 程序产生状态报告, sensors-detect 脚本检查你的系统,并帮助你产生相应的配置文件。

大多数芯片利用 I2 C/SMBus 总线方式向 CPU 提供硬件监控接口。这些设备驱动是 I2 C 客户驱动,但位于 drivers/hwmon/ 目录,而不是 drivers/i2c/chips/ 。具体例子可见 National Semiconductor 公司的 LM87 芯片,它能监控电压、温度和风扇。 drivers/hwmon/lm87.c 为其驱动的具体实现。 I2 C 驱动 ID 号从 1000 到 1999 都保留给了传感器芯片(参见 include/linux/i2c-id.h )。

也有几个传感器芯片和 CPU 之间的接口采用 ISA/LPC 总线,而不是 I2 C/SMBus 。其它输出模拟量的通过模数转换器( ADC )传送给 CPU 。这些芯片的驱动和 I2 C 总线传感器驱动一起都位于 drivers/hwmon/ 目录。非 I2 C 总线传感器驱动的例子是 drivers/hwmon/hdaps.c ,它是加速度传感器驱动,出现在某些 IBM/ 联想的笔记本电脑里,我们在第 7 章 “ 输入驱动 ” 中讨论过。另一个非 I2 C 总线的传感器的例子是 Winbond 83627HF 超级 I/O 芯片,由 drivers/hwmon/w83627hf.c 驱动。

串行外设接口总线( SPI )

串行外设接口( Serial Peripheral Interface , SPI )总线和 I2 C 类似,也是串行的主-从接口,集成于很多微控制器内部。和 I2 C 使用 2 线相比,它使用 4 线:串行时钟( Serial CLocK , SCLK ),片选( Chip Select , CS ) , 主设备输出从设备输入( Master Out Slave In , MOSI ),主设备输入从设备输出( Master In Slave Out , MISO )。 MOSI 用于传送数据至从设备, MISO 用于从从设备读出数据。和 I2 C 不同,由于 SPI 总线有专用的数据线用于数据的发送和接收,因此可以工作于全双工。 SPI 的典型速度为几 MHz ,不像 I2C 为几十~几百 KHz ,因此 SPI 吞吐量大得多。

当前市面上可找到的 SPI 外设包括 RF 芯片、智能卡接口、 EEPROM 、 RTC 、触摸传感器、以及 ACD 。

内核提供了一个核心 API 用于通过 SPI 总线交换信息。典型的 SPI 客户驱动如下:

1. 向 SPI 核心注册 probe() 、 remove() 方法。 suspend() 和 resume() 方法可选。

#include



static struct spi_driver myspi_driver = {

.driver = {

.name = "myspi",

.bus = &spi_bus_type,

.owner = THIS_MODULE,

},

.probe = myspidevice_probe,

.remove = __devexit_p(myspidevice_remove),

}



spi_register_driver(&myspi_driver);

SPI 核心创建对应于此设备的 spi_device 结构体,当调用注册的驱动方法时,用作调用参数。

1. 使用函数如 spi_sync() 和 spi_async() 和 SPI 设备交换数据。前者等待操作完成,后者当数据传输完成时,异步触发对注册的回调程序的调用。这些数据访问例程被从适当的地方调用,如 SPI 中断处理程序, sysfs 方法,或者定时器处理程序。下面的代码片断演示了 SPI 数据的传输:

#include



struct spi_device *spi; /* Representation of a

SPI device */

struct spi_transfer xfer; /* Contains transfer buffer

details */

struct spi_message sm; /* Sequence of spi_transfer

segments */

u8 *command_buffer; /* Data to be transferred */

int len; /* Length of data to be

transferred */



spi_message_init(&sm); /* Initialize spi_message */

xfer.tx_buf = command_buffer; /* Device-specific data */

xfer.len = len; /* Data length */

spi_message_add_tail(&xfer, &sm); /* Add the message */

spi_sync(spi, &sm); /* Blocking transfer request */

作为 SPI 设备的例子,我们可参考第 7 章简单讨论过的触摸屏控制器 ADS7846 。其驱动完成如下操作:

1. 使用 spi_register_driver() 向 SPI 核心注册 probe() , remove() , suspend() 和 resume() 方法。
2. probe() 方法使用 input_register_device() 向输入子系统注册驱动,并使用 request_irq() 请求中断 。
3. 驱动从其中断服务程序中使用 spi_async() 收集触摸坐标。此函数当数据传输完成时,触发对注册的回调程序的调用。
4. 如第 7 章所讨论的,回调函数通过输入事件接口 /dev/input/eventX ,使用 input_report_abs() 和 input_report_key() ,依次报告触摸坐标和点击。诸如 X Windows 和 gpm 这些程序和事件接口紧密合作,响应触摸输入。

通过软件的方式 控制 I/O 引脚,使其符合某种协议进行交互的驱动称为 bit-banging 驱动。 SPI bit-banging 驱动的例子,可参考 drivers/spi/spi_butterfly.c ,它是用于和 Atmel 公司 AVR 处理器系列 Butterfly 板上的 DataFlash 芯片交互的驱动。将你的主机的并口和 AVR Butterfly 连接在一起,使用专用的 dongle 和 spi_butterfly 可以进行 bit-banging 操作。 Documentation/spi/butterfly 提供了关于此驱动更详细的描述。

当前没有类似于 i2c-dev 的、针对用户空间的 SPI 驱动。你只能编写内核驱动和 SPI 设备交互。

在嵌入式系统中,你可能会碰到处理器和集成各种功能的协处理器一起工作的解决方案。譬如,飞思卡尔的电源管理和音频组件( Power Management and Audio Component , PMAC )芯片 MC13783 和基于 ARM9 的 i.MX27 控制器协同工作就是这样的一个例子。 PMAC 集成了 RTC ,电池充电器,触摸屏接口, ADC 模块和音频编码。处理器和 PMAC 之间通过 SPI 通信。 SPI 总线不含中断线,通过配置 GPIO 管脚, PMAC 可以从外部中断处理器。
1-Wire 总线

由 Dallas/Maxim 开发的 1-wire 协议使用 1-wire (或 w1 )总线传送电源和信号。地回路通过其它途径解决。它提供了和慢速设备之间接口的简单途径,减少了空间、费用以及复杂性。使用此协议的设备实例是 ibutton ( www.ibutton.com ),用于感知温度,传送数据,或保存独特的 ID 号。

另一通过单一的引脚提供接口的 w1 芯片是 Dallas/Maxim 的 DS2433 ,它是容量为 4kb 的 1-wire EEPROM 。此芯片的驱动位于 drivers/w1/slaves/w1_ds2433.c ,通过 sysfs 节点提供对 EEPROM 的访问。

和 w1 设备驱动相关的主要数据结构是 w1_family 和 w1_family_ops ,都定义于 w1_family.h 中。
调试

为了收集 I2 C 的调试信息,在内核的配置菜单的 Device Drivers-> I2C Support 下,选中 I2C Core debugging messages , I2C Algorithm debugging messages , I2C Bus debugging messages 和 I2C Chip debugging messages 。类似的,为了调试 SPI ,需要在 Device Drivers->SPI Support 下选中 Debug Support for SPI drivers 。

为了理解总线上 I2 C 包的数据流, 可在运行清单 8.1 时, 将 I2 C 总线分析仪和你的 电路板板连接在一起。 lm-snesor 包包括 i2cdump 工具,用于输出 I2 C 总线上设备的寄存器中的内容。

Linux I2 C 的邮件列表位于: http://lists.lm-sensors.org/mailman/listinfo/i2c .
查看源代码

在 2.4 版本的源码树中,所有 I2 C/SMBus 相关的源码包含在一个单独的目录 drivers/i2c/ 中, 2.6 版本内核中, I2 C 代码被分层次的组织: drivers/i2c/busses/ 目录包括适配器驱动, drivers/i2c/algos/ 目录包含 algorithm 驱动, drivers/i2c/chips/ 目录包含 客户 驱动。你也可以在源码树中其它的地方发现客户驱动。例如, drivers/sound/ 目录包含使用 I2 C 接口的音频芯片组的驱动。在 Documentation/i2c/ 目录下可找到提示以及更多的例子。

内核的 SPI 服务函数位于 drivers/spi/spi.c 。 ADS7846 触摸控制器的 SPI 驱动由 drivers/input/touchscreen/ads7846.c 实现。第 17 章 “ 内存技术设备 ” 讨论的 MTD 子系统实现了 SPI flash 芯片驱动。其例子为 drivers/mtd/devices/mtd_dataflash.c ,它实现了访问 Atmel 的 DataFlash SPI 芯片的驱动。

drivers/w1/ 目录包含了内核对 w1 协议的支持。 w1 接口主机控制器的驱动位于 drivers/w1/masters/ , w1 从设备驱动在 drivers/w1/slaves/ 。

表 8.3 概括了本章使用的主要数据结构,及其在内核源码树中的位置。表 8.4 列出了本章所用到的主要内核编程接口,以及它定义的位置。

表 8.3. 数据机构概述

数据结构


位置


描述

i2c_driver


include/linux/i2c.h


代表一个 I2 C 驱动

i2c_client_address_data


include/linux/i2c.h


I2 C 客户驱动所负责 的从地址

i2c_client


include/linux/i2c.h


用于标识一个连接到 I2 C 总线上的芯片

i2c_msg


include/linux/i2c.h




描述在 I2 C 总线上欲产生的一次传输事务

spi_driver


include/linux/spi/spi.h


代表一个 SPI 驱动

spi_device


include/linux/spi/spi.h


代表一个 SPI 设备

spi_transfer


include/linux/spi/spi.h




SPI 传输缓冲区的细节

spi_message


include/linux/spi/spi.h




spi_transfer 分段序列

w1_family


drivers/w1/w1_family.h


代表 w1 从驱动

w1_family_ops


drivers/w1/w1_family.h


w1 从驱动入口点



表 8.4. 内核编程接口概述

内核接口


位置


描述

i2c_add_driver()


include/linux/i2c.h drivers/i2c/i2c-core.c


向 I2C 核心注册驱动入口点

i2c_del_driver()


drivers/i2c/i2c-core.c


从 I2C 核心移除驱动

i2c_probe()


drivers/i2c/i2c-core.c




定义驱动所负责的从设备地址,如果 i2c 核心探测到某一地址,对应的 attach() 函数将调用

i2c_attach_client()


drivers/i2c/i2c-core.c




向相应主机适配器所服务的客户列表增加一个客户

i2c_detach_client()


drivers/i2c/i2c-core.c


Detach 一个活动的客户 . 通常在客户驱动或关联的主机适配器注销时进行

i2c_check_functionality()


include/linux/i2c.h




验证主机适配器是否支持某功能

i2c_get_functionality()


include/linux/i2c.h




获得主机适配器所支持的所有功能的掩码

i2c_add_adapter()


drivers/i2c/i2c-core.c


注册主机适配器。

i2c_del_adapter()


drivers/i2c/i2c-core.c


注销主机适配器。

SMBus-compatible I2C data access routines


drivers/i2c/i2c-core.c


见表 8.1

i2c_transfer()


drivers/i2c/i2c-core.c


通过 I2C 总线发送 i2c_msg 。此函数和 SMBus 不兼容。

spi_register_driver()


drivers/spi/spi.c


向 SPI 核心注册驱动入口点。

spi_unregister_driver()


include/linux/spi/spi.h


注销 SPI 驱动。

spi_message_init()


include/linux/spi/spi.h


初始化 SPI message.

spi_message_add_tail()


include/linux/spi/spi.h


添加一条 SPI 消息到传输列表。

spi_sync()


drivers/spi/spi.c


通过 SPI 总线同步传输数据。此函数阻塞直至完成。

spi_async()


include/linux/spi/spi.h


使用完成回调机制,通过 SPI 总线异步传输数据。

《Essential Linux Device Drivers》第7章

第7章 输入设备驱动

内核的输入子系统是为了对分散的、多种不同类别的输入设备(如键盘、鼠标、跟踪球、操纵杆、辊轮、触摸屏、加速计和手写板)进行统一处理的驱动。输入子系统带来了如下好处:

· 统一了物理形态各异的相似的输入设备的处理功能。例如,各种鼠标,不论PS/2、USB,还是蓝牙,都被同样处理。

· 提供了用于分发输入报告给用户应用程序的简单的事件(event)接口。你的驱动不必创建、管理/dev节点以及相关的访问方法。因此它能很方便的调用输入API以发送鼠标移动、键盘按键,或触摸事件给用户空间。X Windows这样的应用程序能够无缝地运行于输入子系统提供的event接口之上。

· 抽取出了输入驱动的通用部分,简化了驱动,并提供了一致性。例如,输入子系统提供了一个底层驱动(成为serio)的集合,支持对串口和键盘控制器等硬件输入设备的访问。

图7.1展示了输入子系统的操作。此子系统包括一前一后运行的两类驱动:事件驱动和设备驱动。事件驱动负责和应用程序的接口,而设备驱动负责和底层输入设备的通信。鼠标事件产生者mousedev,是前者的实例;而PS/2鼠标驱动是后者的实例。事件驱动和设备驱动都可以利用输入子系统的高效、可重用的核心提供的服务。
图 7.1. 输入子系统

事件驱动是标准的,对所有的输入类都是可用的,所以你更可能的是实现设备驱动而不是事件驱动。你的设备驱动可以利用一个已经存在的、合适的事件驱动通过输入核心和用户应用程序接口。需要注意的是本章使用的名辞“设备驱动”指的是输入设备驱动,而不是输入事件驱动。
输入事件驱动

输入子系统提供的事件接口已经发展成为很多图形窗口系统理解的标准。事件驱动提供一个硬件无关的抽象,以和输入设备交互;如同帧缓冲接口(在第12章《视频设备驱动》中讨论)提供一个通用的机制以和显示设备通信一样。事件驱动和帧缓冲驱动一起,将图形用户接口(GUI)和各种各样的底层硬件隔离开来。
Evdev接口

Evdev是一个通用的输入事件驱动。Evdev产生的每个事件包都有如下格式,定义于include/linux/input.h:

struct input_event {

struct timeval time; /* Timestamp */

__u16 type; /* Event Type */

__u16 code; /* Event Code */

__s32 value; /* Event Value */

};

为了学习如何使用evdev,让我们来实现一个虚拟鼠标的输入设备驱动。
设备例子:虚拟鼠标

我们的虚拟鼠标工作过程如下:一个应用程序(coord.c)模拟鼠标移动,并通过一个sysfs节点/sys/devices/platform/vms/coordinates分发坐标信息给虚拟鼠标驱动(vms.c)。虚拟鼠标驱动(vms驱动)通过evdev向上层传送这些移动信息。图7.2展示了详细过程:
图 7.2. 虚拟鼠标的输入驱动



通用目的鼠标(gpm)是服务器,让你在文本模式下使用鼠标,而无需X服务器。Gpm能够理解evdev消息,因此vms驱动能够直接和其通信。一切就绪后,你将会看到随着由coord.c产生虚拟鼠标移动,光标在屏幕上跳动。

清单7.1包含coord.c,它连续产生随机的X和Y坐标。鼠标与操纵杆或触摸屏不同,它产生的是相对坐标[MS1] 而非绝对坐标,这就是coord.c所做的工作。vms驱动在清单7.2中。
清单 7.1. 模拟虚拟移动的应用程序(coord.c)

Code View:

#include



int

main(int argc, char *argv[])

{

int sim_fd;

int x, y;

char buffer[10];



/* Open the sysfs coordinate node */

sim_fd = open("/sys/devices/platform/vms/coordinates", O_RDWR);

if (sim_fd < 0) {

perror("Couldn't open vms coordinate file\n");

exit(-1);

}

while (1) {

/* Generate random relative coordinates */

x = random()%20;

y = random()%20;

if (x%2) x = -x; if (y%2) y = -y;



/* Convey simulated coordinates to the virtual mouse driver */

sprintf(buffer, "%d %d %d", x, y, 0);

write(sim_fd, buffer, strlen(buffer));

fsync(sim_fd);

sleep(1);

}



close(sim_fd);

}






清单 7.2. Input Driver for the Virtual Mouse (vms.c)

Code View:

#include

#include

#include

#include

#include



struct input_dev *vms_input_dev; /* Representation of an input device */

static struct platform_device *vms_dev; /* Device structure */



/* Sysfs method to input simulated

coordinates to the virtual

mouse driver */

static ssize_t

write_vms(struct device *dev,

struct device_attribute *attr,

const char *buffer, size_t count)

{

int x,y;

sscanf(buffer, "%d%d", &x, &y);

/* Report relative coordinates via the

event interface */

input_report_rel(vms_input_dev, REL_X, x);

input_report_rel(vms_input_dev, REL_Y, y);

input_sync(vms_input_dev);



return count;

}



/* Attach the sysfs write method */

DEVICE_ATTR(coordinates, 0644, NULL, write_vms);



/* Attribute Descriptor */

static struct attribute *vms_attrs[] = {

&dev_attr_coordinates.attr,

NULL

};



/* Attribute group */

static struct attribute_group vms_attr_group = {

.attrs = vms_attrs,

};



/* Driver Initialization */

int __init

vms_init(void)

{



/* Register a platform device */

vms_dev = platform_device_register_simple("vms", -1, NULL, 0);

if (IS_ERR(vms_dev)) {

PTR_ERR(vms_dev);

printk("vms_init: error\n");

}



/* Create a sysfs node to read simulated coordinates */

sysfs_create_group(&vms_dev->dev.kobj, &vms_attr_group);



/* Allocate an input device data structure */

vms_input_dev = input_allocate_device();

if (!vms_input_dev) {

printk("Bad input_alloc_device()\n");

}



/* Announce that the virtual mouse will generate

relative coordinates */

set_bit(EV_REL, vms_input_dev->evbit);

set_bit(REL_X, vms_input_dev->relbit);

set_bit(REL_Y, vms_input_dev->relbit);



/* Register with the input subsystem */

input_register_device(vms_input_dev);



printk("Virtual Mouse Driver Initialized.\n");

return 0;

}



/* Driver Exit */

void

vms_cleanup(void)

{



/* Unregister from the input subsystem */

input_unregister_device(vms_input_dev);



/* Cleanup sysfs node */

sysfs_remove_group(&vms_dev->dev.kobj, &vms_attr_group);



/* Unregister driver */

platform_device_unregister(vms_dev);



return;

}



module_init(vms_init);

module_exit(vms_cleanup);







让我们仔细阅读清单7.2中的代码。初始化期间,vms驱动注册自身为输入设备驱动。为此,它首先使用内核API input_allocate_device()分配input_dev结构:

vms_input_dev = input_allocate_device();



然后,声明虚拟鼠标产生相对性事件:

set_bit(EV_REL, vms_input_dev->evbit); /* 事件类型为EV_REL */

下一步,声明虚拟鼠标产生的事件的编码:

set_bit(REL_X, vms_input_dev->relbit); /* Relative 'X' movement */

set_bit(REL_Y, vms_input_dev->relbit); /* Relative 'Y' movement */

如果你的虚拟鼠标也能产生按钮点击事件,还需要将其加入vms_init():

set_bit(EV_KEY, vms_input_dev->evbit); /* Event Type is EV_KEY */

set_bit(BTN_0, vms_input_dev->keybit); /* Event Code is BTN_0 */

最后,进行注册:

input_register_device(vms_input_dev);

write_vms()是sysfs中的store()方法,和/sys/devices/platform/vms/coordinates相关联。当coord.c写入X/Y坐标对进此文件时,write_vms()做如下操作:

input_report_rel(vms_input_dev, REL_X, x);

input_report_rel(vms_input_dev, REL_Y, y);

input_sync(vms_input_dev);

第一条语句会产生REL_X事件,或设备在X方向的相对移动。第二条语句产生REL_Y事件,或设备在Y方向的相对移动。input_sync()表明此事件已经完成,因此输入子系统将这两个事件组成一个evdev包,并通过/dev/input/eventX发送出去,eventX中的X是分配给vms驱动的接口序号。读该文件的应用程序将以前面描述的input_event格式接收事件包。为了将gpm关联至此事件接口,从而追逐光标的跳动,需要做如下操作:

bash> gpm -m /dev/input/eventX -t evdev

在“触摸控制器”和后面的“加速度传感器”章节中讨论的ADS7846触摸控制器驱动以及加速度传感器驱动,也都使用了evdev。
更多的事件接口

vms驱动利用通用的evdev事件接口,但像键盘、鼠标和触摸屏这些输入设备必须使用定制的事件驱动。在讨论相应的设备驱动时,我们将一一学习。

为了编写你自己的事件驱动,并通过/dev/input/mydev提供给用户空间,你必须利用input_handler结构体,并将其向输入核心注册:

Code View:

static struct input_handler my_event_handler = {

.event = mydev_event, /* Handle event reports sent by

input device drivers that use

this event driver's services */

.fops = &mydev_fops, /* Methods to manage

/dev/input/mydev */

.minor = MYDEV_MINOR_BASE, /* Minor number of

/dev/input/mydev */

.name = "mydev", /* Event driver name */

.id_table = mydev_ids, /* This event driver can handle

requests from these IDs */

.connect = mydev_connect, /* Invoked if there is an

ID match */

.disconnect = mydev_disconnect, /* Called when the driver unregisters

*/

};



/* Driver Initialization */

static int __init

mydev_init(void)

{

/* ... */



input_register_handler(&my_event_handler);



/* ... */

return 0;

}







mousedev(drivers/input/mousedev.c)的具体实现中可以看到完整的例子。
输入设备驱动

下面,让我们将注意转向键盘、鼠标以及触摸屏这些通用输入设备的驱动上。但首先,让我们快速浏览一下那些输入驱动可以利用的、现成的硬件访问方法。
Serio

Serio层提供了访问老式输入硬件(如i8042兼容键盘控制器和串口)的库例程。PS/2键盘和鼠标与前者相连,串行触摸控制器和后者连接在一起。为了与serio提供服务的硬件通信,如发送命令给PS/2鼠标,需要用serio_register_driver()向serio注册规定的回调例程。

为了添加新的驱动作为serio的一部分,需要用serio_register_port()注册open()/close()/start()/stop()/write()入口函数。drivers/input/serio/serport.c中可以看到具体的例子。

正如你在图7.1中所看到的,serio仅仅是访问底层硬件的一条路径。有些输入设备驱动依赖的是底层的总线层的支持,例如USB和SPI。
键盘

键盘多种多样――从老式的PS/2,到USB,蓝牙以及红外等。每种类型都有特定的输入设备驱动,但所有的都使用相同的键盘事件驱动,以确保提供给用户一致的接口。然而,和其它的事件驱动相比,键盘事件驱动有其独特之处:它传送数据给另一个内核子系统(tty层),而不是通过/dev节点传送给用户空间。
PC键盘

PC键盘(也成为PS/2键盘或AT键盘)通过i8042兼容键盘控制器与处理器接口。桌上电脑通常由专用的键盘控制器,而笔记本电脑的键盘接口由通用嵌入式控制器负责(参见第20章“嵌入式控制器”中的“更多的设备及驱动”)。当你在PC键盘上按下一个键时,会产生以下步骤:

1. 键盘控制器(或嵌入式控制器)扫描键盘矩阵,译码,并做按键去抖处理。

2. 键盘设备驱动在serio的帮助下,针对每个键的按下与释放,从键盘控制器读取原始的扫描码。按下与释放之间的区别在最高位,在释放时最高位被置位。例如,当“a”键被按下、释放后,会产生一对扫描码:0x1e和0x9e。专用键用0xE0做转义,因此按下、释放右箭头键,产生的序列为(0xE0 0x4D 0xE0 0xCD)。你可以使用showkey工具观察控制器发出的扫描码(跟在->符号后面的是对前面内容的解释):

bash> showkey -s

kb mode was UNICODE

[ if you are trying this under X, it might not work since

the X server is also reading /dev/console ]



press any key (program terminates 10s after last

keypress)...

[MS2] ...

0x1e 0x9e -> "a" 键按下与松开

3. 基于输入模式,键盘设备驱动转换接收到的扫描码为键值。为了查看和“a”键对应的键值:

bash> showkey

...

keycode 30 press -> A push of the "a" key

keycode 30 release -> Release of the "a" key

为了向上报告键值,驱动产生一个输入事件,将控制权交给键盘事件驱动。

4. 根据加载的键盘映射,键盘事件驱动进行键值翻译。(查看loadkeys的man帮助,以及在/lib/kbd/keymaps中提供的map文件)。它检查翻译后的键值是否和虚拟控制台或系统重启等相联系。添加如下代码至键盘事件驱动(drivers/char/keyboard.c)的Ctrl+Alt+Del处理程序,可将Ctrl+Alt+Del的行为设置为点亮CAPSLOCK和NUMLOCK灯,而不是重启系统,

static void fn_boot_it(struct vc_data *vc,



struct pt_regs *regs)

{

+ set_vc_kbd_led(kbd, VC_CAPSLOCK);

+ set_vc_kbd_led(kbd, VC_NUMLOCK);

- ctrl_alt_del();

}

5. 对于一般的键,译码后得到的键值被送至相联的虚拟终端,以及N_TTY线路规程(在第6章“串行驱动”中讨论过虚拟终端与线路规程)。这些由drivers/char/keyboard.c中的代码完成:

/* Add the keycode to flip buffer */

tty_insert_flip_char(tty, keycode, 0);

/* Schedule */

con_schedule_flip(tty);

N_TTY线路规程处理从键盘接收的输入,回送至虚拟控制台,以使用户空间的应用程序从与虚拟控制台相连的/dev/ttyX节点读取字符。

图7.3显示了从你按下键盘开始,到回显至虚拟控制台的整个过程的数据流。图的左半部是和特定硬件相关的,右半部是通用的。按照输入子系统的设计目的,底层硬件接口对键盘事件驱动和tty层是透明的。输入核心和定义明确的事件接口将input用户程序和复杂的硬件隔离开来。
图 7.3. PS/2兼容键盘的数据流

USB与蓝牙键盘

USB规范中有关人机接口设备(HID)的部分规定了USB键盘、鼠标、小键盘(Keypad)以及其他输入外设使用的通信协议。在Linux上,它们是通过usbhid USB客户端驱动来实现的,它负责USB HID类(0x03)设备。Usbhid注册自身作为输入设备驱动。它和输入API保持一致,并报告输入事件至相连的HID。

为了理解USB键盘的代码流,回到图7.3中,并修改左半部中的硬件相关部分。将输入硬件框中的键盘控制器用USB控制器代替,serio用USB核心层代替,输入设备驱动框用usbhid驱动代替即可。

对于蓝牙键盘,在图7.3中将键盘控制器用蓝牙芯片代替,serio用蓝牙核心层代替,输入设备驱动框用蓝牙hidp驱动代替。

USB和蓝牙在第11章“通用串行总线”和第16章“无线Linux”中分别详细讨论。
鼠标

类似于键盘,鼠标接口选项多种多样。让我们从通用的看起。
PS/2鼠标

鼠标在X和Y坐标上产生相对移动,有一个或多个按钮,一些还有滚轮(scroll wheel)。PS/2兼容的老式鼠标依赖于serio层和底层的控制器交互。鼠标的输入事件驱动称为mousedev,通过/dev/input/mice报告鼠标事件给用户应用程序。
设备例子:辊轮鼠标(Roller Mouse)

为了感受真实的鼠标设备驱动,让我们将第4章“打下基础”中所讨论的辊轮(roller wheel)变换为通用PS/2鼠标的变种。“辊轮鼠标(roller mouse)”在Y轴产生一维的移动。辊轮(wheel)顺时针和逆时针的旋转分别产生正的和负的相对Y坐标(类似鼠标上的滚轮scroll wheel),按下辊轮则会产生鼠标左按钮事件。辊轮鼠标对于在智能手机、手持设备以及音乐播放器等设备上操作菜单是理想的选择。

辊轮鼠标设备驱动实现于清单7.3,工作于像X Windows这样的窗口系统。查看roller_mouse_init()可清楚此驱动如何实现类似鼠标的功能。不像第4章清单4.1中的辊轮驱动,辊轮鼠标驱动不需要read()和poll()方法,因为这些事件通过输入API来报告。辊轮中断处理例程roller_isr()也做了相应的改变。中断处理例程中的事务管理使用了一个等待队列,一个自旋锁,以及store_movement()例程用于支持read()和poll()。

清单7.3中开始的“+”和“-”指示了和第4章清单4.1中辊轮驱动实现的区别。
清单 7.3. 滚轮鼠标驱动

Code View:

+ #include

+ #include



+ /* Device structure */

+ struct {

+ /* ... */

+ struct input_dev dev;

+ } roller_mouse;



+ static int __init

+ roller_mouse_init(void)

+ {

+ /* Allocate input device structure */

+ roller_mouse->dev = input_allocate_device();

+

+ /* Can generate a click and a relative movement */

+ roller_mouse->dev->evbit[0] = BIT(EV_KEY) | BIT(EV_REL);



+ /* Can move only in the Y-axis */

+ roller_mouse->dev->relbit[0] = BIT(REL_Y);

+

+ /* My click should be construed as the left button

+ press of a mouse */

+ roller_mouse->dev->keybit[LONG(BTN_MOUSE)] = BIT(BTN_LEFT);



+ roller_mouse->dev->name = "roll";

+

+ /* For entries in /sys/class/input/inputX/id/ */

+ roller_mouse->dev->id.bustype = ROLLER_BUS;

+ roller_mouse->dev->id.vendor = ROLLER_VENDOR;

+ roller_mouse->dev->id.product = ROLLER_PROD;

+ roller_mouse->dev->id.version = ROLLER_VER;



+ /* Register with the input subsystem */

+ input_register_device(roller_mouse->dev);

+}



/* Global variables */

- spinlock_t roller_lock = SPIN_LOCK_UNLOCKED;

- static DECLARE_WAIT_QUEUE_HEAD(roller_poll);



/* The Roller Interrupt Handler */

static irqreturn_t

roller_interrupt(int irq, void *dev_id)

{

int i, PA_t, PA_delta_t, movement = 0;



/* Get the waveforms from bits 0, 1 and 2

of Port D as shown in Figure 7.1 */

PA_t = PORTD & 0x07;



/* Wait until the state of the pins change.

(Add some timeout to the loop) */

for (i=0; (PA_t==PA_delta_t); i++){

PA_delta_t = PORTD & 0x07;

}



movement = determine_movement(PA_t, PA_delta_t);



- spin_lock(&roller_lock);

-

- /* Store the wheel movement in a buffer for

- later access by the read()/poll() entry points */

- store_movements(movement);

-

- spin_unlock(&roller_lock);

-

- /* Wake up the poll entry point that might have

- gone to sleep, waiting for a wheel movement */

- wake_up_interruptible(&roller_poll);

-

+ if (movement == CLOCKWISE) {

+ input_report_rel(roller_mouse->dev, REL_Y, 1);

+ } else if (movement == ANTICLOCKWISE) {

+ input_report_rel(roller_mouse->dev, REL_Y, -1);

+ } else if (movement == KEYPRESSED) {

+ input_report_key(roller_mouse->dev, BTN_LEFT, 1);

+ }

+ input_sync(roller_mouse->dev);



return IRQ_HANDLED;

}








指点杆(Trackpoint)

指点杆是一个定点设备,在一些笔记本电脑上和PS/2类型的键盘集成在一起。此设备包括位于键盘中间的操作杆和位于空白键下方的鼠标按钮。指点杆的本质功能类似于鼠标,因此你可以用PS/2鼠标驱动对其操作。

不像普通的鼠标,指点杆提供更多的移动控制。你可以控制指点杆控制器改变其灵敏度和惯量等属性。内核有一个特别的驱动drivers/input/mouse/trackpoint.c,用于创建和管理相应的sysfs节点。对于所有的指点配置选项的设置,可在/sys/devices/platform/i8042/serioX/serioY/下查看。
触摸板(Touchpad)

触摸板是类似于鼠标的定点设备,在笔记本电脑上很常见。与传统的鼠标不同,触摸板没有移动组件。它能产生和鼠标兼容的相对坐标,但通常被操作系统用于在功能更强大的模式下产生绝对坐标。在绝对模式下的通信协议类似于PS/2鼠标协议,但不兼容于PS/2。

对于那些和基本PS/2鼠标协议各种的变种相一致的设备,基本的PS/2鼠标驱动能够对其提供支持。对于新鼠标协议,通过psmouse结构体可实现协议驱动,从而可在基本驱动的基础上添加对该新鼠标协议的支持。例如,如果你的笔记本电脑在绝对模式下使用Synaptics触摸板,基本的PS/2鼠标驱动使用Synaptics协议驱动的服务去解释数据流。为了更好的理解Synaptics协议和基本PS/2驱动共同工作的原理,请阅读下面从清单7.4收集的四个代码片段:

· PS/2鼠标驱动:drivers/input/mouse/psmouse-base.c,用支持的鼠标协议信息(包括Synaptics触摸板协议)实例化了psmouse_protocol结构体。

· psmouse结构:定义于drivers/input/mouse/psmouse.h,和各种PS/2协议绑定在一起。

· synaptics_init():使用对应协议的处理函数地址填充psmouse结构体。

· 协议处理函数synaptics_process_byte()(由synaptics_init()填充):当serio感知到鼠标移动时,从中断上下文中被调用。如果查看synaptics_process_byte(),将会发现触摸板的移动通过mousedev报告给用户应用程序。
清单7.4. Synaptics 触摸板的PS/2鼠标协议驱动

Code View:

drivers/input/mouse/psmouse-base.c:

/* List of supported PS/2 mouse protocols */

static struct psmouse_protocol psmouse_protocols[] = {

{

.type = PSMOUSE_PS2, /* The bare PS/2 handler */

.name = "PS/2",

.alias = "bare",

.maxproto = 1,

.detect = ps2bare_detect,

},

/* ... */

{

.type = PSMOUSE_SYNAPTICS, /* Synaptics TouchPad Protocol */

.name = "SynPS/2",

.alias = "synaptics",

.detect = synaptics_detect, /* Is the protocol detected? */

.init = synaptics_init, /* Initialize Protocol Handler */

},

/* ... */

}





drivers/input/mouse/psmouse.h:

/* The structure that ties various mouse protocols together */

struct psmouse {

struct input_dev *dev; /* The input device */

/* ... */



/* Protocol Methods */

psmouse_ret_t (*protocol_handler)

(struct psmouse *psmouse, struct pt_regs *regs);

void (*set_rate)(struct psmouse *psmouse, unsigned int rate);

void (*set_resolution)

(struct psmouse *psmouse, unsigned int resolution);

int (*reconnect)(struct psmouse *psmouse);

void (*disconnect)(struct psmouse *psmouse);

/* ... */

};



drivers/input/mouse/synaptics.c:

/* init() method of the Synaptics protocol */

int synaptics_init(struct psmouse *psmouse)

{

struct synaptics_data *priv;

psmouse->private = priv = kmalloc(sizeof(struct synaptics_data),

GFP_KERNEL);

/* ... */



/* This is called in interrupt context when mouse

movement is sensed */

psmouse->protocol_handler = synaptics_process_byte;



/* More protocol methods */

psmouse->set_rate = synaptics_set_rate;

psmouse->disconnect = synaptics_disconnect;

psmouse->reconnect = synaptics_reconnect;



/* ... */

}



drivers/input/mouse/synaptics.c:

/* If you unfold synaptics_process_byte() and look at

synaptics_process_packet(), you can see the input

events being reported to user applications via mousedev */

static void synaptics_process_packet(struct psmouse *psmouse)

{

/* ... */

if (hw.z > 0) {

/* Absolute X Coordinate */

input_report_abs(dev, ABS_X, hw.x);

/* Absolute Y Coordinate */

input_report_abs(dev, ABS_Y,

YMAX_NOMINAL + YMIN_NOMINAL - hw.y);

}

/* Absolute Z Coordinate */

input_report_abs(dev, ABS_PRESSURE, hw.z);

/* ... */

/* Left TouchPad button */

input_report_key(dev, BTN_LEFT, hw.left);

/* Right TouchPad button */

input_report_key(dev, BTN_RIGHT, hw.right);

/* ... */

}






USB和蓝牙鼠标

和USB键盘一样,USB鼠标也由同样的输入驱动usbhid所驱动。类似的,支持蓝牙键盘的hidp也支持蓝牙鼠标。

正如你所预想的,USB和蓝牙鼠标的驱动通过mousedev来传输设备数据。
触摸控制器

在第6章中,以N_TCH线路规程的形式实现了串行触摸控制器的设备驱动。输入子系统提供了更方便、更简单的方式以实现驱动。通过如下修改,重新以输入设备驱动的形式实现有限状态机:

1. serio提供serport线路规程,以访问和串口连接的设备。使用serport的服务和触摸控制器交互。

2. 类似在清单7.2中针对虚拟鼠标所做的,通过evdev产生输入报告,而不是传送坐标信息给tty层。

通过这些改变,通过/dev/input/eventX,用户空间可访问触摸屏。实际的驱动实现留做练习。

Analog Devices公司的ADS7846芯片是一个不使用串口的触摸控制器的例子,它使用的是SPI(Serial Peripheral Interface)接口。此设备的驱动使用的是SPI提供的服务,而不是serio。第8章“I2C协议”中的“串行外设接口总线”对SPI有详细讨论。类似于大多数触摸驱动,ADS7846驱动使用evdev接口分发触摸信息给用户应用程序。

一些触摸控制器接口为USB。其例子为3M USB触摸控制器,由drivers/input/touchscreen/usbtouchscreen.c所驱动。

很多PDA都在LCD上叠加有四线电阻触摸板。触摸板的X和Y电极(每个坐标轴两线)和模数转换器(ADC)相连,当触摸屏幕时会产生模拟电压值的变化,ADC将其变换为数字输出。输入驱动接收从ADC来的坐标并分发至用户空间。

由于制造过程的细微差别,同一款触摸板的多个产品可能会产生略有不同的坐标范围(X和Y方向的最大值)。为了使应用程序不受此差别之影响,使用之前会对触摸屏校准。校准通常由GUI来发起,通过在屏幕边界和其它的位置显示交叉记号,并要求用户点击这些点。如果支持自校准,产生的坐标通过使用相应的命令被编程进触摸控制器;否则用于对坐标数据流使用软件进行校正。

输入子系统也包括tsdev事件驱动,tsdev根据Compaq触摸屏协议产生坐标信息。如果你的系统通过tsdev报告触摸事件,支持此协议的应用程序能从/dev/input/tsX获取触摸输入。然而,此驱动将从主线内核中移去,因为用户空间的tslib库受到更多的支持.

Documentation/feature-removal-schedule.txt列出了那些将从内核源码树中移除的特性。
加速度传感器

加速度传感器用于测量加速度。某些IBM/联想的笔记本电脑配有检测突然移动的加速度传感器。产生的信息用于保护硬盘免于损坏。它所使用的机制称为硬盘活动保护系统(Hard Drive Active Protection System,HDAPS),类似于汽车中用于保护乘员免于伤害的安全气囊。HDAPS驱动被实现为平台驱动,向输入子系统注册。它使用evdev对检测的加速度的X和Y分量形成数据流。应用程序能够通过/dev/input/eventX读取加速度事件,以检测诸如振动之类的情况,并执行保护措施,例如停止硬盘驱动头。如果你移动笔记本电脑,执行下列命令将不不断输出信息(假设event3分配给HDAPS):

bash> od –x /dev/input/event3

0000000 a94d 4599 1f19 0007 0003 0000 ffed ffff

...

加速度传感器也提供例如温度、键盘和鼠标的活动性等这些信息,所有这些信息可通过访问/sys/devices/platform/hdaps/中的文件获得。鉴于此,HDAPS驱动是内核源码中硬件监控(hwmon)子系统的一部分。在下一章的“带LM传感器的硬件监控”一节中我们将讨论硬件监控。
输出事件

一些设备驱动也处理输出事件。例如,键盘驱动能点亮CAPSLOCK LED,PC 扬声器驱动能够发出“嘟嘟”声。让我们重点来讨论后者。在初始化期间,扬声器驱动会通过设置相应的evbits以表明其有输出能力,并注册用来处理输出事件的回调函数:

Code View:

drivers/input/misc/pcspkr.c:

static int __devinit pcspkr_probe(struct platform_device *dev)

{

/* ... */



/* Capability Bits */

pcspkr_dev->evbit[0] = BIT(EV_SND);

pcspkr_dev->sndbit[0] = BIT(SND_BELL) | BIT(SND_TONE);



/* The Callback routine */

pcspkr_dev->event = pcspkr_event;



err = input_register_device(pcspkr_dev);

/* ... */

}



/* The callback routine */

static int pcspkr_event(struct input_dev *dev, unsigned int type,

unsigned int code, int value)

{



/* ... */



/* I/O programming to sound a beep */



outb_p(inb_p(0x61) | 3, 0x61);

/* set command for counter 2, 2 byte write */

outb_p(0xB6, 0x43);

/* select desired HZ */

outb_p(count & 0xff, 0x42);

outb((count >> 8) & 0xff, 0x42);



/* ... */

}







为了使扬声器发声,键盘事件驱动产生一个声音事件(EV_SND):

input_event(handle->dev, EV_SND, /* Type */

SND_TONE, /* Code */

hz /* Value */);

这会触发回调函数pcspkr_event()的执行,你将听到“嘟嘟”声。
调试

如果你正在调试输入驱动,你可以使用evbug模块辅助调试。它输出和输入子系统产生的事件对应的(type, code, value)(查看前面定义的input_event结构体)。图7.4是在操作某输入设备时evbug所捕获的数据。
图 7.4. Evbug输出

Code View:

/* Touchpad Movement */

evbug.c Event. Dev: isa0060/serio1/input0: Type: 3, Code: 28, Value: 0

evbug.c Event. Dev: isa0060/serio1/input0: Type: 1, Code: 325, Value: 0

evbug.c Event. Dev: isa0060/serio1/input0: Type: 0, Code: 0, Value: 0



/* Trackpoint Movement */

evbug.c Event. Dev: synaptics-pt/serio0/input0: Type: 2, Code: 0, Value: -1

evbug.c Event. Dev: synaptics-pt/serio0/input0: Type: 2, Code: 1, Value: -2

evbug.c Event. Dev: synaptics-pt/serio0/input0: Type: 0, Code: 0, Value: 0



/* USB Mouse Movement */

evbug.c Event. Dev: usb-0000:00:1d.1-2/input0: Type: 2, Code: 1, Value: -1

evbug.c Event. Dev: usb-0000:00:1d.1-2/input0: Type: 0, Code: 0, Value: 0

evbug.c Event. Dev: usb-0000:00:1d.1-2/input0: Type: 2, Code: 0, Value: 1

evbug.c Event. Dev: usb-0000:00:1d.1-2/input0: Type: 0, Code: 0, Value: 0



/* PS/2 Keyboard keypress 'a' */

evbug.c Event. Dev: isa0060/serio0/input0: Type: 4, Code: 4, Value: 30

evbug.c Event. Dev: isa0060/serio0/input0: Type: 1, Code: 30, Value: 0

evbug.c Event. Dev: isa0060/serio0/input0: Type: 0, Code: 0, Value: 0



/* USB keyboard keypress 'a' */

evbug.c Event. Dev: usb-0000:00:1d.1-1/input0: Type: 1, Code: 30, Value: 1

evbug.c Event. Dev: usb-0000:00:1d.1-1/input0: Type: 0, Code: 0, Value: 0

evbug.c Event. Dev: usb-0000:00:1d.1-2/input0: Type: 1, Code: 30, Value: 0

evbug.c Event. Dev: usb-0000:00:1d.1-2/input0: Type: 0, Code: 0, Value: 0







为了更好的理解图7.4中的输出,需要牢记的是触摸板产生绝对坐标(EV_ABS)或事件类型0x03,指点杆产生相对坐标(EV_REL)或事件0x02,键盘发出键盘事件(EV_KEY)或事件0x01。事件0x0对应于input_sync()的调用,其操作如下:

input_event(dev, EV_SYN, SYN_REPORT, 0);

此操作将转换为(type, code, value)组(0x0, 0x0, 0x0),并完成每个输入事件。
查看源代码

大多数输入事件驱动位于drivers/input/ directory目录。然而键盘事件驱动为drivers/char/keyboard.c,这是因为键盘和虚拟终端绑定,而非与/dev/input/下的设备节点绑定。

你可以在几个位置发现输入设备驱动。老式的键盘、鼠标和操纵杆驱动,位于drivers/input/下单独的子目录。蓝牙输入驱动在net/bluetooth/hidp/下。你也可以在drivers/hwmon/和drivers/media/video/等地方发现输入驱动。事件类型、代码和值定义于include/linux/input.h。

serio子系统位于drivers/input/serio/。serport线路规程的源码文件为drivers/input/serio/serport.c。Documentation/input/包括不同输入接口的更多细节。

表7.1概括了本章中使用的主要数据结构,及其在源码树中的位置。表7.2列出了本章所用到的主要内核编程接口,以及它定义的位置。



表 7.1. 数据结构概述

数据结构


位置


描述

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鼠标驱动支持的方法。



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


卸载一个平台设备。






[MS1]原译文意思译反





[MS2]原文为命令执行结果,不能翻译

《Essential Linux Device Drivers》第6章

第6章 串行设备驱动

串口是被许多技术和应用广泛使用的基本通信通道。通用异步收发器(Universal Asynchronous Receiver Transmitter,UART)常用来实现串行通信。在PC兼容机硬件上,UART是Super I/O芯片组的一部分,如图6.1所示。
图 6.1. PC串口连接图



尽管RS-232串口是常见的串行硬件,内核的串行子系统还是用通用化的方式组织在一起,以服务不同的用户。你将在如下场合接触到串行子系统:

· 通过RS-232串行链路运行终端会话。

· 通过拨号、小区蜂窝或软件modem连接到Internet。

· 和触摸控制器、智能卡、蓝牙芯片,或红外dongle[MS1] 等使用串行传输方式的设备接口。

· 使用USB到串口的转换器模拟串口。

· 通过RS-485通信,RS-485在RS-232的基础上支持多个节点,传输距离更远,抗噪性能更强。

在本章中,让我们看看内核如何组织串行子系统。我们将利用Linux蜂窝电话作为例子学习底层的UART驱动,利用串行触摸控制器做例子,去发现其高级线路规程的实现细节。

PC机上常见的UART是美国国家半导体有限公司(National Semiconductor)的16550,或者是其他厂家的兼容芯片,因此你将会在代码或文档中看到建议参考“16550类型UART”。8250芯片是16550之前使用的,因此PC上Linux的UART驱动被命名为8250.c。
层次结构

正如你所看到的,串行子系统的用户多种多样。因此促使内核开发者使用如下的构建模块去构造分层次的串行结构:

1. 底层的驱动关注UART或其它底层串行硬件的特征。

2. tty驱动层提供和底层驱动的接口。tty驱动将上层驱动和形形色色的硬件进行了隔离。

3. 线路规程加工用于和tty驱动交换的数据。线路规程勾勒串行层的行为,有助于复用底层的代码来支持不同的技术。

为了帮助用户实现驱动,串行子系统也提供了一些内核API,它们描绘出了子系统中各层的共性。

图6.2展现了不同层次之间的联系。N_TTY,N_IRDA,和N_PPP这3种不同的线路规程分别为串行子系统提供了终端、红外和拨号网络的支持。图6.3显示了串行子系统与内核源文件的映射关系。
图 6.2. 串行子系统层间连接关系图


图 6.3. 串行子系统到内核源文件的映射关系

让我们用一个例子来说明层次化的串行结构的好处。假设你在没有串口的笔记本电脑上使用USB串口适配器以获得串口功能。可能的场景就是你将笔记本电脑作为主机端,使用kgdb(kgdb将在第21章“调试设备驱动”中讨论)调试某个嵌入式目标设备的内核,就像图6.4中所展示的。
图6.4. 使用USB到串口转接头



正如在图6.3中所看到的,在你的笔记本电脑上,你首先需要一个合适的USB物理层驱动(和UART驱动相对应的USB部分)。在内核的USB子系统drivers/usb/中提供了此驱动。其次,在USB物理层之上你需要tty驱动。usbserial驱动(drivers/usb/serial/usb-serial.c)为核心层,它实现了基于USB串口转换器的通用tty。usbserial驱动和与设备相关的tty函数一起组成了tty层。其中tty函数由适配器驱动注册(例如,如果你使用Keyspan适配器,其驱动为drivers/usb/serial/keyspan.c)。最后,但不仅仅是这些,你需要N_TTY线路规程以处理终端I/O。

tty驱动将底层的USB的内部特性与线路规程及高层进行了隔离。实际上,线路规程仍然认为其运行在传统的UART上。其原因是因为tty驱动从USB请求块(USB Request Blocks,将在第11章“通用串行总线”中讨论)中获取数据,并且将数据封装成N_TTY线路规程期望的格式。层次化的结构简化了实现——所有从线路规程上传的数据能够不作改变的复用。

前述的例子使用了专用的tty驱动和通用的线路规程,相反的用法也很常见。第16章中讨论的红外栈,“无线Linux”,采用的就是通用的tty驱动和被称为N_IRDA的专用的线路规程。

正如你在图6.2和图6.3中所看到的,虽然UART驱动是字符驱动,但它们并不像我们在前面章节中见到的通常的字符驱动那样,将接口直接暴露给内核系统调用。相反,UART驱动(像下一章中讨论的键盘驱动)提供服务给另一内核层:tty层。I/O系统调用的旅程首先从顶层的线路规程开始,通过tty层,止于UART驱动层。

在本章的剩下章节中,让我们近距离接触串行层的不同驱动组件。我们从底含UART驱动的串行栈的底层开始,行至中间的tty驱动,再向顶层的线路规程驱动进军。
UART驱动

UART驱动围绕三个关键的数据结构展开。这三个数据结构都定义于include/linux/ serial_core.h中:

1. 特定UART相关的(per-UART)驱动结构,uart_driver:

struct uart_driver {

struct module *owner; /* 拥有此结构体的模块 */

const char *driver_name; /* 名称 */

const char *dev_name; /* /dev 节点名称,例如ttyS */

/* ... */

int major; /* 主设备号 */

int minor; /* 次设备号 */

/* ... */

struct tty_driver *tty_driver; /* tty 驱动 */

};

结构体中每个域的注释解释了其作用。owner域的作用和前面章节中讨论的file_operations结构中owner域相同。

2. uart_port结构。UART驱动拥有的每个端口,都存在uart_port结构的一个实例。

struct uart_port {

spinlock_t lock; /* 端口锁 */

unsigned int iobase; /* in/out[bwl]*/

unsigned char __iomem *membase; /* read/write[bwl]*/

unsigned int irq; /* 中断号 */

unsigned int uartclk; /* base uart clock */

unsigned char fifosize; /* 传输fifo大小*/

unsigned char x_char; /* xon/xoff 流控 */

/* ... */

};

3. uart_ops结构。这个结构是每个UART驱动必须支持的物理硬件上可完成的操作的入口函数的集合。

struct uart_ops {

uint (*tx_empty)(struct uart_port *); /* Is TX FIFO empty? */

void (*set_mctrl)(struct uart_port *,

unsigned int mctrl); /* Set modem control params */

uint (*get_mctrl)(struct uart_port *); /* Get modem control params */

void (*stop_tx)(struct uart_port *); /* Stop xmission */

void (*start_tx)(struct uart_port *); /* Start xmission */



/* ... */

void (*shutdown)(struct uart_port *); /* Disable the port */

void (*set_termios)(struct uart_port *,

struct termios *new,

struct termios *old); /* Set terminal interface

params */

/* ... */

void (*config_port)(struct uart_port *,

int); /* Configure UART port */

/* ... */

};

UART驱动为了将自身和内核联系起来,必须完成两个重要的步骤:

1.


通过调用uart_register_driver(struct uart_driver *)向串口核心层注册。

2.


调用uart_add_one_port(struct uart_driver *, struct uart_port *)注册其支持的每个端口。如果你的串口硬件支持热插拔,探测到设备存在后,从入口点向内核注册。第10章“PCI”的清单10.4中的CardBus Modem驱动,就是串口设备热插拔的例子。需要注意的是一些驱动使用封装的注册函数serial8250_register_port(struct uart_port *),在其内部调用了uart_add_one_port()。

这些数据结构和注册函数组成了UART驱动的最基本的共同点。了解了这些结构和例程后,让我们来开发一个UART驱动实例。
设备例子:手机

考虑围绕嵌入式片上系统构建的Linux手机。此片上系统有两个集成的UART,但如图6.5所示,都已经被占用。其中一个用于和调制解调器通信,另一个用于和蓝牙芯片组接口。由于没有空闲的UART用于调试,所以此电话使用了两个USB到串口的转换芯片,一个提供给PC主机作为调试终端,另一个作为备用端口。正如你在本章前面所看到的,USB到串口的转换器使你在PC上通过USB可以连接串口设备,在第11章中,我们将会作更详细的讨论。
图6.5. Linux蜂窝电话上的USB_UART端口.

两个USB到串口的转换芯片的串口一侧和片上系统通过CPLD(见第18章《嵌入式Linux》中的“CPLD/FPGA”相关章节)连接在一起。CPLD通过创建了两个虚拟的UART(或USB_UART),它提供了三个寄存器接口以访问每个USB/串口转换器,如表6.1所,这些寄存器为状态寄存器、读数据寄存器和写数据寄存器。为了写一个字符到USB_UART,程序需要循环检测状态寄存器,当芯片内部的发送FIFO有空间时,状态寄存器位被置位,就可以向写数据寄存器写入字节了。为了读取一个字符,程序将等待直至状态寄存器的相应位显示接收FIFO中有数据,然后从读数据寄存器读取数据。

表 6.1. USB_UART上的寄存器分布

寄存器名


描述


相对USB_UART内存基址偏移

UU_STATUS_REGISTER


检查发送FIFO是否满或接收FIFO是否空的比特位


0x0

UU_READ_DATA_REGISTER


从USB_UART读一个字符


0x1

UU_WRITE_DATA_REGISTER


写一个字符至USB_UART


0x2

在PC一端,使用相应的Linux usbserial驱动(例如,如果在蜂窝电话上使用的是FT232AM芯片,其驱动为drivers/usb/serial/ftdi_sio.c)以创建并管理对应于USB/串口端口的/dev/ttyUSBX设备节点。你可以运行基于这些设备节点的终端仿真器,例如minicom,以获得控制台或调试终端。在蜂窝电话一端,我们必须为USB_UART实现UART驱动。此驱动创建并管理负责通信的/dev/ttyUUX节点。

图6.5中的蜂窝电话充当蓝牙设备到GSM网络,以及Internet的智能网关。例如,此电话能够将你的基于蓝牙的血压监控器上的数据传输至你的健康护理提供者在Internet上的服务器上。在和你的基于蓝牙的心率监控器通信过程监测到异常时,它也能够向医生报警。第13章《音频设备驱动》中所使用的蓝牙MP3播放器,以及第16章中使用的喂药棒都是可通过Linux蜂窝电话连接Internet的设备例子。

清单6.1实现了USB_UART驱动。它是作为一个平台(platform)设备驱动实现的。platform可看作一种伪总线,通常用于将集成进片上系统的的轻量级设备和Linux设备模型连接在一起。platform由如下部分组成:

1. platform设备。使用和特定结构的安装程序platform_device_register()或者其简化版本platform_device_register_simple()添加platform设备。你也可以用platform_add_devices()一次添加多个platform设备。定义于include/linux/platform_device.h的platform_device结构体代表了一个platform设备:

struct platform_device {

const char *name; /* 设备名称 */

u32 id; /* 此域用于注册一个platform设备的多个实例。

在本例中,两个USB_UART有不同的ID。 */

struct device dev; /* 包括一个release() 方法和一个平台数据 */

/* ... */

};

1. platform驱动。platform驱动使用platform_driver_register()将自身注册进平台中。结构体platform_driver亦定义于include/linux/platform_device.h中,代表了platform驱动:

struct platform_driver {

int (*probe)(struct platform_device *); /*Probe 方法*/

int (*remove)(struct platform_device *);/*Remove 方法*/

/* ... */

/* The name field in the following structure should match

the name field in the associated platform_device

structure */

struct device_driver driver;

};

关于platform设备和platform驱动更详细的文档可参考Documentation/driver-model/platform.txt。为了讨论简单,我们的例子驱动注册了platform设备和platform驱动。

在初始化过程中,首先USB_UART驱动用uart_register_driver()向串口核心层注册自身。初始化成功后,在/proc/tty/drivers中你将会发现以usb_uart开始的新行。其次,驱动用platform_device_register_simple()注册两个platform设备(每个USB_UART一个)。正如前面提到的,platform设备通常在电路板引导过程中注册。随后,驱动用platform_driver_register()注册platform驱动入口点(probe(), remove(), suspend(), and resume())。USB_UARTplatform驱动和前面的platform设备联系在一起,并有一个相应的名字(usb_uart)。在以上步骤完成后,你将会发现在sysfs下出现两个新目录,每一个和相应的USB_UART端口相对应:/sys/devices/platform/usb_uart.0/ 和 /sys/devices/platform/usb_uart.1/

因为Linux设备层开始检测到和注册的USB_UART platform设备相匹配的platform驱动,它调用属于platform驱动的probe()入口点[[1]](usb_uart_probe()),每个USB_UART一次。probe入口点用uart_add_one_port()添加相应的USB_UART端口。以上步骤会触发config_port()入口点(前面讨论的uart_ops结构的一部分)的调用:声明并映射USB_UART寄存器空间。如果所有的USB_UART端口都被成功的添加,串口核心层会发送如下内核消息:

ttyUU0 at MMIO 0xe8000000 (irq = 3) is a USB_UART

ttyUU1 at MMIO 0xe9000000 (irq = 4) is a USB_UART

但是,直到某个应用程序打开USB_UART端口,才会占用中断号。当应用程序关闭USB_UART端口,中断号被释放。表6.2追溯了驱动代码中声明和释放内存区域与中断号的整个过程。

Table 6.2. Claiming and Freeing Memory and IRQ Resources

Module Insert


usb_uart_init()


uart_register_driver()


usb_uart_probe()


uart_add_one_port()


usb_uart_config_port()


request_mem_region()

Module Unload


usb_uart_exit()


usb_unregister_driver()


usb_uart_remove()


uart_remove_one_port()


usb_uart_release_port()


release_mem_region()

Open /dev/ttyUUX


usb_uart_startup()


request_irq()













Close /dev/ttyUUX


usb_uart_shutdown()


free_irq()













在发送过程中,驱动收集和UART端口相关的循环缓冲区中的待发送数据。在UART驱动的start_tx()的入口函数usb_uart_start_tx()可以看到,数据存放于port->info->xmit.buf[port->info->xmit.tail]。

在接收过程中,驱动使用tty_insert_flip_char()和tty_flip_buffer_push()将从USB_UART收到的数据推出至tty驱动。这些在收中断处理例程usb_uart_rxint()中完成的。读者可在读完下一节“TTY驱动”后,再重读此例程。

清单6.1中的注释解释了驱动入口函数和其操作的目的。在uart_ops中留下了一些入口函数未作实现,以避免让读者陷入过多的细节。
清单6.1. Linux蜂窝电话的USB_UART驱动

Code View:

#include

#include

#include

#include

#include

#include

#include

#include



#define USB_UART_MAJOR 200 /* You've to get this assigned */

#define USB_UART_MINOR_START 70 /* Start minor numbering here */

#define USB_UART_PORTS 2 /* The phone has 2 USB_UARTs */

#define PORT_USB_UART 30 /* UART type. Add this to

include/linux/serial_core.h */



/* Each USB_UART has a 3-byte register set consisting of

UU_STATUS_REGISTER at offset 0, UU_READ_DATA_REGISTER at

offset 1, and UU_WRITE_DATA_REGISTER at offset 2 as shown

in Table 6.1 */

#define USB_UART1_BASE 0xe8000000 /* Memory base for USB_UART1 */

#define USB_UART2_BASE 0xe9000000 /* Memory base for USB_UART2 */

#define USB_UART_REGISTER_SPACE 0x3



/* Semantics of bits in the status register */

#define USB_UART_TX_FULL 0x20 /* TX FIFO is full */

#define USB_UART_RX_EMPTY 0x10 /* TX FIFO is empty */

#define USB_UART_STATUS 0x0F /* Parity/frame/overruns? */



#define USB_UART1_IRQ 3 /* USB_UART1 IRQ */

#define USB_UART2_IRQ 4 /* USB_UART2 IRQ */

#define USB_UART_FIFO_SIZE 32 /* FIFO size */

#define USB_UART_CLK_FREQ 16000000



static struct uart_port usb_uart_port[]; /* Defined later on */



/* Write a character to the USB_UART port */

static void

usb_uart_putc(struct uart_port *port, unsigned char c)

{

/* Wait until there is space in the TX FIFO of the USB_UART.

Sense this by looking at the USB_UART_TX_FULL bit in the

status register */

while (__raw_readb(port->membase) & USB_UART_TX_FULL);



/* Write the character to the data port*/

__raw_writeb(c, (port->membase+1));

}



/* Read a character from the USB_UART */

static unsigned char

usb_uart_getc(struct uart_port *port)

{

/* Wait until data is available in the RX_FIFO */

while (__raw_readb(port->membase) & USB_UART_RX_EMPTY);



/* Obtain the data */

return(__raw_readb(port->membase+2));

}



/* Obtain USB_UART status */

static unsigned char

usb_uart_status(struct uart_port *port)

{

return(__raw_readb(port->membase) & USB_UART_STATUS);

}



/*

* Claim the memory region attached to USB_UART port. Called

* when the driver adds a USB_UART port via uart_add_one_port().

*/

static int

usb_uart_request_port(struct uart_port *port)

{

if (!request_mem_region(port->mapbase, USB_UART_REGISTER_SPACE,

"usb_uart")) {

return -EBUSY;

}

return 0;

}



/* Release the memory region attached to a USB_UART port.

* Called when the driver removes a USB_UART port via

* uart_remove_one_port().

*/

static void

usb_uart_release_port(struct uart_port *port)

{

release_mem_region(port->mapbase, USB_UART_REGISTER_SPACE);

}



/*

* Configure USB_UART. Called when the driver adds a USB_UART port.

*/

static void

usb_uart_config_port(struct uart_port *port, int flags)

{

if (flags & UART_CONFIG_TYPE && usb_uart_request_port(port) == 0)

{

port->type = PORT_USB_UART;

}

}



/* Receive interrupt handler */

static irqreturn_t

usb_uart_rxint(int irq, void *dev_id)

{

struct uart_port *port = (struct uart_port *) dev_id;

struct tty_struct *tty = port->info->tty;



unsigned int status, data;

/* ... */

do {

/* ... */

/* Read data */

data = usb_uart_getc(port);

/* Normal, overrun, parity, frame error? */

status = usb_uart_status(port);

/* Dispatch to the tty layer */

tty_insert_flip_char(tty, data, status);

/* ... */

} while (more_chars_to_be_read()); /* More chars */

/* ... */

tty_flip_buffer_push(tty);



return IRQ_HANDLED;

}

/* Called when an application opens a USB_UART */

static int

usb_uart_startup(struct uart_port *port)

{

int retval = 0;

/* ... */

/* Request IRQ */

if ((retval = request_irq(port->irq, usb_uart_rxint, 0,

"usb_uart", (void *)port))) {

return retval;

}

/* ... */

return retval;

}



/* Called when an application closes a USB_UART */

static void

usb_uart_shutdown(struct uart_port *port)

{

/* ... */

/* Free IRQ */

free_irq(port->irq, port);



/* Disable interrupts by writing to appropriate

registers */

/* ... */

}



/* Set UART type to USB_UART */

static const char *

usb_uart_type(struct uart_port *port)

{

return port->type == PORT_USB_UART ? "USB_UART" : NULL;

}



/* Start transmitting bytes */

static void

usb_uart_start_tx(struct uart_port *port)

{

while (1) {

/* Get the data from the UART circular buffer and

write it to the USB_UART's WRITE_DATA register */

usb_uart_putc(port,

port->info->xmit.buf[port->info->xmit.tail]);

/* Adjust the tail of the UART buffer */

port->info->xmit.tail = (port->info->xmit.tail + 1) &

(UART_XMIT_SIZE - 1);

/* Statistics */

port->icount.tx++;

/* Finish if no more data available in the UART buffer */

if (uart_circ_empty(&port->info->xmit)) break;

}

/* ... */

}



/* The UART operations structure */

static struct uart_ops usb_uart_ops = {

.start_tx = usb_uart_start_tx, /* Start transmitting */

.startup = usb_uart_startup, /* App opens USB_UART */

.shutdown = usb_uart_shutdown, /* App closes USB_UART */

.type = usb_uart_type, /* Set UART type */

.config_port = usb_uart_config_port, /* Configure when driver

adds a USB_UART port */

.request_port = usb_uart_request_port,/* Claim resources

associated with a

USB_UART port */

.release_port = usb_uart_release_port,/* Release resources

associated with a

USB_UART port */

#if 0 /* Left unimplemented for the USB_UART */

.tx_empty = usb_uart_tx_empty, /* Transmitter busy? */

.set_mctrl = usb_uart_set_mctrl, /* Set modem control */

.get_mctrl = usb_uart_get_mctrl, /* Get modem control

.stop_tx = usb_uart_stop_tx, /* Stop transmission */

.stop_rx = usb_uart_stop_rx, /* Stop reception */

.enable_ms = usb_uart_enable_ms, /* Enable modem status

signals */

.set_termios = usb_uart_set_termios, /* Set termios */

#endif

};



static struct uart_driver usb_uart_reg = {

.owner = THIS_MODULE, /* Owner */

.driver_name = "usb_uart", /* Driver name */

.dev_name = "ttyUU", /* Node name */

.major = USB_UART_MAJOR, /* Major number */

.minor = USB_UART_MINOR_START, /* Minor number start */

.nr = USB_UART_PORTS, /* Number of UART ports */

.cons = &usb_uart_console, /* Pointer to the console

structure. Discussed in Chapter

12, "Video Drivers" */

};



/* Called when the platform driver is unregistered */

static int

usb_uart_remove(struct platform_device *dev)

{

platform_set_drvdata(dev, NULL);



/* Remove the USB_UART port from the serial core */

uart_remove_one_port(&usb_uart_reg, &usb_uart_port[dev->id]);

return 0;

}



/* Suspend power management event */

static int

usb_uart_suspend(struct platform_device *dev, pm_message_t state)

{

uart_suspend_port(&usb_uart_reg, &usb_uart_port[dev->id]);

return 0;

}



/* Resume after a previous suspend */

static int

usb_uart_resume(struct platform_device *dev)

{

uart_resume_port(&usb_uart_reg, &usb_uart_port[dev->id]);

return 0;

}



/* Parameters of each supported USB_UART port */

static struct uart_port usb_uart_port[] = {

{

.mapbase = (unsigned int) USB_UART1_BASE,

.iotype = UPIO_MEM, /* Memory mapped */

.irq = USB_UART1_IRQ, /* IRQ */

.uartclk = USB_UART_CLK_FREQ, /* Clock HZ */

.fifosize = USB_UART_FIFO_SIZE, /* Size of the FIFO */

.ops = &usb_uart_ops, /* UART operations */

.flags = UPF_BOOT_AUTOCONF, /* UART port flag */

.line = 0, /* UART port number */

},

{

.mapbase = (unsigned int)USB_UART2_BASE,

.iotype = UPIO_MEM, /* Memory mapped */

.irq = USB_UART2_IRQ, /* IRQ */

.uartclk = USB_UART_CLK_FREQ, /* CLock HZ */

.fifosize = USB_UART_FIFO_SIZE, /* Size of the FIFO */

.ops = &usb_uart_ops, /* UART operations */

.flags = UPF_BOOT_AUTOCONF, /* UART port flag */

.line = 1, /* UART port number */

}

};



/* Platform driver probe */

static int __init

usb_uart_probe(struct platform_device *dev)

{

/* ... */



/* Add a USB_UART port. This function also registers this device

with the tty layer and triggers invocation of the config_port()

entry point */

uart_add_one_port(&usb_uart_reg, &usb_uart_port[dev->id]);

platform_set_drvdata(dev, &usb_uart_port[dev->id]);

return 0;

}



struct platform_device *usb_uart_plat_device1; /* Platform device

for USB_UART 1 */

struct platform_device *usb_uart_plat_device2; /* Platform device

for USB_UART 2 */



static struct platform_driver usb_uart_driver = {

.probe = usb_uart_probe, /* Probe method */

.remove = __exit_p(usb_uart_remove), /* Detach method */

.suspend = usb_uart_suspend, /* Power suspend */

.resume = usb_uart_resume, /* Resume after a suspend */

.driver = {

.name = "usb_uart", /* Driver name */

},

};

/* Driver Initialization */

static int __init

usb_uart_init(void)

{

int retval;



/* Register the USB_UART driver with the serial core */

if ((retval = uart_register_driver(&usb_uart_reg))) {

return retval;

}



/* Register platform device for USB_UART 1. Usually called

during architecture-specific setup */

usb_uart_plat_device1 =

platform_device_register_simple("usb_uart", 0, NULL, 0);

if (IS_ERR(usb_uart_plat_device1)) {

uart_unregister_driver(&usb_uart_reg);

return PTR_ERR(usb_uart_plat_device1);

}



/* Register platform device for USB_UART 2. Usually called

during architecture-specific setup */

usb_uart_plat_device2 =

platform_device_register_simple("usb_uart", 1, NULL, 0);

if (IS_ERR(usb_uart_plat_device2)) {

uart_unregister_driver(&usb_uart_reg);

platform_device_unregister(usb_uart_plat_device1);

return PTR_ERR(usb_uart_plat_device2);

}



/* Announce a matching driver for the platform

devices registered above */

if ((retval = platform_driver_register(&usb_uart_driver))) {

uart_unregister_driver(&usb_uart_reg);

platform_device_unregister(usb_uart_plat_device1);

platform_device_unregister(usb_uart_plat_device2);

}

return 0;

}



/* Driver Exit */

static void __exit

usb_uart_exit(void)



{

/* The order of unregistration is important. Unregistering the

UART driver before the platform driver will crash the system */



/* Unregister the platform driver */

platform_driver_unregister(&usb_uart_driver);



/* Unregister the platform devices */

platform_device_unregister(usb_uart_plat_device1);

platform_device_unregister(usb_uart_plat_device2);



/* Unregister the USB_UART driver */

uart_unregister_driver(&usb_uart_reg);

}



module_init(usb_uart_init);

module_exit(usb_uart_exit);








RS-485

与RS-232不同,RS-485并不是标准的PC接口,但在嵌入式领域,你可能会碰到计算机和控制系统之间为了可靠通信而使用RS-485的情况。RS-485使用差分信号,因此其传输距离可达数千英尺;而RS-232的传输距离仅为数十英尺。在处理器一端,RS-485接口是半双工的UART操作。因此从发送FIFO发送数据至电缆之前,UART设备驱动需要禁止接收器,使能发送器,这一操作可以通过设置相应GPIO引脚的电平来实现。而为了从电缆上获取数据并传送至接收FIFO,UART驱动需要完成相反的操作。

你必须在串口层中恰当的地方使能/禁止RS-485的接收器/发送器。如果你太早地禁止了发送器,它可能没有足够的时间清空发送FIFO中最后的几个字节数据,这将导致发送数据丢失。相反的,如果你太早地禁止了发送器,你就会阻止此段时间的数据接收,这将导致接收数据丢失。

RS-485支持多节点,因此如果你有多个设备和总线相连,高层的协议必须实现恰当的寻址机制。RS-485不支持使用RTS(Request To Send)和CTS(Clear To Send)的硬流控。

TTY驱动

让我们开始看看tty驱动的核心结构体和注册函数。有三个结构体非常重要:

1. 定义于include/linux/tty.h中的tty_struct结构体。此结构体包含了和打开的tty相关的所有的状态信息。其结构体成员众多,下面列出了一些重要的成员:

struct tty_struct {

int magic; /* Magic marker */

struct tty_driver *driver; /* tty驱动指针 */

struct tty_ldisc ldisc; /* Attached Line

discipline */

/* ... */

struct tty_flip_buffer flip; /* Flip Buffer. See

below. */

/* ... */



wait_queue_head_t write_wait; /* See the section

"Line Disciplines" */

wait_queue_head_t read_wait; /* See the section

"Line Disciplines" */

/* ... */

};

1. tty_struct结构体中的tty_flip_buffer结构体。这是数据收集和处理机制的中枢:

struct tty_flip_buffer {

/* ... */

struct semaphore pty_sem; /* Serialize */

char *char_buf_ptr; /* Pointer to the flip

buffer */

/* ... */

unsigned char char_buf[2*TTY_FLIPBUF_SIZE]; /* The flip

buffer */

/* ... */

};

底层的串行驱动将flip缓冲区的一半用于数据收集,线路规程则使用另一半来进行数据处理。数据处理伴随着串行驱动和线路规程所使用的缓冲区指针的移动而持续进行。从drivers/char/tty_io.c文件的flush_to_ldisc()函数中可看到flip的确切行为。

在最近的内核中,tty_flip_buffer结构体有些改动,该结构体目前由缓冲区头部(tty_bufhead)和缓冲区链表(tty_buffer)组成:

struct tty_bufhead {

/* ... */

struct semaphore pty_sem; /* Serialize */

struct tty_buffer *head, tail, free; /* See below */

/* ... */

};



struct tty_buffer {

struct tty_buffer *next;

char *char_buf_ptr; /* Pointer to the flip buffer */

/* ... */

unsigned long data[0]; /* The flip buffer, memory for

which is dynamically

allocated */

};

1. 定义于include/linux/tty_driver.h文件中的tty_driver结构体。它规定了tty驱动和高层的编程接口:

struct tty_driver {

int magic; /* Magic number */

/* ... */

int major; /* Major number */

int minor_start; /* Start of minor number */

/* ... */

/* Interface routines between a tty driver and higher

layers */

int (*open)(struct tty_struct *tty, struct file *filp);

void (*close)(struct tty_struct *tty, struct file *filp);

int (*write)(struct tty_struct *tty,

const unsigned char *buf, int count);

void (*put_char)(struct tty_struct *tty,

unsigned char ch);

/* ... */

};

像UART驱动一样,tty驱动也需要完成两个步骤以向内核注册自身:

1.


调用tty_register_driver(struct tty_driver *tty_d)向tty核心注册自身。

2.


调用

tty_register_device(struct tty_driver *tty_d,

unsigned device_index,

struct device *device)

注册它支持的每个单独的tty。

本章不会给出tty驱动的实例,在Linux内核中有一些通用的tty驱动:

* 第16章讨论的蓝牙模拟串口,就是用tty驱动的形式实现的。此驱动(drivers/net/bluetooth/rfcomm/tty.c)在初始化阶段调用tty_register_driver(),在处理每个到来的蓝牙连接时调用tty_register_device()。
* 在Linux桌面上,为了使用系统控制台,如果你工作在字符模式下,将需要虚拟终端(virtual terminals,VTs)服务,如果你处在图形模式下,将需要伪终端(pseudo terminals,PTYs)服务。虚拟终端和伪终端都是用tty驱动实现的,分别位于drivers/char/vt.c和drivers/char/pty.c。
* 传统的UART使用的tty驱动位于drivers/serial/serial_core.c。
* USB到串口转换器的tty驱动位于drivers/usb/serial/usb-serial.c。

线路规程

线路规程提供了一套灵活的机制,使得用户运行不同的应用时,使用相同的串行驱动。底层的物理驱动和tty驱动完成从硬件上收发数据,而线路规程则负责处理这些数据,并在内核空间和用户空间之间进行数据的传递。

串行子系统支持17种标准的线路规程。当你打开串口时系统会绑定默认的线路规程N_TTY,它实现了终端I/O处理。N_TTY负责加工从键盘接收到的字符。根据用户需要,它完成控制字符到“新起一行”的映射,进行小写字符至大写字符的转换,将tab、echo字符传递给关联的虚拟终端。N_TTY也支持原始编辑模式,此时,它将所有前述的处理都交给用户程序。下一章《输入设备驱动》中的图7.3展示了键盘子系统如何和N_TTY相关联。前一节“TTY驱动”中列出的tty驱动例子默认就是使用N_TTY。

线路规程也实现通过串行传输协议的网络接口。PPP(N_PPP)和SLIP(N_SLIP)子系统中的线路规程完成将包组帧、分配相应的网络数据结构并将数据传送至相应的网络协议栈的工作。其它的线路规程还包括红外数据(N_IRDA)和蓝牙主机控制接口(N_HCI)。
设备例子:触摸控制器

本节通过实现一个简单的串行触摸屏控制器的线路规程,来深入线路规程的内幕。图6.6显示了触摸控制器和嵌入式掌上电脑的连接。由于触摸控制器的有限状态机(FSM)能够很好地描述串行层提供的接口和功用,因此将可根据它实现线路规程。
图 6.6. PC-derivative上触摸控制器连接图


Open与Close

为了创建线路规程,需要定义tty_ldisc结构体,并向内核注册指定的入口函数集。清单6.2包括了实现了以上功能的触摸控制器实例的部分代码。
清单 6.2. 线路规程操作

Code View:

struct tty_ldisc n_touch_ldisc = {

TTY_LDISC_MAGIC, /* Magic */

"n_tch", /* 线路规程名 */

N_TCH, /* 线路规程ID号 */

n_touch_open, /* 打开线路规程 */

n_touch_close, /* 关闭线路规程 */

n_touch_flush_buffer, /* Flush the line discipline's read

buffer */

n_touch_chars_in_buffer, /* Get the number of processed characters in

the line discipline's read buffer */

n_touch_read, /* Called when data is requested

from user space */

n_touch_write, /* Write 方法 */

n_touch_ioctl, /* I/O 控制命令 */

NULL, /* We don't have a set_termios

routine */

n_touch_poll, /* 轮询 */

n_touch_receive_buf, /* 由底层的驱动调用,

用于向用户空间传送数据 */

n_touch_receive_room, /* Returns the room left in the line

discipline's read buffer */

n_touch_write_wakeup /* Called when the low-level device

driver is ready to transmit more

data */

};



/* ... */



if ((err = tty_register_ldisc(N_TCH, &n_touch_ldisc))) {

return err;

}







在清单6.2中,n_tch是线路规程名,N_TCH是线路规程的ID号。你需要在include/linux/tty.h中定义其值(此头文件中包括所有线路规程的定义)。在/proc/tty/ldiscs可发现正使用的线路规程。

线路规程从tty的flip缓冲区对应的部分收集、处理数据,然后拷贝处理后的数据至本地读缓冲区。对于N_TCH,n_touch_receive_room()返回读缓冲区中的剩余内存数,n_touch_chars_in_buffer()返回读缓冲区中已经处理过的、准备送至用户空间的字符个数。由于N_TCH是只读设备, n_touch_write() 和 n_touch_write_wakeup()将不进行任何操作。n_touch_open()用于为线路规程的主要数据结构分配内存,可参照清单6.3。
清单6.3. 打开线路规程

Code View:

/* Private structure used to implement the Finite State Machine

(FSM) for the touch controller. The controller and the processor

communicate using a specific protocol that the FSM implements */

struct n_touch {

int current_state; /* Finite State Machine */

spinlock_t touch_lock; /* Spinlock */

struct tty_struct *tty; /* Associated tty */

/* Statistics and other housekeeping */

/* ... */

} *n_tch;





/* Device open() */

static int

n_touch_open(struct tty_struct *tty)

{

/* Allocate memory for n_tch */

if (!(n_tch = kmalloc(sizeof(struct n_touch), GFP_KERNEL))) {

return -ENOMEM;

}

memset(n_tch, 0, sizeof(struct n_touch));



tty->disc_data = n_tch; /* Other entry points now

have direct access to n_tch */

/* Allocate the line discipline's local read buffer

used for copying data out of the tty flip buffer */

tty->read_buf = kmalloc(BUFFER_SIZE, GFP_KERNEL);

if (!tty->read_buf) return -ENOMEM;



/* Clear the read buffer */

memset(tty->read_buf, 0, BUFFER_SIZE);



/* Initialize lock */

spin_lock_init(&ntch->touch_lock);



/* Initialize other necessary tty fields.

See drivers/char/n_tty.c for an example */

/* ... */



return 0;

}







当打开连有触摸控制器的串口时,你可能想将N_TCH而非N_TTY设置为默认的线路规程,“改变线路规程”一节中介绍的方法可以实现从用户空间改变线路规程的目的。
读数据过程

对于中断驱动的设备,读取数据的过程通常由一前一后两个线程组成:

1. 由于发起读数据请求而从用户空间发起的顶层线程;
2. 由中断处理程序(接收来自设备的数据)唤醒的底层线程。

图6.7显示了这两个与读数据流程相关的线程。中断处理程序将receive_buf()(在我们的例子中是n_touch_receive_buf())函数当作任务进行排队。通过设置tty->low_latency可重载这一行为。
图 6.7. 线路规程读数据过程



在触摸控制器的数据手册详细描述了触摸控制器和处理器之间的专用通信协议,而驱动则用前面讨论过的有限状态机来实现此协议。清单6.4中将有限状态机作为receive_buf()入口点n_touch_receive_buf()的一部分。
清单 6.4. n_touch_receive_buf() 方法

Code View:

static void

n_touch_receive_buf(struct tty_struct *tty,

const unsigned char *cp, char *fp, int count)

{



/* Work on the data in the line discipline's half of

the flip buffer pointed to by cp */

/* ... */



/* Implement the Finite State Machine to interpret commands/data

arriving from the touch controller and put the processed data

into the local read buffer */

....................................................................................

/* Datasheet-dependent Code Region */

switch (tty->disc_data->current_state) {

case RESET:

/* Issue a reset command to the controller */

tty->driver->write(tty, 0, mode_stream_command,

sizeof(mode_stream_command));

tty->disc_data->current_state = STREAM_DATA;

/* ... */

break;

case STREAM_DATA:

/* ... */

break;

case PARSING:

/* ... */

tty->disc_data->current_state = PARSED;

break;

case PARSED:

/* ... */

}

....................................................................................



if (tty->disc_data->current_state == PARSED) {

/* If you have a parsed packet, copy the collected coordinate

and direction information into the local read buffer */

spin_lock_irqsave(&tty->disc_data->touch_lock, flags);

for (i=0; i < PACKET_SIZE; i++) {

tty->disc_data->read_buf[tty->disc_data->read_head] =

tty->disc_data->current_pkt[i];

tty->disc_data->read_head =

(tty->disc_data->read_head + 1) & (BUFFER_SIZE - 1);

tty->disc_data->read_cnt++;

}

spin_lock_irqrestore(&tty->disc_data->touch_lock, flags);



/* ... */ /* See Listing 6.5 */



}

}







n_touch_receive_buf() 处理从串行驱动来的数据。它和触摸控制器进行一系列命令/响应的交互,将接收的坐标和压下/释放信息放入线路规程读缓冲区。对读缓冲区的访问必须借助自旋锁的加锁能力来依次进行,如图6.7所示,该自旋锁被ldisc.receive_buf() 和 ldisc.read() 线程(在我们的例子中分别是n_touch_receive_buf() 和 n_touch_read())同时使用。如清单6.4,n_touch_receive_buf()通过直接调用串行驱动的write()入口函数将命令分发给触摸控制器。

n_touch_receive_buf()需要做如下操作:

1. 如果没有数据可获得,图6.7中的顶层的read()线程会将调用进程置为休眠状态。因此n_touch_receive_buf()必须将其唤醒,使其读取刚处理过的数据。
2. 如果线路规程耗尽了读缓冲空间,n_touch_receive_buf()必须要求串行驱动中止从设备接收数据。当它将数据搬移至用户空间并释放读缓冲区中的内存后,ldisc.read()负责重新开启数据接收。串行驱动利用软件或硬件流控机制完成数据接收的中止和重启。

清单6.5实现了上面的操作。
清单 6.5. 唤醒读线程和中止串行驱动

/* n_touch_receive_buf() continued.. */



/* Wake up any threads waiting for data */

if (waitqueue_active(&tty->read_wait) &&

(tty->read_cnt >= tty->minimum_to_wake))

wake_up_interruptible(&tty->read_wait);

}

/* If we are running out of buffer space, request the

serial driver to throttle incoming data */

if (n_touch_receive_room(tty) < TOUCH_THROTTLE_THRESHOLD) {

tty->driver.throttle(tty);

}

/* ... */



等待队列tty->read_wait用于在ldisc.read()和ldisc.receive_buf()线程之间实现同步。当ldisc.read()未发现可读的数据时,将调用进程加入等待队列,当有数据可读时,ldisc.receive_buf()唤醒ldisc.read()线程。因此,n_touch_read()完成如下操作:

· 当仍然无可读数据时,将调用进程放入read_wait队列中使其休眠。当数据到来时由n_touch_receive_buf()唤醒此进程。

· 若数据可获得,从本地读缓冲区(tty->read_buf[tty->read_tail])中收集数据,并分发至用户空间。

· 若串行驱动被中止,并且在读操作后读缓冲区中又有了足够的可用空间,请求串行驱动重启。

网络线路规程通常分配sk_buff(第15章“网络接口卡”中将讨论到的基本的Linux网络数据结构),并用它作读缓冲区。由于网络线路规程的receive_buf()将接收的拷贝至sk_buff并将其直接传送至相应的协议栈,因此它没有read()函数。
写数据过程

线路规程的write()入口函数需要完成一些数据传送至底层驱动之前必要的后处理工作。

如果底层驱动不能接受线路规程提供的所有数据,线路规程会将请求发送数据的线程置于休眠状态。当驱动准备好接受更多的数据时,驱动的中断处理例程将线路规程唤醒。为了达成此目的,驱动调用由线路规程注册的write_wakeup()方法。类似于前面章节中讨论过的read_wait实现同步,此处通过等待队列tty->write_wait来实现相应的同步。

很多网络线路规程没有write()方法。协议实现时直接将数据帧向下传送给串行设备驱动。然而,这些线路规程通常仍然有write_wakeup()入口点,以响应串行驱动的传输更多数据的请求。

因为触摸控制器是只读设备,N_TCH也没有write()方法。正如在清单6.4中所见,当需要发送命令帧给控制器时,接收路径中的例程直接和底层的UART驱动交互。
I/O 控制

像第5章《字符设备驱动》中讨论的那样,用户程序可以通过调用ioctl()向设备发送命令。当一个应用程序打开一个串口设备时,它通常可向其发出三类ioctl:

· 串行驱动设备支持的命令,例如设置modem信息的TIOCMSET。

· tty驱动支持的命令,例如改变关联的线路规程的TIOCSETD。

· 关联的线路规程支持的命令,例如在N_TCH例子中,复位触摸控制器的命令。

N_TCH中实现的ioctl()基本是标准的。支持的命令依赖于触摸控制器的数据手册中描述的协议。
其他操作

另一个线路规程操作是flush_buffer(),用于清理读缓冲区中未处理的数据。当线路规程关闭时,也会调用flush_buffer()。它唤醒所有等待数据的读线程,其操作如下:

if (tty->link->packet){

wake_up_interruptible(&tty->disc_data->read_wait);

}

还有一个入口函数(N_TCH不支持)为set_termios()。N_TTY线路规程支持set_termios()接口,用于进行和线路规程数据处理相关的配置。例如你可以用set_termios()将线路规程置为原始模式或加工模式。一些触摸控制器特定的配置(如改变波特率、校验和停止位)由底层设备驱动的set_termios()方法提供。

其他的入口函数例如poll()都是一些标准入口函数,如果需要可以参阅第五章。

你可以将线路规程编译进内核或编译为模块动态加载。如果你选择编译为模块,就必须提供模块初始化或释放需要调用的函数。前者通常使用和init()类似的方法,后者需要清空私有的数据结构,并注销线路规程。注销线路规程的操作为:

tty_unregister_ldisc(N_TCH);

驱动串行触摸控制器更简单的途径是利用内核输入子系统提供的服务和内嵌的serport线路规程。在后续章节中我们将看到其技术细节。
修改线路规程

当用户空间程序打开和触摸控制器相连的串口时,N_TCH将被绑定到底层的串行驱动。但有时,用户空间的应用程序可能需要给设备绑定其他的线路规程。例如,你可能想编写程序清空触摸控制器接收的所有原始数据而不处理它。清单6.6打开触摸控制器,并修改线路规程为N_TTY并清空所有接收的数据。
清单 6.6. 从用户空间修改线路规程

fd = open("/dev/ttySX", O_RDONLY | O_NOCTTY);



/* At this point, N_TCH is attached to /dev/ttySX, the serial port used

by the touch controller. Switch to N_TTY */

ldisc = N_TTY;

ioctl(fd, TIOCSETD, &ldisc);



/* Set termios to raw mode and dump the data coming in */

/* ... */



ioctl()的TIOCSETD命令关闭当前的线路规程并打开新选定的线路规程。
查看源代码

串行核心位于drivers/serial/,tty实现和底层的驱动分散在内核源码树中。例如,驱动文件可参考图6.3,位于四个不同的目录中:drivers/serial/,drivers/char/,drivers/usb/serial/和 drivers/net/irda/。drivers/serial/目录包含UART驱动,而在2.4版本的内核中无此目录,UART相关的代码过去通常分散在drivers/char/ 和 arch/your-arch/目录中。现在的代码划分较之以前更有逻辑性,因为UART驱动不是串行层的唯一访问者,USB到串口转换器以及红外dongle等设备也需要和串行核心交互。

drivers/serial/imx.c是一个实际的、底层的UART驱动,它驱动的是飞思卡尔(Freescale)i.MX系列嵌入式控制器的UART。

Linux支持的线路规程列表可以参照include/linux/tty.h文件。如果想了解网络线路规程,可以阅读相应的源文件:PPP(drivers/net/ppp_async.c),蓝牙(drivers/bluetooth/hci_ldisc.c), 红外(drivers/net/irda/irtty-sir.c)和SLIP(drivers/net/slip.c)。

表6.3包括本章中用到的主要数据结构的概述和它们在源码树中被定义的位置。表6.4列出了本章用到的主要内核编程接口和其定义位置。

表 6.3. 数据结构概述

数据结构


位置


描述

uart_driver


include/linux/serial_core.h


代表底层的UART驱动。

uart_port


include/linux/serial_core.h


代表一个UART端口。

uart_ops


include/linux/serial_core.h


UART驱动支持的入口函数。

platform_device


include/linux/platform_device.h


代表platform设备。

platform_driver


include/linux/platform_device.h


代表一个platform驱动。

tty_struct


include/linux/tty.h


tty的状态信息。

tty_bufhead, tty_buffer


include/linux/tty.h


这两个结构体实现了和tty关联的flip缓冲区。

tty_driver


include/linux/tty_driver.h


tty驱动和高层之间的编程接口。

tty_ldisc


include/linux/tty_ldisc.h


线路规程支持的入口函数。



表 6.4. 内核编程接口概述

内核接口


位置


描述

uart_register_driver()


drivers/serial/sderial_core.c


向串行核心注册UART驱动。

uart_add_one_port()


drivers/serial/sderial_core.c


注册UART驱动支持的UART端口。

uart_unregister_driver()


drivers/serial/sderial_core.c


从串行核心移除UART驱动。

platform_device register()

platform_device_register_simple()

platform_add_devices()




drivers/base/platform.c


注册platform设备。

platform_device_unregister()


drivers/base/platform.c


卸载platform设备。

platform_driver_register()/

platform_driver_unregister()




drivers/base/platform.c


注册/卸载platform驱动。

tty_insert_flip_char()


include/linux/tty_flip.h


向tty flip缓冲区添加一个字符。

tty_flip_buffer_push()


drivers/char/tty_io.c


排队一个将flip缓冲区推向线路规程的请求。

tty_register_driver()


drivers/char/tty_io.c


向串行核心注册tty驱动。

tty_unregister_driver()


drivers/char/tty_io.c


从串行核心注销tty驱动。

tty_register_ldisc()


drivers/char/tty_io.c


通过注册指定的入口函数,创建线路规程。

tty_unregister_ldisc()


drivers/char/tty_io.c


从串行核心移除线路规程。

一些串行数据的传送场景较为复杂。正如你在图6.3中所见,你可能需要混用和匹配多个不同的串行层模块。某些情况下,数据传送可能需要穿越多个线路规程来完成。例如,设置一个使用蓝牙建立的拨号连接就需要数据在HCI线路规程和PPP线路规程中传输。你可以尝试建立此连接,并使用内核调试器单步跟踪此代码流。



[1] 平台设备通常不支持热插拔。对平台设备的probe()方法的调用不同于在后面章节中将学到的热插拔设备(如PCMCIA,PCI,和USB),它只是为了使驱动入口点的结构相同,以保持Linux设备模型一致、连续性。




[MS1]此词可不译,是非常流行的说法,至今无准确的中文翻译,其含义基本只可以意会。