USB驱动开发——基于windows的WDM模型
2009/11/11 留下评论
译自 Programming the Microsoft Windows Driver Model / Walter Oney — 2nd ed,第12章,第2节——Working with the Bus Driver,加入了个人一些理解,希望对大家编写USB设备驱动有一定帮助,欢迎指正。
和其他设备驱动不同,USB设备驱动不直接与底层硬件进行通信,而是先建立一个称为USB请求块(USB request blocks,URB)的数据结构,把它发送给父级驱动,父级驱动根据URB中的信息对底层硬件进行相应操作,这里父级驱动通常就是指USB总线驱动。发送URB可以使用主功能码为IRP_MJ_INTERNAL_DEVICE_CONTROL的IRP来实现,也可以直接调用父级驱动提供的接口调用函数来实现。
1. 初始化请求
URB时一种预先定义的数据结构,包含许多域(field)。为了建立一个URB,首先要开辟一个存储空间来存储这个URB,然后运行初始化程序向这个空间来填充一些数据,即设置URB的各个域,例如,若令设备能够对一个IRP_MN_START_DEVICE请求做出相应,那么首先需要做的一个工作就是读取设备描述符,可能需要使用类似下面的一段代码:
USB_DEVICE_DESCRIPTOR dd;
URB urb;
UsbBuildGetDescriptorRequest(&urb,
sizeof(_URB_CONTROL_DESCRIPTOR_REQUEST),
USB_DEVICE_DESCRIPTOR_TYPE, 0, 0, &dd, NULL,
sizeof(dd), NULL);
这里声明了一个名为urb的URB类型的局部变量,用它来存储要构建的URB。URB这种数据结构是在USBDI.H中进行定义的,USBDI.H可以在DDK开发软件安装目录下找到。URB是一种共用体结构,里面又定义了一些子结构体,每个结构体用于存放特定的USB请求。URB具体定义如下:
typedef struct _URB {
union {
struct _URB_HEADER UrbHeader;
struct _URB_SELECT_INTERFACE UrbSelectInterface;
struct _URB_SELECT_CONFIGURATION UrbSelectConfiguration;
struct _URB_PIPE_REQUEST UrbPipeRequest;
struct _URB_FRAME_LENGTH_CONTROL UrbFrameLengthControl;
struct _URB_GET_FRAME_LENGTH UrbGetFrameLength;
struct _URB_SET_FRAME_LENGTH UrbSetFrameLength;
struct _URB_GET_CURRENT_FRAME_NUMBER UrbGetCurrentFrameNumber;
struct _URB_CONTROL_TRANSFER UrbControlTransfer;
struct _URB_BULK_OR_INTERRUPT_TRANSFER UrbBulkOrInterruptTransfer;
struct _URB_ISOCH_TRANSFER UrbIsochronousTransfer;
// for standard control transfers on the default pipe
struct _URB_CONTROL_DESCRIPTOR_REQUEST UrbControlDescriptorRequest;
struct _URB_CONTROL_GET_STATUS_REQUEST UrbControlGetStatusRequest;
struct _URB_CONTROL_FEATURE_REQUEST UrbControlFeatureRequest;
struct _URB_CONTROL_VENDOR_OR_CLASS_REQUEST UrbControlVendorClassRequest;
struct _URB_CONTROL_GET_INTERFACE_REQUEST UrbControlGetInterfaceRequest;
struct _URB_CONTROL_GET_CONFIGURATION_REQUEST UrbControlGetConfigurationRequest;
};
} URB, *PURB;
这里类似于 _URB_**** 的代码也是在USBDI.H预先定义的某种结构体类型,例如: _URB_CONTROL_GET_STATUS_REQUEST在USBDI.H中定义如下:
struct _URB_CONTROL_DESCRIPTOR_REQUEST {
#ifdef OSR21_COMPAT
struct _URB_HEADER;
#else
struct _URB_HEADER Hdr; // function code indicates get or set.
#endif
PVOID Reserved;
ULONG Reserved0;
ULONG TransferBufferLength;
PVOID TransferBuffer;
PMDL TransferBufferMDL; // *optional*
struct _URB *UrbLink; // *optional* link to next urb request
// if this is a chain of commands
struct _URB_HCD_AREA hca; // fields for HCD use
USHORT Reserved1;
UCHAR Index;
UCHAR DescriptorType;
USHORT LanguageId;
USHORT Reserved2;
};
初始化URB可以通过类似于UsbBuildGetDescriptorRequest这样的函数来完成,以下给出一个表来说明:
这些函数是在另一个头文件USBDLIB.H中定义的,在DDK安装目录下查找即可,对于表中的函数都可以在DDK的说明文档里找到,利用这些函数就可以实现对URB的初始化。这里以UsbBuildGetDescriptorRequest为例,说明一个函数具体功能,有些参数我也不能完全讲清其含义,把英文直接贴在上面了。
VOID
UsbBuildGetDescriptorRequest(
IN OUT PURB Urb, // 指向一个要初始化的URB首地址
IN USHORT Length, // 确定URB的长度
IN UCHAR DescriptorType, // 确定描述符类型
IN UCHAR Index, // Specifies the device-defined index of the descriptor that is to be retrieved
IN USHORT LanguageId, // Specifies the language ID of the descriptor to be retrieved when USB_STRING_DESCRIPTOR_TYPE is set in DescriptorType. This parameter must be zero for any other value in DescriptorType.
IN PVOID TransferBuffer OPTIONAL, // 读回的描述符后所存放的地址
IN PMDL TransferBufferMDL OPTIONAL, // Pointer to a resident buffer to receive the descriptor data or is NULL if an MDL is supplied in TransferBufferMDL.
IN ULONG TransferBufferLength, // Specifies the length of the buffer specified in TransferBuffer or described in TransferBufferMDL.
IN PURB Link OPTIONAL // 必需为NULL
);
2. 发送URB
发送一个URB需要建立并发送一个内部IOCTL请求到父级驱动,很多时候需要等待设备的应答。发送URB的函数如下:
NTSTATUS SendAwaitUrb(PDEVICE_OBJECT fdo, PURB urb)
{
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
KEVENT event; //用于建立一个同步的IRP
KeInitializeEvent(&event, NotificationEvent, FALSE);
IO_STATUS_BLOCK iostatus;
PIRP Irp = IoBuildDeviceIoControlRequest (IOCTL_INTERNAL_USB_SUBMIT_URB, pdx->LowerDeviceObject, NULL, 0, NULL, 0, TRUE, &event, &iostatus); //建立一个IOCTL
PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp);
stack->Parameters.Others.Argument1 = (PVOID) urb;//将URB发送到特定地址
NTSTATUS status = IoCallDriver(pdx->LowerDeviceObject, Irp);
if (status == STATUS_PENDING)
{
KeWaitForSingleObject(&event, Executive, KernelMode,
FALSE, NULL);
status = iostatus.Status;
}
return status;
}
3.URB 的返回状态
发送一个URB到USB总线驱动后,会收到一个NTSTATUS类型的状态码,它描述了操作的结果。在总线驱动内部使用另一种状态码,类型名为USBD_STATUS。当总线驱动完成一个URB后,它会设置URB的UrbHeader.Status域,这个域值一种USBD_STATUS值。开发人员可以在自己的驱动中检查这个域值来获取关于URB处理过程中的一些信息。DDK中URB_STATUS宏定义如下:
NTSTATUS status = SendAwaitUrb(fdo, &urb);
USBD_STATUS ustatus = URB_STATUS(&urb);
…
没有特定的协议来保存URB_STATUS类型的状态值并将其传给驱动程序,开发人员可以有很大的自由度来处理这些状态值。
4. 配置(configuration)
总线驱动能够自动侦测新接入的USB设备,然后读取设备描述符来判断是那种类型的设备,设备描述符的vedor和product identifier域及其它一些描述符决定了需要导入的驱动。
通常配置管理器会调用驱动的AddDevice函数,AddDevice会建立一个设备对象,并将其与驱动链接等等。配置管理器最终会向驱动发送一个IRP_MN_START_DEVICE Plug and Play 请求,这会使驱动调用一个名为StartDevice 的函数,其大体框架如下:
NTSTATUS StartDevice(PDEVICE_OBJECT fdo)
{
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->DeviceExtension;
<configure device>
return STATUS_SUCCESS;
}
5.系统如何装载驱动
假设USB设备的vendor ID为0x0547,product ID 为0x102A,那么设备接入时PnP管理器会寻找一个包含设备名为 USB\VID_0547&PID_102A的注册表入口,如果没有匹配的入口,那么PnP管理器会触发一个找到新硬件的向导,要求定位一个能描述这个设备的INF文件,根据INF文件向导会自动安装相应位置的驱动,并更新注册表。一旦PnP管理器实现了对注册表入口的定位,就可以动态装载驱动。
在StartDevice需要做的工作如下:首先为设备选择一个配置,然后选择一个或多个接口,此后发送一个选择配置的URB到总线驱动,总线驱动根据URB来配置设备,并建立一个通信管道,可以与被选择接口中的设备端点通信,总线驱动会提供可以访问管道的句柄。至此配置过程完成。
6. 读取配置描述符
必须读取配置描述符到一个连续的存储空间,因为硬件不允许直接访问接口和端点描述符,下面的代码指出了如何使用2个URB来读取一个配置描述符:
ULONG iconfig = 0;
URB urb;
USB_CONFIGURATION_DESCRIPTOR tcd;
UsbBuildGetDescriptorRequest(&urb,
sizeof(_URB_CONTROL_DESCRIPTOR_REQUEST),
USB_CONFIGURATION_DESCRIPTOR_TYPE,
iconfig, 0, &tcd, NULL, sizeof(tcd), NULL);
SendAwaitUrb(fdo, &urb);
ULONG size = tcd.wTotalLength;
PUSB_CONFIGURATION_DESCRIPTOR pcd =
(PUSB_CONFIGURATION_DESCRIPTOR) ExAllocatePool(
NonPagedPool, size);
UsbBuildGetDescriptorRequest(&urb,
sizeof(_URB_CONTROL_DESCRIPTOR_REQUEST),
USB_CONFIGURATION_DESCRIPTOR_TYPE,
iconfig, 0, pcd, NULL, size, NULL);
SendAwaitUrb(fdo, &urb);
ExFreePool(pcd);
程序中使用一个URB来读取配置描述符,并存入一个名为tcd临时描述符存储区域,tcd包含了配置、接口和端点描述符总的长度(wTotalLength),依据此长度来分配存储空间(pcd),使用第2个URB来读取整个描述符,并存入分配好的连续存储空间。
7. 选择配置
驱动通过给设备发送一系列的控制命令来进行设置并使能接口,即选择了某种配置。使用USBD_CreateConfigurationRequestEx函数建立建立URB来实现这些控制命令,函数声明如下:
PURB USBD_CreateConfigurationRequestEx( IN PUSB_CONFIGURATION_DESCRIPTOR ConfigurationDescriptor, IN PUSBD_INTERFACE_LIST_ENTRY InterfaceList );
参数说明如下:
ConfigurationDescriptor —— 指向一个配置描述符的指针,这个配置描述符包含了从USB设备获取的所有接口、端点、厂商和class-specific描述符。
InterfaceList —— Pointer to the first element in a variable-length array of USBD_INTERFACE_LIST_ENTRY structures
感觉单纯的翻译不能无法说清基于WDM的USB驱动开发,因为这里面确实涉及到了WDM相当多的基础知识,理解上有很大困难。此外,文中只是一个USB驱动开发的大略步骤,并不能形成一个完整的实例以供参考,所以还是建议大家根据一个具体的驱动代码来分析。目前我也在反复的看 Programming the Microsoft Windows Driver Model 这本书的其它章节,一边翻译一边结合例子来看,有些吃力,不过坚持下来总会有所收获的。
路漫漫其修远兮,吾将上下而求索~ 加油…..