随着USB设备的不断增加,我们这些开发人员也就多了对USB设备进行驱动程序开发的工作。通过本文,我希望能让更多的朋友理解Windows CE下对USB设备的驱动模型及样例程序中的实现过程,以样例代码为基础理顺USB设备驱动程序的开发思路。在看样例程序之前,我们就先来看下图:
在此图中,我们可以非常清晰的看到主机和物理外设之间的结构方式,在主机端,通过USBD模块和HCD模块使用默认的PIPE访问一个通用的逻辑设备,实际上就是说USBD和HCD是一组抽象出来的访问所有USB设备的逻辑接口,它们负责管理所有USB设备的连接、加载、移除、数据传输和通用的配置。其中HCD是主机控制驱动,是为USBD提供底层的功能访问服务,USBD是USB总线驱动,位于HCD的上层,利用HCD的服务提供较高层次抽象的功能。
在数据传输的过程中,操作流程通常按下列的次序进行:①USB设备驱动程序进行数据传输的初始化,即通过USBD接口函数给USBD模块发送数据传输的请求。②USBD模块将该请求分成一些单独的事务。③HCD模块排出事务次序。④主控制器硬件执行事务。
有了对这个结构的认识,我们可以明确的是我们要写的就是最上端的USB设备驱动程序,它是工作于USBD之上,所以实际上我们的工作就变成了利用USBD提供的接口针对特定的物理设备来完成USB设备驱动程序,而暂时与其他的部分无关。
(1)BSP中的USB文件夹
以6.5为例,路径是PUBLIC/COMMON/OAK/DRIVERS/USB。
在USB文件夹下,分成了CLASS,CLIENTS,COMMON,HCD,INC,USBD几个文件夹,其中INC和COMMON里面有一个lock.c的程序,代码很简单,只是一个类似临界区的封装体,可以保护多线程对同一内存区域的读写访问,可以先不去管它。CLIENTS文件夹可能最初微软的开发人员是用来放置设备驱动程序的,但是后来没有放,而发布的时候也没有删除,所以遗留了下来。USBD和HCD是前述的底层驱动,里面含有很多子文件夹和程序,由于我们只针对USB设备驱动,因此对这两部分不做分析。
重点就在CLASS文件夹了,展开来看,里面又包含了COMMON、HID、PRINTER、STORAGE几个文件夹。同样,COMMON里面存放的源程序是为HID、PRINTER、STORAGE所共有的。HID是USB输入设备如键盘/鼠标的样例驱动程序,PRINTER是USB打印机的样例驱动程序,STORAGE是USB存储设备如U盘的样例程序。
(2)驱动的头文件
我们此次以USB存储设备为例,所以再来展开STORAGE文件夹,其中的INC文件夹里面是头文件,CLASS是USB存储设备的驱动程序,DISK是磁盘驱动程序。这里为什么有两个驱动程序呢,我来简要解释一下。
驱动程序工作在硬件与操作系统之间,它有两个功能,一个是将操作系统转发来的操作以符合指定硬件设备的形式控制硬件设备;另一个是向操作系统提供这个访问接口。比如说U盘,一方面驱动程序要把操作系统对U盘的识别、读、写等操作转换成U盘的动作;另一方面又告诉操作系统这是个U盘,可以当成一个文件夹或文件系统来用,能够接受标准的文件操作命令。所以此处存在两个驱动。
另外还有一个文件夹/PUBLIC/COMMON/DDK/INC,这里面是与设备驱动有关的头文件,对于USB设备,相关的文件有USB100.H, USBTYPES.H, USBDI.H,这里面前两个里面关于USB的定义是完全符合USB规范的,而USBDI.H文件里的内容就是USBD总线驱动程序向USB设备驱动程序提供的接口描述,在开发USB设备驱动时必须要包含此头文件,这样才可以得到USBD接口的原型。
找到USBDI.H,我们首先看到的一个大的结构体就是_USB_DRIVER_SETTINGS,注意这个结构体不是USB规范中的USB设备描述,而是为了CE设备管理器(device.exe)加载USB设备驱动程序方便而建立的。该结构体中对供应商描述、设备描述和Interface的描述是用来匹配注册表中对USB设备驱动的注册表键,当设备管理器发现你设备的这些值与注册表中的这些值相符时,就会加载你的驱动。也就是说它是与你的设备唯一对应的东西,是一种标识。该结构体的供应商部分的描述需要根据你的设备的供应商信息来填,设备描述的设备类、子类、协议等可以在USB规范中找到。
(3)设备驱动的主要函数
在接下来有三个函数是必须由USB设备驱动程序实现的,这几个函数就是:
USBDeviceAttach:设备加载的时候由系统调用
USBInstallDriver:设备第一次加载的时候由系统调用,用来安装注册表配置以便搜索设备
USBUnInstallDriver:设备移除后清理由上一个函数写入的注册表配置
USBDeviceAttach是当USB 设备连接到主计算机时运行,USBD模块会调用这个函数初始化USB设备,取得USB设备信息和配置USB设备,并且申请必需的资源。USBInstallDrive是在第一次加载USB设备驱动程序时首先被调用,它使得驱动程序能创建需要的注册键,用于将一个驱动程序所需的注册表信息写入到HKEY_LOCAL_MACHINE/Drivers/USB/ClientDrivers目录下,例如设备名称等。需要注意的是,USB设备驱动程序不使用标准的注册表函数,而是使用RegisterClientDriverID()、RegisterClientSettings()函数来注册相应的设备信息。USBUninstallDriver是在用户删除USB设备驱动程序时调用,负责删除注册键并释放其它相关资源。它通过调用 UnRegisterClientSettings()和UnRegisterClientDriverID()函数来删除由驱动程序的USBInstallDriver()函数创建的所有注册键。因此,我们在驱动程序中就需要严格按照这三个函数的原型来实现,否则就不能为设备管理器所识别。
其实除了这三个,还有一个函数指针所指向的函数: *LPDEVICE_NOTIFY_ROUTINE。这个指针所指向的函数是用来接收通知消息的,既然微软说任何USB设备必须实现USB_CLOSE_DEVICE消息的响应,那么这个指针所指向的函数自然也就是必须要实现的了。
继续向下看,是一组函数的原型,这些函数就是USBD向设备驱动程序提供的服务接口,有些函数是可以任意调用的,用来完成版本信息读取、注册表操作和设备驱动程序注册,这些函数有:GetUSBDVersion RegisterClientDriverID UnRegisterClientDriverID RegisterClientSettings UnRegisterClientSettings OpenClientRegistryKey。还有大量的函数是必须通过指针调用的,通常只允许在驱动程序中调用,为了方便使用,在这里给出一个_USB_FUNCS的结构体,每一个结构体成员对应了一个函数指针,这样在驱动程序中要想使用USBD函数只能通过一个结构体变量来进行了,在这里我们要记住这个结构体的名字,并且微软还对这个结构体变量进行了以下的类型定义:typedef struct _USB_FUNCS USB_FUNCS, * PUSB_FUNCS, * LPUSB_FUNCS;
到此我们发现,大部分的USB工作都已经被USBD完成了,我们为了实现自己的设备驱动,只需要利用这些指针或函数,来实现四个我们自己的函数,然后在其中匹配上我们自己的设备就可以了。
(4)理解USB STORAGE实例
我们就进入到DRIVERS/USB/CLASS/STORAGE/CLASS文件夹下,先来了解两个头文件,分别是STORAGE/INC/usbmsc.h和STORAGE/CLASS/usbmscp.h,其中前者是USB存储设备公用的头文件,后者是需要按照自己的设备更改的头文件。
我们先来看前者。在usbmsc.h这个头文件中,前边定义了很多常量,包括子类和协议的常量,这是从哪里来的呢?这些量值是依据USB设备规范得来的,在规范上都作了定义,所以此处的值必须与USB规范中的相一致。再向下的命令块结构体和数据块结构体是用来与USB设备通讯用的,可以通过这两个结构体的实例与USB设备传输数据。
再来看usbmscp.h这个头文件,这个头文件是要按照自己的需要和USB设备来进行修改的,比如DRIVER_NAME_SZ是驱动程序的名字,RESET_TIMEOUT 是一个超时的默认值。还有很重要的一个就是USBMSC_DRIVER_SETTINGS的设置,这个设置是与USBDI.H中的USB_DRIVER_SETTINGS结构体一一对应的,为了符合我自己的设备,通常要把dwVendorId和dwProductId等设置成设备的对应值,比如我的U盘的VendorID是0x058F,ProductID是0x9321,那我就会把这两个值对应的写在相应的位置上。同时在系统注册表中也会利用这两个值修改注册表的键以便设备管理器可以顺利的找到我的设备驱动。下面还有一个_USBMSC_DEVICE结构体,它是用来描述你自己的USB存储设备的,是封装了USBD函数表指针、磁盘设备指针、管道和配置项的最重要的数据结构,在驱动程序实现上此数据结构就是重点的参数,
好了,对这两个头文件有所了解以后,我们就进入最关键的部分,源程序。我们接下来来看usbmsc.c这个文件。为什么要先看这个文件而不是同一文件夹下的其他几个文件呢?在这个文件夹中有一个usbmsc.def的文件,大家都知道它是定义了导出函数的,通常与它同名的程序文件都会含有DllEntry的入口,既然入口在这,那我们自然就先来看这个文件了。DllEntry入口函数的下面,就是USBInstallDriver()这个函数了,它的作用是进行与USB设备相关的注册表操作,主要的语句是:
bRc = RegisterClientDriverID( wsUsbDeviceID );
bRc = RegisterClientSettings( szDriverLibFile, wsUsbDeviceID, NULL, &usbDriverSettings );
即先注册设备类别,然后是设备细节。这几个与注册有关的函数在前面我们提到过,这里我们可以看到USBD对设备驱动程序的重要性。
(5)STORAGE实例函数讲解
继续向下看,我们发现了USBDeviceAttach()函数,这可是最重要的地方了,当有USB设备插入插口以后,操作系统是如何识别它的呢,如何将其做为一个文件夹加以访问的呢?
我们来看程序的第4行,这里有一个判断语句,它是在判断插入的设备是否是USBMSC_INTERFACE_CLASS类型的,这个常量是在usbmsc.h文件中定义的,也就是说如果设备不是USB存储设备,那么就结束这个函数,也就是此驱动只能处理USB存储设备。当发现设备符合此驱动程序的要求后,就通过函数ParseUsbDescriptors()来解析这个设备,这个函数在下面的程序中将被实现,我们可以看一下该函数的函数体,很显然,它是在为设备进行各种配置。
再往下,分配内存,设置标志,从注册表中读取信息。注意,这里读取到的注册表信息是
[HKEY_LOCAL_MACHINE/Drivers/USB/ClientDrivers/Mass_Storage_Class/6]
"DLL"="USBDISK6.DLL"
"Prefix"="DSK"
"Folder"="USB Disk"
"IOCTL"=dword:4
"IClass"="{A4E7EDDA-E575-4252-9D6B-4195D48BB865}"
由此可以看出,通过此处的注册表读取,驱动程序可以知道这个设备将通过哪种形式以及哪个DLL向操作系统提供接口。
最关键的部分就在接下来的LoadDriver()那句,加载了另一个驱动程序的DLL文件,就是上述注册表中的USBDISK6.DLL文件,计数器增一,取到该文件中UsbDiskAttach函数及UsbDiskDetach函数的地址。当调用了USBDISK6.DLL后,操作系统就会按该文件中的程序以一个磁盘的形式或文件夹的形式进行处理,通过文件系统的操作,就可以对其进行读写控制了。我们也可以看一下HID设备的这个函数,它也是通过这种方式让操作系统知道把USB设备识别成鼠标设备的。接下来我们要弄明白的就是到底操作系统是如何通过抽象的DISK读写具体的设备呢?
我们先看USB/CLASS/STORAGE/DISK/SCSI2/usbdisk6.def文件。在这个文件中可以看到,该DLL一共导出了14个函数,其中两个是上次内容当中被设备驱动程序调用的UsbDiskAttach和UsbDiskDetach,余下的是一组以DSK开头的流驱动接口,易见,USBDISK是以流驱动的形式向操作系统提供服务的。
(6)USB设备的流驱动
实现USB加载流驱动程序大致需要完成以下步骤:
(1)选择代表设备的文件名前缀。前缀非常重要,设备管理器在注册表中通过前缀来识别设备。同时,在流接口命名时也将这个前缀作为入口点函数的前缀,如果设备前缀为XXX,那么流接口对应为XXX_Close,XXX_Init等。
(2)设置驱动的各个入口点函数。所谓入口点是指提供给设备管理器的标准文件I/O接口。在生成一个DLL后,就用设备文件名前缀替换名字中的XXX。因此,每个加载式流接口驱动程序必须实现XXX_Init()、XXX_IOControl()以及XXX_PowerUp()等一组标准的函数,用来完成标准的文件I/O函数和电源管理等。
(3)建立.DEF文件。当设备管理器初始化USB设备编译出来的流接口函数后,还必须建立一个.def文件。DEF文件定义了DLL要导出的接口集,而且加载式流驱动大多是以DLL形式存在的,所以应将DLL和DEF的文件名统一起来。DEF文件告诉链接程序需要输出什么样的函数,最后将驱动程序编译到内核中去,这样这个USB设备流接口驱动程序就可以被应用程序调用。
(4)在注册表中为驱动程序建立表项。在注册表中建立驱动程序入口点,这样设备管理器才能识别和管理这个驱动。此外,注册表中还能存储额外的信息,这些信息可以在驱动运行之后被使用到。
我们来看DISK.C这个程序文件。找到DSK_Read和DSK_Write两个函数,这两个函数都是形如
UNREFERENCED_PARAMETER(pDevice);这样的实现,也就是说用户无法通过常规的ReadFile和WriteFile函数使用这个设备,那怎么办?我们应该马上想到DSK_IOControl()这个函数,当遇到某些设备无法用常规的文件操作函数操作时,我们有DeviceIoControl()用户函数可以使用,而这个函数就会调用到驱动程序中的DSK_IOControl函数。
在这个函数中,我们找到了对IOCTL_DISK_READ等命令的处理程序,其中最关键的一句就是ScsiRWSG(pDevice, pSgReq, pDevice->Lun, bRead),即调用了一个ScsiRWSG的函数。在这个函数中我们发现它再次调用了ScsiReadWrite()这个函数进行读写操作,里面有我们最重要的一行调用,即调用了UsbsDataTransfer()函数,它就是在USB设备的驱动程序当中。
通过这一过程我们发现,那些Scsi的函数都只是在准备一些缓冲区、数据结构等,并没有对硬件进行操作,真正要操作硬件设备的还是由USB驱动程序来完成的,回到usbmsc.c程序中来,找到UsbsDataTransfer函数,这个函数很简单,根据传输协议调用CBIT_DataTransfer()或BOT_DataTransfer() 即可。
参考原文:http://hi.baidu.com/xjh_sz520/blog/item/a05ddddec444eeb1cd1166d2.html
参考原文:http://hi.baidu.com/btkxql/blog/item/4ca38023b4cec75f9822ed2b.html