4.3 设备驱动的基本特点
4.3.1 驱动代码执行环境——任务和中断上下文
设备驱动代码是执行在内核环境下的,这是笼统的说法。实际上,驱动代码具有两个执行环境,一个是任务上下文,一个是中断上下文。除了中断响应函数执行在中断上下文之外,其他所有的函数均执行在任务上下文。这个任务上下文对应的任务就是当前对设备进行某种服务请求的用户任务或内核任务。在VxWorks下处于一个任务的上下文中仅仅是指在执行驱动代码时可以被挂起,代码使用的栈是任务栈,代码可被中断抢断,包括其自身中断。所以,如果驱动中某个函数与驱动中断处理程序共享同一资源,则要避免形成资源的破坏,在这种情况下,大多使用intLock+taskLock组合保护任务上下文。内核提供的回调函数虽然不是驱动自身的代码,但是也是在驱动中被调用,这些回调函数一般由底层驱动中断响应函数进行调用,故是执行在中断上下文中的。内核在实现这些回调函数时已经注意到这一点,故在中断上下文中执行不会造成问题。关于内核回调函数的具体实例,读者可参考本书“串口驱动”一章。
4.3.2 设备基本分类
根据设备的工作方式和数据存储或者来源不同,可以将设备分为三大类型。
(1)字符设备类型
字符设备即以字节流的方式被访问,就如同一个文件,但不同于文件之处是字符设备一般不可以移动文件偏移指针,而只能顺序地访问数据。终端设备以及串口设备都属于字符设备类型。字符设备驱动至少需要实现open、close、read和write四个系统调用底层实现函数。
(2)块设备类型
块设备一般通过文件系统访问,而块设备使用最多的方式也是文件方式。块设备一般不能对单个字节进行访问,而是以一个块的方式(如硬盘以一个扇区(512B)为单元进行访问)进行。块设备允许同一数据的反复读取和写入。最典型的块设备就是硬盘,Flash设备也是一种块设备。
(3)网络设备类型
这是比较特殊的一类设备,这类设备用于与网络上其他主机进行通信,其数据读取方式类似于字符设备,不可以对同一数据进行反复读写,只能顺序读写数据,且该类型设备区别于字符设备和块设备的一个很大的不同是,其不提供文件节点,任务若要访问一个网络设备,必须使用另一套网络套接字接口函数进行,与文件系统则完全不相关。网络设备底层数据传输上以块的方式进行,但是又不同于块设备中数据块的概念,网络设备中块的大小可以改变,但是有一个区间范围。
以上只是设备类型的划分方式之一,事实上,按以上的划分标准,某些设备接口在某些情况下可以表现为以上三种形式的任意一种,如USB接口;可以是一个字符设备,如USB串口;也可以是一个块设备,如USB内存卡;还可以是一个网络设备,如USB网络接口。
4.3.3 驱动代码安全性——参数合法性检查
VxWorks不支持驱动程序的动态加载,驱动程序一般实现为内核代码的一部分,故安全性方面的问题出现较少,但是在编程时应遵循的基本安全规则必须牢记:对于用户输入的任何参数都必须经过检查后方可使用,且对于不合法或不合理的参数,必须终止服务。
在某些特殊情况下,对于一个外设硬件的控制,可能通过一个用户层任务进行,此时可以认为这个驱动实现在用户层,这时就需要特别注意。对于VxWorks这样一个不区分运行级别的操作系统而言,不存在通用操作系统下的用户层和内核层的概念。通常,我们在VxWorks下讲到用户层,仅仅是指任务运行的代码是用户代码,而非内核代码,且仅此而已。
事实上,在VxWorks下,用户可以直接调用任意的内核函数而不会出现调用权限不够的情况,这主要是由VxWorks操作系统应用环境决定的。故安全性在VxWorks下更多的是指尽量排除代码中固有的BUG,而不是防止某些用户恶意的行为。
4.3.4 驱动基本工作模式——轮询和中断
设备驱动从总体上分为两种工作模式:轮询模式和中断模式。轮询模式通过检测相关寄存器状态位来决定是否进行下一步操作;而中断则用以通知某个操作已经完成,可以进行下一个操作了。二者的根本区别在于轮询方式在等待操作完成的过程中需要CPU等待,而中断则将CPU从等待中解放出来,在硬件完成一个用户请求的过程中,CPU可以运行其他任务,当硬件完成请求后,发出一个中断,再次引起驱动的注意,驱动可以在中断响应函数中启动下一个操作。从CPU使用的效率而言,好像中断方式一定优于轮询方式,其实不然。轮询方式被使用在很多场合,即便这些场合支持中断模式,如串口、SPI口等速率较低的设备,实际工作中,很多都是通过查询状态位来进行读写的。
以串口为例,其支持中断工作模式,但是使用中断有如下缺点:如果每发送或者接收一个字节就产生一个中断,由于中断响应需要消耗资源,频繁地中断不但不能加快数据收发速率,而且会极大地影响着整个系统的性能。
为了更好地服务中断,现在串口设备大多内部集成FIFO,可以设置FIFO级别、控制中断发生时FIFO的空闲度或者占用度,将串口从单个字节的中断中解放出来,但是这种FIFO一般容量较小(如常见的16B),当串口进行大量数据的传输时,中断频率依然较大,但相比查询方式而言,效率还是有很大的提高;其次,对于少量数据,由于没有达到中断产生的字节数,这些数据一直无法提供给驱动,造成数据的延迟,在某些情况下,这是不可容忍的。所以需要根据实际情况选择工作模式。
通常情况下,串口只是用于少量信息传输的实现,VxWorks下,Shell通常也是建立在串口之上,所以需要使用串口传递信息量较少的命令,此时使用FIFO就有些不合适,故大多数情况下,串口都工作在轮询模式下。以发送为例,每次驱动将一个字节的数据写入串口数据发送寄存器后,就不断地检查状态寄存器,查看这个字节是否被发送出去,如果没有,则不断地进行查询,一旦查询到这个字节已成功发送,则取下一个待发送的字节进行发送,直到当前内核串口缓存中所有的字节均已发送完毕。这种方式可以将写入串口的数据及时通过串口打印出来。只不过在打印之时会占用CPU资源,不过从宏观上来看,除了对进行打印的任务造成一些延迟,并不会对整个系统的性能造成影响。所以从表面上看,中断好像优于轮询模式,这只是单从CPU使用的角度考虑问题的,从实际工作情况出发,二者使用的范围都比较广。
可以做如下总结:中断使用在数据量较大的场合;轮询使用在数据量较少的场合。
4.3.5 驱动与硬件数据的交互方式——DMA和直接复制
驱动与硬件之间的数据交互方式主要分为两种:DMA方式和直接复制方式。DMA方式将CPU从数据复制过程中解放出来,数据复制操作由DMA控制器专门负责,最典型的使用实例即硬盘数据复制。DMA方式不能单独工作,一定要借助中断,所以DMA方式一般使用在大批量数据的复制上,即便数据输入/输出速率很大的网口设备也较少使用DMA方式,而是驱动直接从网卡设备硬件缓冲区中复制网络数据包。DMA方式在嵌入式系统下使用的概率较小,因为嵌入式系统很少有机会进行大批量数据的传输,而且DMA方式从创建数据复制环境到数据复制完成,再到CPU的后续处理,都具有较大的延迟,不利于对速度要求较高的场合。DMA最大的好处是数据在复制过程中无须CPU的干预,CPU可以独立出来运行其他任务,然而在很多场合,如果数据没有完成复制,CPU也没有其他什么“活”可以做,这一点在嵌入式系统下就尤为突出。故虽然很多书中在介绍与硬件之间的数据交互方式时,都会介绍DMA方式,但是实际上从外设驱动的角度看,DMA使用的概率比较小。
4.3.6 其他注意事项——Volatile关键字
外设驱动代码编写中有一个问题值得注意,不单是外设驱动,所有对外设寄存器进行操作的代码都必须注意一个问题,即对外设寄存器的操作必须使用volatile修饰符。虽然可以在VxWorks提供的sysPhysMemDesc数组初始化时将外设寄存器区间设置为non-cachable,但是还是要使用volatile修饰符,因为MMU机制并非在任何条件下都有效,如bootrom运行期间就不使用MMU机制,且VxWorks启动的早期阶段也没有MMU机制的帮助,而在这个过程中都需要对外设进行操作。
很多驱动程序出现一些异常的问题,仔细检查每个寄存器的配置都没有问题,甚至从网上下载一个针对该硬件的标准配置程序,在其他平台上可用,但是就是在自己的平台上运行不正常,此时就需要特别注意设备的寄存器区域是否都定义在volatile型。volatile是一个C语言规范中定义的修饰符,当一个变量使用该修饰符进行定义时,就表示CPU对该变量的每次访问都从RAM中(寄存器实际上也是RAM)取,而不要使用CPU内部的cache值,或者简单地说,使用volatile修饰符就是从单个变量的层次上禁止cache。