APP下载

嵌入式Linux的USB读卡器驱动深入研究*

2018-09-07

单片机与嵌入式系统应用 2018年9期
关键词:描述符读卡器驱动程序

(江西财经大学 现代经济管理学院,南昌330013)

引 言

Linux内核支持两种主要类型的USB驱动程序:宿主(host)系统上的驱动程序和设备(device)上的驱动程序。从宿主的观点来看(一个普通的USB宿主是一个桌面计算机),宿主系统的USB驱动层次控制插入其中的USB设备,而USB设备的驱动程序控制该设备如何作为一个USB设备和主机通信。设备驱动程序一般存放在内核的drivers/usb/gadget目录中,针对某个具体设备的USB驱动程序一般都是指宿主设备驱动,本文的USB读卡器驱动就是这一类。在Linux内核中,USB设备驱动是分很多层的,驱动层只是整个框架中的一小部分,要所有层的配合才能将设备驱动起来。USB驱动程序存在于不同的内核子系统(块设备、网络设备、字符设备等)和USB硬件控制器中。

USB核心为USB驱动程序提供了一个用于访问和控制USB硬件的接口,而不必考虑系统当前存在的各种不同类型的USB硬件控制器,在USB 核心中对USB需要使用的各种资源进行分配及初始化,如注册USB总线usb_bus_type、初始化USB主控制器、初始化USB HUB等;同时还提供了一些接口给其它层使用,如usb_alloc_dev函数(当有USB设备插入时,用于分配并初始化usb_device)。USB控制器主要负责处理各个USB设备的通信,解析驱动发送给每个设备的请求,并按照请求通知USB设备进行相应的处理,同时发送处理结果给驱动程序,并调用驱动传入的complete函数。

1 USB驱动层

1.1 USB设备基础

USB设备是一个非常复杂的东西。如图1所示,USB设备由配置、接口、端点组成,而USB驱动是绑定到接口上的,每个接口对应于一个设备驱动,对于某些多接口的设备(如带音频接口的USB键盘),此设备就同时需要USB键盘和USB音频这2个驱动。

图1 USB设备框架

如图1所示,一个USB设备通常包含一个或多个配置,一个配置通常包含一个或多个接口,一个接口通常包含0个或多个端点。层次结构如图2所示。

图2 USB设备层次结构

(1)端点

USB通信的最基本形式是通过端点来实现的,驱动和接口的通信都是通过端点,一个接口可以有0个或多个端点,端点根据传输方式的不同分为以下4种:控制端点、中断端点、等时端点和批量端点,分别对应于4种不同传输方式。

控制端点主要通过向USB设备发送请求来设置或获取USB设备的配置、状态等信息,每个USB设备都有一个名为“端点0”的控制端点,USB核心使用该端点在插入时进行设备的配置;中断端点主要实现以一个固定的速率来传输少量的数据,中断端点是USB键盘、鼠标等设备所使用的主要传输方式;批量端点用于传输大量数据,这些端点通常比中断端点大得多,常用于需要确保没有数据丢失的设备(如打印机、存储设备、网络设备);等时端点同样可以传输大批量数据,当数据是否到达没有保证,等时端点用于可以应付数据丢失的设备,这类设备更注重于保持一个恒定的数据流(如USB音频、视频)。

(2)接口

USB端点被捆绑为接口,USB接口对应于一个设备驱动,对应于一种功能的设备,对于多接口的复合设备就需要多个驱动。

(3)配置

USB接口被捆绑为配置,一个USB设备可以有多个配置,而且可以在配置之间切换已改变设备的状态。例如一些允许下载固件到其上的设备包含多个配置以完成这个工作,而某一个时刻只能激活一个配置。

1.2 USB urb

Linux内核中的USB代码通过一个称为urb(USB请求块)的东西和所有的USB设备通信。这个请求块使用struct urb结构体来描述,可以从include/linux/usb.h文件中找到。urb被用来以一种异步的方式向/从特定的USB 设备上的特定USB端点发送/接收数据。USB设备驱动程序可能会为单个端点分配许多urb,也可能对许多不同的端点重用单个的urb,这取决于驱动程序的需要。设备中的每个端点都可以处理一个urb队列,所以多个urb可以在队列为空之前发送到同一个端点。一个urb的典型生命周期如下:

①由USB设备驱动程序创建;②分配给一个特定USB 设备的特定端点;③由USB设备驱动程序递交到USB 核心;④由USB核心递交到特定设备的特定USB 主控制器驱动程序;⑤由USB主控制器驱动程序处理,从设备进行USB 传送;⑥当urb结束之后,USB主控制器驱动程序通知USB设备驱动程序。

