USB 设备的 WDM 驱动程序设计
卡拉
河海大学计算机及信息工程学院
摘要:本文介绍了WDM驱动模型,并且针对USB总线给出了USB设备的WDM驱动程序的
设计方法。
关键词: WDM、USB 总线、驱动程序
一, WDM 驱动模型介绍
为了方便用户进行 WINDOWS 平台的驱动程序开发,Microsoft 于 1996 年宣布了一种
适用于当前广泛使用的 WIN2000 及 WINDOWS XP 操作系统的 Win32 驱动程序模型。这个
驱动模型就是 WDM(Windows Driver Model)——WINDOWS 驱动模型。WDM 驱动具有
以下几个特点。详见参考文献[1]第一章。
1) 可移植性好,采用 WDM 模型的设备驱动程序在不同的 Windows 操作系统上基本上是
二进制兼容的,而在不同的 CPU 平台 Windows 操作系统上是源代码兼容的。
2) 分层结构。WDM 模型使用了如图 1 的层次结构。图中左边是一个设备对象堆栈。设备
对象是系统为帮助软件管理硬件而创建的数据结构。一个物理硬件可以有多个这样的数
据结构。处于堆栈最底层的设备对象称为物理设备对象(physical device object),或简称
为 PDO。在设备对象堆栈的中间某处有一个对象称为功能设备对象(functional device
object),或简称 FDO。在 FDO 的上面和下面还会有一些过滤器设备对象(filter device
object)。位于 FDO 上面的过滤器设备对象称为上层过滤器,位于 FDO 下面(但仍在 PDO
之上)的过滤器设备对象称为下层过滤器。
3) 可配置性好。WDM 驱动程序通过其底层的类(总线)驱动程序枚举和访问硬件资源,
使得设备的配置变得相对简单和容易。而且 WDM 驱动程序还可以支持 WMI(Windows
Management Instrumentation),通过 WMI,用户可以控制驱动程序的某些行为和属性。
4) 支持即插即用和电源管理。
通用串行总线(USB)支持热拔插,是真正的即插即用 。最新的 协议增加
了高速传输,速率达到了 480Mbps。由于它的方便快捷,从现在到未来,都将拥有广阔的市
场。
]2[
二,WDM 相关知识介绍
在 WIN2000 和 WINDOWS XP 操作系统下,对于硬件实现操作的流程如图 1:
- 1 -
WIN32 API 调用
用户模式
内核模式 系统服务接口
传递 IRP 给驱动程序派遣函数
HAL 调用
平台相关操作
图 1 操作系统结构
应用程序
WIN32 子系统
IO 管理器
设备驱动程序
硬件抽象层
硬件
对于用户可见的应用程序运行在用户模式,当需要从设备读取数据时,它调用 WIN32
API 函数,Win32 子系统模块(如 )通过调用平台相关的系统服务接口实现该
API,而平台相关的系统服务将调用内核模式支持例程。在内核模式有许多服务例程为应用
程序请求提供服务,它们可以以某种方式与设备交互。我们把这些服务例程的集合称为“IO
管理器” 。IO 管理器首先检查传递过来的参数以保护系统安全或防止用户模式程序非法
存取数据,然后创建一个称为“I/O 请求包(IRP)”的数据结构,并把这个数据结构送到某个
驱动程序的入口点。
]2[
我们的驱动程序在接收到 IRP 后,进行相应的处理,和硬件进行交互。需要说明的是,
我们的 USB 驱动程序是建立在微软提供的底层驱动程序之上的。这些底层驱动程序包括主
控制器驱动程序( 或者 ),hub 驱动程序(),和一个类
驱动程序()。这些驱动程序对下管理硬件连接和管道通信,对上则给我们的驱动
程序提供了 USB 总线操作的接口。所以,我们的驱动程序只需要调用底层驱动程序提供的
例程就可以完成对硬件的操作。DDK 文档内对这些例程进行了详细的说明。
二, 用户模式与内核模式交互说明
IRP 是 WDM 驱动中最基本的一个结构,它由头部和 I/O 堆栈区组成,。IRP 的头部结
构包含了一些必须要设置的标志位域,返回状态域,规定了与用户模式进行数据交互的方式。
而 I/O 堆栈为一个叫做 IO_STACK_LOCATION 结构数组,数组中的每个堆栈单元都对应一
个将处理该 IRP 的驱动程序,而每个堆栈单元中包含该 IRP 的类型代码和参数信息。
因为用户模式的应用程序无法直接访问设备,所以我们必须调用操作系统提供的函数。
在我们应用程序内使用了三个函数 。 ]1[
CreatFile 函数用来打开我们的设备。当应用程序调用这个函数时,内核通过参数中的设
备名称找到我们的驱动程序,并向驱动程序发送主功能名域(MajorFunction 域)设置为
- 2 -
IRP_MJ_CREATE 的 IRP,请求使用设备。当关闭设备时,我们就调用 CloseFile 函数,内
核则发送主功能名域(MajorFunction)为 IRP_MJ_CLOSE 的 IRP,减去对驱动的一个引用
(因为可能同时有几个应用程序在调用一个驱动)。
我们为了完成操作,在应用程序里调用 DeviceIoControl 函数。这个函数由操作系统提
供,它通过向相应的设备驱动发送一个控制代码,使这个设备驱动执行相应的操作。
BOOL DeviceIoControl(
HANDLE hDevice, //设备句柄,指向我们的设备
DWORD dwIoControlCode, //代表操作的控制代码,可由用户自己定义
LPVOID lpInBuffer, //指向输入缓冲,内含操作需要的数据
DWORD nInBufferSize, //输入缓冲区大小
LPVOID lpOutBuffer, //指定输出缓冲,操作完成后可以从这里返回数据
DWORD nOutBufferSize, //输出缓冲区大小
LPDWORD lpByteReturned, //指出输出缓冲区内返回的数据的长度
LPOVERLAPPED lpOverlapped //指向一个数据结构,一般我们设置为 NULL
);
我们向操作系统发送 DeviceIoControl 函数后,驱动程序就会收到主功能码为
IRP_MJ_DEVICE_CONTROL 的 IRP,而此 IRP 的副功能码(MinorFunction)则为函数中发
出的控制代码。所以,我们的驱动根据收到的这个控制代码,并接收 IRP 中相应域中获得输
入缓冲区的数据指针,进行相应的处理来满足应用程序的要求。
四,WDM 驱动程序初始化
驱动程序使用 DriverEntry 作为入口点,与我们在 Windows 应用程序中定义的 WinMain
相似。通常情况下,它是驱动的默认入口点。驱动程序实际上也是基于消息和中断驱动的,
这个消息就指的是 IRP。DriverEntry 的第一个参数是 DRIVER_OBJECT,这个数据结构内
一些域(如 MajorFunction)包含了消息函数的入口。我们在自己的驱动内对不同请求类型
的 IRP 编写相应的派遣函数,并在 DRIVER_OBJECT 结构中初始化。DriverEntry 的第二个
参数是注册表中本设备服务键的键名字符串,驱动可以把一些需要保存的信息保存在该键
中。 DriverEntry 入口函数如下:
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
// 下面设置设备的属性,标明各个消息函数的入口
DriverObject->MajorFunction[IRP_MJ_CREATE] = MsgCreateDevice;
//此函数在应用程序首先连接设备对象时由内核调用,用来判断驱动是否允许被访问
DriverObject->MajorFunction[IRP_MJ_CLOSE] = MsgCloseDevice; //关闭设备消息
//此函数在应用程序要关闭设备对象连接时被调用。
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MsgIOCTL;
- 3 -
//如前所述,此函数用来完成应用程序期望的操作。
DriverObject->MajorFunction[IRP_MJ_PNP] = MsgPNP;
//操作系统即插即用消息,
DriverObject->MajorFunction[IRP_MJ_POWER] = MsgPower;
//此函数用于实现电源管理,可以在此函数处理电源状态,也可以直接将 IRP 下传。
DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = MsgSysCTL;
//此函数用于支持 WMI(Windows Management Instrumentation)。
DriverObject->DriverExtension->AddDevice = MsgAddDevice;
DriverObject->DriverUnload = DriverUnload; //关闭设备时用来释放占用的资源
return STATUS_SUCCESS;
}
一个驱动程序可以同时为多个相同的设备服务,然而不管系统中有多少相同的设备,驱
动的 DriverEntry 函数只会被调用一次,但 AddDevice 函数会被分别调用一次 AddDevice 派
遣函数完成的工作有下面几项:详见参考文献[1]第 21 章。
1.调用 IoCreateDevice 创建设备对象(FDO),并建立一个私有的设备扩展对象。
2.创建符号连接,使得应用程序可以根据该符号连接访问本驱动。
3.初始化设备对象的 Flag 成员。
4.初始化私有的设备扩展对象
5.调用 IoAttachDeviceToDeviceStack 函数把新设备对象放到堆栈上。
6.注册一个 Interface,应用程序可以通过该 Interface 来访问驱动或获取 PNP 消息
五,发送 URB 包
如前所述,USB设备驱动程序是架构于操作系统提供的USB总线驱动程序()
上的。所以,我们的 USB 设备驱动程序从不直接与硬件对话。我们只需创建 URB(USB
Request Block——USB 请求块),并把 URB 附加到 IRP 相应域内,提交给 USB 总线驱动程
序就可完成硬件操作。
为了构建一个 URB,应首先为 URB 分配内存,然后调用与各种 URB 相对应的初始化
例程 ( 宏 ) 把 URB 结构中的各个域填入请求要求的内容,例如,当你为响应
IRP_START_DEVICE 请求而配置设备时,首要的任务就是读取该设备的设备描述符。下面
例程可以完成这个任务:
UsbBuildGetDescriptorRequest(&urb, //我们要创建 URB 的地址
sizeof(_URB_CONTROL_DESCRIPTOR_REQUEST), //URB 大小
USB_DEVICE_DESCRIPTOR_TYPE, //描述符类型,这里是设备描述符
0,
0,
&dd, //本地缓存,用来保存获得的描述符
NULL,
sizeof(dd), //本地缓存大小
NULL);
- 4 -
UsbBuildGetDescriptorRequest 看上去象一个正常的服务例程,实际上它是一个宏,用于生成
“读描述符请求子结构”各个域的初始化语句。在 DDK 头文件中定义了这些创建各种 URB
的宏,见表 1。
辅助宏 事务类型
UsbBuildInterruptOrBulkTransferRequest 对中断或块端点的输入和输出
UsbBuildGetDescriptorRequest 端点 0 的 GET_DESCRIPTOR 控制请求
UsbBuildGetStatusRequest 对设备、接口、端点的 GET_STATUS 请求
UsbBuildFeatureRequest 对 设 备 、 接 口 、 端 点 的 SET_FEATURE 或
CLEAR_FEATURE 请求
UsbBuildSelectConfigurationRequest SET_CONFIGURATION
UsbBuildSelectInterfaceRequest SET_INTERFACE
UsbBuildVendorRequest 任何厂商定义的控制请求
表 1 用于创建 URB 的辅助宏
通过上面的宏,我们可以创建完成各种 USB 协议操作的 URB。创建完 URB 后,需要
创建并发送一个内部 I/O 控制(IOCTL)请求到 USBD 驱动程序,USBD 驱动程序位于驱动程
序层次结构的低端。发送 URB 大致有以下步骤。 调用 IoBuildDeviceIoControlRequest 函数,
创建一个 I/O 控制代码为 IOCTL_INTERNAL_USB_SUBMIT_URB 的 IRP,该 IRP 的类型为
IRP_MJ_INTERNAL_DEVICE_CONTROL。 被提交的 URB 的地址被填入
的 Argument1 域。我们用 IoCallDriver 把请求发送到下一层驱动程序,并等待该 IRP 被完成
下面是我们驱动内发送 URB 的一个函数实现:
NTSTATUS
UsbCallUSBD(
IN PADAPTER Adapter,
IN PURB Urb
)
{
NTSTATUS Status = STATUS_SUCCESS;
PIRP Irp;
KEVENT Event;
IO_STATUS_BLOCK IoStatus;
PIO_STACK_LOCATION NextStack;
LARGE_INTEGER RunTime;
= -20000 * USBCOMD_TIMEOUT_INTERVAL; // 等待时间
KeInitializeEvent(&Event, NotificationEvent, FALSE); //设置事件量,同步内核
Irp = IoBuildDeviceIoControlRequest( //创建 IRP 包
IOCTL_INTERNAL_USB_SUBMIT_URB,
Adapter->StackDeviceObject,
NULL,
0,
- 5 -
NULL,
0,
TRUE,
&Event,
&IoStatus);
// 链接 URB 至该 IRP
NextStack = IoGetNextIrpStackLocation(Irp); //获得对应与下一层驱动的 IRP 堆栈
NextStack-> = Urb; //将 URB 粘贴进 IRP
Status = IoCallDriver( Adapter->StackDeviceObject, Irp); // 调用下层驱动,传递 URB
if ( Status == STATUS_PENDING) { // 等待该 IRP 结束
Status=KeWaitForSingleObject(
&Event,
Suspended,
KernelMode,
FALSE,
&RunTime
);
if(Status==STATUS_TIMEOUT){ // 该 IRP 已经超时,应该被取消
Status=STATUS_UNSUCCESSFUL;
IoCancelIrp(Irp); 取消 IRP
KeWaitForSingleObject(
&Event,
Suspended,
KernelMode,
FALSE,
NULL
); // 等待该 IRP 被取消
}
}
return Status;
}
六,USB 设备初始化
USB 总线驱动程序自动检测新插入的 USB 设备。然后它读取设备内的设备描述符以查
明插入的是何种设备,并据此报告配置管理器有新设备插入。配置管理器根据描述符中的厂
商和产品标识以及其它描述符一同决定具体安装哪一个驱动程序。首先驱动程序的
DriverEntry 函数被调用,然后配置管理器调用驱动程序的 AddDevice 函数。AddDevice 做前
面已经讲述的任务:创建设备对象,把设备对象连接到驱动程序堆栈上,等等。最后,配置
管理器向驱动程序发送一个即插即用请求 IRP_MN_START_DEVICE。对于 PCI 设备或其它
总线类型的设备,收到 IRP_MN_START_DEVICE 请求后应获取本设备的 I/O 资源信息。而
对于 USB 设备不需要任何的 I/O 资源,此时应做的则是获取各类描述符并选用一个可用的
- 6 -
接口。其步骤如下。
1.获取设备描述符
2.获取配置描述符
3.获取接口描述符
4.选择可用的接口和配置
下面是实现以上步骤的一个函数实现,从下面的实现中也可以看出如何创建并发送一个控制
传输 URB
NTSTATUS
IoControlStartDevice(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)
{
PIO_STACK_LOCATION IrpStack;
PADAPTER Adapter; // 硬件相关的结构
NTSTATUS Status = STATUS_SUCCESS;
PURB pUrb=NULL;
ULONG dwSize;
PUSB_DEVICE_DESCRIPTOR deviceDescriptor = NULL;
PUSB_CONFIGURATION_DESCRIPTOR ConfigurationDescriptor = NULL;
UCHAR InterfaceNumber = 0;
PUSBD_INTERFACE_INFORMATION InterfaceObject;
USBD_INTERFACE_LIST_ENTRY InterfaceList[2];
Adapter = (PADAPTER) DeviceObject->DeviceExtension;
Adapter->Interface =NULL;
// 1. 获取设备描述符
pUrb = (PURB)ExAllocatePool( NonPagedPool,
sizeof(struct _URB_CONTROL_DESCRIPTOR_REQUEST));
if (pUrb){
dwSize = sizeof(USB_DEVICE_DESCRIPTOR);
deviceDescriptor = (PUSB_DEVICE_DESCRIPTOR)ExAllocatePool(NonPagedPool,
dwSize);
if (deviceDescriptor){
UsbBuildGetDescriptorRequest(pUrb,
(USHORT) sizeof (struct _URB_CONTROL_DESCRIPTOR_REQUEST),
USB_DEVICE_DESCRIPTOR_TYPE,
0,
0,
- 7 -
deviceDescriptor,
NULL,
dwSize,
NULL);
Status = UsbCallUSBD(Adapter, pUrb); // 发送该 URB
}else{
Status = STATUS_NO_MEMORY;
}
if (NT_SUCCESS(ntStatus)){
Adapter->UsbDeviceDescriptor = deviceDescriptor;
} else if (deviceDescriptor){
ExFreePool(deviceDescriptor);
Adapter->UsbDeviceDescriptor = NULL;
}
ExFreePool(pUrb);
} //if (pUrb)
else {
ntStatus = STATUS_NO_MEMORY;
return Status;
} //End of if (pUrb)
// 2. 获取设备接口
do{ // 此处使用 do 仅仅是为了单出口的需要
pUrb = (PURB)ExAllocatePool(NonPagedPool, sizeof(
struct _URB_CONTROL_DESCRIPTOR_REQUEST));
if ( !pUrb ){
Status = STATUS_NO_MEMORY;
break;
}
Size = sizeof(USB_CONFIGURATION_DESCRIPTOR) + 16;
ConfigurationDescriptor = (PUSB_CONFIGURATION_DESCRIPTOR)
ExAllocatePool(NonPagedPool, Size);
if ( ConfigurationDescriptor == NULL ){
Status = STATUS_NO_MEMORY;
break;
}
UsbBuildGetDescriptorRequest(
Urb,
(USHORT) sizeof (struct _URB_CONTROL_DESCRIPTOR_REQUEST),
- 8 -
USB_CONFIGURATION_DESCRIPTOR_TYPE,
0,
0,
ConfigurationDescriptor,
NULL,
sizeof (USB_CONFIGURATION_DESCRIPTOR),
NULL
);
Status = UsbCallUSBD( Adapter, Urb); // 第一次是为了得到大小
if ( !NT_SUCCESS( Status ) ){
break;
}
// 根据该空间大小重新分配一个 ConfigurationDescriptor
Size = ConfigurationDescriptor->wTotalLength + 16;
ExFreePool(ConfigurationDescriptor);
ConfigurationDescriptor = (PUSB_CONFIGURATION_DESCRIPTOR)
ExAllocatePool(NonPagedPool, Size);
if ( !ConfigurationDescriptor ){
Status = STATUS_NO_MEMORY;
break;
}
UsbBuildGetDescriptorRequest(
Urb,
(USHORT) sizeof (struct _URB_CONTROL_DESCRIPTOR_REQUEST),
USB_CONFIGURATION_DESCRIPTOR_TYPE,
0,
0,
ConfigurationDescriptor,
可以把 看作是接受 URB 的实体,向 USBD 的调用被转化为带有主功能代码为
IRP_MJ_INTERNAL_DEVICE_CONTROL 的 IRP。然后 USBD 再调度总线时间,发出 URB
中指定的操作。
如前所述,WDM 驱动会接收从 IO 管理器发来的 IRP。IRP 是 WDM 驱动中最基本的一
个结构,它主要由两部分组成,即 IRP 的头部和 I/O 堆栈区,DDK 提供了专门的函数来访
问相关的域。IRP 的头部结构包含了一些必须要设置的标志位域,返回状态域,规定与用户
模式数据交互的方式。而 IRP 的堆栈为一个叫做 IO_STACK_LOCATION 结构数组:数组中
的每个堆栈单元都对应一个将处理该 IRP 的驱动程序,如图二。而每个堆栈单元中包含该
IRP 的类型代码和参数信息以及完成函数的地址。
- 9 -
图 2 驱动程序和 IRP 堆栈之间的平行关系
Windows 系统 I/O 管理器使用驱动程序对象( DriverObject)来代表每个设备驱动程序, 驱动
程序对象也是是部分不透明的 。DRIVER_OBJECT 结构中对我们最有用的是一个
MajorFunction (PDRIVER_DISPATCH)的函数指针表。它指向存在于驱动程序中的二十多种
IRP 处理函数。这些 IRP 里一般需要实现的有 IRP_MJ_CREATE、IRP_MJ_CLOSE、
IRP_MJ_PNP , IRP_MJ_POWER , IRP_MJ_DEVICE_CONTROL 等 。 DriverObject 内
DriverExtension 域内指向一个子结构,里面只有一个 AddDevice 函数指针可供我们访问。我
们在自己的程序内实现 AddDevice 函数来加载我们的设备对象到对象栈上。
]1[
七,结束语
本文介绍了WDM驱动模型,并且针对USB总线给出了USB设备的WDM驱动程序的设
计方法。讲解了windows WDM驱动程序初始化的过程,发送URB的过程及USB设备初始化
的过程。对在windows下进行USB驱动开发过程做了一定的介绍,并给出了一定量的代码。
但是,若想能够很好地掌握驱动程序的开发,最根本的是阅读驱动文档并自己动手编写驱动
程序。
参考文献
[1] CHRIS WDM 设备驱动程序开发指南[M].北京:机械工业出版社,2000 .
[2] 张念淮,江浩.USB 总线接口开发指南[M].北京:国防工业出版社,2001.
[3] 许永和,健莲科技.USB 外围设备设计与应用[M].北京:中国电力出版社, 2002.
- 10 -
Design of WDM Driver for USB Device
Kala
Computer Information and Engineering Department of
HOHAI University
Abstract:This article introduces WDM driver for USB,we give the way to design
a WDM driver for USB device.
Keywords: WDM、USB、drivers
- 11 -