为 OSR USB FX2 开发 WDF USB 内核模式驱动程序






4.94/5 (58投票s)
本文介绍了使用 WDF 内核模式驱动程序框架 (WDF Kernel Mode Driver Foundation) 开发 USB 内核模式设备驱动程序的过程。
引言
本文解释并演示了使用 WDF 内核模式驱动程序框架 (KMDF) 开发内核模式设备驱动程序的步骤。
本文使用的特定 USB 设备是 OSR USB-FX2 学习套件,可在 OSR Online 获得。当然,讨论的内容对于其他 USB 设备也适用,但示例代码仅适用于 FX2 套件。
本文讨论或涉及以下内容:
- 通用 USB 驱动程序问题。
- USB 中断处理。
- 读取、写入和 IO 控制操作。
- 通用电源管理问题。
- 设备挂起和唤醒。
编译驱动程序、使用 INF 文件部署驱动程序以及其他基本知识等内容本文不作解释。这些内容都在我之前的文章中进行了详细介绍:构建和部署基本的 WDF 内核模式驱动程序。
背景
曾经,我开始对驱动程序开发产生了兴趣。我一直对通过软件控制硬件的行为着迷。我开始阅读关于驱动程序开发的资料,购买了 Oney 的书,并最终购买了 OSR USB FX2 学习套件。
正如我在上一篇文章中提到的,学习 WDM 非常困难。你需要花费大量的时间。我决定过了一段时间就放弃 WDM,我的 FX2 套件也随之被闲置。
然后,在 2005 年 12 月,KMDF 发布了。我通过 KMDF 学习了 USB 设备驱动程序编程,并决定写一篇文章来介绍它。在我写这篇文章的过程中,我需要解释很多内容,所以我决定先写一篇关于 KMDF 驱动程序开发基础的文章。
这第二篇文章介绍了 USB 特定的主题和驱动程序功能。
必备组件
使用这些代码的先决条件列表非常简短:
- 最新版本的 WDF DDK。您可以在此处下载。
- 用于测试驱动程序的 Windows XP 或更高版本。KMDF 的 1.1 版本还将支持 Windows 2000,但该版本尚未发布。
- 如果您想实际运行本文中的代码,则需要 OSR USB-FX2 学习套件。
- 用于查看 KdPrint 消息的 DebugView 实用程序。可在sysinternals获得。
此驱动程序中使用的新概念
在展示 USB 驱动程序的实现之前,需要对一些 KMDF 概念进行解释,以便代码更易于理解。
WDF 内存管理
为了便于安全地处理内存,WDF 使用 WDFMEMORY
对象。这些对象对程序员来说是不透明的。您只能通过它们的句柄来使用它们。
WDF 内存对象同时包含内存块的缓冲区指针和大小描述符。这意味着,当您将内存对象句柄传递给另一个函数时,它总是会携带安全使用它的方法。
同样,如果您从框架获取内存句柄,您始终可以验证数据缓冲区是否足够大以供您使用。
与所有其他框架对象一样,WDFMEMORY
对象都有父对象,并且是引用计数的。这意味着,只要它们允许存在于设备对象生命周期内,您就不需要显式删除创建的内存对象。
它们是引用计数的这一事实也允许您在内存对象的正常生命周期结束后继续使用它。假设您想在写入操作完成后使用写入请求的输入缓冲区。为此,只需增加其引用计数。这保证了即使在对象正常生命周期结束时已被删除,您也可以安全地继续使用它,直到您减少其引用计数。
为了兼顾安全性和易用性,WDF DDK 包含 WdfMemoryCopyToBuffer
和 WdfMemoryCopyFromBuffer
函数,您可以使用它们来安全地将数据复制到 WDFMEMORY
对象中以及从中复制。
创建 WDF 内存对象有不同的函数,但一个特别有趣的函数是 WdfMemoryCreatePreallocated
。此函数可用于将 WDFMEMORY
对象包装到现有的原始数据缓冲区周围。这是我稍后使用的一种技术。
USB 基础知识
USB 开发的原因之一是为旧式串行接口提供现代替代品,并为 Firewire 提供低成本替代方案。如果您查看硬件和协议规范,您会注意到 USB - 即使它有一些花哨的功能 - 实际上只不过是一个老式的串行接口,它支持同一总线上的多个热插拔设备。
USB 最重要的原则之一是,有一个总线控制器(PC)和多个可能的从属设备。所有数据传输都由主设备初始化。如果主设备不请求数据,从设备就无法发送它。
这甚至适用于 USB 中断。设备无法向主设备发送中断。主设备必须定期轮询中断状态。如果中断传输成功,USB 主控制器将中断 USB 驱动程序,就像传输是“真正的”中断一样。
配置、接口和终结点
USB 协议允许设备非常灵活地使用。这也意味着您在开始任何操作之前需要了解很多事情。另一方面,框架为您处理了大部分工作,因此您无需了解底层细节。
您需要首先考虑的是配置。USB 设备配置可以被视为物理功能的分类。大多数设备只有一个配置,即它有一个物理表示。设备可以允许多个配置。
我知道只有一个这样的设备:一个 USB 芯片,它可以同时作为 USB 到 RS-232 转换器和 8 位数字 IO 设备。由于这两种设备类型完全不同,永远无法同时使用,因此让它具有两种不同的配置是有意义的。
然而,99% 的时间,每个物理设备只有一个配置。
一旦设备获得配置,它就可以导出多个接口。接口可以被认为是设备功能的一个独立部分。例如,您可能有一个同时具有模拟和数字 IO 功能的数据采集设备。如果这些设备部件可以独立操作,那么提供两个接口是有意义的:一个用于数字 IO,一个用于模拟 IO。
最后,每个接口可以有一个或多个终结点。终结点是实际数据传输的目标。每次您想将数据发送到设备时,都必须指定数据发送到哪个终结点。每个终结点都有特定的数据传输类型与之相关联。
数据传输类型
USB 协议中有四种数据传输类型。每种都有其用途:
- 中断:数据在 USB 板上发生事件后发送到驱动程序。此传输类型通常用于事件通知。
- 等时:数据以固定的时间间隔发送,并有时间保证。此传输类型主要用于声音等实时流式数据。
- 批量:数据以可能很大的数量发送,但没有实时保证。此传输类型用于数据采集设备、便携式存储等。
- 控制:数据被发送到板上以控制其行为或更改设置。USB 到串行转换器是一个很好的控制传输示例。您将使用控制传输来更改波特率设置等内容,而普通的读取和写入操作则使用批量传输。
每个终结点在软件中都由所谓的管道 (pipe) 表示。管道的原理很简单:您在一端推入某些东西,它就会从另一端出来。
USB 中断
USB 中断不是像 PCI 中断那样的真正中断。它们无法中断系统。相反,如果为设备启用了 USB 中断,USB 总线驱动程序将以可配置的周期性间隔轮询中断终结点。
如果收到中断数据包,框架将执行之前注册的回调函数。中断数据本身将被打包在一个 WDFMEMORY
对象中,并作为参数提供给回调函数。
这个过程看起来如此简单,以至于您不会充分惊叹于它,除非您知道后台正在发生的 WDM 魔术。为了接收 USB 中断,驱动程序必须为中断终结点排队一个未决的读取请求。
一旦读取请求成功,USB 总线驱动程序就可以完成读取请求。当然,在处理上一个中断的同时,USB 板上可能会发生新的中断事件。
为了防止在这种情况下丢失数据,驱动程序必须排队多个读取请求。这样,当 USB 板生成中断数据包时,总会有一个未决的请求。
当然,这并非唯一的问题。在所有这些过程中,存在竞态条件、可能的 PNP 和电源事件以及 IO 取消问题。幸运的是,框架会在后台处理所有这些问题。
USB 控制命令
控制命令在 USB 通信中是特殊情况。所有 USB 设备都必须在索引 0 处有一个控制终结点,无论其设备类型如何。此终结点用于设备配置以及在初始化阶段需要发生的所有事情,例如加载固件。
这意味着终结点 0 始终处于活动状态,即使设备尚未收到其配置。设备也无法拒绝立即处理控制请求,因为 USB 标准规定它们具有高优先级。
正如您将看到的,当驱动程序使用 KMDF 函数向 USB 设备发送控制命令时,它不必提供 USB 管道。这是因为请求将始终发送到正确的控制终结点。唯一需要的是 USB 设备句柄。
有三种不同类型的控制请求:
- 标准:请求由 USB 协议本身定义。
- 类:请求由特定设备类定义。
- 厂商:请求由厂商定义。
我的目的不是要概述 USB 标准要求或允许的所有请求。那将使我们离题太远。特别是,因为 KMDF 会为我们在初始化和配置阶段处理所有必需的请求。
如果您确实想了解所有这些底层细节,可以在 USB 联盟的网站上找到规范。
设备 IO 控制
使用设备驱动程序的用户模式应用程序通常希望向驱动程序发送特殊命令,以使其执行特殊操作、进行配置或获取状态信息。读取和写入操作不适合此目的。
这时就轮到设备 IO 控制了。设备 IO 控制是发送到设备驱动程序的特殊命令。发送设备 IO 控制的用户模式接口如下所示:
BOOL DeviceIoControl( HANDLE hDevice, DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize, LPVOID lpOutBuffer, DWORD nOutBufferSize, LPDWORD lpBytesReturned, LPOVERLAPPED lpOverlapped );
如您所见,设备 IO 控制可以(可选地)具有输入和输出缓冲区。然而,从驱动程序的角度来看,最重要的参数是 dwIoControlCode
。设备驱动程序将使用此数值代码来确定它必须做什么。
该值本身在用户模式应用程序中意义不大,但可用于了解命令的一些信息。DWORD
中的 32 位被划分为不同的部分。每个部分对系统都有特殊含义:
- 位 31:通用。此位设置为所有非预定义类型的驱动程序。
- 位 30 - 16:设备类型。此值指定设备是否为预定义类型。如果设备不是预定义类型,则该值应大于或等于 0x8000。
- 位 15 - 14:所需访问权限。此值指示调用者在打开设备句柄时必须请求的访问级别。例如,如果此字段指定读取访问权限,则仅当调用者以读取访问权限打开设备时,IO 控制才会发送到驱动程序。
- 位 13:自定义。此位设置为函数代码大于 0x800。
- 位 12 - 2:函数代码。此值指定驱动程序必须执行的操作。如果函数是厂商定义的,则此数字应大于 0x800。
- 位 1 - 0:传输类型。此值指示函数是使用缓冲 IO 还是直接 IO 进行数据传输。
由此,我们可以得出结论,操作码不仅被驱动程序用来确定它必须做什么,而且还允许驱动程序程序员配置访问控制和 IO 配置。
Windows 使用控制代码来确定如何将数据移到驱动程序,以及执行安全检查。这使得能够限制设备的使用仅限于特定的用户组。
实现配置设备驱动程序的代码
以下章节将解释 USB 设备驱动程序的各种配置和初始化阶段。
驱动程序入口点
如果您查看我上一篇文章中的 WDF 基本驱动程序的 DriverEntry
代码,您会发现它与此设备驱动程序的 DriverEntry
完全相同。这是因为,与许多驱动程序一样,没有全局数据需要初始化或清理。
DriverEntry
函数的唯一目的是注册添加新设备的事件回调函数。
NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ) { WDF_DRIVER_CONFIG config; NTSTATUS status; WDF_DRIVER_CONFIG_INIT(&config, EvtDeviceAdd); status = WdfDriverCreate( DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfDriverCreate failed with status 0x%08x\n", status)); } return status; }
添加新设备
EvtDeviceAdd
函数由框架为添加到系统并注册由我们的驱动程序处理的每个新设备执行。
在驱动程序执行其他任何操作之前,它会覆盖 KMDF 框架的一些默认 PNP 和电源管理功能。准确地说,驱动程序将实现其自己的 EvtDevicePrepareHardware
、EvtDeviceD0Entry
和 EvtDeviceD0Exit
函数版本。
驱动程序将其数据 IO 配置为缓冲。KMDF 使缓冲 IO 和直接 IO 之间的差异变得非常透明。
使用缓冲 IO 时,用户空间和内核空间之间会有额外的内存复制操作,但驱动程序知道它可以信任它获得的缓冲区。使用直接 IO 时,没有额外的内存,但驱动程序需要执行一些检查以确保它可以对提供的指针进行读写操作。
最后的初始化步骤是使用 WdfDeviceCreate
创建设备对象。新设备现在在 KMDF 框架中有了一个表示。
由于 FX2 是一个 USB 设备,因此可以合理地假设用户会在不使用“安全删除硬件”选项的情况下将其从计算机上拔下。为了防止任何恼人的系统消息,驱动程序将 PNP 属性 Removable
和 SurpriseRemovalOK
设置为 WdfTrue
。这样,操作系统就知道驱动程序能够毫无问题地处理这种情况。
出于我稍后解释的原因,驱动程序需要存储 FX2 上 LED 阵列的状态。为了简化这一点,一个 WDF 内存对象被包装在 D0LEDArrayState
变量周围。这允许子例程将 WDF 内存句柄传递给某些 IO 函数,而无需创建和删除 WDFMEMORY
对象。
然后创建不同的设备 IO 队列(参见下一章),最后,注册设备接口。用户应用程序可以通过枚举所有导出此接口的设备来找到设备。
NTSTATUS EvtDeviceAdd( IN WDFDRIVER Driver, IN PWDFDEVICE_INIT DeviceInit ) { NTSTATUS status; WDFDEVICE device; PDEVICE_CONTEXT devCtx = NULL; WDF_OBJECT_ATTRIBUTES attributes; WDF_PNPPOWER_EVENT_CALLBACKS pnpPowerCallbacks; WDF_DEVICE_PNP_CAPABILITIES pnpCapabilities; UNREFERENCED_PARAMETER(Driver); /*set the callback functions that will be executed on PNP and Power events*/ WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks); pnpPowerCallbacks.EvtDevicePrepareHardware = EvtDevicePrepareHardware; pnpPowerCallbacks.EvtDeviceD0Entry = EvtDeviceD0Entry; pnpPowerCallbacks.EvtDeviceD0Exit = EvtDeviceD0Exit; WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, &pnpPowerCallbacks); WdfDeviceInitSetIoType(DeviceInit, WdfDeviceIoBuffered); /*initialize storage for the device context*/ WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, DEVICE_CONTEXT); /*create a device instance.*/ status = WdfDeviceCreate(&DeviceInit, &attributes, &device); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfDeviceCreate failed with status 0x%08x\n", status)); return status; } /*set the PNP capabilities of our device. we don't want an annoying popup if the device is pulled out of the USB slot.*/ WDF_DEVICE_PNP_CAPABILITIES_INIT(&pnpCapabilities); pnpCapabilities.Removable = WdfTrue; pnpCapabilities.SurpriseRemovalOK = WdfTrue; WdfDeviceSetPnpCapabilities(device, &pnpCapabilities); devCtx = GetDeviceContext(device); /*create a WDF memory object for the memory that is occupied by the WdfMemLEDArrayState variable in the device context. this way we have the value itself handy for debugging purposes, and we have a WDF memory handle that can be used for passing to the low level USB functions. this alleviates the need to getting the buffer at run time.*/ status = WdfMemoryCreatePreallocated(WDF_NO_OBJECT_ATTRIBUTES, &devCtx->D0LEDArrayState, sizeof(devCtx->D0LEDArrayState), &devCtx->WdfMemLEDArrayState); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfMemoryCreatePreallocated" " failed with status 0x%08x\n", status)); return status; } status = CreateQueues(device, devCtx); if(!NT_SUCCESS(status)) return status; status = WdfDeviceCreateDeviceInterface(device, &GUID_DEVINTERFACE_FX2, NULL); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfDeviceCreateDeviceInterface failed" " with status 0x%08x\n", status)); return status; } return status; }
创建 IO 队列
用于创建不同 IO 队列的代码已放入单独的函数中,以提高可读性。
驱动程序使用五个队列:
- 一个并行设备 IO 控制请求队列。这将是发送到驱动程序的所有 IO 控制操作的默认入口。
- 一个串行设备 IO 控制请求队列。驱动程序将在此处排队必须串行化的 IO 控制操作。
- 一个串行写入请求队列。系统将所有写入 IO 请求发送到此处。
- 一个串行读取请求队列。系统将所有读取 IO 请求发送到此处。
- 一个手动请求队列。驱动程序将在此处临时存储
IOCTL_WDF_USB_GET_SWITCHSTATE_CHANGE
IO 控制请求,直到它们可以完成。
驱动程序中没有默认 IO 处理程序。其结果是,所有不是 IO 控制、读取或写入请求的请求都将自动失败。
默认情况下,默认队列将接收所有 IO 请求,除非特定请求类型的调度被路由到其他队列。通过函数 WdfDeviceConfigureRequestDispatching
进行请求重路由。
对于手动队列,没有必要调用 WdfDeviceConfigureRequestDispatching
,因为驱动程序会在适当的时候显式地从队列中检索请求。您还会注意到,未为串行 IO 控制请求队列指定请求路由。这是因为驱动程序本身决定将哪些请求推入该队列。
NTSTATUS CreateQueues(WDFDEVICE Device, PDEVICE_CONTEXT Context) { NTSTATUS status = STATUS_SUCCESS; WDF_IO_QUEUE_CONFIG ioQConfig; /*create the default IO queue. this one will be used for ioctl request entry. this queue is parallel, so as to prevent unnecessary serialization for IO requests that can be handled in parallel.*/ WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&ioQConfig, WdfIoQueueDispatchParallel); ioQConfig.EvtIoDeviceControl = EvtDeviceIoControlEntry; status = WdfIoQueueCreate(Device, &ioQConfig, WDF_NO_OBJECT_ATTRIBUTES, &Context->IoControlEntryQueue); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfIoQueueCreate failed with status 0x%08x\n", status)); return status; } /*create the IO queue for serialize IO requests. This queue will be filled by the IO control entry handler with the requests that have to be serialized for execution.*/ WDF_IO_QUEUE_CONFIG_INIT(&ioQConfig, WdfIoQueueDispatchSequential); ioQConfig.EvtIoDeviceControl = EvtDeviceIoControlSerial; status = WdfIoQueueCreate(Device, &ioQConfig, WDF_NO_OBJECT_ATTRIBUTES, &Context->IoControlSerialQueue); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfIoQueueCreate failed with status 0x%08x\n", status)); return status; } /*create the IO queue for write requests*/ WDF_IO_QUEUE_CONFIG_INIT(&ioQConfig, WdfIoQueueDispatchSequential); ioQConfig.EvtIoWrite = EvtDeviceIoWrite; status = WdfIoQueueCreate(Device, &ioQConfig, WDF_NO_OBJECT_ATTRIBUTES, &Context->IoWriteQueue); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfIoQueueCreate failed with status 0x%08x\n", status)); return status; } status = WdfDeviceConfigureRequestDispatching(Device, Context->IoWriteQueue, WdfRequestTypeWrite); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfDeviceConfigureRequestDispatching failed with status 0x%08x\n", status)); return status; } /*create the IO queue for read requests*/ WDF_IO_QUEUE_CONFIG_INIT(&ioQConfig, WdfIoQueueDispatchSequential); ioQConfig.EvtIoRead = EvtDeviceIoRead; status = WdfIoQueueCreate(Device, &ioQConfig, WDF_NO_OBJECT_ATTRIBUTES, &Context->IoReadQueue); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfIoQueueCreate failed with status 0x%08x\n", status)); return status; } status = WdfDeviceConfigureRequestDispatching(Device, Context->IoReadQueue, WdfRequestTypeRead); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfDeviceConfigureRequestDispatching failed with status 0x%08x\n", status)); return status; } /*create a manual queue for storing the IOCTL_WDF_USB_GET_SWITCHSTATE_CHANGE IO control requests. If a file handle associated with one or more requests in the queue is closed, the requests themselves are automatically removed from the queue by the framework and cancelled.*/ WDF_IO_QUEUE_CONFIG_INIT(&ioQConfig, WdfIoQueueDispatchManual); status = WdfIoQueueCreate(Device, &ioQConfig, WDF_NO_OBJECT_ATTRIBUTES, &Context->SwitchChangeRequestQueue); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfIoQueueCreate for manual queue failed with status 0x%08x\n", status)); return status; } return status; }
准备硬件运行
由于此函数必须执行许多操作,因此将其分解为多个子例程。
在执行其他任何操作之前,驱动程序必须初始化与 USB 设备的连接。如果成功,则必须配置不同的 USB 管道。之后,就可以设置我们驱动程序的电源管理了。
为了接收 USB 中断,驱动程序必须配置连续的读取操作。框架将为我们的驱动程序维护一个始终处于挂起状态的读取请求队列,并为每个已完成的读取请求执行回调函数 EvtUsbDeviceInterrupt
。
默认的挂起读取请求数量是 2。您可以将此数量提高到最多 10 个请求,以防止设备产生大量中断时数据丢失。对我们的驱动程序来说,默认值就足够了。
值得一提的是,这种相同的原理可用于批量请求输入终结点。例如,这对于必须将数据连续流式传输到计算机的数据采集设备可能很有用。
NTSTATUS EvtDevicePrepareHardware( IN WDFDEVICE Device, IN WDFCMRESLIST ResourceList, IN WDFCMRESLIST ResourceListTranslated ) { NTSTATUS status; PDEVICE_CONTEXT devCtx = NULL; WDF_USB_CONTINUOUS_READER_CONFIG interruptConfig; UNREFERENCED_PARAMETER(ResourceList); UNREFERENCED_PARAMETER(ResourceListTranslated); devCtx = GetDeviceContext(Device); status = ConfigureUsbInterface(Device, devCtx); if(!NT_SUCCESS(status)) return status; status = ConfigureUsbPipes(devCtx); if(!NT_SUCCESS(status)) return status; status = InitPowerManagement(Device, devCtx); if(!NT_SUCCESS(status)) return status; /*set up the interrupt endpoint with a continuous read operation. that way we are guaranteed that no interrupt data is lost.*/ WDF_USB_CONTINUOUS_READER_CONFIG_INIT(&interruptConfig, EvtUsbDeviceInterrupt, devCtx, sizeof(BYTE)); status = WdfUsbTargetPipeConfigContinuousReader( devCtx->UsbInterruptPipe, &interruptConfig); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfUsbTargetPipeConfigContinuousReader " "failed with status 0x%08x\n", status)); return status; } return status; }
配置 USB 设备
在对 USB 设备进行任何操作之前,驱动程序必须使用函数 WdfUsbTargetDeviceCreate
连接到 USB 驱动程序。执行此函数时,将为我们的设备创建一个 USB 设备对象,并打开与总线驱动程序的连接。
正如我之前提到的,必须选择一个配置和一个接口。FX2 只有一个可能的配置,它只有一个接口。这意味着驱动程序可以使用 WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_SINGLE_INTERFACE
函数来初始化 USB 接口配置结构。
然后通过执行 WdfUsbTargetDeviceSelectConfig
使选择生效。无需特殊属性。配置好的 USB 接口句柄保存在设备上下文中。
NTSTATUS ConfigureUsbInterface(WDFDEVICE Device, PDEVICE_CONTEXT DeviceContext) { NTSTATUS status = STATUS_SUCCESS; WDF_USB_DEVICE_SELECT_CONFIG_PARAMS usbConfig; status = WdfUsbTargetDeviceCreate(Device, WDF_NO_OBJECT_ATTRIBUTES, &DeviceContext->UsbDevice); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfUsbTargetDeviceCreate failed with status 0x%08x\n", status)); return status; } /*initialize the parameters struct so that the device can initialize and use a single specified interface. this only works if the device has just 1 interface.*/ WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_SINGLE_INTERFACE(&usbConfig); status = WdfUsbTargetDeviceSelectConfig(DeviceContext->UsbDevice, WDF_NO_OBJECT_ATTRIBUTES, &usbConfig); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfUsbTargetDeviceSelectConfig failed with status 0x%08x\n", status)); return status; } /*put the USB interface in our device context so that we can use it in future calls to our driver.*/ DeviceContext->UsbInterface = usbConfig.Types.SingleInterface.ConfiguredUsbInterface; return status; }
配置 USB 管道
现在已经选择了 USB 配置和接口,就可以配置数据管道了。
FX2 有三个终结点(不包括驱动程序不直接使用的控制终结点):一个中断终结点和两个批量数据终结点。框架将处理这些终结点的底层配置。驱动程序本身只需遍历终结点列表并确定如何处理它们。
最后,驱动程序检查是否找到了所有三个预期的终结点。如果找不到一个或多个预期的终结点,则会生成错误。
有一点需要提及:默认情况下,框架假定驱动程序仅执行 USB 传输,这些传输是 USB 传输数据包大小的精确倍数。由于这种情况极不可能发生(对于中断终结点来说,甚至是不可行的),因此驱动程序禁用了该检查。
NTSTATUS ConfigureUsbPipes(PDEVICE_CONTEXT DeviceContext) { NTSTATUS status = STATUS_SUCCESS; BYTE index = 0; WDF_USB_PIPE_INFORMATION pipeConfig; WDFUSBPIPE pipe = NULL; DeviceContext->UsbInterruptPipe = NULL; DeviceContext->UsbBulkInPipe = NULL; DeviceContext->UsbBulkOutPipe = NULL; WDF_USB_PIPE_INFORMATION_INIT(&pipeConfig); do { pipe = WdfUsbInterfaceGetConfiguredPipe(DeviceContext->UsbInterface, index, &pipeConfig); if(NULL == pipe) break; /*none of our data transfers will have a guarantee that the requested data size is a multiple of the packet size.*/ WdfUsbTargetPipeSetNoMaximumPacketSizeCheck(pipe); if(WdfUsbPipeTypeInterrupt == pipeConfig.PipeType) { DeviceContext->UsbInterruptPipe = pipe; } else if(WdfUsbPipeTypeBulk == pipeConfig.PipeType) { if(TRUE == WdfUsbTargetPipeIsInEndpoint(pipe)) { DeviceContext->UsbBulkInPipe = pipe; } else if(TRUE == WdfUsbTargetPipeIsOutEndpoint(pipe)) { DeviceContext->UsbBulkOutPipe = pipe; } } index++; } while(NULL != pipe); if((NULL == DeviceContext->UsbInterruptPipe) || (NULL == DeviceContext->UsbBulkInPipe) || (NULL == DeviceContext->UsbBulkOutPipe)) { KdPrint((__DRIVER_NAME "Not all expected USB pipes were found.\n")); return STATUS_INVALID_PARAMETER; } return status; }
配置电源管理
驱动程序如何初始化电源管理取决于设备本身的功能。对于 FX2,我们可以假设其功能是固定的,但清晰的解决方案是动态检索此信息。这是通过 WdfUsbTargetDeviceRetrieveInformation
函数完成的。
此函数返回的数据是 WDF_USB_DEVICE_INFORMATION
结构。该结构有三个有趣的参数:
UsbdVersionInformation
:此参数包含设备支持的 USB 版本和 USB 接口版本号。HcdPortCapabilities
:一组标识 HCD 支持的端口功能的位标志。目前只有一个标志:USB_HCD_CAPS_SUPPORTS_RT_THREADS
。此标志指示主机控制器是否支持实时线程。Traits
:一个位掩码,指定 USB 设备的功能。
只有 Traits
参数是感兴趣的。在此位掩码中,驱动程序可以找到设备是否启用了从睡眠状态唤醒系统,设备是否是自供电的,以及设备是否是高速的。
唯一有影响的位是 WDF_USB_DEVICE_TRAIT_REMOTE_WAKE_CAPABLE
标志。此标志指示 USB 设备是否支持从系统睡眠唤醒。对于 FX2,就是这种情况。
有两个明确的属性需要配置:S0 空闲设置和 Sx 唤醒设置。
WdfDeviceAssignS0IdleSettings
可用于配置设备在系统处于 S0 状态后进入低功耗状态的空闲时间。这样做可以防止不必要的功耗。
使用 WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS_INIT
函数初始化 S0 空闲设置结构。对于 USB 设备,必须使用功能标志 IdleUsbSelectiveSuspend
来启用设备在 S0 系统状态下的睡眠。USB 设备将自动进入的默认设备电源状态是 PowerDeviceD2
。
WdfDeviceAssignSxWakeSettings
函数指定设备在两者都处于低功耗状态时唤醒系统的能力。它使用 WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS
结构,其中包含以下参数:
DxState
:设备将被设置为唤醒的最低功耗状态。默认情况下,这是设备仍能触发系统唤醒的最低 D 状态。对于 FX2,这是PowerStateD2
。这也意味着 Sx 到 Dx 的映射决定了设备可以触发系统唤醒的最深 Sx 状态。例如:如果 S2 是仍具有等于 D2 的设备功耗状态的最深睡眠状态,则 S2 是设备可以触发系统的最深睡眠状态。
UserControlOfWakeSettings
:此设置可用于允许或禁止用户启用或禁用唤醒功能。Enabled
:启用或禁用唤醒功能。
NTSTATUS InitPowerManagement( IN WDFDEVICE Device, IN PDEVICE_CONTEXT Context) { NTSTATUS status = STATUS_SUCCESS; WDF_USB_DEVICE_INFORMATION usbInfo; KdPrint((__DRIVER_NAME "Device init power management\n")); WDF_USB_DEVICE_INFORMATION_INIT(&usbInfo); status = WdfUsbTargetDeviceRetrieveInformation( Context->UsbDevice, &usbInfo); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfUsbTargetDeviceRetrieveInformation failed with status 0x%08x\n", status)); return status; } KdPrint((__DRIVER_NAME "Device self powered: %d", usbInfo.Traits & WDF_USB_DEVICE_TRAIT_SELF_POWERED ? 1 : 0)); KdPrint((__DRIVER_NAME "Device remote wake capable: %d", usbInfo.Traits & WDF_USB_DEVICE_TRAIT_REMOTE_WAKE_CAPABLE ? 1 : 0)); KdPrint((__DRIVER_NAME "Device high speed: %d", usbInfo.Traits & WDF_USB_DEVICE_TRAIT_AT_HIGH_SPEED ? 1 : 0)); if(usbInfo.Traits & WDF_USB_DEVICE_TRAIT_REMOTE_WAKE_CAPABLE) { WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS idleSettings; WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS wakeSettings; WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS_INIT(&idleSettings, IdleUsbSelectiveSuspend); idleSettings.IdleTimeout = 10000; status = WdfDeviceAssignS0IdleSettings(Device, &idleSettings); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfDeviceAssignS0IdleSettings failed with status 0x%08x\n", status)); return status; } WDF_DEVICE_POWER_POLICY_WAKE_SETTINGS_INIT(&wakeSettings); wakeSettings.DxState = PowerDeviceD2; status = WdfDeviceAssignSxWakeSettings(Device, &wakeSettings); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfDeviceAssignSxWakeSettings failed with status 0x%08x\n", status)); return status; } } return status; }
实现电源管理代码
一旦配置了设备对象,驱动程序就会开始接收电源管理。对于 FX2,只有两个事件是重要的:EvtDeviceD0Entry
和 EvtDeviceD0Exit
。可以处理其他电源事件,但它们对我们的驱动程序不重要。
代码中不明显的一点是设备通电或断电时 IO 队列的状态。这是因为所有这些都由框架处理。
如果 IO 队列受到电源管理(在我们的情况下就是如此),那么只要队列中还有请求,框架就不会让设备离开 D0 状态。一旦设备进入较低的功耗状态,框架就会暂停所有新请求,而不是将它们放入队列。
如果设备处于低功耗状态只是因为它处于空闲状态,框架会在将请求传递给驱动程序之前将设备电源状态恢复到 D0。
电源管理函数始终在 IRQL=PASSIVE 调用。但是,这并不意味着您可以将它们放入可分页部分,或者允许访问可分页数据。
原因是分页设备在电源状态转换期间可能无法完全正常工作。因此,任何访问分页数据的尝试都可能导致 bug 检查。
幸运的是,这种行为是可以配置的。驱动程序可以调用 WdfDeviceInitSetPowerPageable
来指示它希望在电源转换期间访问可分页数据。在这种情况下,系统将确保驱动程序的电源管理函数仅在分页设备运行时执行。
默认允许分页,所以除非您通过调用 WdfDeviceInitSetPowerNotPageable
指定其他选项,否则您可以自由地将电源管理函数放在可分页代码段中。
设备通电
现在硬件已配置好,电源管理功能也已设置好,设备就可以进入其正常工作状态:PowerDeviceD0
。
之前创建的 USB 设备必须启动才能执行 USB 通信。WdfIoTargetStart
函数执行此操作。
为了获得与 USB 设备关联的 IO 目标,框架提供了 WdfUsbTargetPipeGetIoTarget
函数,该函数检索与特定 USB 管道关联的 IO 目标。配置的任何 USB 管道都可以,因为它们都使用相同的 IO 目标。
EvtDeviceD0Entry
函数在每次设备进入 D0 状态时调用,无论之前的电源状态如何。在此函数中实现了一个额外的检查。如果前一个状态是 PowerDeviceD3
(断电状态),则驱动程序将恢复 LED 阵列的状态。
NTSTATUS EvtDeviceD0Entry( IN WDFDEVICE Device, IN WDF_POWER_DEVICE_STATE PreviousState ) { NTSTATUS status = STATUS_SUCCESS; PDEVICE_CONTEXT devCtx = NULL; KdPrint((__DRIVER_NAME "Device D0 Entry. Coming from %s\n", PowerName(PreviousState))); devCtx = GetDeviceContext(Device); status = WdfIoTargetStart(WdfUsbTargetPipeGetIoTarget( devCtx->UsbInterruptPipe)); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfIoTargetStart failed with status 0x%08x\n", status)); return status; } /*restore the state of the LED array if the device is waking up from a D3 power state.*/ if(PreviousState == PowerDeviceD3) { status = llSetLightBar(devCtx, devCtx->WdfMemLEDArrayState); } return status; }
设备断电
断电序列与通电序列相反。如果目标状态是 PowerDeviceD3
,则 LED 阵列的状态将保存在设备上下文中,以便以后可以恢复。
完成后,为我们的驱动程序创建的 USB 设备对象需要停止,以便它也可以开始其断电序列。任何未完成的 IO 请求都保持挂起状态。这样就不需要重新启动为驱动程序提供 USB 中断的连续读取操作。
一旦 IO 目标重新启动,驱动程序就可以再次接收中断。
NTSTATUS EvtDeviceD0Exit( IN WDFDEVICE Device, IN WDF_POWER_DEVICE_STATE TargetState ) { NTSTATUS status = STATUS_SUCCESS; PDEVICE_CONTEXT devCtx = NULL; devCtx = GetDeviceContext(Device); KdPrint((__DRIVER_NAME "Device D0 Exit. Going to %s\n", PowerName(TargetState))); /*save the state of the LED array if the device is waking up from a D3 power state.*/ if(TargetState == PowerDeviceD3) { status = llGetLightBar(devCtx, devCtx->WdfMemLEDArrayState); if(!NT_SUCCESS(status)) return status; } WdfIoTargetStop(WdfUsbTargetPipeGetIoTarget(devCtx->UsbInterruptPipe), WdfIoTargetLeaveSentIoPending); return status; }
实现 IO
为了准备好接收 IO 请求和 USB 中断,已经进行了很多活动。一旦设备电源状态为 PowerDeviceD0
,IO 队列将接受 IO 请求并执行正确的事件回调函数。
处理设备 IO 控制命令
USB 设备驱动程序执行的大多数操作都以设备 IO 控制请求的形式接收。原因很简单。设备通常有很多功能,而读取和写入操作只能用于一件事:读取和写入。
所有其他功能都必须以某种方式访问。这就是 IO 控制处理程序的作用。
设备 IO 控制处理程序的功能只是确定要执行的正确函数,并将请求转发给该函数。您可以在下面的代码中看到这一点。任何最终未被我们的驱动程序处理的 IO 控制都将以错误状态完成。
为了提供灵活高效的处理机制,IO 控制处理分为两个阶段。第一阶段是 EvtDeviceIoControlEntry
函数,它将最初处理发送到驱动程序的所有请求。正如您之前看到的,IO 控制入口队列被创建为并行队列,这意味着可以同时服务多个请求。
如果请求不需要与其他请求同步,则可以立即处理。另一方面,如果需要同步,则会将其转发到串行 IO 控制队列,该队列一次只处理一个请求。
该机制的优点在于,它允许快速执行所有非同步请求,同时还为需要串行化的请求提供同步。
VOID EvtDeviceIoControlEntry( IN WDFQUEUE Queue, IN WDFREQUEST Request, IN size_t OutputBufferLength, IN size_t InputBufferLength, IN ULONG IoControlCode ) { switch(IoControlCode) { case IOCTL_WDF_USB_GET_SWITCHSTATE: IoCtlGetSwitchPack(Queue, Request, OutputBufferLength, InputBufferLength); break; case IOCTL_WDF_USB_GET_SWITCHSTATE_CHANGE: IoCtlGetSwitchPackChange(Queue, Request, OutputBufferLength, InputBufferLength); break; default: { PDEVICE_CONTEXT devCtx = GetDeviceContext(WdfIoQueueGetDevice(Queue)); WdfRequestForwardToIoQueue(Request, devCtx->IoControlSerialQueue); } break; } }
IO 处理程序的第二阶段是串行 IO 控制处理程序。它将处理尚未由并行处理程序处理的任何请求。它还会使它不认识的任何请求失败。
随着驱动程序功能随时间的演变,您可以简单地将 IO 控制处理添加到最合适的位置。因此,即使您的驱动程序只有需要串行化的 IO 控制,使用此机制仍然是个好主意,因为它允许您在需求发生变化时清晰地添加功能。
VOID EvtDeviceIoControlSerial( IN WDFQUEUE Queue, IN WDFREQUEST Request, IN size_t OutputBufferLength, IN size_t InputBufferLength, IN ULONG IoControlCode ) { switch(IoControlCode) { case IOCTL_WDF_USB_SET_LIGHTBAR: IoCtlSetLightBar(Queue, Request, OutputBufferLength, InputBufferLength); break; case IOCTL_WDF_USB_GET_LIGHTBAR: IoCtlGetLightBar(Queue, Request, OutputBufferLength, InputBufferLength); break; default: WdfRequestComplete(Request, STATUS_INVALID_PARAMETER); break; } }
获取实际开关包状态
这是可以并行处理的请求之一。它只是原子地从设备上下文中读取一个值,因此不需要串行化。
每次开关位置更改时,开关包的值都会发送到 PC。设备通电到 D0 状态时也会发送。开关包值存储在设备上下文中。
为了获取此值,用户应用程序必须向驱动程序发送 IO 控制请求。这是驱动程序中最简单的 IO 控制函数。首先,它检查输出缓冲区是否足够大以包含开关包状态。
然后检索输出缓冲区指针,以便将数据复制到其中。完成后,IO 请求就可以完成。复制的字节数作为完成信息提供,以便正确地将读取的字节数报告给发送请求的应用程序。
VOID IoCtlGetSwitchPack( IN WDFQUEUE Queue, IN WDFREQUEST Request, IN size_t OutputBufferLength, IN size_t InputBufferLength) { NTSTATUS status = STATUS_SUCCESS; BYTE *outChar = NULL; size_t length = 0; PDEVICE_CONTEXT devCtx = NULL; UNREFERENCED_PARAMETER(InputBufferLength); if(OutputBufferLength < sizeof(BYTE)) { KdPrint((__DRIVER_NAME "IOCTL_WDF_USB_GET_SWITCHSTATE" " OutputBufferLength < sizeof(BYTE)\n")); WdfRequestComplete(Request, STATUS_INVALID_PARAMETER); return; } status = WdfRequestRetrieveOutputBuffer(Request, sizeof(BYTE), &outChar, &length); if(NT_SUCCESS(status)) { ASSERT(length >= sizeof(BYTE)); devCtx = GetDeviceContext(WdfIoQueueGetDevice(Queue)); *outChar = devCtx->ActSwitchPack; } WdfRequestCompleteWithInformation(Request, status, sizeof(BYTE)); }
请求开关包更改通知
这是第二个不需要同步的 IO 控制。这是因为处理程序实际上什么也不做。
假设应用程序需要随时了解开关包的最新值。一种选择是定期向驱动程序发送 IOCTL_WDF_USB_GET_SWITCHSTATE
IO 控制以获取最新值,但有几个原因表明这是一个坏主意。
它会导致不必要的处理开销。有很多活动,而大多数时候结果与之前相同。它还会导致大量的上下文切换,从而损害性能。
有一个更好的解决方案:IOCTL_WDF_USB_GET_SWITCHSTATE_CHANGE
IO 控制。
应用程序向驱动程序发送此类 IO 控制。驱动程序不是完成此请求,而是将其放入手动请求队列,然后将其忽略。请注意,一旦转发,驱动程序就会失去对请求的所有权,因此之后不应该对其进行任何操作。请求可能被取消或完成,尝试使用请求句柄会导致 bug 检查。
如果请求是同步的,则调用线程将被阻塞,直到请求完成。如果请求是异步发送的,则调用线程不会被阻塞,但必须定期检查请求是否已完成。这可以通过多种方式完成,但这超出了本文的范围。
完成请求是在 USB 中断处理程序中完成的。每个中断数据包完成一个 IO 请求。最终结果是,用户应用程序只需为每次实际的开关包更改执行一个 IO 操作,而不是通过轮询开关包状态浪费数百个 IO 操作。
VOID IoCtlGetSwitchPackChange( IN WDFQUEUE Queue, IN WDFREQUEST Request, IN size_t OutputBufferLength, IN size_t InputBufferLength) { NTSTATUS status = STATUS_SUCCESS; PDEVICE_CONTEXT devCtx = NULL; UNREFERENCED_PARAMETER(InputBufferLength); UNREFERENCED_PARAMETER(OutputBufferLength); UNREFERENCED_PARAMETER(Queue); devCtx = GetDeviceContext(WdfIoQueueGetDevice(Queue)); /*If the request is succesfull the request ownership is also transferred back to the framework.*/ status = WdfRequestForwardToIoQueue(Request, devCtx->SwitchChangeRequestQueue); /*if the request cannot be forwarded it has to be completed with the appropriate status code because the driver still owns the request.*/ if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfRequestForwardToIoQueue failed " "with code 0x%08x.\n", status)); WdfRequestComplete(Request, status); } }
到目前为止,您可能在问自己:“这很好,但是当调用应用程序关闭其设备句柄时,IO 请求会发生什么?”我也问了自己同样的问题。
正确的答案是:“驱动程序不必关心。”真的。就是这么简单!
坦白说,我已经在尝试使用 EvtFileCleanup
函数来手动从手动队列中检索请求并取消它们,这时我才发现框架免费为我完成了这项工作。
如果队列中有挂起的 IO 请求,当关联的文件句柄关闭时,IO 请求将被取消并从队列中移除。USB 中断处理程序永远不会知道。
当设备被拔出 USB 连接器从系统中移除时,情况也是如此。所有挂起的请求都将被自动取消。
获取 LED 阵列状态
读取 LED 阵列状态的 IO 控制请求被转发到 IoCtlGetLightBar
函数。我省略了对该函数的描述,因为其控制流程与之前的 IO 控制操作完全相同。
IoCtlGetLightBar
首先检查输出缓冲区是否足够大。之后,它执行实际命令。完成后,请求将以已执行命令的状态代码和读取的字节数完成。
这里唯一有趣的是 llGetLightBar
函数的实现,该函数执行底层命令。
在执行任何操作之前,它会从提供的 WDF 内存对象中提取缓冲区指针。这对于实际的 USB 操作不是必需的,但驱动程序稍后需要它来重新格式化数据包。
然后为 WDF 内存句柄创建内存描述符,因为发送请求的函数需要内存描述符而不是 WDFMEMORY
句柄。
获取 LED 状态的实际通信在 FX2 上实现为厂商控制消息。所有 USB 控制请求都需要一个 WDF_USB_CONTROL_SETUP_PACKET
结构来保存请求信息。
驱动程序使用控制请求方向(BmRequestDeviceToHost
)、命令接收方(BmRequestToDevice
)和特定控制命令的数值来初始化控制数据包。
控制请求以同步方式发送到 USB 设备。即,该函数只在请求完成或出现错误时返回。请注意,驱动程序不需要指定要使用的 USB 管道,因为驱动程序知道将请求发送到哪个终结点。
与开关包状态一样,LED 阵列状态需要从其物理表示转换为逻辑表示。涉及的算法与开关包的算法不同,因为编码方式不同。
NTSTATUS llGetLightBar( IN PDEVICE_CONTEXT Context, IN WDFMEMORY State ) { NTSTATUS status = STATUS_SUCCESS; WDF_USB_CONTROL_SETUP_PACKET controlPacket; WDF_MEMORY_DESCRIPTOR memDescriptor; BYTE logicalVal = 0; BYTE *inChar = NULL; size_t length = 0; KdPrint((__DRIVER_NAME "entering llGetLightBar\n")); inChar = WdfMemoryGetBuffer(State, &length); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "Could not retrieve the lightbar memory pointer\n")); return status; } ASSERT(length >= sizeof(BYTE)); ASSERT(NULL != inChar); /*initialize the descriptor that will be passed to the USB driver*/ WDF_MEMORY_DESCRIPTOR_INIT_HANDLE(&memDescriptor, State, NULL); WDF_USB_CONTROL_SETUP_PACKET_INIT_VENDOR( &controlPacket, BmRequestDeviceToHost, BmRequestToDevice, VC_GET_LIGHT_BAR, 0, 0); status = WdfUsbTargetDeviceSendControlTransferSynchronously( Context->UsbDevice, NULL, NULL, &controlPacket, &memDescriptor, NULL); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfUsbTargetDeviceSendControlTransferSynchronouslyfailed with status 0x%08x\n", status)); return status; } /*translate the supplied physical value to a value that represents the values of the LEDs in the logical light array.*/ logicalVal = ((*inChar & 0x1F) << 3) | ((*inChar & 0xE0) >> 5); KdPrint((__DRIVER_NAME "Original value = 0x%x, new value = 0x%x\n", *inChar, logicalVal)); *inChar = logicalVal; return status; }
设置 LED 阵列中 LED 的状态
设置 LED 状态的代码与获取 LED 状态的代码几乎相同,因此我不再在此重复。
唯一的显著区别是使用了不同的数值控制代码(VC_GET_LIGHT_BAR
而不是 VC_SET_LIGHT_BAR
),并且在发送请求之前,LED 阵列状态现在从逻辑值转换为物理值。
处理 USB 中断
USB 中断处理程序是为中断终结点上的连续读取而成功执行的每个读取请求注册的回调函数。
此函数是我们驱动程序中唯一一个在 IRQL=DISPATCH 调用中的函数。这意味着它不应该长时间阻塞,并且只应使用在该 IRQL 下安全的函数。最后,它不应访问任何可分页数据。这也意味着此函数是我们驱动程序中唯一一个不在可分页代码段中的函数。
如您所见,EvtUsbDeviceInterrupt
只获取中断数据并将其复制到实际开关包状态的值中。
这里发生的唯一额外事情是,开关包中的不同位被移到了不同的位置。原因是传入的值是开关包在硬件中的存储方式,而不是开关的逻辑顺序。
值以非直观的方式排序是很常见的,因为在软件中这样做比在硬件中更容易且成本更低。排序可能仅仅是由于 PCB 上的跟踪布线限制。
当数据被转换后,驱动程序会检查手动请求队列中是否有一个挂起的 IO 请求。如果存在此类请求,则将其完成。每个中断只完成一个请求。请求被放入手动队列是在 IOCTL_WDF_USB_GET_SWITCHSTATE_CHANGE
IO 控制的处理程序中。
由于从检索 IO 控制到完成它几乎没有延迟,因此不需要做更多的事情。如果在这两个操作之间有任何冗长的处理,驱动程序应该启用请求的取消。
如果您启用请求取消,则必须提供一个事件回调函数,该函数由框架调用,以便驱动程序可以以受控方式停止请求。然后驱动程序必须在实际完成请求之前禁用请求取消,以确保仍然允许它访问它。
但在本驱动程序的案例中,这是不必要的,因为在接收请求所有权和完成请求之间没有延迟。
VOID EvtUsbDeviceInterrupt( WDFUSBPIPE Pipe, WDFMEMORY Buffer, size_t NumBytesTransferred, WDFCONTEXT Context ) { NTSTATUS status; BYTE temp; size_t size; PDEVICE_CONTEXT devCtx = Context; WDFREQUEST Request = NULL; BYTE *packState = WdfMemoryGetBuffer(Buffer, &size); UNREFERENCED_PARAMETER(Pipe); ASSERT(size == sizeof(BYTE)); ASSERT(NumBytesTransferred == size); ASSERT(packState != NULL); temp = *packState; temp = (temp & 0x01) << 7 | (temp & 0x02) << 5 | (temp & 0x04) << 3 | (temp & 0x08) << 1 | (temp & 0x10) >> 1 | (temp & 0x20) >> 3 | (temp & 0x40) >> 5 | (temp & 0x80) >> 7; KdPrint((__DRIVER_NAME "Converted switch pack from 0x%02x to 0x%02x\n", (ULONG)*packState, (ULONG)temp)); devCtx->ActSwitchPack = ~temp; /*is there an io control queued? if so then complete the first one*/ status = WdfIoQueueRetrieveNextRequest(devCtx->SwitchChangeRequestQueue, &Request); if(NT_SUCCESS(status)) { BYTE* outBuffer; status = WdfRequestRetrieveOutputBuffer(Request, sizeof(BYTE), &outBuffer, NULL); if(NT_SUCCESS(status)) { /*do not use the value in the device context, since that may already have changed because of a second interrupt while this one was handled.*/ *outBuffer = temp; WdfRequestCompleteWithInformation(Request, status, sizeof(BYTE)); } else WdfRequestComplete(Request, status); KdPrint((__DRIVER_NAME "Completed async pending IOCTL.\n")); } }
处理设备读/写功能
驱动程序仍然缺少最后一件事情是读/写功能。对于 FX2,它们是对称的。写入“输入”终结点的一切都会被循环回“输出”终结点。这些终结点是双缓冲的,因此可以在前一个数据包仍在传输时发送一个新的数据包。
如果数据未被读回,“写入”请求将暂停,直到缓冲区再次清空。在应用程序级别,这意味着必须始终有一个长度与写入请求匹配的挂起读取请求。
幸运的是,驱动程序不必关心这一点。这是使用此设备驱动程序的用户应用程序的责任。
处理写入请求
驱动程序本身无法执行实际的写入操作。它必须请求 USB 总线驱动程序执行写入请求。为此,驱动程序会重新格式化传入的请求,使其成为 USB IO 目标的写入请求。
为了在请求完成后能够完成它,EvtIoWriteComplete
函数与写入请求相关联作为完成例程。当底层 USB 写入请求完成时,它将被执行。
如果这些中间操作中的任何一个失败,请求将立即以正确的状态代码失败。否则,此函数将返回而不更改请求。剩下的工作由完成例程来完成。
VOID EvtDeviceIoWrite( IN WDFQUEUE Queue, IN WDFREQUEST Request, IN size_t Length ) { NTSTATUS status = STATUS_SUCCESS; PDEVICE_CONTEXT devCtx = NULL; WDFMEMORY requestMem; devCtx = GetDeviceContext(WdfIoQueueGetDevice(Queue)); KdPrint((__DRIVER_NAME "Received a write request of %d bytes\n", Length)); status = WdfRequestRetrieveInputMemory(Request, &requestMem); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfRequestRetrieveInputMemory failed with status 0x%08x\n", status)); WdfRequestComplete(Request, status); return; } status = WdfUsbTargetPipeFormatRequestForWrite( devCtx->UsbBulkOutPipe, Request, requestMem, NULL); if(!NT_SUCCESS(status)) { KdPrint((__DRIVER_NAME "WdfUsbTargetPipeFormatRequestForWrite " "failed with status 0x%08x\n", status)); WdfRequestComplete(Request, status); return; } WdfRequestSetCompletionRoutine(Request, EvtIoWriteComplete, devCtx->UsbBulkOutPipe); if(FALSE == WdfRequestSend(Request, WdfUsbTargetPipeGetIoTarget(devCtx->UsbBulkOutPipe), NULL)) { KdPrint((__DRIVER_NAME "WdfRequestSend failed with status 0x%08x\n", status)); status = WdfRequestGetStatus(Request); WdfRequestComplete(Request, status); } else return; }
完成写入请求
写入请求的完成函数非常简单。它从 USB 请求完成参数中获取状态和传输长度,并使用该信息完成请求。
VOID EvtIoWriteComplete( IN WDFREQUEST Request, IN WDFIOTARGET Target, IN PWDF_REQUEST_COMPLETION_PARAMS Params, IN WDFCONTEXT Context) { PWDF_USB_REQUEST_COMPLETION_PARAMS usbCompletionParams; UNREFERENCED_PARAMETER(Context); UNREFERENCED_PARAMETER(Target); usbCompletionParams = Params->Parameters.Usb.Completion; if(NT_SUCCESS(Params->IoStatus.Status)) { KdPrint((__DRIVER_NAME "Completed the write request with %d bytes\n", usbCompletionParams->Parameters.PipeWrite.Length)); } else { KdPrint((__DRIVER_NAME "Failed the read request with status 0x%08x\n", Params->IoStatus.Status)); } WdfRequestCompleteWithInformation(Request, Params->IoStatus.Status, usbCompletionParams->Parameters.PipeWrite.Length); }
处理读取请求
从该驱动程序的角度来看,读取请求与写入请求几乎相同。唯一的区别是使用了另一个批量管道,并且读取了不同的完成参数。
测试驱动程序
您可以从此页面顶部下载设备驱动程序。有关如何构建和安装驱动程序的更多详细信息,您可以阅读我之前的文章。
还有一个演示应用程序可供下载。测试应用程序可以枚举所有导出 GUID_DEVINTERFACE_FX2
设备接口的设备。
一旦打开设备句柄,一个辅助线程将使用实际的开关包状态初始化用户界面上的开关,然后等待开关更改通知。
用户还可以通过按钮使用以下功能:
- 将文件通过设备回传到磁盘上的另一个文件。在数据传输过程中,LED 阵列用作二进制数字,每接收 10 个 USB 数据包,FX2 上的该数字就会增加。
- 获取 LED 阵列状态。每个 LED 由一位表示,该位可视化为复选框。
- 设置 LED 阵列状态。对话框上复选框的状态将设置在 FX2 上的相应 LED 上。
所有设备错误都将弹出消息框。如果在操作过程中拔出电缆,也没有问题。当前操作将优雅地失败,设备句柄将被关闭。
如果您测试应用程序,您可能会注意到,如果开关移动得非常快,屏幕上的开关可能与 FX2 上的实际状态不匹配。这仅仅是因为我的应用程序只使用一个 IO 控制进行开关更改通知。
如果您想接收所有通知,您的应用程序必须创建一个挂起的 IO 控制队列,该队列在前一个完成时会被填充。
结论
呼……
我知道这是一篇非常长的文章。感谢您的阅读。我希望您喜欢阅读它,就像我喜欢写它并弄清楚一切一样。
这篇文章如此之长的原因是它很完整,或者至少在不将其变成百科全书或复制整个 DDK 帮助集合的情况下,尽可能完整。
本文介绍了编写一个功能齐全的 USB 设备驱动程序所涉及的所有问题,该驱动程序使用控制请求、USB 中断和批量传输。
通过本文,您应该能够理解 USB 设备驱动程序开发中的概念。如果您想开发自己的 USB 驱动程序,本文为您提供了一个很好的起点。
有一点需要提及:当前设计不允许多个应用程序接收开关更改通知,因为每个中断只完成一个请求。为了添加此功能,我将不得不添加并解释其他主题,如文件对象处理和同步函数。
这将使本文更长,而本文的目的是解释 USB 机制。KMDF 文件对象处理和其他主题将留待后续文章。
老实说,开发驱动程序、DLL 和测试应用程序所花费的时间远远少于写这篇文章所花费的时间。最耗时的工作之一是进行所有研究,以确保本文在事实上的准确性。
如果您发现本文中有任何错误、歧义、疏忽或其他问题,请告诉我,以便我能保持其准确性和最新性。
历史
本文已发布以下版本:
- 1.0:本文的初始版本。特别感谢 Doron Holan 和 Vishal Manan 的反馈和建议。