urb可以在任何时刻被递交该urb的驱动程序取消掉,或者被USB核心取消,如果该设备已从系统中移除。urb被动态地创建,它包含一个内部引用计数,使得它们可以在最后一个使用者释放它们时自动地销毁。

struct urb结构体不能在驱动程序中或者另一个结构体中静态地创建,因为这样会破坏USB核心对urb所使用的引用计数机制。它必须使用usb_alloc_urb函数来创建。该函数原型如下:

struct urb *usb_alloc_urb(int iso_packets, int mem_flags);

第一个参数iso_packets是该urb应该包含的等时数据包的数量。如果不打算创建等时urb,该值应该设置为0。第二个参数mem_flags和传递给用于从内核分配内存的kmalloc函数的标志有相同的类型。如果该函数成功地为urb分配了足够的内存空间,指向该urb的指针将被返回给调用函数。如果返回值为NULL,说明USB核心内发生了错误,驱动程序需要进行适当的清理。

当一个urb被创建之后,在它可以被USB核心使用之前必须被正确地初始化。

驱动程序必须调用usb_free_urb函数来告诉USB核心驱动程序已经使用完urb。该函数只有一个参数:

void usb_free_urb(struct urb *urb);

这个参数指向所需释放的struct urb的指针。在该函数被调用之后,urb结构体就消失了,驱动程序不能再访问它。

一旦urb被USB驱动程序正确地创建和初始化之后,就可以提交到USB核心以发送到USB设备了。这是通过调用usb_submit_urb函数来完成的:

int usb_submit_urb(struct urb *urb, int mem_flags);

urb参数是指向即将被发送到设备的urb的指针。mem_flags参数等同于传递给kmalloc调用的同一个参数,用于通知USB核心如何在此时及时地分配内存缓冲区。

当一个urb被成功地提交到USB核心之后,在接收函数被调用之前不能访问该urb结构体中的任何字段。因为usb_submit_urb函数可以在任何时刻调用(包括从一个中断上下文中),mem_flags变量的内容必须是正确的。其实只有三个有效的值可以被使用,取决于usb_submit_urb何时被调用:

(1)GFP_ATOMIC

只要下列条件成立就应该使用该值:

①调用者是在一个urb结束处理例程、中断处理例程、底半部、tasklet或者定时器回调函数中。

②调用者正持有一个自旋锁或读写锁,注意如果持有了信号量,该值就不需要了。

③current->state不是TASK_RUNNING,该状态永远是TASK_RUNNING,除非驱动程序自己改变了当前的状态。

(2)GFP_NOIO

如果驱动程序处于块I/O路径中应该使用该值,在所有存储类型的设备的错误处理路径中也应该使用它。

(3)GFP_KERNEL

该值应该在前述类别之外的所有情况中使用。

1.3 USB驱动写法

在写USB驱动前,首先需要确定当前设备的一些基本信息,例如当前设备属于哪类设备,有哪些接口,每个接口有哪些端点,设备、接口、端点、配置描述符的信息是什么,这些信息都可以通过软件来获取,常用软件有windriver、USBtrace、bushound等,其中windriver不但可以看到设备的相关信息,还可以向设备发送标准请求,也可以直接向端点传输数据,通过此工具也可以验证写好的驱动通信是否正确。

U盘一般提供4个端点:一个控制端点、一个中断端点、两个批量端点,进入每个端点后就可以对各个端点进行操作了,对于控制端点可以对其发送各种USB标准请求,这里就可以发送获取描述符的请求了,对于两个bulk端点可以直接对其进行读写。

2 USB读卡器驱动的设计与实现

2.1 获取读卡器信息

刚刚拿到读卡器,首先当然是要获取USB读卡器的相关信息,通过工具USBtrace获取各描述符信息如表1所列。

表1 USBtrace获取各描述符信息

续表1

从这张表中可以获取到一些有用的信息,USB设备的venderID为0x3EB,deviceID为0x6124,设备有两个接口,第一个接口的类型为0x2,即Communications and CDC Control,这是一个通信类的接口,子类型为0x2 (Abstract Control Model),这个接口提供了1个端点,端点类型为中断端点,方向为in。第二个接口类型为0xa(CDC DATA),这个接口一般用于传输数据,所以这个结构提供2个bulk端点,一个作为输入,一个作为输出。当然每个设备都会有控制端点,控制端点既可作为输入又可作为输出。每个端点都是最大允许传输数据大小,2个bulk端点最大可传输64 B,而中断端点最大8 B。至此信息基本获取完毕,开始写驱动。

2.2 读卡器驱动设计与实现

