USB接口通信(驱动)的设计与实现

415172536

贡献于2012-06-26

字数:49551 关键词:

 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 引言 WDM是“Windows驱动程序模型”的简称,即“Windows Driver Model”。实际上它是一系列集成在操作系统之中的常规系统服务集,用于简化硬件驱动程序的编写,并保证它们在Windows 98/Me/2000中的二进制兼容,WDM(Windows Driver Model)模型是从WinNT3.51和WinNT4的内核模式设备驱动程序发展而来的。WDM主要的变化是增加了对即插即用、电源管理、Windows Management Interface(WMI)、设备接口的支持。WDM模型的主要目标,是实现能够跨平台使用、更安全、更灵活、编制更简单的Windows设备驱动程序。WDM采用了“基于对象”的技术,建立了一个分层的驱动程序结构。WDM首先在Windows98中实现,在Windows2000中得到了进一步的完善,并在后续开发的Windows操作系统中都将存在,比如Windows Me和Windows XP。微软在通过WDM模型的引入,希望减轻设备驱动程序的开发难度和周期,逐渐规范设备驱动程序的开发,应该说,WDM将成为以后设备驱动程序的主流。 USB技术的全称是通用串行总线,是英文Universal Serial Bus的缩写。它是一种应用在PC领域的新型接口技术,虽然USB2.0已经被广泛应用,但是初始的Windows 2000是支持USB1.0协议的,如果希望支持USB2.0协议,需要在微软网站上下载升级包。实际上,对于键盘或者鼠标来说,传输的速度非常小,使用USB1.0或者是USB2.0的区别并不大。闪存盘之类的存储设备,则需要重视传输速度。USB1.0版本主要应用在鼠标,键盘等HID设备上,这就是本驱动程序中引用的头文件版本是USB1.0的原因。 本毕业设计的目的是希望对Windows 2000操作系统体系结构和驱动程序开发以及调试等方面的问题有一个比较深入的了解,对USB协议和USB体系有做一个比较深入的了解。并开发出一个USB键盘驱动。这个USB键盘驱动程序应当可以替代系统原有的键盘驱动程序,并可以正常工作。 本论文设计的驱动程序在Windows 2000下运行,开发环境为VC6.0和DDK2000。 1 WDM驱动程序模型概述 驱动程序在任何操作系统下都和系统内核有着密切的关系。设备驱动程序是一个包含了许多操作系统可调用例程的容器,这句Walter Oney曾说过的话,抽象的描述了设备驱动程序的本质。 1.1 Windows 2000概述 图1-1中概括了Windows 200系统中的组件,Windows 2000操作系统是由不同层次的模块共同组成的。该图着重描述了驱动程序开发者所关心的特征。工作在Windows 2000操作系统平台上的软件要么执行在用户模式中,要么执行在内核模式中。当用户模式程序需要读取设备数据时,就调用Win32 API函数,如ReadFile. Win32子系统模块通过调用平台相关的系统服务接口实现API,而平台相关的系统服务将调用内核模式支持例程。在ReadFile调用中,调用首先到达系统DLL(NTDLL.DLL)中的一个入口点,NtReadFile函数。然后这个用户模式的NtReadFile函数接着调用系统服务接口,最后由系统服务接口调用内核模式中的服务例程,该例程同样名为NtReadFile。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 应用程序 Win32子系统 设备驱动 硬件抽象层 硬件 IO管理器 用户模式 内核模式 Win32API调用 系统服务接口 传递IRP给驱动程序派遣函数 HAL调用 平台相关操作 图1-1 Windows组件模型 系统中还有许多与NtReadFile相似的服务例程;它们同样运行在内核模式中,为应用程序请求提供服务,并以某种方式与设备交互。这些服务例程首先检查从用户态传递给它们的参数以保护系统安全或防止用户态程序非法存取数据,然后创建一个称为“I/0请求包(IRP)”的数据结构,并把这个数据结构送到某个驱动程序的入口点。 驱动程序完成一个I/0操作后,通过调用一个特殊的内核模式服务例程来完成该IRP。完成操作是处理IRP的最后动作,它使等待的应用程序恢复运行。 1.2 Windows 2000中的驱动程序类型 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 虚拟设备 驱动程序(VDD) 内核模式 驱动程序 文件系统 驱动程序 遗留设备 驱动程序 PnP 驱动程序 显示 驱动程序 WDM 驱动程序 类 驱动程序 微型(mini) 驱动程序 图1-2 Windows 2000中的设备驱动程序种类 Windows 2000系统可以使用多种驱动程序,图1-2显示了其中几种。 ·虚拟设备驱动程序(VDD)可以使DOS应用程序访问x86平台上的硬件。VDD通过屏蔽I/O权限掩码来捕获端口存取操作,它基本上是模拟硬件操作,这对于那些直接对裸机硬件编程的应用程序特别有用。尽管这种驱动程序在Windows 98和Windows 2000中共享一个名称并且有相同的功能,但实际上它们的工作方式完全不同。我们用VDD缩写代表这种驱动程序,用VxD缩写代表Windows 98中的虚拟设备驱动程序以示区别。 内核模式驱动程序的分类包含许多子类。PnP驱动程序就是一种遵循Windows 2000 即插即用协议的内核模式驱动程序。 ·WDM驱动程序是一种PnP驱动程序,它同时还遵循电源管理协议,并能在Windows98和Windows 2000间实现源代码级兼容。WDM驱动程序还细分为类驱动程序(classdriver)和微型驱动程序(minidriver),类驱动程序管理属于己定义类的设备,微型驱动程序向类驱动程序提供厂商专有的支持。 ·显示驱动程序是用于显示和打印设备的内核模式驱动程序。 ·文件系统驱动程序在本地硬盘或网络上实现标准PC文件系统模型(包括多层次目录 结构和命名文件概念)。 ·遗留设备驱动程序也是一种内核模式驱动程序,它直接控制一个硬件设备而不用其它驱动程序帮助。这种驱动程序主要包括Windows NT早期版本的驱动程序,它们可以不做修改地运行在Windows 2000中。 1.3 WDM驱动程序类型 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 WDM(Windows Driver Model)模型是从WinNT3.51和WinNT4的内核模式设备驱动程序发展而来的。WDM主要的变化是增加了对即插即用、电源管理、Windows Management Interface(WMI)、设备接口的支持。WDM模型的主要目标,是实现能够跨平台使用、更安全、更灵活、编制更简单的Windows设备驱动程序。WDM采用了“基于对象”的技术,建立了一个分层的驱动程序结构。WDM首先在Windows98中实现,在Windows2000中得到了进一步的完善,并在后续开发的Windows操作系统中都将存在,比如Windows Me和Windows XP。微软在通过WDM模型的引入,希望减轻设备驱动程序的开发难度和周期,逐渐规范设备驱动程序的开发,应该说,WDM将成为以后设备驱动程序的主流。 在WDM模型中,每个硬件设备至少有两个驱动程序:一个功能驱动程序(function driver)和一个总线驱动程序(bus driver)。一个设备还可能有过滤驱动程序(filter driver),用来变更标准设备驱动程序的行为。这些服务于同一个设备的驱动程序组成了一个链表,称为设备栈。详细的描述见图1-3。 可选的上层过滤驱动程序 功能驱动程序 可选的底层过滤驱动程序 可选的总线过滤驱动程序 总线驱动程序 设备驱动程序 总线驱动程序 图1-3驱动程序的种类 总线驱动程序 总线驱动程序为实际的I/O总线服务,比如IEEE 1394。在WDM的定义中,一个总线是这样的设备,它用来连接其他的物理的、逻辑的、虚拟的设备。总线包括传统的总线SCSI和PCI,也包括并口、串口、以及i8042端口。微软已经为Windows操作系统提供了总线驱动程序。总线驱动程序已经包含在操作系统里了,用户不必安装。一个总线驱动程序负责以下的工作: ·枚举总线上的设备; 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 ·向操作系统报告总线上的动态事件; ·响应即插即用和电源管理的I/O请求; ·提供总线的多路存取(对于一些总线); ·管理总线上的设备; 功能驱动程序 功能驱动程序是物理设备的主要驱动程序,它实现设备的具体功能,一般由设备的生产商来编写。功能驱动程序的主要功能是: ·提供对设备的操作接口; ·操作对设备的读写; ·管理设备的电源策略; ·过滤驱动程序 过滤驱动程序是一个可选项,当一个用户需要改变或新添一些功能到一个设备、一类设备或一种总线时,就可以编写一个过滤驱动程序。在设备栈里,过滤驱动程序安装在一个或几个设备驱动程序的上面或下面。过滤驱动程序拦截对具体设备、类设备、总线的请求,做相应的处理,以改变设备的行为或添加新的功能。但过滤驱动程序只处理那些它所关心的I/O请求,对于其他的请求可以交给其他的驱动程序来处理,这样可以非常灵活改变设备的行为,至少用户会这样看。比如: ·一个USB键盘的上层过滤驱动程序可以强制执行附加的安全检查。 ·一个鼠标的低层过滤驱动程序,通过对鼠标移动的数据做非线性的转换,可以得到一个有加速效果的鼠标轨迹。 功能驱动程序的组成 功能驱动程序由类驱动程序和微型驱动程序(Minidriver)组成。类驱动程序实现了某一类设备的常用操作,由微软提供,驱动程序的开发者可以只编写非常小的微型驱动程序,去处理具体设备特殊的操作,而对于其他大量的常规操作,可以调用该类的类驱动程序,这也是WDM驱动程序的优点之一。 微软提供的类驱动程序处理常用的系统任务,比如,即插即用功能和电源管理。类驱动程序保证了操作系统在处理类似的任务时的一致性,从而提高了系统的稳定性。 设备生产商提供微型驱动程序,以实现自己设备的特殊功能,同时调用合适的类驱动程序完成其他的通用工作。将大量的标准操作的代码通过各种类驱动程序来实现,并集成在操作系统中,这样的方式可以有效的减少具体设备的微型驱动程序的大小,也就减小了程序出错的可能。 如果某一类设备存在着工业标准,微软就会提供一个该类设备的WDM类驱动程序。这个类驱动程序实现了该类设备所有必须的任务,但不实现任何具体设备所特有的东西。比如,微软提供的HID(人工输入设备)类驱动程序的实现,是根据USB HID 类规范v.11的规定,但并不实现任何一种具体设备的特殊功能,比如,USB键盘、鼠标、游戏控制等等。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 本文所设计的驱动程序就是一个功能驱动程序,它是将USB驱动程序与微型驱动程序(Minidriver)结合起来,驱动USB键盘的一个驱动程序. 微软支持的WDM总线和类驱动程序 图1-4 微软支持的WDM总线和类驱动程序 对于图1-4,本文只描述其中的人工输入设备(HID)和USB部分。因为这是在USB键盘驱动程序设计中所涉及到的两个方面。USB总线驱动程序枚举和控制低速的USB总线。USB客户驱动程序使用各种IOCTL通过USB类驱动程序访问它们的设备。人工输入设备(HID)类驱动程序管理多种总线(如USB)间的数据与指令语法翻译。大多数时候,本类驱动控制由用户交互接口传来的数据,如键盘,鼠标和游戏杆等。 1.4 驱动程序的分层结构 FiDO FDO FiDO PDO 上层过滤驱动程序 功能驱动程序 低层过滤驱动程序 总线驱动程序 IRP 图1-5 WDM中设备对象和驱动程序的层次结构 WDM模型使用了如图1-5的层次结构。图中左边是一个设备对象堆栈。设备对象是系统为帮助软件管理硬件而创建的数据结构。一个物理硬件可以有多个这样的数据结构。处于堆栈最底层的设备对象称为物理设备对象(physical device object),或简称为PDO。在设备对象堆栈的中间某处有一个对象称为功能设备对象(functional device object),或简称FDO。在FDO的上面和下面还会有一些过滤器设备对象(filter device object)。位于FDO上面的过滤器设备对象称为上层过滤器,位于FDO下面(但仍在PDO之上)的过滤器设备对象称为下层过滤器。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 操作系统中的即插即用管理器(PnP Manager)根据设备驱动程序的指令来建立这个数据对象堆栈。前面我们已经知道,总线驱动程序的作用之一是枚举总线上的设备,当总线驱动程序检测到一个设备时,PnP管理器就马上建立一个PDO。当建立好PDO之后,PnP管理器通过查找注册表来找到其他的过滤驱动程序和功能驱动程序。设备的安装程序负责建立这些注册表里的表项,驱动程序的安装,是根据INF文件中的指令进行的。注册表中的表项指明了各种驱动程序在数据对象堆栈中的位置,于是PnP管理器开始装载最低层的过滤驱动程序,并调用该驱动程序的AddDevice函数。该函数在数据对象堆栈中建立一个FiDO,同时也将前面建立的PDO和这个FiDO联系在一起。PnP管理器反复的实现该过程,装载其他位置靠上的低层过滤驱动程序、功能驱动程序、上层过滤驱动程序,直到该堆栈完成。 应用程序对设备的存取通过提交IO请求包(IRP)来进行。在操作系统中,对设备的存取过程是这样的:操作系统中的I/O管理器接受I/O请求(通常是由用户态的应用程序发出的),建立相应的IRP来描述它,将IRP发送给合适的驱动程序,然后跟踪执行过程,当操作完成后,将返回的状态通知请求的发起者。操作系统中的I/O管理器、即插即用管理器、电源管理器都使用IRP来与内核模式驱动程序、WDM驱动程序进行通信,并且,各驱动程序之间的通信也是依靠IRP。 在WDM驱动程序中,IRP首先从最上层进入,如图1-5里右手边的箭头,然后,依次往下传送。在每一层,驱动程序自行决定对IRP的处理。有时,一个驱动程序除了把继续IRP向下传递外,并不做任何事情。有时,一个驱动程序会完全接管IRP,不再把它向下传递了。当然,一个驱动程序也可以处理IRP后,再把它继续向下传递。这取决于驱动程序的功能和IRP的含义。 从这里,可以知道微软在WDM模型中使用分层的驱动程序结构的原因了,通过分层的方法,在处理对设备的I/O请求时,利用添加合适的驱动程序层的方法,从而非常灵活的改变设备的行为,以实现不同设备的功能。 1.5 IO请求包(IRP) 操作系统使用I/0请求包(IRP)数据结构与内核模式驱动程序通信。这个数据结构很重要,需要了解它的创建、发送、处理,以及最后的销毁。可以说,IO请求包(IRP)才是WDM驱动程序结构的最重点,只有真正了解处理IRP的过程,才算是真正懂得了设备驱动的原理。 1.5.1IRP结构 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 图1-6 I/O请求包数据结构 MdlAddress(PMDL)域指向一个内存描述符表(MDL),该表描述了一个与该请求关联的用户模式缓冲区。如果顶级设备对象的Flags域为DO_DIRECT_IO,则I/O管理器为IRP_MJ_READ或IRP_MJ_WRITE请求创建这个MDL。如果一个IRP_MJ_DEVICE_CONTROL请求的控制代码指定METHOD_IN_DIRECT或METHOD_OUT_DIRECT操作方式,则I/O管理器为该请求使用的输出缓冲区创建一个MDL。MDL本身用于描述用户模式虚拟缓冲区,但它同时也含有该缓冲区锁定内存页的物理地址。为了访问用户模式缓冲区,驱动程序必须做一点额外工作。 Flags(ULONG)域包含一些对驱动程序只读的标志。但这些标志与WDM驱动程序无关。 AssociatedIrp(union)域是一个三指针联合。其中,与WDM驱动程序相关的指针是AssociatedIrp.SystemBuffer。 SystemBuffer指针指向一个数据缓冲区,该缓冲区位于内核模式的非分页内存中。对于IRP_MJ_READ和IRP_MJ_WRITE操作,如果顶级设备指定DO_BUFFERED_IO标志,则I/O管理器就创建这个数据缓冲区。对于IRP_MJ_DEVICE_CONTROL操作,如果I/O控制功能代码指出需要缓冲区(见第九章),则I/O管理器就创建这个数据缓冲区。I/O管理器把用户模式程序发送给驱动程序的数据复制到这个缓冲区,这也是创建IRP过程的一部分。这些数据可以是与WriteFile调用有关的数据,或者是DeviceIoControl调用中所谓的输入数据。对于读请求,设备驱动程序把读出的数据填到这个缓冲区,然后I/O管理器再把缓冲区的内容复制到用户模式缓冲区。对于指定了METHOD_BUFFERED的I/O控制操作,驱动程序把所谓的输出数据放到这个缓冲区,然后I/O管理器再把数据复制到用户模式的输出缓冲区。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 IoStatus(IO_STATUS_BLOCK)是一个仅包含两个域的结构,驱动程序在最终完成请求时设置这个结构。IoStatus.Status域将收到一个NTSTATUS代码,而IoStatus.Information的类型为ULONG_PTR,它将收到一个信息值,该信息值的确切含义要取决于具体的IRP类型和请求完成的状态。Information域的一个公认用法是用于保存数据传输操作,如IRP_MJ_READ,的流量总计。某些PnP请求把这个域作为指向另外一个结构的指针,这个结构通常包含查询请求的结果。 RequestorMode将等于一个枚举常量UserMode或KernelMode,指定原始I/O请求的来源。驱动程序有时需要查看这个值来决定是否要信任某些参数。 PendingReturned(BOOLEAN)如果为TRUE,则表明处理该IRP的最低级派遣例程返回了STATUS_PENDING。完成例程通过参考该域来避免自己与派遣例程间的潜在竞争。 Cancel(BOOLEAN)如果为TRUE,则表明IoCancelIrp已被调用,该函数用于取消这个请求。如果为FALSE,则表明没有调用IoCancelIrp函数。取消IRP是一个相对复杂的主题,我将在本章的最后详细描述它。 CancelIrql(KIRQL)是一个IRQL值,表明那个专用的取消自旋锁是在这个IRQL上获取的。当你在取消例程中释放自旋锁时应参考这个域。 CancelRoutine(PDRIVER_CANCEL)是驱动程序取消例程的地址。你应该使用IoSetCancelRoutine函数设置这个域而不是直接修改该域。 UserBuffer(PVOID) 对于METHOD_NEITHER方式的IRP_MJ_DEVICE_CONTROL请求,该域包含输出缓冲区的用户模式虚拟地址。该域还用于保存读写请求缓冲区的用户模式虚拟地址,但指定了DO_BUFFERED_IO或DO_DIRECT_IO标志的驱动程序,其读写例程通常不需要访问这个域。当处理一个METHOD_NEITHER控制操作时,驱动程序能用这个地址创建自己的MDL。 Tail.Overlay是Tail联合中的一种结构,它含有几个对WDM驱动程序有潜在用途的成员。由于篇幅有限,这里不再讨论。 1.5.2IRP处理的“标准模型” (1)创建IRP IRP开始于某个实体调用I/O管理器函数创建它。在上图中,我使用术语“I/O管理器”来描述这个实体,尽管系统中确实有一个单独的系统部件用于创建IRP。事实上,更精确地说,应该是某个实体创建了IRP,并不是操作系统的某个例程创建了IRP。例如,你的驱动程序有时会创建IRP,而此时出现在图中第一个方框中的实体就应该是你的驱动程序。 可以使用下面任何一种函数创建IRP: ·IoBuildAsynchronousFsdRequest 创建异步IRP(不需要等待其完成)。该函数和下一个函数仅适用于创建某些类型的IRP。 ·IoBuildSynchronousFsdRequest 创建同步IRP(需要等待其完成)。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 ·IoBuildDeviceIoControlRequest 创建一个同步IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求。 ·IoAllocateIrp 创建上面三个函数不支持的其它种类的IRP。 前两个函数中的Fsd表明这些函数专用于文件系统驱动程序(FSD)。虽然FSD是这两个函数的主要使用者,但其它驱动程序也可以调用这些函数。DDK还公开了一个IoMakeAssociatedIrp函数,该函数用于创建某些IRP的从属IRP。WDM驱动程序不应该使用这个函数。 (2)发往派遣例程 创建完IRP后,你可以调用IoGetNextIrpStackLocation函数获得该IRP第一个堆栈单元的指针。然后初始化这个堆栈单元。在初始化过程的最后,你需要填充MajorFunction代码。堆栈单元初始化完成后,就可以调用IoCallDriver函数把IRP发送到设备驱动程序: PDEVICE_OBJECT DeviceObject; //给定的设备对象 PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp); //获得指针 stack->MajorFunction = IRP_MJ_Xxx; <其他栈的初始化> NTSTATUS status = IoCallDriver(DeviceObject, Irp); IoCallDriver函数的第一个参数是你在某处获得的设备对象的地址。我将在本章的结尾处描述获得设备对象指针的两个常用方法。在这里,我们先假设你已经有了这个指针。 IRP中的第一个堆栈单元指针被初始化成指向该堆栈单元之前的堆栈单元,因为I/O堆栈实际上是IO_STACK_LOCATION结构数组,你可以认为这个指针被初始化为指向一个不存在的“-1”元素,因此当我们要初始化第一个堆栈单元时我们实际需要的是“下一个”堆栈单元。IoCallDriver将沿着这个堆栈指针找到第0个表项,并提取我们放在那里的主功能代码,在上例中为IRP_MJ_Xxx。然后IoCallDriver函数将利用DriverObject指针找到设备对象中的MajorFunction表。IoCallDriver将使用主功能代码索引这个表,最后调用找到的地址(派遣函数)。 派遣函数要对IRP的处理做出决定,有三种选择: ·派遣函数立即完成该IRP。 ·把该IRP传递到处于同一堆栈的下层驱动程序。 ·排队该IRP以便由这个驱动程序中的其它例程来处理。 每处理一个IRP,I/O管理器就调用一次StartIo例程: StartIo例程在DISPATCH_LEVEL级上获得控制,这意味着该函数不能生成任何页故障。另外,设备对象的CurrentIrp域和Irp参数都指向I/O管理器送来的IRP。 StartIo的工作是就着手处理IRP。如何做要完全取决于你的设备。通常你需要访问硬件寄存器,但可能有其它例程,如你的中断服务例程,或者是驱动程序中的其它例程也需要访问这些寄存器。实际上,有时着手一个新操作的最容易的方式是在设备扩展中保存某些状态信息,然后伪造一个中断。由于这些方法的执行都需要在一个自旋锁的保护之下,而这个自旋锁与保护你的ISR所使用的是同一个自旋锁,所以正确的方法是调用KeSynchronizeExecution函数。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 对于图中的中断服务例程,当设备完成数据传输后,它将以硬件中断形式发出通知。 DpcForIsr例程在DISPATCH_LEVEL级上获得控制。通常,它的工作就是完成IRP(导致最近的中断发生)。但一般情况下,它通过调用IoCompleteRequest函数把剩余的工作交给完成例程来做。 图1-7 I/O请求包处理流程 1.5.3完成I/O请求 派遣函数也可以在下面这两种情况下完成IRP: ·如果请求是错误的(可以以容易的检测方式查明,例如要求打印机倒纸请求或卸载键盘请求),则派遣例程应以失败方式完成该请求并返回适当的出错代码。    ·如果请求要求得到的仅是派遣函数可以容易确定的信息(例如一个询问驱动程序版本号的控制请求),则派遣例程应立即给出回答并完成请求,返回成功状态码。 完成机制是这样的,完成一个IRP必须先填充IoStatus块的Status和Information成员,然后调用IoCompleteRequest例程。Status值就是NTSTATUS.H中定义的状态代码。表中简要地列出了常用的状态代码。而Information值要取决于你完成的是何种类型的IRP以及是成功还是失败。通常情况下,如果IRP完成失败(即,完成的结果是某种错误状态),你应把Information域置0。如果你成功地完成了一个数据传输IRP,通常应该把Information域设置成传输的字节量。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 表1-1 状态代码 状态代码 描述 STATUS_SUCCESS 正常完成 STATUS_UNSUCCESSFUL 请求失败,没有描述失败原因的代码 STATUS_NOT_IMPLEMENTED 一个没有实现的功能 STATUS_INVALID_HANDLE 提供给该操作的句柄无效 STATUS_INVALID_PARAMETER 参数错误 STATUS_INVALID_DEVICE_REQUEST 该请求对这个设备无效 STATUS_END_OF_FILE 到达文件尾 STATUS_DELETE_PENDING 设备正处于被从系统中删除过程中 STATUS_INSUFFICIENT_RESOURCES 没有足够的系统资源(通常是内存)来执行该操作 为了了解低级驱动程序的I/O请求的结果,需要安装一个完成例程,调用IoSetCompletionRoutine函数: IoSetCompletionRoutine(Irp, CompletionRoutine, context, InvokeOnSuccess, InvokeOnError, InvokeOnCancel); Irp就是你要了解其完成的请求。CompletionRoutine是被调用的完成例程的地址,context是任何一个指针长度的值,将作为完成例程的参数。InvokeOnXxx参数是布尔值,它们指出在三种不同的环境中是否需要调用完成例程: ·InvokeOnSuccess 你希望完成例程在IRP以成功状态(返回的状态代码通过了NT_SUCCESS测试)完成时被调用。 ·InvokeOnError 你希望完成例程在IRP以失败状态(返回的状态代码未通过了NT_SUCCESS测试)完成时被调用。 ·InvokeOnCancel 如果驱动程序在完成IRP前调用了IoCancelIrp例程,你希望在此时调用完成例程。IoCancelIrp将在IRP中设置取消标志,该标志也是调用完成例程的条件。一个被取消的IRP最终将以STATUS_CANCELLED(该状态代码不能通过NT_SUCCESS测试)或任何其它状态完成。如果IRP以失败方式完成,并且你也指定了InvokeOnError参数,那么是InvokeOnError本身导致了完成例程的调用。相反,如果IRP以成功方式完成,并且你也指定了InvokeOnSuccess参数,那么是InvokeOnSuccess本身导致了完成例程的调用。在这两种情况中,InvokeOnCancel参数将是多余的。如果你省去InvokeOnSuccess和InvokeOnError中的任何一个参数或两个都省去,并且IRP也被设置了取消标志,那么InvokeOnCancel参数将导致完成例程的调用。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 这三个标志中至少有一个设置为TRUE。注意,IoSetCompletionRoutine是一个宏,所以你应避免使用有副作用的参数。这三个标志参数和一个函数指针参数在宏中被引用了两次。 IoSetCompletionRoutine将把完成例程地址和上下文参数安装到下一个IO_STACK_LOCATION中,即下一层驱动程序将在那个堆栈单元中找到这些参数。因此,最底层的驱动程序不应该安装一个完成例程。 1.5.4向下级传递请求 WDM使用分层设备对象结构的目的就是使IRP能方便地从一层驱动程序传递到下一层驱动程序。 有两种情况,有时候我们需要考虑IRP传递到下层驱动程序之后的事情,这时需要复制堆栈单元。这里一般不考虑。 1.5.5取消I/O请求 程序有时会取消它们原来请求的IRP。应用程序可能发出某些需要长时间才能完成的请求,然后这个应用程序结束执行,而这个IRP仍然是未完成的。这种情况在WDM模型中尤为常见,例如当新硬件插入系统时,驱动程序必须停止执行以等待配置管理器重新分配硬件资源,设备电源关闭时也是这样。 为了在内核模式中取消一个请求,IRP的创建者需调用IoCancelIrp函数。如果某线程终止时,它发出的请求仍然未完成,则操作系统自动为每个IRP调用IoCancelIrp。用户模式应用程序调用CancelIo函数可以取消给定线程发出的所有未完成的异步操作。IoCancelIrp仅仅是简单地设置IRP的Cancel标志位然后调用IRP的取消例程。即:它并不知道是否修改过IRP指针,也不知道是否正在处理这个IRP,所以它必须依靠一个提供的取消例程来做大部分IRP取消工作。 1.6 本设计开发的驱动程序描述 根据前面的描述,本论文所涉及的驱动程序,是一个功能驱动程序,它涉及到USB和HID两个类。这个驱动程序之上并没有过滤驱动程序,功能驱动程序将调用总线驱动程序的一些功能来完成自己的功能。 从功能方面来说,一个驱动程序可以做的工作有: ·初始化它自己 ·创建和删除设备 ·处理Win32打开和关闭文件句柄的请求 ·处理Win32输入/输出(I/O)请求 ·串行化对设备的访问 ·访问硬件 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 ·调用其他驱动程序 ·取消I/O请求 ·超时I/O请求 ·处理一个可热插拔的设备被加入或删除的情况 ·处理电源管理请求 ·使用WMI(Windows Management Instrumentation)和NT事件向系统管理员报告 只有“初始化”模块是必不可少的,本设计中,只用到了其中部分模块。因为对一个键盘驱动来说,许多功能是用不到的。 2 设计方案及设计工具、环境选择 2.1 键盘驱动程序设计方案及设计工具 键盘驱动有很多种设计工具,除了用DDK开发之外,还可以用Windriver,DriverStudio等开发工具开发。一般的来说,使用封装的更高层的工具象Windriver,开发起来周期较短,也更容易些,但是出了问题也更难调试。作为一个毕业设计,为了更深入的了解Windows驱动模型,应该选择使用DDK开发。 了解到键盘首先是一个HID设备,Windows系统是将键盘作为HID设备处理。因此,在开发键盘驱动的时候,是在一个HID minidriver的框架下来实现的,HID类驱动中有一些IOCTL(输入输出控制),我们所做的就是要建立起来这个框架,并且填充这些IOCTL,来实现驱动程序的完全功能。 那么怎么来实现读写键盘的功能成为下一步考虑的问题,因为键盘是一个USB键盘,这时候我们用到了USB类的IOCTL,在第一章里提到了Windows包含的各种类驱动,可以看到USB类和HID类都列在其中。事实上在这里它们结合起来组成了一个完整的USB键盘驱动程序。通过调用USB类的USBDI,我们可以实现读写键盘,启动键盘(USB设别),停止键盘(USB设备),移除键盘(USB设备)等一系列必须的事件的响应。将实现的代码添加到HID minidriver中。 这样,HID类的接口得以实现,对下面的一层则使用了USB驱动程序接口(USBDI)。一个完整的驱动程序的设计方案大致如上。事实上,如果只是做一个简单的访问键盘的程序,而不是将其嵌入在系统中,作为驱动程序的话,只需要USB类的特性就够了。那样的话,整个题目的难度会降低一些。 2.2 环境设置 2.2.1DDK的安装 DDK是驱动程序开发工具包,不同的操作系统有不同的版本,本论文设计的驱动程序是在Windows 2000环境下,因此使用Windows 2000DDK。安装DDK之后,需要把DDK的bin目录加入到VC++的目录列表中,这样一些使用到DDK头文件的客户程序,可以方便的找到它们要用的头文件。而不用专门拷贝出来。 DDK当中有一个setenv.bat,它来为VC++使用DDK进行开发做一些环境设置,并且检查开发工具VC++的版本,是否安装。DDK2000支持VC5和VC6,这也是为什么作者一开始选择VC++.NET作为开发工具,但是后来又转向VC6.0的原因。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 2.2.1makefile构造环境 当创建新的Makefile项目时,Visual Studio缺省提供两个build配置“Win32 Debug”和“Win32 Release”,build命令行中的设置,根据程序所在驱动器位置的不同而需要改变。build命令行运行MakeDrvr.bat批处理文件,使用DDKROOT环境变量,如果在Visual Studio中请求一个完整的重新构造,把选项-nmake /a添加到这个命令行。设置输出文件名,使得在build菜单中显示正确的名字。 代码清单2-1 Win32自由配置设置 build命令行 MakeDrvr %DDKroot% e:\lcsDriver checked 全部重新构造选项 -nmake /a 输出文件名 lcsDriver.sy 浏览信息文件名 objchk\i386\lcsDriver.bsc 如果在以上配置中,checked改为free,那么在浏览信息文件名中,应改为objfre\ i386\lcsDriver.bsc。 makefile文件时必不可少的,它是一个标准文件,激活DDK inc目录中的标准构造文件makefile.def。不要试图来编辑这个文件,为了把它添加到我们的工程中去,在SOURCES文件的SOURCES宏中,写入了它。 代码清单2-2 Makefile文件内容 # # DO NOT EDIT THIS FILE!!! Edit .\sources. if you want to add a new source # file to this component. This file merely indirects to the real make file # that is shared by all the driver components of the Windows NT DDK # !INCLUDE $(NTMAKEENV)\makefile.def 2.2.2build目录 与普通的生成程序不同,在Windows2000中,build分开保存自由构造版本和检查构造版本文件。如果TARGETPATH(目标路径)是OBJ,自由构造x86目标文件和最终的驱动程序进入OBJFRE\i386目录中,检查构造目标文件和驱动程序进入OBJCHK\i386目录中,在本论文的驱动程序中,使用的是后者。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 2.2.3MakeDrvr 在要求Visual Studio构造驱动程序时,批处理文件MakeDrvr.bat运行,它总是至少传递四个参数:DDK基目录,源驱动器,源目录和构造类型(“free”或“checked”)。任何其他的参数直接传递给build。 MakeDrvr先对传递的参数进行一些基本的检查,然后调用DDK setenv命令为build目录正确设置环境变量,改变目录为源驱动器和目录,最后调用build。-b选项保证显示完全的错误文本,-w选项保证在屏幕输出上出现警告,可以在Visual Studio的build Output窗口中发现它们。 MakeDrvr命令文件的屏幕输出出现在Visual Studio Output窗口中。 代码清单2-3 MakeDrvr文件 @echo off if "%1"=="" goto usage if "%3"=="" goto usage if not exist %1\bin\setenv.bat goto usage call %1\bin\setenv %1 %4 %2 cd %3 build -b -w %5 %6 %7 %8 %9 goto exit :usage echo usage MakeDrvr DDK_dir Driver_Drive Driver_Dir free/checked [build_options] echo eg MakeDrvr %%DDKROOT%% C: %%WDMBOOK%% free -cef :exit 2.2.4DebugPrint的使用 驱动程序没有任何保护,因为它是内核的一部分,要特别仔细地完全测试驱动程序,否则会丢失数据。驱动程序出错的方式有以下几种。 ·崩溃 ·内核转储 ·驱动程序不启动 ·挂起 ·资源遗漏 ·时间依赖性 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 DebugPrint软件用来允许我们使用格式化的打印语句跟踪驱动程序的执行。详细的信息来自于www.phdcc.com\DebugPrint。首先我安装了DebugPrint驱动程序,安装方法于安装驱动程序类似。它的用户态监视程序监视测试驱动程序的打印跟踪事件。 在程序中使用DebugPrint的方法如下: 首先我们要把DebugPrint.c和DebugPrint.h这两个标准文件复制到驱动程序项目中,然后在驱动程序的主头文件中包括DebugPrint.h。修改SOURCES文件,使得DebugPrint.c被构造。 驱动程序中调用DebugPrint函数,只能在DISPATCH_LEVEL或更低的IRQL调用这些函数,这意味着可以在DriverEntry例程,主IRP分发例程和StartIo及延迟过程调用(DPC)例程中调用DebugPrint函数,但不能在中断处理例程中调用。DebugPrintInit例程必须在PASSIVE_LEVEL IRQL调用。 DebugPrint调用在驱动程序的执行中只引起很小的延迟,DebugPrint调用的主要工作在以低实时优先级在后台运行的系统线程中发生。在本论文的驱动程序代码中一个典型的例子如下: 代码清单2-4 DebugPrint调用实例 NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject , IN PUNICODE_STRING RegistryPath) { #if DBG DebugPrintInit("Lcs_Kbd checked "); #else DebugPrintInit("Lcs_Kbd free "); …. 3 使用USB 3.1 USB类概述 图3-1 主要的USB类 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 图3-1列出了主要的USB设备类,由图中我们可以看出人工输入设备类(HID类)与USB类是有关系的,在下一章中,可以看出HID类和USB类的关系远不止如此。事实上,HID协议本来就是USB协议的一个子部分,最初的时候HID是因USB而生,但是现在它的作用不止如此。 当然HID设备是不一定在USB上运行的,但是因为一些特定的关系,它们能够很好的适合于USB设备模型。 3.2访问USB键盘的实现 3.2.1USBDI的IOCTL 使用Windows USB驱动程序接口(USBDI),可以编写访问USB键盘的驱动程序,并读取原始键盘输入数据。并可以对键盘进行控制,使得键盘上的LED灯发光或是熄灭。Windows 2000使用的是USBDI 2.00版,因为这个驱动程序中并没有用到WMI(Windows Management Instrumentation)数据,所以受版本的影响不大。表3-1中是USB驱动程序接口的内部IOCTL。 表3-1 USB驱动程序接口的内部IOCTL IOCTL_INTERNAL_USB_SUBMIT_URB 发出URB停止等待结果 IOCTL_INTERNAL_USB_RESET_PORT 复位并重新启用一个端口 IOCTL_INTERNAL_USB_GET_PORT_STATUS 得到状态位: USBD_PORT_ENABLED USBD_PORT_CONNECTED IOCTL_INTERNAL_USB_ENABLE_PORT 重新启用一个被禁止的端口 IOCTL_INTERNAL_USB_GET_HUB_COUNT 集线器驱动程序内部使用 IOCTL_INTERNAL_USB_CYCLE_PORT 模拟设备拔出和再次插入 IOCTL_INTERNAL_USB_GET_ROOTHUB_PDO 集线器驱动程序内部使用 IOCTL_INTERNAL_USB_GET_HUB_NAME 得到集线器驱动程序的设备名 IOCTL_INTERNAL_USB_GET_BUS_INFO 获得USB总线信息 IOCTL_INTERNAL_USB_GET_CONTROLLER_NAME 得到主机控制器设备名 最重要的内部IOCTL是IOCTL_INTERNAL_USB_SUBMIT_URB,它发出USB请求块(URB)由USB类驱动程序处理。有30多个不同的URB功能代码。USB客户使用URB做它们大多数的工作。URB结构是一个联合,含有16个不同的_URB_*结构。 3.2.2使用USBDI访问USB键盘 对一个USB键盘来说,它有这么几种操作,复位设备,配置设备,和读写设备。在使用USB设备之前,第一件事就是要复位它,来确保它可用。然后主机负责配置设备。配置设备的过程与物理特性相关。在USB键盘的配置中,我们所要做的是获得设备的配置描述符,然后找出接口,设备接口的每个管道的最大传输数据包为8,创建配置设备的URB请求发送给USBDI。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 具体的配置过程在第五章中描述,在这里主要涉及的是,如何使用USBDI。 USB设备驱动程序从不直接与硬件对话。它仅靠创建URB(USB请求块)并把URB提交到总线驱动程序就可完成硬件操作。系统当中的文件USBD.SYS,是接受URB的实体。向USBD的调用被转化为带有主功能代码为IRP_MJ_INTERNAL_DEVICE_CONTROL的IRP。然后USBD再调度总线时间,发出URB中指定的操作。 (1)为了创建一个URB,首先应该为URB分配内存,然后调用初始化例程把URB结构中的各个域填入请求要求的内容 (2)创建完URB后,你需要创建并发送一个内部I/O控制(IOCTL)请求到USBD驱动程序,USBD驱动程序位于驱动程序层次结构的低端。在大多数情况下,你需要等待设备回应。 (3)当提交一个URB到USB总线驱动程序时,最终将收到一个描述该操作结果的NTSTATUS代码。当USBD完成一个URB时,它就把URB的UrbHeader.Status域设置为某个USBD_STATUS值。DDK中的URB_STATUS宏可以简化这个值的存取。 这就是调用USBDI的完整过程。 3.3 USB键盘的一个特点 这个问题本来可以在后面inf文件的部分予以说明,但是因为设计到USB,所以在这里描述。下面是标准安装文件中的一部分,它的意思是,这个设备驱动程序所对应的设备属于USB类,ClassGUID给出的是设备的标识符。 Class=USB ClassGUID={36FC9E60-C465-11CF-8056-444553540000} 替代Windows 2000中的键盘驱动,用这样的配置文件是不可能的,Windows 2000有对USB键盘的内置支持,在这两个操作系统中,它们装入人工输入设备(HID)类驱动程序,然后标准键盘驱动程序通过HID客户驱动程序得到键输入。 有两种方法可以使生成的.sys文件得以安装到系统中,第一种来自于一本描述Windows 2000 wdm驱动程序设计的书,在参考书目中列出了它的具体信息。这个方法是将用生成的.sys文件(本设计中是Lcs_Kbd.sys)替换HID USB小驱动程序HidUsb.sys。Windows使用HidUsb.sys处理HID USB设备,这种替换是可以的,但是却使用户不能使用USB键盘进行常规的输入。 另外一种方法是将标准安装文件改为: Class=Keyboard ClassGUID={4D36E96B-E325-11CE-BFC1-08002BE10318} 4 HID小驱动程序实现 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 人工输入设备模型(HID),是与用户输入设备交互的标准方法,HID设备使用各种描述符定义它的功能。报告描述符详细说明它可以产生的输入报告呵可以接收的输出报告。 HID规范是用于控制计算机的大多数人工输入设备的抽象模型,例如,一个输入设备可以是普通的老式键盘,汽车模拟方向盘或计算机的软开关按钮。 HID规范一开始是为USB设备写的,紧密遵循USB描述符,这就是为什么上面说HID设备可以很好的遵循USB的原因,但是HID用于其它类型的设备是非常常见的。 HID设备大多数时候为计算机提供输入数据,但是,也可以输出到HID设备(如打开键盘上的LED灯)。以可以控制设备的特性,如用于现实设备的字体或者LED颜色,但是这些不在本设计的范围之内了。 在Windows中,系统HID类驱动程序提供输入设备的一个抽象视图。HID设备自身可以是USB设备,IEEE 1394设备,甚至只要为普通的PC兼容的设备编写一个合适的HID小驱动程序与总线或设备接口,它也可以成为HID设备。 图4-1 驱动程序结构图 图4-1说明了在Windows中,HID设备是如何被使用的。首先从输入说起,HID提供输入设备的一个抽象模型,这样客户程序不关心输入来自什么地方。HID把一个设备描述为代表该设备的各种控制。本论文所涉及到的是键盘,对于一个键盘,它通常是有单个位控制,代表每个修饰键的状态,并且有一个扫描代码字节控制数据,代表目前按下的其他键。 系统HID类驱动程序做大多数的工作,它把它的硬件交互委托给HID小驱动程序,Windows包括一个用于USB总线上的设备的HID小驱动程序HidUsb.sys,有合适的HID接口类常量和HID描述符的新USB设备不需要新的小驱动程序。系统USB HID小驱动程序调用系统USB驱动程序USBD.sys,可选的通过所有低层USB过滤驱动程序,然后系统USB驱动程序访问设备。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 所以现在可以得出我将要在详细设计里描述的键盘驱动程序的大致结构,HID客户只访问HID类驱动程序,HID小驱动程序调用USB驱动程序接口(USBDI),这样得以实现一个完整的键盘驱动程序。 4.1 HID概述 HID小驱动程序需要返回特定的数据结构给HID类驱动程序,告诉它设备的功能。对于许多新设备来说,它们由设备自身提供。关键的数据结构是: ·HID描述符 ·设备属性 ·报告描述符 ·身体部位描述符 ·字符串描述符 在本论文中,只介绍跟本驱动程序最为相关的HID描述符和报告描述符。 4.2 HID模型 4.2.1HID描述符 HID描述符提供关于设备的第一个信息,它主要确认这是一个HID设备,并提供其报告描述符的长度和所有身体部位描述符。 表中给出了HID_DESCRIPTOR结构以及典型的值。DescriptorList域通常只有一个元素,详细说明报告描述符和它的长度。但是,它也可以指定身体部位描述符,提供关于激活设备上的控制的人类身体部位的信息。 图4-2 HID描述符结构 本论文所设计的驱动程序使用倒的HID描述符,基本上和上表中是一样的。它们的值在最右边的一栏。bLength标识HID描述符的长度为9,bDescriptorType表明描述符的类型是HID_HID_DESCRIPTOR_TYPE,后面跟着的0x21,是它的宏。bcdHID是HID规范版本,它的值是0x0100,国家代码为bCountry,值是0,未指定的意思就是任何国家都可以使用。bNumDescriptors所代表的值,是指在系统中用到了除本描述符之外的几个描述符, 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 在本论文设计的驱动程序中,用到了另外一个报告描述符,结构如下: struct _HID_DESCRIPTOR_DESC_LIST { UCHAR bDescriptorType; //描述符类型 USHORT wDescriptorLength; //描述符长度 }DescriptorList[1]; 在这个结构中,第一个UCHAR bDescriptorType变量规定了使用的描述符的类型,图中给出来的0x22,代表的是报告描述符。本论文设计的驱动程序中只用到了这一个描述符,如果要用到更多,可以在定义这个结构体数组的时候,赋给它需要的数组元素数,但是我做的这个驱动程序没有涉及,因此不再叙述。 4.2.2HID报告描述符 如上面HID描述符所述,这个驱动程序需要一个编号为0x22的报告描述符。报告描述符使一个稍微复杂的结构,它详细说明设备的功能。可以描述多个功能。 本驱动程序还要提供一个报告描述符,其长度为0x3F(即63)字节。其相应的值如表所示: 表4-1报告描述符的结构 char ReportDescriptor[63] = { 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x06, // USAGE (Keyboard) 0xa1, 0x01, // COLLECTION (Application) 0x05, 0x07, // USAGE_PAGE (Keyboard) 0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl) 0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x75, 0x01, // REPORT_SIZE (1) 0x95, 0x08, // REPORT_COUNT (8) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x08, // REPORT_SIZE (8) 0x81, 0x03, // INPUT (Cnst,Var,Abs) 0x95, 0x05, // REPORT_COUNT (5) 0x75, 0x01, // REPORT_SIZE (1) 0x05, 0x08, // USAGE_PAGE (LEDs) 0x19, 0x01, // USAGE_MINIMUM (Num Lock) 0x29, 0x05, // USAGE_MAXIMUM (Kana) 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 0x91, 0x02, // OUTPUT (Data,Var,Abs) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x03, // REPORT_SIZE (3) 0x91, 0x03, // OUTPUT (Cnst,Var,Abs) 0x95, 0x06, // REPORT_COUNT (6) 0x75, 0x08, // REPORT_SIZE (8) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x65, // LOGICAL_MAXIMUM (101) 0x05, 0x07, // USAGE_PAGE (Keyboard) 0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated)) 0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application) 0x81, 0x00, // INPUT (Data,Ary,Abs) 0xc0 // END_COLLECTION }; 表4-1是C语言格式定义的报告描述符。报告描述符不像其它描述符那样简单,它并不是简单的值的列表。报告描述符长度是可变的,这取决于描述符中项目的个数,报告描述符用项目来描述设备的信息。项目的第一部分包括三个域:项目类型、项目标记和项目大小,这三个域一起来识别项目提供的信息。项目类型有三种:Main、Global和Local。Main项目有五种标记。下面说明本论文的驱动程序中使用的报告描述符的含义,也就是表4-1中列出的数据的含义。 前两行说明此此设备是一键盘;从第三行开始到结束是一个Collection集合,在这个集合中定义了数据的输入输出格式和输入输出数据的范围;第四行是一个用法页(Keyboard)的开始;第五行和第六行定义此用法页的数据表达的范围是从左CTRL键到右GUI键;第七行和第八行定义此用法页数据逻辑值的最大值1和最小值0;第九行定义此用法页中的每个数据占用的位数为1位;第十行定义此用法页数据共有8个;第十一行定义一输入数据。至此第一个输入数据定义完成,就是输入数据中的第一个字节的8位的分别表示从CTRL键到GUI键的8个键;第十二行到第十四行又定义8个保留的输入数据,每个数据占一位。第十五行到第二十行定义了5个输出数据,用来控制5个键盘上的LED灯,在本论文中的驱动程序中只有三个LED灯。五个LED灯是从Num Lock到Kana,每个数据占一位,共五个。这个输出数据的用法页为LEDs。第二十一行到二十三行定义了三个保留的输出数据,每个数据一位,这样输出数据就刚好是一个字节了。第二十四行到第三十一行定义了六个输入数据,每个输入数据占8位,其取值范围是0到101,在Keyboard用法页中,他们用来表示键盘上按下的键的代码,也就是说键盘上有键按下时,最多能记录六个同时按下的键的代码。第三十二行是End Collection,表示这个集合定义完成了。 本论文中的驱动程序使用的报告描述符描述的输入输出数据的格式如图4-3所示。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 图4-3 键盘输入输出报告的设计(Layout of keyboard input and output reports) 4.3 HID键盘的控制实现 4.3.1HIDCLASS小驱动的IOCTL 如同前面USB驱动程序接口的内部IOCTL一样,HID类也有自己的IOCTL,它们列在下面: 表4-2 HIDCLASS小驱动的IOCTL IOCTL_GET_PHYSICAL_DESCRIPTOR Gets USB-standard physical descriptor IOCTL_HID_GET_DEVICE_ATTRIBUTES Returns information about device as if it were USB IOCTL_HID_GET_DEVICE_DESCRIPTOR Returns a USB-standard HID descriptor IOCTL_HID_GET_FEATURE Reads a feature report IOCTL_HID_GET_INDEXED_STRING Returns a USB-standard string descriptor IOCTL_HID_GET_STRING Returns a USB-standard string descriptor IOCTL_HID_GET_REPORT_DESCRIPTOR Returns a USB-standard report descriptor IOCTL_HID_READ_REPORT Reads a report conforming to the report descriptor IOCTL_HID_SEND_IDLE_NOTIFICATION Idles the device (new in Windows XP) IOCTL_HID_SET_FEATURE Writes a feature report 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 IOCTL_HID_WRITE_REPORT Writes a report 但是本论文涉及的驱动程序中只用到了IOCTL_HID_GET_DEVICE_ATTRIBUTES,IOCTL_HID_GET_DEVICE_DESCRIPTOR,IOCTL_HID_GET_REPORT_DESCRIPTOR和IOCTL_HID_READ_REPORT,IOCTL_HID_WRITE_REPORT这几个内部IOCTL。 这些内部IOCTL在IRP_MJ_INTERNAL_DEVICE_CONTROL例程中处理,详细的描述在第五章中。 4.3.2使用HID类IOCTL实现控制 本论文设计的驱动程序中只用到了IOCTL_HID_GET_DEVICE_ATTRIBUTES(获取设备属性信息),IOCTL_HID_GET_DEVICE_DESCRIPTOR(获取HID描述符信息),IOCTL_HID_GET_REPORT_DESCRIPTOR(获取报告描述符信息)和IOCTL_HID_READ_REPORT(键盘读取报告),IOCTL_HID_WRITE_REPORT(写键盘报告)这几个内部IOCTL。 实现这些内部IOCTL,是HID Minidriver的主要要求,具体的实现在IRP_MJ_INTERNAL_DEVICE_CONTROL例程中。更具体的描述,在第五章中。 5 系统开发流程 5.1 程序的结构设计 5.1.1结合USB和HID 图5-1HID USB键盘I/O请求处理 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 由图5-1可以看出,HID类与USB类之间的调用关系,键盘驱动程序是系统中的一个中层驱动,它的下层驱动接口是USBDI,通过对USBDI的调用,实现对USB键盘的读写操作,以及配置设备等。它的上层是HID类驱动程序,本论文涉及的键盘驱动程序要为上层的HID类驱动程序提供接口。下面的两节分别描述了对USB键盘的操作,和HID类小驱动程序的实现。这两者嵌套在一起,实现了一个完整的USB键盘驱动。 5.1.2代码文件结构 一个makefile工程是这样的,它所编译的文件都列在Sources文件中的宏里。Makefile工程有几个设置文件,来设置编译的环境和其他信息。VC++的build项规定了将DriverEntry函数作为驱动程序的入口,事实上这个入口可以通过修改build项来改变,但是这不重要。我们知道这个工程虽然没有一个主函数,但是它的确是从DriverEntry开始的。DriverEntry函数当中规定了一些项,这些项规定了各个例程在哪个函数中。具体运行的时候会找到这些代码并执行。 为了方便起见,我们将程序当中的文件分为三种,一种是头文件,第二种是实现文件,第三种是配置文件,头文件有resource.h ,ReportDescriptor.h,Lcs_Kbd.h等,它们定义了一些结构,象键盘报告描述符,系统中用到的函数定义等。实现文件有StartIo.cpp ,Usb.cpp, Pnp.cpp, InterIoctl.cpp, Init.cpp等,它们实现各个例程的函数。配置的文件则有MakeDrvr.bat,MAKEFILE,Sources等,象MAKEFILE这样的文件,它的内容是不推荐改变的。MakeDrvr是一个设置环境变量的批处理文件,将在后面描述。 我们并不是十分关心函数在每个文件里是如何被放置的,在DriverEntry中已经做了一些设置,还是取几乎是公例的文件名和函数名的原因,是编写规范代码的需要。一个函数名文件名规范,代码编写规范的好的源码,是便于调试和阅读的。 5.2 调用USBDI实现USB键盘控制 5.2.1复位设备 USB设备交互的第一件事是复位设备,确保设备可用。向USBDI发送IOCTL_INTERNAL_USB_GET_PORT_STATUS请求,获取USB设备状态信息。如果设备是未连接状态,驱动程序不会安装。如果设备没有处于使能状态,就要发送IOCTL_INTERNAL_USB_RESET_PORT请求给USBDI来复位设备。 5.2.2配置设备 USB设备在正常被使用以前,必须被配置,由主机负责配置设备。主机一般会从USB设备获取配置信息后再判定此设备有哪些功能。在本驱动程序的Usb.cpp文件中的UsbSelectConfiguration函数,是配置设备的过程,首先要获得设备的配置描述符(见UsbGetConfigurationDescriptors函数),然后找出接口,设备接口的每个管道的最大传输数据包为8,创建配置设备的URB请求发送给USBDI(见CreateConfigurationRequest函数),配置设备,配置设备成功之后保存配置句柄和管道句柄,以备将来使用 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 5.2.3读写键盘 读键盘容易理解,写键盘则是设置键盘上的LED灯。键盘的数据格式在4.2.2节的报告描述符中已经描述过。另外需要提供一个数据缓冲区来保存数据,上一节提到最大数据包的大小为8,因此这个缓冲区的大小不能小于8字节,如果小于8字节,应该提示出错。在翻译过的USB2.0标准文档里提到了各种数据传输的方式,USB键盘适合中断传输方式,用创建URB的接口创建一个_URB_BULK_OR_INTERRUPT_TRANSFER类型的URB请求,发送到USBDI,处理之后,URB中就会有输入的数据。完成IRP,将数据返回给请求者。读操作完成(详细的描述在Usb.cpp文件中的UsbDoInterruptTransfer函数中)。 写操作是设置键盘上的LED灯,输出格式就是4.2.2节当中报告描述符的输出数据格式,首先要创建一个_URB_CONTROL_VENDOR_OR_CLASS_REQUEST 类型的URB请求。然后发送给USBDI,USBDI处理这个请求,设置键盘上的LED灯,完成IRP,通知请求者,键盘的写操作完成(详细的描述在Usb.cpp文件中的UsbSendOutputReport函数中)。 5.3 为HID类提供接口 从某种意义上可以说,HID小驱动程序提供了对HID类设备进行操作的框架。在一些例程中,我们需要添加一些东西,来实现对HID类设备的操作。 5.3.1DriverEntry例程实现 DriverEntry例程除了自己需要做的工作之外,它需要注册成为HID小驱动程序,注册的示例代码如下: 代码清单5-1 1DriverEntry例程 extern "C"  NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject,   PUNICODE_STRING RegistryPath)   { //例程的函数   DriverObject->DriverExtension->AddDevice = AddDevice;   DriverObject->DriverUnload = DriverUnload;   DriverObject->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL] =     DispatchInternalControl;   DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp;   DriverObject->MajorFunction[IRP_MJ_POWER] = DispatchPower; //注册成为HID Minidriver   HID_MINIDRIVER_REGISTRATION reg;   RtlZeroMemory(®, sizeof(reg)); 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页   reg.Revision = HID_REVISION;   reg.DriverObject = DriverObject;   reg.RegistryPath = RegistryPath;   reg.DeviceExtensionSize = sizeof(DEVICE_EXTENSION);   reg.DevicesArePolled = FALSE;  // 以具体硬件为准   return HidRegisterMinidriver(®);   } HidRegisterMinidriver是注册的系统例程,可以看到首先是填写一个注册HID小驱动的结构体HID_MINIDRIVER_REGISTRATION,然后用这个结构体注册。 5.3.2IRP_MJ_INTERNAL_DEVICE_CONTROL例程实现 这个例程中有一些IOCTL,它们用来与HID设备交互。在4.2.3 节HIDCLASS小驱动的IOCTL中,有对这几个IOCTL的详细描述。 ·IOCTL_HID_GET_DEVICE_ATTRIBUTES这个IOCTL返回给HID类小驱动程序设备属性信息,程序填写一个PHID_DEVICE_ATTRIBUTES结构,并将其返回。 ·IOCTL_HID_GET_DEVICE_DESCRIPTOR这个IOCTL返回HID描述符,程序填充一个HID_DESCRIPTOR结构,并将其返回。 ·IOCTL_HID_GET_REPORT_DESCRIPTOR这个IOCTL给HID小驱动程序返回报告描述符,将ReportDescriptor变量复制到HID类小驱动程序的缓冲区。 ·IOCTL_HID_READ_REPORT这个IOCTL是读取键盘数据,程序清单如下: 代码清单5-2 IOCTL_HID_READ_REPOR if(outLen<8) //不小于8字节 { status = STATUS_BUFFER_TOO_SMALL; break; } if(dx->bStarted==FALSE) { status = STATUS_DEVICE_NOT_READY; break; } if(Buffer==NULL) //缓冲区为空? { status=STATUS_INVALID_PARAMETER; 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 break; } IoMarkIrpPending(Irp); //置IRP为未处理 IoStartPacket(fdo,Irp,0,NULL); return STATUS_PENDING; //返回未处理状态 先判断缓冲区的大小是不是比最大值8小,如果小于8个字节,则返回错误信息STATUS_BUFFER_TOO_SMALL,并退出。然后检查系统线程的状态,检查缓冲区是否已经有值,如果错误,返回错误信息并退出。接下来的IoMarkIrpPending函数是将IRP置为未处理状态,然后放入队列,返回未处理的状态。 ·IOCTL_HID_WRITE_REPORT是用来设置LED灯的,代码如下: 代码清单5-3 IOCTL_HID_WRITE_REPORT if(inLenAddDevice 指向驱动程序的AddDevice函数。PnP管理器将为每个硬件实例调用一次AddDevice例程。 ·DriverStartIo 如果驱动程序使用标准的IRP排队方式,应该设置该成员,使其指向驱动程序的StartIo例程。 ·MajorFunction 是一个指针数组,I/O管理器把每个数组元素都初始化成指向一个哑派遣函数,这个哑派遣函数仅返回失败。驱动程序可能仅需要处理几种类型的IRP,所以至少应该设置与那几种IRP类型相对应的指针元素,使它们指向相应的派遣函数。 5.4.2AddDevice例程与实现 通常,一个驱动程序可以被多个设备利用。WDM驱动程序有一个特殊的AddDevice函数,PnP管理器为每个设备实例调用该函数。该函数的原型如下: NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo) { } DriverObject参数指向一个驱动程序对象,就是你在DriverEntry例程中初始化的那个驱动程序对象。pdo参数指向设备堆栈底部的物理设备对象。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 对于功能驱动程序,其AddDevice函数的基本职责是创建一个设备对象并把它连接到以pdo为底的设备堆栈中。相关步骤如下: (1)调用IoCreateDevice创建设备对象,并建立一个私有的设备扩展对象。 (2)寄存一个或多个设备接口,以便应用程序能知道设备的存在。另外,还可以给出设备名并创建符号连接。 (3)初始化设备扩展和设备对象的Flag成员。 (4)调用IoAttachDeviceToDeviceStack函数把新设备对象放到堆栈上。 5.4.3Dispatch例程与实现 Dispatch例程由I/O管理器定义如下: NTSTATUS (*PDRIVER_DISPATCH) ( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ); 通常,大多数驱动程序必须处理一些或者所有以下请求: ·LRP_MJ_PNP表明一个包括PnP设备识别的请求,硬件配置,或者资源分配的请求。这类请求显然从PnP管理器或者从一个与较高层驱动程序密切相关的设备发送到一个设备驱动程序。 ·IRP_MJ_POWER表明一个属于设备或者系统的电源状态请求。这类请求通过电源管理器或者一个密切连接的较高层驱动程序发送到设备驱动程序。 ·IRP_MJ_CREATE表明用户模式保护子系统,可能代表应用或者特定子系统驱动程序,已请求一个与目标设备对象有关的文件对象的控制,或者一个较高层的驱动程序把其设备对象连接或者附加到目标设备对象。 ·IRP_MJ_CLEANUP表明,为代表目标设备对象的文件对象的处理被关闭,因此任何在清除IRP的驱动程序I/O栈位置中找到的,在给定文件对象目标设备的当前队列中的IRP,应该被取消。 ·IRP_MJ_CLOSE表明代表目标设备对象的文件对象句柄或指向目标设备对象的指针已经被关闭,还表明没有未完成的、对文件对象指针的引用。 ·IRP_MJ_READ表明I/O请求从下层物理设备向系统传输数据。 ·IRP_MJ_WRITE表明I/O请求从系统向下层物理设备传输数据。 ·IRP_MJ_DEVlCE_CONTROL表明一个带有系统定义的,指定一个被发送给设备驱动程序的具体设备操作的特定设备类型I/O控制码的请求。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 ·IRP_MJ_INTERNAL_DEVICE_CONTROL表明一个传给设备驱动程序的请求,大多数情况下是来自一个紧密连接的较高层驱动程序,通常带有一个私有的、驱动程序特定的及特定设备类型的或特定设备的I/O控制码,该控制码请求一项特定设备类型的或特定设备的操作。 仅仅某些种类的驱动程序要去处理系统定义的内部设备I/O控制请求,包括SCSI驱动程序,键盘或鼠标设备驱动程序,及与系统支持的驱动程序进行互操作的并行驱动程序。 任何驱动程序都必须在驱动程序对象中设置的Dispatch例程入口点根据下层物理设备的类型和功能的不同而不同。 在本论文所涉及的驱动中,用到了LRP_MJ_PNP例程,它实现了USB键盘的即插即用功能。 5.4.4StartIo例程与实现 StartIo例程由I/O管理器定义如下: VOID ( *PDRIVER_STARTIO) ( IN PDRIVER_OBJECT DeviceObject, IN PIRP Irp ); StartIo例程运行在位于IRQLDISPATCH_LEVEL上的任意线程环境中。 运行在IRQLDISPATCH_LEVEL上限制了这组StartIo例程可以调用的支持例程。因此我们需要在一个系统线程中调用StartIo例程,关于系统线程,因为在本毕业设计中用到的地方较少,会用很少的篇幅来讨论它。 Windows NT/Windows 2000在多任务及多线程环境中支持异步I/O,I/O请求可以比驱动程序处理完成更快地到达,特别是在一个多处理器的机器中。因此,任何具体设备在忙与处理另一个IRP时,必须让超出限度的IRP在驱动程序上排队。 因此,最低层驱动程序必需以下之一: ·StartIo例程,I/O管理器用一个IRP调用它来启动一个该驱动程序的设备上的I/O操作 ·它自己的IRP排队机制,它用来控制比驱动程序的处理速度更快地到达的IRP 仅有一个最低层设备驱动程序能在其Dispatch例程中满足和完成所有可能的IRP,而不需要StartIo例程和/或驱动程序管理的IRP队列. 较高层驱动程序几乎从来没有StartIo例程。大多数中间层驱动程序也没有内部队列;一个中间层驱动程序通常从其Dispatch例程传送带有效参数的IRP,并且为任何其IoCompletion例程中的IRP做必要的处理后操作。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 大多数设备驱动程序有StartIo例程,因为大多数PC外设同时仅能处理一个设备I/O操作。对于有一个StartIo例程的驱动程序,I/O管理器提供IoStartPacket和IoStartNextPacket来管理驱动程序中的IRP队列。 5.4.5IoCompletion例程与实现 I/O管理器对IoCompletion例程的定义如下: NTSTATUS {*PIO_COMPLETION_ROUTINE}( IN PDEVICE_OBJEXT DeviceObject, IN PIRP Irp, IN PVOID Context ); 高层驱动程序完成各种操作的IRP需要多少个IoCompletion例程,就可以建立多少个IoCompletion例程。 IoCompletion例程是运行在等于或低于DISPATCH_LEVEL的IRQL上和专用线程环境中的。驱动程序无法预先知道IoCompletion例程将在哪一级IRQL上被调用。 本论文涉及到的程序中,处理IRP_MN_START_DEVICE例程的时候,用到了完成例程。 5.4.6Unload例程与实现 任何在系统运行时可以被替换或卸载并重载的驱动程序都必须有Unload例程。 I/O管理器对Unload例程的定义如下: VOID (*PDRIVER_UNLOAD)( IN PDRIVER_OBJECT DriverObject ); DriverObject参数是传递给驱动程序的DriverEntry例程的指针。 视频、键盘或鼠标设备和能保存系统当前页文件的海量存储设备的驱动程序没有Unload例程,因为这些类型的设备必须在当前系统运行时始终可用。位于这些设备驱动程序之上的高层驱动程序也没有Unload例程。 Unload例程负责在驱动程序自身被卸载之前释放驱动程序使用的所有系统对象和资源。像DriverEntry例程一样,Unload例程运行在PASSIVE_LEVEL上并在系统线程的环境中被调用。 在本论文涉及到的程序中,DriverEntry例程没有分配资源,因此Unload例程并没有用到,但是了解它,也是很有意义的。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 5.5 实现即插即用 5.5.1即插即用 即插即用不是一个简单的问题,它的具体实现有许多方面。为了有一个总体上的概念,我将介绍一个即插即用的系统应该能满足的以下两个目标: (1)当新的设备加入系统时,处理这些新设备。可以在软件中配置的设备必须被告知使用哪些资源和地址。 (2)通过提供标准驱动程序,使驱动程序更容易访问复杂的设备。对于通用串行总线这样的相对复杂的总线,在其他驱动程序在想访问该总线时提供必须使用的标准驱动程序是有用的。 如果所有的工作都由我来做,那将是一个庞大的系统。几乎不可能完成,幸好PnP驱动程序包揽了大多数的任务,我们只需要响应其PnP消息,做有限的工作,就可以实现想要的功能了。 在本论文所设计的键盘驱动程序中,我们需要响应的功能代码有: IRP_MN_START_DEVICE IRP_MN_REMOVE_DEVICE IRP_MN_SURPRISE_REMOVAL IRP_MN_STOP_DEVICE 实际上,一个驱动程序要实现一些基本的处理,比如,处理设备的添加和删除,得到分配的资源,处理查询停止和查询删除消息,处理停止设备消息,处理意外删除消息。 在我做的这个键盘驱动里,只处理了以上几个Pnp消息。 5.5.2IRP_MJ_PNP例程 代码清单5-4是IRP_MJ_PNP例程的主要处理代码,这个处理的函数在DriverEntry例程中注册,因此系统可以找到它并执行。 代码清单5-4 IRP_MJ_PNP例程主要处理代码 switch(MinorFunction) { case IRP_MN_START_DEVICE: status = PnpStartDeviceHandler(fdo,Irp); break; case IRP_MN_REMOVE_DEVICE: status = PnpRemoveDeviceHandler(fdo,Irp); return status; case IRP_MN_STOP_DEVICE: 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 status = PnpStopDeviceHandler(fdo,Irp); break; case IRP_MN_SURPRISE_REMOVAL: status = PnpSurpriseRemovalHandler(fdo,Irp); break; default: status = PnpDefaultHandler(fdo , Irp); break; } 每一个功能代码的处理都有一个函数,在Pnp.cpp里有具体的描述,这里只说一下PnpDefaultHandler的功能。它向设备栈下传递PnP IRP,由底层设备驱动程序处理,当希望有关驱动程序处理IRP时,调用IoCallDriver。具体的代码清单如下: 代码清单5-5 PnpDefaultHandler函数清单 NTSTATUS PnpDefaultHandler(IN PDEVICE_OBJECT fdo , IN PIRP Irp) { DebugPrintMsg("PnpDefaultHandler "); IoSkipCurrentIrpStackLocation(Irp); return IoCallDriver(LDO(fdo),Irp); } 5.6 将驱动程序嵌入到系统中 5.6.1驱动程序安装的方法 安装驱动程序的方法有很多,如果手工安装,则需要把编译生成的驱动程序拷贝到Windows 2000系统中的\system32\drivers子目录下,然后使用REGEDIT.EXE在Registry下注册,添加适当的项。首先在HKEY_LOCAL_MACHINE\system\CurrentcontrolSet\Services\下添加驱动程序的服务健,因为Windows 2000是把已经安装的驱动程序看作是服务。然后在这个服务键中添加关于这个服务的信息。然后重新启动系统,具体的键值的含义不再叙述,因为本论文用.inf文件来安装驱动程序。 5.6.2inf文件实现 INF文件包含一些名字由方括号括起来的段。大部分段都含有一系列“keyword = value”形式的指令。INF文件开始于一个Version段,该段确定文件中描述的设备类型: [Version] 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 Signature=$CHICAGO$ Class=Sample ClassGuid={894A7460-A033-11d2-821E-444553540000} Signature可以是三种特定值之一:$Chicago$、$Windows NT$(含有一个空格)、$Windows 95$(也含有一个空格)。Class确定设备类。ClassGuid唯一地标识该设备类。DDK头文件DEVGUID.H定义了标准设备类的GUID,DDK文档中关于Version段的内容也描述了这些标准设备类。 6 结论 最初选择这个题目的原因,是因为对计算机体系结构的兴趣,从开始着手看一些驱动程序开发方面的书,到最终完成驱动程序并通过测试,大约花费了两个月的时间。 刚开始的时候对驱动程序一点都不懂,如果不是因为用WDM来做,并没有设计到很底层的电气特性或者是物理层的一些东西的话,可能难度要更多一些。最初的时候看了一些书,脑子里大约有了Windows 2000组件模型的概念。然后看WDM模型的构造,也许不是很难,但是因为没有这方面的基础,加上没有实践过,理解起来很是吃力。真正理解WDM模型,是在做这个驱动的过程中,而不是在最初看书的时候,令我更加相信计算机是实践者的科学这句话。 在做这个题目的过程中,老师给了我许多帮助,他给了我一个USB键盘,使得我能够测试自己的程序,从最开始的简单的框架程序开始,到后面基本上完整的驱动程序,离开了测试是不行的。他向我介绍的《Programming the Microsoft Windows Driver Model》第二版,是开发驱动程序方面的经典之作,我的许多疑惑不解的地方,在这本书上得到了答案。 回过头来看写这个驱动程序的过程,真正编码的时间并不多,整个程序中的代码行数也很少,大部分的时间都花在理解驱动程序模型这个概念上了。在数字图书馆,以及各种网络资源中,可以找到一些资料,代码等,但是没有完整的USB键盘驱动程序,我开始理解驱动程序的代码,是在研究一个简单的读写USB键盘的程序的时候。当时那个程序所能做的,只是简单的读写USB键盘,没有涉及到HID类小驱动,因此并没有嵌入到操作系统中。但是还是给了我很大的启示,学习一门开发技术,应该实践和理论并行才能学的比较扎实。 在我认为理论上的东西有了点准备的时候,从网上下载了一个DDK2000开发包,安装在自己的机子上,一开始不太懂,想用VC.NET来做我的驱动程序,但是总不成功,DDK2000中有一个检测环境设置的程序,它提示我,并没有安装合适的VC版本,应该是VC5,或是VC6,所以重新设置了开发环境,安装了VC6.0。 开发环境准备妥当了,我开始创建自己的makefile工程,但是关于这方面的资料太少,可以说找了一段时间,最初在网上找到了一个文档,大约有60多页,看了看,实在太多,没有时间来翻它。还是搜索,在一个论坛上找到了一些makefile工程设置的资料,一个简单的makefile工程就做出来了,这时候这个工程是空白的,除了必不可少的几个配置文件,没有任何其他文件在里面。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 理解HID(人体接口设备)类和USB(通用串行总线)类也用去了最多的时间,USB设备和USB体系方面的资料并不少,但是HID就有点相形见绌,在此之前,我翻译了USB2.0的文档,翻译的大多是关于USB体系结构和USB传输模式的内容,后来为了理解HID设备类,决定翻译《Programming the Microsoft Windows Driver Model,2nd》一书的HID部分,这部分是第二版新添加的内容,书上的这部分介绍了HID设备类,HID的一些IOCTL(输入输出控制),HID类小驱动,HID描述符等内容,看了之后觉得明白了许多。 可是还是不明白是怎么一回事,作为一个USB键盘,即属于USB类,又属于HID类,到底该怎么编写?慢慢的读了一些HID设备的书,其中有一本是《Windows WDM 设备驱动程序开发指南》,关于HID设备的开发占据了很大的篇幅,然后才知道两者之间的关系,HID类驱动在上层,我所设计的键盘驱动,就是为上层的HID类提供接口,对下面则是调用USBDI(USB驱动程序接口),来实现一些读写功能。明白了这个之后,就开始做真正的键盘驱动程序。 编码的过程相对比较顺利,也许是前面的准备工作做的足够多,没有用很多的时间,把驱动程序顺利写出来了。当中遇到了一些错误,大多数是在设置上的问题,VC的环境设置,设备的配置等等。设备的配置是一个比较容易出错的地方,跟硬件打交道是容不得半点马虎的,驱动程序写完之后,我参照例子写了一个inf文件,用来安装这个驱动程序,可是怎么也安装不上,系统提示没有合适的驱动。想到USB键盘不但属于USB类,而且属于Keyboard类,修改了一下我的inf文件,再试着安装,得以成功。 值得一提的是,对USB键盘的读写操作,我并没有直接写在InterIoctl.cpp中的IRP_MJ_INTERNAL_DEVICE_CONTROL处理里,而是在写在了同一个文件的一个函数中,这个函数是一个系统线程,这个系统线程一直在查询当前是否有读写操作,如果有就进行。这是因为IRP_MJ_INTERNAL_DEVICE_CONTROL例程运行在DISPATCH_LEVEL优先级上,这个优先级上不能调用IoBuildDeviceIoControlRequest函数,而这个函数是建立IRP的,是非常重要的,所以在系统线程中调用它。 回过头来看看开发的过程,感触最深的就是理解的时间要比编码的时间多,做之前要先想好,才能少走弯路。 谢 辞 谢谢陈宏老师,谢谢计算机系,即将结束大学四年的生活,毕业设计是最后一门大课,能够顺利完成,算是对这四年学习生活的一个好的结束。老师和系里面给我的帮助使得我能够完成这次毕业设计。您们辛苦了! 感谢陈老师在毕业设计过程中给我的无私的帮助,因为有老师的正确指导,我才能从对设备驱动一点都不了解开始,直到做出一个设备驱动来。通过跟老师的交流,不止是在毕业设计方面,在其他方面也收获良多。另外计算机系的好的学习氛围也是做毕业设计的一个强有力的动力。 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 再次感谢计算机系的各位老师。 参考文献 [1] Walter Oney. Programming the Microsoft Windows Driver Model 2nd.Microsoft Press,2002 [2] 张惠娟.Windows环境下的设备驱动程序设计.西安电子科技大学出版社,2002 [3] 郭益昆.VC++.NET开发驱动程序详解.北京:希望电子出版社,2002:1~90 [4] compaq,Hewlett-Packard,Intel,et al. Universal Serial Bus Specification(Revision 2.0), 2000 [5] Dov Bulka. Efficient C++ Performance Programming Techniques,清华大学出版社,2003 [6] 王成儒,李英伟.USB2.0原理与工程开发.国防工业出版社,2004 [7] 陈启美,丁传锁.计算机USB接口技术. 南京 :南京大学出版社,2003 [8] MindShare,Inc.Universal Serial Bus System Architecture(Second Edition),中国电力出版社,2003 [9] 科欣翻译组译.Windows NT设备驱动程序设计指南,机械工业出版社,1997 [10] 孙守阁.Windows设备驱动程序技术内幕.北京:清华大学出版社,2000 [11] Peter G.Viscarola.Windows NT与Windows 2000设备驱动程序开发,电子工业出版社,2000 [12] Stanley B.lippman著,侯捷译,Essential C++中文版,华中科技大学出版社,2001 [13] Charis Cant.Windows WDM设备驱动开发指南,机械工业出版社,2000附 录 // Usb.cpp // 使用USBDI访问USB设备 #include "Lcs_Kbd.h" /***************************************************************** 调用USBDI ******************************************************************/ NTSTATUS CallUSBDI(IN PLcs_KBD_EXTENSION dx,IN PVOID UrbEtc, IN ULONG IoControlCode/*=IOCTL_INTERNAL_USB_SUBMIT_URB*/, IN ULONG Arg2/*=0*/) { IO_STATUS_BLOCK IoStatus; KEVENT event; //事件 KeInitializeEvent(&event, NotificationEvent,FALSE); //创建IRP 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 PIRP Irp = IoBuildDeviceIoControlRequest( IoControlCode,dx->NextStack, NULL,0, // Input buffer NULL,0, // Output buffer TRUE,&event,&IoStatus); PIO_STACK_LOCATION NextIrpStack = IoGetNextIrpStackLocation(Irp); NextIrpStack->Parameters.Others.Argument1=UrbEtc; NextIrpStack->Parameters.Others.Argument2=(PVOID)Arg2; NTSTATUS status = IoCallDriver(dx->NextStack,Irp); if(status == STATUS_PENDING) { status = KeWaitForSingleObject(&event,Suspended,KernelMode,FALSE,NULL); status = IoStatus.Status; } return status; } /****************************************************************** 获取端口状态,为复位设备做准备 ******************************************************************/ NTSTATUS UsbGetPortStatus(IN PLcs_KBD_EXTENSION dx, OUT ULONG &PortStatus) { DebugPrintMsg("Getting port status"); PortStatus=0; NTSTATUS status = CallUSBDI(dx,&PortStatus,IOCTL_INTERNAL_USB_GET_PORT_STATUS); DebugPrint("Got port status %x",PortStatus); return status; } /****************************************************************** 复位端口 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 ******************************************************************/ NTSTATUS UsbResetPort(IN PLcs_KBD_EXTENSION dx ) { DebugPrintMsg("Resetting port"); NTSTATUS status = CallUSBDI(dx,NULL,IOCTL_INTERNAL_USB_RESET_PORT); DebugPrint("Port reset %x",status); return status; } /****************************************************************** 复位设备 在Lcs_KbdInitDevice函数中使用 为初始化设备 ******************************************************************/ NTSTATUS UsbResetDevice(IN PLcs_KBD_EXTENSION dx) { ULONG PortStatus; NTSTATUS status=UsbGetPortStatus(dx,PortStatus); if(!NT_SUCCESS(status)) return status; if(!(PortStatus & USBD_PORT_CONNECTED)) return STATUS_NO_SUCH_DEVICE; if(PortStatus & USBD_PORT_ENABLED) return status; status = UsbResetPort(dx); if(!NT_SUCCESS(status)) return status; status = UsbGetPortStatus(dx,PortStatus); if(!NT_SUCCESS(status)) return status; 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 if(!(PortStatus & USBD_PORT_CONNECTED)||!(PortStatus & USBD_PORT_ENABLED)) return STATUS_NO_SUCH_DEVICE; DebugPrintMsg("UsbResetDevice : success"); return status; } /****************************************************************** 获取配置,描述符,并为其分配内存 UsbSelectConfiguration函数调用了它 在IOCTL_GET_CONFIGURATION_DESCRIPTORS使用 ******************************************************************/ NTSTATUS UsbGetConfigurationDescriptors(IN PLcs_KBD_EXTENSION dx, OUT PUSB_CONFIGURATION_DESCRIPTOR& descriptors, IN UCHAR ConfigIndex, OUT ULONG& DescriptorsSize) { USHORT UrbSize=sizeof(struct _URB_CONTROL_DESCRIPTOR_REQUEST); PURB urb=(PURB)ExAllocatePool(NonPagedPool,UrbSize); if(urb==NULL) { DebugPrintMsg("UsbGetConfigurationDescriptors : No URB memory"); return STATUS_INSUFFICIENT_RESOURCES; } DescriptorsSize=sizeof(USB_CONFIGURATION_DESCRIPTOR); descriptors=(PUSB_CONFIGURATION_DESCRIPTOR)ExAllocatePool(NonPagedPool,DescriptorsSize+16); if(descriptors==NULL) { DebugPrintMsg("UsbGetConfigurationDescriptors : No initial descriptor memory"); return STATUS_INSUFFICIENT_RESOURCES; } 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 UsbBuildGetDescriptorRequest( urb,UrbSize, USB_CONFIGURATION_DESCRIPTOR_TYPE,ConfigIndex,0, descriptors,NULL,DescriptorsSize,NULL); DebugPrintMsg("Getting basic configuration descriptor"); NTSTATUS status = CallUSBDI(dx,urb); if(!NT_SUCCESS(status)||!USBD_SUCCESS(urb->UrbHeader.Status)) { DebugPrint("UsbGetConfigurationDescriptors :status %x URB status %x",status,urb->UrbHeader.Status); status = STATUS_UNSUCCESSFUL; goto fail; } // Reallocate memory for config descriptor and associated descriptors DescriptorsSize = descriptors->wTotalLength; ExFreePool(descriptors); descriptors=(PUSB_CONFIGURATION_DESCRIPTOR)ExAllocatePool(NonPagedPool,DescriptorsSize+16); if(descriptors==NULL) { DebugPrintMsg("UsbGetConfigurationDescriptors : No full descriptors memory"); return STATUS_INSUFFICIENT_RESOURCES; } UsbBuildGetDescriptorRequest( urb,UrbSize, USB_CONFIGURATION_DESCRIPTOR_TYPE,ConfigIndex,0, descriptors,NULL,DescriptorsSize, NULL); DebugPrintMsg("Getting full configuration descriptors"); status=CallUSBDI(dx,urb); if(!NT_SUCCESS(status)||!USBD_SUCCESS(urb->UrbHeader.Status)) { 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 DebugPrint("status %x URB status %x",status,urb->UrbHeader.Status); status=STATUS_UNSUCCESSFUL; goto fail; } DescriptorsSize=urb->UrbControlDescriptorRequest.TransferBufferLength; fail: ExFreePool(urb); return status; } /****************************************************************** 配置程序 ******************************************************************/ NTSTATUS UsbSelectConfiguration(IN PLcs_KBD_EXTENSION dx) { DebugPrintMsg("UsbSelectConfiguration"); dx->UsbPipeHandle=NULL; // Get all first configuration descriptors PUSB_CONFIGURATION_DESCRIPTOR Descriptors=NULL; ULONG size; NTSTATUS status=UsbGetConfigurationDescriptors(dx,Descriptors,0,size); if(!NT_SUCCESS(status)) { DebugPrint("UsbGetConfigurationDescriptors failed %x",status); FreeIfAllocated(Descriptors); return status; } USHORT siz; PURB urb=USBD_CreateConfigurationRequest(Descriptors,&siz); if(urb) { PUSB_INTERFACE_DESCRIPTOR id=USBD_ParseConfigurationDescriptorEx( 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 Descriptors,Descriptors,-1,-1,3,1,1); if(!id) { DebugPrintMsg("USBD_ParseConfigurationDescriptorEx fails"); return STATUS_INSUFFICIENT_RESOURCES; } PUSBD_INTERFACE_INFORMATION Interface=&urb->UrbSelectConfiguration.Interface; for(ULONG i=0;iNumberOfPipes;i++) Interface->Pipes[i].MaximumTransferSize=8; } else DebugPrintMsg("USBD_CreateConfigurationRequest fails ."); /////////////////////////////////////////////////////////// UsbBuildSelectConfigurationRequest(urb,(USHORT)siz,Descriptors); DebugPrintMsg("Selecting configuration"); status=CallUSBDI(dx,urb); if(!NT_SUCCESS(status)||!USBD_SUCCESS(urb->UrbHeader.Status)) status=STATUS_UNSUCCESSFUL; else { DebugPrintMsg("Select configuration worked"); dx->UsbConfigurationHandle=urb->UrbSelectConfiguration.ConfigurationHandle; // Find pipe handle of first pipe , ie interrupt pipe that returns input HID reports PUSBD_INTERFACE_INFORMATION InterfaceInfo = &urb->UrbSelectConfiguration.Interface; DebugPrint("Interface Class %d NumberOfPipes %d",InterfaceInfo->Class,InterfaceInfo->NumberOfPipes); if(InterfaceInfo->NumberOfPipes>0) { PUSBD_PIPE_INFORMATION pi=&InterfaceInfo->Pipes[0]; dx->UsbPipeHandle=pi->PipeHandle; DebugPrint("PipeHandle = %x",dx->UsbPipeHandle); DebugPrint("Pipes[0] EndpointAddress %2x Interval %dms PipeType %d MaximumTransferSize %d", 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 pi->EndpointAddress,pi->Interval,pi->PipeType,pi->MaximumTransferSize); } if(dx->UsbPipeHandle==NULL) status = STATUS_UNSUCCESSFUL; } FreeIfAllocated(urb); FreeIfAllocated(Descriptors); return status; } /****************************************************************** 获取USB总线的信息 在初始化设备中用到 ******************************************************************/ NTSTATUS UsbGetUsbInfo(IN PLcs_KBD_EXTENSION dx) { #if _WIN32_WINNT>=0x0500 USB_BUS_NOTIFICATION BusInfo; DebugPrintMsg("Getting bus info"); NTSTATUS status = CallUSBDI(dx,&BusInfo,IOCTL_INTERNAL_USB_GET_BUS_INFO); DebugPrint("Bus info : TotalBandwidth %d, ConsumedBandwidth %d and ControlerNameLength %d", BusInfo.TotalBandwidth, BusInfo.ConsumedBandwidth, BusInfo.ControllerNameLength); int len=BusInfo.ControllerNameLength+50; PUSB_HUB_NAME HubName=(PUSB_HUB_NAME)ExAllocatePool(NonPagedPool,len); RtlZeroMemory(HubName,len); if(HubName==NULL) return STATUS_INSUFFICIENT_RESOURCES; 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 status = CallUSBDI(dx,HubName,IOCTL_INTERNAL_USB_GET_CONTROLLER_NAME,BusInfo.ControllerNameLength); if(NT_SUCCESS(status)) DebugPrint("Controller name is %*S",HubName->ActualLength,HubName->HubName); else DebugPrintMsg("Cannot get controller name"); ExFreePool(HubName); return status; #else return STATUS_NOT_SUPPORTED; #endif } /****************************************************************** 获取配置描述符,并且为其分配内存 在IOCTL_FABKBD_GET_SPECIFIED_DESCRIPTOR 和设备初始化中都用到了这个函数 ******************************************************************/ NTSTATUS UsbGetSpecifiedDescriptor(IN PLcs_KBD_EXTENSION dx, OUT PVOID& Descriptor, IN UCHAR DescriptorType, IN OUT ULONG & Size) { USHORT UrbSize = sizeof(_URB_CONTROL_DESCRIPTOR_REQUEST); PURB urb = (PURB)ExAllocatePool(NonPagedPool,UrbSize); if(urb==NULL) { DebugPrintMsg("UsbGetSpecifiedDescriptor : No URB memory"); return STATUS_INSUFFICIENT_RESOURCES; } Descriptor = ExAllocatePool(NonPagedPool,Size); 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 if(Descriptor==NULL) { ExFreePool(urb); DebugPrintMsg("UsbGetSpecifiedDescriptor : No descriptor memory"); return STATUS_INSUFFICIENT_RESOURCES; } UsbBuildGetDescriptorRequest( urb,UrbSize, DescriptorType,0,0, Descriptor,NULL,Size, NULL); DebugPrint("Getting descriptor type %2x",DescriptorType); NTSTATUS status = CallUSBDI(dx,urb); if(!NT_SUCCESS(status)||!USBD_SUCCESS(urb->UrbHeader.Status)) { DebugPrint("status %x URB status %x",status,urb->UrbHeader.Status); status=STATUS_UNSUCCESSFUL; } ExFreePool(urb); return status; } /****************************************************************** 总线空闲 ******************************************************************/ const UCHAR GET_IDLE = 0x02; NTSTATUS UsbGetIdleRate(IN PLcs_KBD_EXTENSION dx) { USHORT UrbSize = sizeof(struct _URB_CONTROL_VENDOR_OR_CLASS_REQUEST); PURB urb = (PURB)ExAllocatePool(NonPagedPool,UrbSize); if(urb==NULL) { DebugPrintMsg("No URB memory"); 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 return STATUS_INSUFFICIENT_RESOURCES; } UCHAR IdleRate = 0; UsbBuildVendorRequest(urb, URB_FUNCTION_CLASS_INTERFACE,UrbSize, USBD_TRANSFER_DIRECTION_IN, 0, GET_IDLE, 0x0000, 0, &IdleRate,NULL,1, NULL); DebugPrintMsg("Sending Get Idle request"); NTSTATUS status = CallUSBDI(dx,urb); if(!NT_SUCCESS(status)||!USBD_SUCCESS(urb->UrbHeader.Status)) { DebugPrint("status %x URB status %x",status,urb->UrbHeader.Status); status=STATUS_UNSUCCESSFUL; } DebugPrint("Idle rate is %d units of 4ms",IdleRate); ExFreePool(urb); return status; } /****************************************************************** 配置设备 在关闭设备的时候用到 ******************************************************************/ NTSTATUS UsbDeselectConfiguration(IN PLcs_KBD_EXTENSION dx) { dx->UsbPipeHandle=NULL; USHORT UrbSize=sizeof(struct _URB_CONTROL_DESCRIPTOR_REQUEST); 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 PURB urb=(PURB)ExAllocatePool(NonPagedPool,UrbSize); if(urb==NULL) { DebugPrintMsg("No URB memory"); return STATUS_INSUFFICIENT_RESOURCES; } UsbBuildSelectConfigurationRequest(urb,UrbSize,NULL); DebugPrintMsg("Deselecting configuration"); NTSTATUS status = CallUSBDI(dx,urb); if(!NT_SUCCESS(status)||!USBD_SUCCESS(urb->UrbHeader.Status)) { DebugPrint("status %x URB status %x",status,urb->UrbHeader.Status); status = STATUS_UNSUCCESSFUL; } ExFreePool(urb); return status; } /****************************************************************** 获取设备描述符 ******************************************************************/ NTSTATUS UsbGetDeviceDescriptor(IN PLcs_KBD_EXTENSION dx, OUT PUSB_DEVICE_DESCRIPTOR& deviceDescriptor, OUT ULONG &Size) { USHORT UrbSize=sizeof(struct _URB_CONTROL_DESCRIPTOR_REQUEST); PURB urb=(PURB)ExAllocatePool(NonPagedPool,UrbSize); if(urb==NULL) { DebugPrintMsg("UsbGetDeviceDescriptor : No URB memory"); return STATUS_INSUFFICIENT_RESOURCES; } ULONG sizeDescriptor=sizeof(USB_DEVICE_DESCRIPTOR); 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 deviceDescriptor=(PUSB_DEVICE_DESCRIPTOR)ExAllocatePool(NonPagedPool,sizeDescriptor); if(deviceDescriptor==NULL) { ExFreePool(urb); DebugPrintMsg("UsbGetDeviceDescriptor : No descriptor memory"); return STATUS_INSUFFICIENT_RESOURCES; } UsbBuildGetDescriptorRequest( urb,UrbSize, USB_DEVICE_DESCRIPTOR_TYPE,0,0, deviceDescriptor,NULL,sizeDescriptor, NULL); DebugPrintMsg("Getting device descriptor"); NTSTATUS status = CallUSBDI(dx,urb); if(!NT_SUCCESS(status)||!USBD_SUCCESS(urb->UrbHeader.Status)) { DebugPrint("status %x URB status %x",status,urb->UrbHeader.Status); status=STATUS_UNSUCCESSFUL; } Size=urb->UrbControlDescriptorRequest.TransferBufferLength; ExFreePool(urb); return status; } /****************************************************************** 中断传输 接受固定大小的8字节输入报告,这个报告不能是空的 ******************************************************************/ NTSTATUS UsbDoInterruptTransfer(IN PLcs_KBD_EXTENSION dx,IN PVOID UserBuffer , ULONG & UserBufferSize) { if(dx->UsbPipeHandle==NULL) 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 return STATUS_INVALID_HANDLE; ULONG InputBufferSize=UserBufferSize; UserBufferSize=0; if(UserBuffer==NULL||InputBufferSize<8) return STATUS_INVALID_PARAMETER; NTSTATUS status=STATUS_SUCCESS; ULONG OutputBufferSize=8; USHORT UrbSize=sizeof(struct _URB_BULK_OR_INTERRUPT_TRANSFER); PURB urb=(PURB)ExAllocatePool(NonPagedPool,UrbSize); if(urb==NULL) { DebugPrintMsg("No URB memory"); return STATUS_INSUFFICIENT_RESOURCES; } // 循环直到读取到键盘报告 DebugPrintMsg("begin loop"); while(true) { UsbBuildInterruptOrBulkTransferRequest( urb,UrbSize, dx->UsbPipeHandle, UserBuffer,NULL,OutputBufferSize, USBD_TRANSFER_DIRECTION_IN, NULL); status=CallUSBDI(dx,urb); if(!NT_SUCCESS(status)||!USBD_SUCCESS(urb->UrbHeader.Status)) { DebugPrint("status %x RUB status %x",status,urb->UrbHeader.Status); status=STATUS_UNSUCCESSFUL; break; } // 如果传输的数据大小不是8字节,则放弃 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 // 当有键按下时,就会获得非空的8字节报告数据 __int64* pData=(__int64*)UserBuffer; if(*pData!=0i64) break; } UserBufferSize=urb->UrbBulkOrInterruptTransfer.TransferBufferLength; if(NT_SUCCESS(status)) { PUCHAR bd=(PUCHAR)UserBuffer; #define CHCH(ii) (bd[ii]-4)>=0 ? (char)('a'+bd[ii]-4):' ' DebugPrint("Transfer data %2x %2x %2x %2x %2x %2x %2x %2x --- %c %c %c %c %c %c %c %c", bd[0],bd[1],bd[2],bd[3],bd[4],bd[5],bd[6],bd[7], CHCH(0),CHCH(1),CHCH(2),CHCH(3),CHCH(4),CHCH(5),CHCH(6),CHCH(7)); } else DebugPrintMsg("UsbDoInterruptTransfer fails . "); ExFreePool(urb); return status; } /****************************************************************** 生成输出报告 在写键盘中用到,即设置LEDs ******************************************************************/ const UCHAR SET_REPORT = 0x09; NTSTATUS UsbSendOutputReport(IN PLcs_KBD_EXTENSION dx,IN UCHAR OutputData) { USHORT UrbSize=sizeof(struct _URB_CONTROL_VENDOR_OR_CLASS_REQUEST); PURB urb = (PURB)ExAllocatePool(NonPagedPool,UrbSize); 桂林电子工业学院毕业设计(论文)专用纸 第页 共67页 if(urb==NULL) { DebugPrintMsg("No URB memory"); return STATUS_INSUFFICIENT_RESOURCES; } // 生成并在默认的管道上发送URB UsbBuildVendorRequest(urb, URB_FUNCTION_CLASS_INTERFACE,UrbSize, USBD_TRANSFER_DIRECTION_OUT, 0, SET_REPORT, 0x0200, 0, &OutputData,NULL,1, NULL); DebugPrintMsg("Sending set report"); NTSTATUS status=CallUSBDI(dx,urb); if(!NT_SUCCESS(status)||!USBD_SUCCESS(urb->UrbHeader.Status)) { DebugPrint("status %x URB status %x",status , urb->UrbHeader.Status); status=STATUS_UNSUCCESSFUL; } ExFreePool(urb); return status; }

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享文档获得金币 ]
0 人已下载

下载文档

相关文档