号设备的第2号管道。Phase一栏标识传输是输入还是输出。在Data一栏中记录着一次传输的具体内容。
其中,bRequest代表不同的USB请求,它们分别是以下的几种请求,定义在DDK的usb100.h文件中:
#define USB_REQUEST_GET_STATUS 0x00 #define USB_REQUEST_CLEAR_FEATURE 0x01 #define USB_REQUEST_SET_FEATURE 0x03 #define USB_REQUEST_SET_ADDRESS 0x05 #define USB_REQUEST_GET_DESCRIPTOR 0x06 #define USB_REQUEST_SET_DESCRIPTOR 0x07 #define USB_REQUEST_GET_CONFIGURATION 0x08 #define USB_REQUEST_SET_CONFIGURATION 0x09 #define USB_REQUEST_GET_INTERFACE 0x0A #define USB_REQUEST_SET_INTERFACE 0x0B
|
17.2.3 设备描述符
在控制管道发起USB设备请求,其中很常见的请求是USB_REQUEST_GET_ DESCRIPTOR,即请求USB设备回答设备或者管道描述符。在请求描述符时,bmRequestType可以指定是针对设备还是针对管道的。
当请求设备描述符后,设备会回答主机该设备的设备描述符,设备描述符是一种固定的数据结构,它定义在DDK中的usb100.h文件中。
typedef struct _USB_DEVICE_DESCRIPTOR { UCHAR bLength; UCHAR bDescriptorType; USHORT bcdUSB; UCHAR bDeviceClass; UCHAR bDeviceSubClass; UCHAR bDeviceProtocol; UCHAR bMaxPacketSize0; USHORT idVendor; USHORT idProduct; USHORT bcdDevice; UCHAR iManufacturer; UCHAR iProduct; UCHAR iSerialNumber; UCHAR bNumConfigurations; } USB_DEVICE_DESCRIPTOR, *PUSB_DEVICE_DESCRIPTOR;
|
bLength:设备描述符的bLength域应等于18。
bDescriptorType:bDescriptorType域应等于1,以指出该结构是一个设备描述符。
bcdUSB:bcdUSB域包含该描述符遵循的USB规范的版本号(以BCD编码)。现在,设备可以使用值0x0100或0x0110来指出它所遵循的是1.0版本还是1.1版本的USB规范。
bDeviceClass:指出设备类型。
bDeviceSubClass:指出设备子类型。
bDeviceProtocol:指出设备类型所使用的协议。
bMaxPacketSize0:设备描述符的bMaxPacketSize0域,给出了默认控制端点(端点0)上的数据包容量的最大值。
idVendor:厂商代码。
idProduct:厂商专用的产品标识。
bcdDevice:bcdDevice指出设备的发行版本号(0x0100对应版本1.0)。
iManufacturer、iProduct、iSerialNumber:iManufacturer、iProduct和iSerialNumber域指向一个串描述符,该串描述符用人类可读的语言描述设备生产厂商、产品和序列号。这些串是可选的,0值代表没有描述串。如果在设备上放入了序列号串,Microsoft建议应使每个物理设备的序列号唯一。
bNumConfigurations:bNumConfigurations指出该设备能实现多少种配置。Microsoft的驱动程序仅工作于设备的第一种配置(1号配置)。 如图17-18所示为笔者用BusHound截获的USB移动硬盘的请求设备描述符,前面已经介绍过,在控制管道中,传输分为三个阶段。第一阶段是令牌阶段,这里Host向设备发送“80 06 00 01 00 00 12 00”8个字节,可以参见表17-1中的解释。第二阶段是数据传输阶段,方向是由设备传给主机,这个例子中设备给主机传递了18(0x12)个字节,这18个字节对应着USB_DEVICE_DESCRIPTOR数据结构。第三阶段是握手阶段,在BusHound软件中没有体现出来。
|
(点击查看大图)图17-18 用BusHound抓取设备描述符
|
17.2.4 配置描述符
每个设备有一个或多个配置描述符,它们描述了设备能实行的各种配置方式。DDK中定义的配置描述符结构如下:
typedef struct _USB_CONFIGURATION_DESCRIPTOR { UCHAR bLength; UCHAR bDescriptorType; USHORT wTotalLength; UCHAR bNumInterfaces; UCHAR bConfigurationValue; UCHAR iConfiguration; UCHAR bmAttributes; UCHAR MaxPower; } USB_CONFIGURATION_DESCRIPTOR, *PUSB_CONFIGURATION_DESCRIPTOR;
|
bLength:bLength应该为9。
bDescriptorType:bDescriptorType应该为2,即是一个9字节长的配置描述符。
wTotalLength:wTotalLength域为该配置描述符长度加上该配置内所有接口和端点描述符长度的总和。通常,主机在发出一个GET_DESCRIPTOR请求并正确接收到9字节长的配置描述符后,就会再发出一个GET_DESCRIPTOR请求并指定这个总长度。第二个请求把这个大联合描述符传输回来。
bNumInterfaces:指出该配置有多少个接口。
bConfigurationValue:bConfigurationValue域是该配置的索引值。
iConfiguration:iConfiguration域是一个可选的串描述符索引,指向描述该配置的Unicode字符串。此值为0表明该配置没有串描述符。
bmAttributes:bmAttributes字节包含描述该配置中设备电源和其他特性的位掩码。
MaxPower:MaxPower域中指出要从USB总线上获取的最大电流量。
如图17-19所示为笔者用BusHound截获的USB移动硬盘的请求配置描述符。其中第一行是Host向Device发送Token令牌,而第二行是Device向Host返回的数据。
|
(点击查看大图)图17-19 用BusHound抓取设配置描述符
|
17.2.5 接口描述符
每个配置有一个或多个接口描述符,它们描述了设备提供功能的接口。
typedef struct _USB_INTERFACE_DESCRIPTOR { UCHAR bLength; UCHAR bDescriptorType; UCHAR bInterfaceNumber; UCHAR bAlternateSetting; UCHAR bNumEndpoints; UCHAR bInterfaceClass; UCHAR bInterfaceSubClass; UCHAR bInterfaceProtocol; UCHAR iInterface; } USB_INTERFACE_DESCRIPTOR, *PUSB_INTERFACE_DESCRIPTOR;
|
bLength:bLength应该为9。
bDescriptorType:bDescriptorType域应为4。
bInterfaceNumber:bInterfaceNumber是索引值。
bAlternateSetting:bAlternateSetting是索引值。用在SET_INTERFACE控制事务中以指定要激活的接口。
bNumEndpoints:bNumEndpoints域指出该接口有多少个端点,不包括端点0,端点0被认为总是存在的,并且是接口的一部分。
bInterfaceClass:bInterfaceClass为接口类。
bInterfaceSubClass:bInterfaceSubClass为子接口类。
bInterfaceProtocol:bInterfaceProtocol为协议。
iInterface:iInterface是一个串描述符的索引,0表示该接口无描述串。
如图17-20所示为笔者用BusHound截获的USB移动硬盘的请求配置描述符和接口描述符。其中第一行是Host向Device发送Token令牌,而第二行是Device向Host返回的数据。可以看出图中有一个配置描述符,后面紧接着一个接口描述符(“09 04 00 00 02 00 06 50 05”),后面还有两个接口描述符(下一节介绍)。
|
(点击查看大图)图17-20 用BusHound抓取接口描述符
|
17.2.6 端点描述符
接口可以没有或有多个端点描述符,它们描述了处理事务的端点。DDK中定义的端点描述符结构如下:
typedef struct _USB_ENDPOINT_DESCRIPTOR { UCHAR bLength; UCHAR bDescriptorType; UCHAR bEndpointAddress; UCHAR bmAttributes; USHORT wMaxPacketSize; UCHAR bInterval; } USB_ENDPOINT_DESCRIPTOR, *PUSB_ENDPOINT_DESCRIPTOR;
|
bLength:bLength应为7。
bDescriptorType:bDescriptorType应该为5。
bEndpointAddress:bEndpointAddress域编码端点的方向性和端点号。
bmAttributes:bmAttributes的低两位指出端点的类型。0代表控制端点,1代表等时端点,2代表批量端点,3代表中断端点。
wMaxPacketSize:wMaxPacketSize值指出该端点在一个事务中能传输的最大数据量。
bInterval:中断端点和等时端点描述符还有一个用于指定循检间隔时间的bInterval域。
17.3 USB驱动开发实例
本节具体介绍如何进行USB驱动的开发,本节采用的源码来源自DDK的源程序,其位置在DDK子目录的src\wdm\usb\bulkusb目录下。该示例很全面地支持了即插即用IRP的处理,也很全面地支持了电源管理,同时很好地支持了USB设备的bulk读写。如果从头开发USB驱动,往往很难达到USB驱动的稳定性,所以强烈建议读者在此驱动修改的基础上进行USB驱动开发。
17.3.1 功能驱动与物理总线驱动
DDK已经为USB驱动开发人员提供了功能强大的USB物理总线驱动(PDO),程序员需要做的事情是完成功能驱动(FDO)的开发。驱动开发人员不需要了解USB如何将请求转化成数据包等细节,程序员只需要指定何种管道,发送何种数据即可。
当功能驱动想向某个管道发出读写请求时,首先构造请求发给USB总线驱动。这种请求是标准的USB请求,被称为URB(USB Request Block),即USB请求块。这种URB被发送到USB物理总线驱动以后,被USB总线驱动所解释,进而转化成请求发往USB HOST驱动或者USB HUB驱动,如图17-21所示。
可以看出,USB总线驱动完成了大部分工作,并留给USB功能驱动标准的接口,即URB请求。USB驱动开发人员只需要根据不同的USB设备的设计要求,在相应的管道中发起URB请求即可。
17.3.2 构造USB请求包
USB驱动在与USB设备通信的时候,如在控制管道中获取设备描述符、配置描述符、端点描述符,或者在Bulk管道中获取大量数据,都是通过创建USB请求包(URB)来完成的。URB中填充需要对USB的请求,然后将URB作为IRP的一个参数传递给底层的USB总线驱动。在USB总线驱动中,能够解释不同URB,并将其转化为USB总线上的相应数据包。
DDK提供了构造URB的内核函数UsbBuildGetDescriptorRequest,其声明如下:
VOID UsbBuildGetDescriptorRequest( IN OUT PURB Urb, IN USHORT Length, IN UCHAR DescriptorType, IN UCHAR Index, IN USHORT LanguageId, IN PVOID TransferBuffer OPTIONAL, IN PMDL TransferBufferMDL OPTIONAL, IN ULONG TransferBufferLength, IN PURB Link OPTIONAL );
|
Urb:用来输出的URB结构的指针。
Length:用来描述该URB结构的大小。
DescriptorType:描述该URB的类型。它可以是USB_DEVICE_DESCRIPTOR_TYPE、USB_CONFIGURATION_DESCRIPTOR_TYPE和USB_STRING_DESCRIPTOR_ TYPE。
Index:用来描述设备描述符的索引。
LanguageId:用来描述语言ID。
TransferBuffer:如果用缓冲区读取设备,TransferBuffer是缓冲区内存的指针。
TransferBufferMDL:如果用直接读取内存时,TransferBufferMDL是直接读取内存时MDL的指针。
TransferBufferLength:对于该URB所操作内存的大小。
在功能驱动中,所有与USB的通信,都需要用这个函数创建URB,并通过IRP发送到底层USB总线驱动,以下是一个最基本的示例。
#001 UsbBuildGetDescriptorRequest( #002 urb, #003 (USHORT) sizeof(struct _URB_ CONTROL_DESCRIPTOR_REQUEST), #004 USB_DEVICE_DESCRIPTOR_TYPE, #005 0, #006 0, #007 deviceDescriptor, #008 NULL, #009 siz, #010 NULL);
|
17.3.3 发送USB请求包
功能驱动将URB包构造完毕后,就可以发送到底层总线驱动上了。URB包要和一个IRP相关联起来,这就需要用IoBuildDeviceIoControlRequest创建一个IO控制码的IRP,然后将URB作为IRP的参数,用IoCallDriver将URB发送到底层总线驱动上。由于上层驱动无法知道底层驱动是同步还是异步完成的,因此需要做一个判断。if语句判断当异步完成IRP时,用事件等待总线驱动完成这个IRP。
#001 //该函数实现对发送URB到USB物理总线驱动 #002 NTSTATUS #003 CallUSBD( #004 IN PDEVICE_OBJECT DeviceObject, #005 IN PURB Urb #006 ) #007 { #008 PIRP irp; #009 KEVENT event; #010 NTSTATUS ntStatus; #011 IO_STATUS_BLOCK ioStatus; #012 PIO_STACK_LOCATION nextStack; #013 PDEVICE_EXTENSION deviceExtension; #014 #015 //首先是变量初始化 #016 irp = NULL; #017 deviceExtension = DeviceObject->DeviceExtension; #018 //初始化事件 #019 KeInitializeEvent(&event, NotificationEvent, FALSE); #020 //创建IO控制码相关的IRP #021 irp = IoBuildDeviceIoControlRequest (IOCTL_INTERNAL_USB_SUBMIT_URB, #022 deviceExtension-> TopOfStackDeviceObject, #023 NULL, #024 0, #025 NULL, #026 0, #027 TRUE, #028 &event, #029 &ioStatus); #030 #031 if(!irp) { #032 //如果IRP创建失败则返回 #033 BulkUsb_DbgPrint(1, ("IoBuildDeviceIo ControlRequest failed\n")); #034 return STATUS_INSUFFICIENT_RESOURCES; #035 } #036 //得到下一层设备栈 #037 nextStack = IoGetNextIrpStackLocation(irp); #038 ASSERT(nextStack != NULL); #039 nextStack->Parameters.Others.Argument1 = Urb; #040 BulkUsb_DbgPrint(3, ("CallUSBD::")); #041 BulkUsb_IoIncrement(deviceExtension); #042 //通过IoCallDriver将IRP发送到底层驱动 #043 ntStatus = IoCallDriver(deviceExtension-> TopOfStackDeviceObject, irp); #044 //如果IRP是异步完成时,等待其结束 #045 if(ntStatus == STATUS_PENDING) { #046 //等待IRP结束 #047 KeWaitForSingleObject(&event, #048 Executive, #049 KernelMode, #050 FALSE, #051 NULL); #052 ntStatus = ioStatus.Status; #053 } #054 //调用结束 #055 BulkUsb_DbgPrint(3, ("CallUSBD::")); #056 BulkUsb_IoDecrement(deviceExtension); #057 return ntStatus; #058 }
|
此段代码可以在配套光盘中本章的sys目录下找到。
17.3.4 USB设备初始化
USB驱动的初始化和一般驱动类似,首先是进入入口函数DriverEntry,在DriverEntry函数中,分别指定各个IRP的派遣函数地址、指定AddDevice例程函数地址、指定Unload例程函数地址等。
在AddDevice例程中,创建功能设备对象,然后将该对象挂载在总线设备对象之上,从而形成设备栈。另外为设备创建一个设备链接,便于应用程序可以找到这个设备。也可以根据具体需要,从注册表中读取一些必要的设置。
17.3.5 USB设备的插拔
由于USB设备驱动是基于WDM框架的,因此需要对即插即用消息进行处理。BulkUSB程序对即插即用IRP的支持非常完善,具体可以参照其代码,这里简单提一下其对插拔的处理。
插拔设备会设计4个即插即用IRP,包括IRP_MN_START_DEVICE、IRP_MN_STOP_ DEVICE、IRP_MN_EJECT和IRP_MN_SURPRISE_REMOVAL。其中,IRP_MN_START_DEVICE消息是当驱动争取加载并运行时,操作系统的即插即用管理器会将这个IRP发往设备驱动。因此,当获得这个IRP后,USB驱动需要获得USB设备类别描述符,如设备描述符、配置描述符、接口描述符、端点描述符等。并通过这些描述符,从中获取有用信息,记录在设备扩展中。
IRP_MN_STOP_DEVICE是设备关闭前,即插即用管理器发的IRP。USB驱动获得这个IRP时,应该尽快结束当前执行的IRP,并将其逐个取消掉。另外,在设备扩展中还应该有表示当前状态的变量,当IRP_MN_STOP_DEVICE来临时,将当前状态记录成停止状态。
IRP_MN_EJECT是设备被正常弹出,而IRP_MN_SURPRISE_REMOVAL则是设备非自然弹出,有可能意外掉电或者强行拔出等。在这种IRP到来的时候,应该强迫所有未完成的读写IRP结束并取消。并且将当前的设备状态设置成设备被拔掉。
17.3.6 USB设备的读写
USB设备接口主要是为了传送数据,80%的传输是通过Bulk管道。在BulkUSB驱动中,Bulk管道的读取是在IRP_MJ_READ和IRP_MJ_WRITE的派遣函数中,这样在应用程序中就可以通过ReadFile和WriteFile等API对设备进行操作了。
在IRP_MJ_READ和IRP_MJ_WRITE的派遣例程中设置了完成例程,如图17-22所示。其原理是将读写的大小分成单位为BULKUSB_MAX_TRANSFER_SIZE的若干块,依次将请求发往底层USB总线驱动。第一个块是派遣例程先设置BULKUSB_MAX_TRANSFER_SIZE大小的读写,并设置完成例程,然后将请求发往USB总线驱动。当USB总线驱动完成BULKUSB_MAX_TRANSFER_SIZE大小的读写后,会调用读写的完成例程。
这时候在完成例程中再次发起BULKUSB_MAX_TRANSFER_SIZE大小的读写,并将请求发往底层USB总线驱动,当USB总线驱动完成后,又会进入完成例程。之后发送第三个数据块,并且依此类推直到传送完毕。
以下是BulkUSB的读写派遣函数的部分代码:
#001 NTSTATUS #002 BulkUsb_DispatchReadWrite( #003 IN PDEVICE_OBJECT DeviceObject, #004 IN PIRP Irp #005 ) #006 { #007 PMDL mdl; #008 PURB urb; #009 ULONG totalLength; #010 ULONG stageLength; #011 ULONG urbFlags; #012 BOOLEAN read; #013 NTSTATUS ntStatus; #014 ULONG_PTR virtualAddress; #015 PFILE_OBJECT fileObject; #016 PDEVICE_EXTENSION deviceExtension; #017 PIO_STACK_LOCATION irpStack; #018 PIO_STACK_LOCATION nextStack; #019 PBULKUSB_RW_CONTEXT rwContext; #020 PUSBD_PIPE_INFORMATION pipeInformation; #021 #022 //初始化变量 #023 urb = NULL; #024 mdl = NULL; #025 rwContext = NULL; #026 totalLength = 0; #027 irpStack = IoGetCurrentIrpStackLocation(Irp); #028 fileObject = irpStack->FileObject; #029 read = (irpStack->MajorFunction == IRP_MJ_READ) ? TRUE : FALSE; #030 deviceExtension = (PDEVICE_EXTENSION) DeviceObject->DeviceExtension; #031 //....略 #032 #033 //设置完成例程的参数 #034 rwContext = (PBULKUSB_RW_CONTEXT) ExAllocatePool(NonPagedPool, #035 sizeof(BULKUSB_RW_CONTEXT)); #036 //...略 #037 if(Irp->MdlAddress) { #038 totalLength = MmGetMdlByteCount(Irp->MdlAddress); #039 } #040 //设置URB标志 #041 urbFlags = USBD_SHORT_TRANSFER_OK; #042 virtualAddress = (ULONG_PTR) MmGetMdlVirtualAddress(Irp->MdlAddress); #043 #044 //判断是读还是写 #045 if(read) { #046 urbFlags |= USBD_TRANSFER_DIRECTION_IN; #047 } #048 else { #049 urbFlags |= USBD_TRANSFER_DIRECTION_OUT; #050 } #051 #052 //设置本次读写的大小 #053 if(totalLength > BULKUSB_MAX_TRANSFER_SIZE) { #054 stageLength = BULKUSB_MAX_TRANSFER_SIZE; #055 } #056 else { #057 stageLength = totalLength; #058 } #059 #060 //建立MDL #061 mdl = IoAllocateMdl((PVOID) virtualAddress, #062 totalLength, #063 FALSE, #064 FALSE, #065 NULL); #066 //将新MDL进行映射 #067 IoBuildPartialMdl(Irp->MdlAddress, #068 mdl, #069 (PVOID) virtualAddress, #070 stageLength); #071 #072 //申请URB数据结构 #073 urb = ExAllocatePool(NonPagedPool,sizeof (struct _URB_BULK_OR_INTERRUPT_ TRANSFER)); #074 #075 //建立Bulk管道的URB #076 UsbBuildInterruptOrBulkTransferRequest( #077 urb, #078 sizeof(struct _URB_BULK_OR_INTERRUPT_TRANSFER), #079 pipeInformation->PipeHandle, #080 NULL, #081 mdl, #082 stageLength, #083 urbFlags, #084 NULL); #085 #086 //设置完成例程参数 #087 rwContext->Urb = urb; #088 rwContext->Mdl = mdl; #089 rwContext->Length = totalLength - stageLength; #090 rwContext->Numxfer = 0; #091 rwContext->VirtualAddress = virtualAddress + stageLength; #092 rwContext->DeviceExtension = deviceExtension; #093 //设置设备堆栈 #094 nextStack = IoGetNextIrpStackLocation(Irp); #095 nextStack->MajorFunction = IRP_MJ_INTERNAL_DEVICE_CONTROL; #096 nextStack->Parameters.Others.Argument1 = (PVOID) urb; #097 nextStack->Parameters.DeviceIoControl.IoControlCode = #098 IOCTL_INTERNAL_USB_SUBMIT_URB; #099 //设置完成例程 #100 IoSetCompletionRoutine(Irp, #101 (PIO_COMPLETION_ROUTINE)BulkUsb_ReadWriteCompletion, #102 rwContext, #103 TRUE, #104 TRUE, #105 TRUE); #106 //将当前IRP阻塞 #107 IoMarkIrpPending(Irp); #108 //将IRP转发到底层USB总线驱动 #109 ntStatus = IoCallDriver(deviceExtension->TopOfStackDeviceObject, #110 Irp); #111 //...略去对不成功时的处理 #112 return STATUS_PENDING; #113 }
|
此段代码可以在配套光盘中本章的sys目录下找到。
17.4 小结
本章介绍了USB总线协议的基本框架,其中包括USB总线的拓扑结构,USB通信的流程,还有USB的四种传输模式。笔者用一些工具软件带领读者分析了各种USB令牌、设备描述符等。
USB驱动程序的主要功能就是设置这些USB令牌,和获取USB设备描述符。USB驱动程序将这些请求最终转化为USB请求包(URB包),然后发往USB总线驱动程序。USB总线驱动提供了丰富的功能,它封装了USB协议,提供了标准的接口。这使得USB驱动程序的编写变得简单,程序员不必过多地了解USB总线协议,就可以编写出功能强大的USB驱动程序。
|