首先注册usb_driver到设备模型,前面讲了是通过函数usb_register实现的,下面就是实现usb_driver了,在这里只实现了4个成员:id_table、probe、disconnect、name。id_table就是利用前面获取的venderID、deviceID来判断设备,probe和disconnect就是相应的初始化和卸载函数。

2.2.1 初始化、卸载设备

初始化工作在probe函数中实现,主要就是针对前面获取的端点信息,对每个端点进行初始化。首先要获取设备的端点,从设备描述符中看到设备有2个bulk端点和1个interrupt端点。Probe函数传入的是当前接口,即一个usb_interface结构体,如何从usb_interface中获取端点信息呢?在usb_interface结构中有个成员cur_altsetting,表示当前设置,这是一个usb_host_interface结构体,其中又有一个成员endpoint,这是一个usb_host_endpoint结构体的数组,每一个元素代表一个端点,但这里不保存控制端点,因为控制端点是被单独保存在设备结构体usb_device中的。

找到每个端点后开始为其创建相应管道。USB通信中有4种端点:控制、等时、中断、批量,同时对应4种传输方式,也对应了4种管道,其中4种管道中又分in或out管道,内核中提供了8个宏来创建管道,定义如下:

#defineusb_sndctrlpipe(dev,endpoint)/*创建out控制管道*/

((PIPE_CONTROL << 30) | __create_pipe(dev,endpoint))

#defineusb_rcvctrlpipe(dev,endpoint)/*创建in控制管道*/

((PIPE_CONTROL << 30) | __create_pipe(dev,endpoint) | USB_DIR_IN)

#defineusb_sndisocpipe(dev,endpoint)/*创建out等时管道*/

((PIPE_ISOCHRONOUS << 30) | __create_pipe(dev,endpoint))

#defineusb_rcvisocpipe(dev,endpoint)/*创建in等时管道*/

((PIPE_ISOCHRONOUS << 30) | __create_pipe(dev,endpoint) |USB_DIR_IN)

#defineusb_sndbulkpipe(dev,endpoint)/*创建out批量管道*/

((PIPE_BULK << 30) | __create_pipe(dev,endpoint))

#defineusb_rcvbulkpipe(dev,endpoint)/*创建in批量管道*/

((PIPE_BULK << 30) | __create_pipe(dev,endpoint) | USB_DIR_IN)

#defineusb_sndintpipe(dev,endpoint)/*创建out中断管道*/

((PIPE_INTERRUPT << 30) | __create_pipe(dev,endpoint))

#defineusb_rcvintpipe(dev,endpoint)/*创建in中断管道*/

((PIPE_INTERRUPT << 30) | __create_pipe(dev,endpoint) | USB_DIR_IN)

其中__create_pipe也是一个宏,定义如下:

static inline unsigned int __create_pipe(struct usb_device *dev,

unsigned int endpoint){

return (dev->devnum << 8) | (endpoint << 15);

}

从以上几个宏可以知道管道的组成,其实管道就是提供了一个通信地址,让HC知道这个urb包应该发到哪个设备、哪个端点、是什么类型的包。在这里看到了4个宏,其中PIPE_ISOCHRONOUS就是标志等时通道、PIPE_INTERRUPT就是中断通道、PIPE_CONTROL就是控制通道、PIPE_BULK 就是BULK通道。

在内核里使用一个unsigned int类型的变量来表征一个pipe,其中8~14位是设备号,即devnum,15~18位是端点号,即endpoint。宏USB_DIR_IN用来在pipe里面标志数据传输方向,一个管道要么只能输入,要么只能输出。在pipe里面,第7位(bit 7)是表征方向的。所以这里0x80也就是说让bit 7 为1,这就表示传输方向是由设备向主机,也就是所谓的in,而如果这一位是0,就表示传输方向是由主机向设备的,也就是所谓的out。正是因为USB_DIR_OUT是0,而USB_DIR_IN是1,所以定义管道的时候只用到了USB_DIR_IN,而没有用到USB_DIR_OUT,因为它是0,任何数和0相或都没有意义。

图3 循环缓冲区

接下来为每一个端点分配一个缓冲区,这个缓冲区就是urb结构体中的transfer_buffer,这个缓冲区就是传输过程中用来保存数据的,这时使用的分配函数是usb_buffer_alloc,这个函数的作用就是先分配一段内存空间,然后对其进行dma映射,就是从dma缓冲池分配一段内存,即usb_hcd->pool[i]。

最后将接口注册到字符设备层,通过函数usb_register_dev实现,由于客户需要定位每个USB口,这里笔者做了一个封装,做了一点点简单的改动,下面会讲到。

2.2.2 读、写数据

所谓读、写设备就是实现file_operations中的read、write函数。

这里的read、write就是实现对usb的读、写数据和USB通信,这里在一个接口函数基础上笔者做了一个简单的封装,根据实际需求改进了一点小地方,函数声明如下:

int usb_reader_bulk_msg(struct urb *urb, struct usb_reader_data *usb_reader, unsigned int pipe,void *data, int len,int *actual_length, int timeout);

函数传入7个参数,第一个参数为需要发送的urb包,这个urb在probe函数中已分配好内存,usb_reader就是读卡器相关信息结构体,这是为该驱动专门定义的结构体,data是传输数据的缓冲区,len为希望传输多少数据,actual_length为实际传输的数据大小,timeout为传输的超时值,若在timeout内未完成传输则返回传输失败。

这个函数主要实现填充传入的urb,主要填充管道、缓冲区、传输数据长度、dma缓冲区、complete函数等成员,最后通过usb_submit_urb将urb提交到HC层做处理,usb_submit_urb在讲HC层时已经分析过了,这里只要提交过去就行了。

在实现读写函数时,使用了一个4 096 B大小的循环缓冲区,每次应用程序调用write时,先将应用程序传入的数据写到USB读卡器,然后马上从读卡器上读取返回数据,并将返回数据写到缓冲区中。当应用程序调用read时,将循环缓冲区的数据返回给用户,而不是直接从设备读取数据再返回。这样处理的好处就是允许用户进行多次连续写入操作,最后只要通过一个读取操作就能把前几次写入操作的结果全部读取出去。

循环缓冲区写入时将数据存入数组的尾部,读取时从数组的另一端开始,当写入数据到达数组的尾部时,回到数组头部继续写。因此,一个循环缓冲区需要一个数组以及两个索引值:一个用于下一个要写入的数据的位置,另一个用于指定下一个要从缓冲区中移走的位置。

如图3所示,这个缓存被定义成一个空情况,由读写指针相同来指示, 而满情况发生在写指针紧跟在读指针后面的时候(小心解决绕回!)。

2.2.3 ioctl函数

这里还提供了一个ioctl接口,主要用于实现USB的标准请求,USB标准请求都是通过控制端点来实现的,控制端点的管道创建比较特殊,最后一个参数为0,创建函数如下:

usb_rcvctrlpipe(usb_reader->dev, 0);

usb_sndctrlpipe(usb_reader->dev, 0);

标准请求和请求参数通过setup包传送给USB设备,一个setup包由8个字节组成,在内核中用usb_ctrlrequest结构体表示,结构体定义如下:

struct usb_ctrlrequest{

__u8bRequestType;

__u8bRequest;

__le16wValue;

__le16wIndex;

__le16wLength;

} __attribute__((packed));

usb_ctrlrequest被保存在urb->setup_packet里,共8个字节,意义如下:

byte0:bmRequestType,注意在刚才代码中数据结构struct ctrlrequest 里边写的是bRequestType,但是它们对应的是相同的内容。而之所以USB协议里写成bmRequestType,是因为它实际上又是一个位图(m表示map),也就是说,尽管它只有1个字节,但是仍然被当作8位来用。

USB协议定义了11个标准请求,各请求对应setup包的各成员的值。

每个标准请求对应的代码见表2,这个表对应的代码和内核中为每个请求定义的宏是一一对应的。

对于DESCRIPTOR设置或获取请求,需要填充一个描述符种类或索引,USB设备描述符对应的索引见表3。

表中列出了USB设备的5大描述符,USB设备的所有信息都保存在这些描述符中,在内核中为这5大描述符分别定义了相应的结构体,分别是usb_device_descriptor、usb_config_descriptor、usb_string_descriptor、usb_interface_descriptor、usb_endpoint_descriptor,上面提到的一些标准请求就是围绕这些结构体来展开的。

表2 USB标准请求对应代码

表3 设备描述符代码

结 语

猜你喜欢

描述符读卡器驱动程序
基于结构信息的异源遥感图像局部特征描述符研究
与淘汰命运抗争 看懂笔记本的读卡器
基于AKAZE的BOLD掩码描述符的匹配算法的研究
计算机硬件设备驱动程序分析
Linux单线程并发服务器探索
利用CNN的无人机遥感影像特征描述符学习
基于EMV非接通信规范的非接触读卡器设计
为二维码识别的献礼之作——评测平治二维码门禁读卡器
MATLAB实现组态王中TCP/IP读卡器监控方法
基于MPC8280的CPU单元与内部总线驱动程序设计