了解 USB 客户端驱动程序代码结构 (KMDF)
在本主题中,你将了解基于 KMDF 的 USB 客户端驱动程序的源代码。 代码示例由 Microsoft Visual Studio 2019 附带的 USB 用户模式驱动程序模板生成。
这些部分提供有关模板代码的信息。
有关生成 KMDF 模板代码的说明,请参阅 如何编写第一个 USB 客户端驱动程序 (KMDF) 。
驱动程序源代码
驱动程序对象表示 Windows 在内存中加载驱动程序后客户端驱动程序的实例。 驱动程序对象的完整源代码位于 Driver.h 和 Driver.c 中。
Driver.h
在讨论模板代码的详细信息之前,让我们看一下头文件 (Driver.h) 中与 KMDF 驱动程序开发相关的一些声明。
Driver.h 包含这些文件,包含在 Windows 驱动程序工具包 (WDK) 中。
#include <ntddk.h>
#include <wdf.h>
#include <usb.h>
#include <usbdlib.h>
#include <wdfusb.h>
#include "device.h"
#include "queue.h"
#include "trace.h"
始终包含 Ntddk.h 和 Wdf.h 头文件,用于 KMDF 驱动程序开发。 头文件包括编译 KMDF 驱动程序所需的方法和结构的各种声明和定义。
Usb.h 和 Usbdlib.h 包括 USB 设备客户端驱动程序所需的结构和例程的声明和定义。
Wdfusb.h 包括与框架提供的 USB I/O 目标对象进行通信所需的结构和方法的声明和定义。
WDK 中不包括 Device.h、Queue.h 和 Trace.h。 这些头文件由模板生成,本主题稍后将讨论。
Driver.h 中的下一个块为 DriverEntry 例程以及 EvtDriverDeviceAdd 和 EvtCleanupCallback 事件回调例程提供函数角色类型声明。 所有这些例程都由驱动程序实现。 角色类型可帮助静态驱动程序验证程序 (SDV) 分析驱动程序的源代码。 有关角色类型的详细信息,请参阅 使用 KMDF 驱动程序的函数角色类型声明函数。
DRIVER_INITIALIZE DriverEntry;
EVT_WDF_DRIVER_DEVICE_ADD MyUSBDriver_EvtDeviceAdd;
EVT_WDF_OBJECT_CONTEXT_CLEANUP MyUSBDriver_EvtDriverContextCleanup;
实现文件 Driver.c 包含以下代码块,这些代码块使用 alloc_text
pragma 指定 DriverEntry 函数和事件回调例程是否位于可分页内存中。
#ifdef ALLOC_PRAGMA
#pragma alloc_text (INIT, DriverEntry)
#pragma alloc_text (PAGE, MyUSBDriver_EvtDeviceAdd)
#pragma alloc_text (PAGE, MyUSBDriver_EvtDriverContextCleanup)
#endif
请注意, DriverEntry 标记为 INIT,而事件回调例程标记为 PAGE。 INIT 部分指示 DriverEntry 的可执行代码可分页,并在驱动程序从 DriverEntry 返回后立即放弃。 PAGE 部分指示代码不必始终保留在物理内存中;当页面文件未使用时,可以将其写入页面文件。 有关详细信息,请参阅 锁定可分页代码或数据。
加载驱动程序后不久,Windows 会分配表示驱动程序 的DRIVER_OBJECT 结构。 然后,它会调用驱动程序的入口点例程 DriverEntry,并将指针传递到结构。 由于 Windows 按名称查找例程,因此每个驱动程序都必须实现名为 DriverEntry 的例程。 例程执行驱动程序的初始化任务,并将驱动程序的事件回调例程指定为框架。
以下代码示例演示模板生成的 DriverEntry 例程。
NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
WDF_DRIVER_CONFIG config;
NTSTATUS status;
WDF_OBJECT_ATTRIBUTES attributes;
//
// Initialize WPP Tracing
//
WPP_INIT_TRACING( DriverObject, RegistryPath );
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
//
// Register a cleanup callback so that we can call WPP_CLEANUP when
// the framework driver object is deleted during driver unload.
//
WDF_OBJECT_ATTRIBUTES_INIT(&attributes);
attributes.EvtCleanupCallback = MyUSBDriver_EvtDriverContextCleanup;
WDF_DRIVER_CONFIG_INIT(&config,
MyUSBDriver_EvtDeviceAdd
);
status = WdfDriverCreate(DriverObject,
RegistryPath,
&attributes,
&config,
WDF_NO_HANDLE
);
if (!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DRIVER, "WdfDriverCreate failed %!STATUS!", status);
WPP_CLEANUP(DriverObject);
return status;
}
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Exit");
return status;
}
DriverEntry 例程有两个参数:指向 Windows 分配的DRIVER_OBJECT结构的指针,以及驱动程序的注册表路径。 RegistryPath 参数表示注册表中特定于驱动程序的路径。
在 DriverEntry 例程中,驱动程序执行以下任务:
分配驱动程序生存期内所需的全局资源。 例如,在模板代码中,客户端驱动程序通过调用 WPP_INIT_TRACING 宏来分配 WPP 软件跟踪所需的资源。
向框架注册某些事件回调例程。
若要注册事件回调,客户端驱动程序首先指定指向某些 WDF 结构中 EvtDriverXxx 例程实现的指针。 然后,驱动程序调用 WdfDriverCreate 方法,并提供这些结构, (下一步) 中讨论。
调用 WdfDriverCreate 方法并检索 框架驱动程序对象的句柄。
客户端驱动程序调用 WdfDriverCreate 后,框架将创建一个框架驱动程序对象来表示客户端驱动程序。 调用完成后,客户端驱动程序会收到 WDFDRIVER 句柄,并且可以检索有关驱动程序的信息,例如其注册表路径、版本信息等, (请参阅 WDF 驱动程序对象引用) 。
请注意,框架驱动程序对象不同于 DRIVER_OBJECT 描述的 Windows 驱动程序对象。 客户端驱动程序随时可以使用 WDFDRIVER 句柄并调用 WdfGetDriver 方法获取指向 WindowsDRIVER_OBJECT 结构的指针。
在 WdfDriverCreate 调用后,框架与客户端驱动程序合作以与 Windows 通信。 框架充当 Windows 和驱动程序之间的抽象层,并处理大多数复杂的驱动程序任务。 客户端驱动程序向框架注册驱动程序感兴趣的事件。 发生某些事件时,Windows 会通知框架。 如果驱动程序为特定事件注册了事件回调,框架将通过调用已注册的事件回调来通知驱动程序。 这样,驱动程序就有机会处理事件(如果需要)。 如果驱动程序未注册其事件回调,框架将继续执行其事件的默认处理。
驱动程序必须注册的事件回调之一是 EvtDriverDeviceAdd。 框架准备好创建设备对象时,框架调用驱动程序的 EvtDriverDeviceAdd 实现。 在 Windows 中,设备对象是加载客户端驱动程序的物理设备的功能的逻辑表示形式, (本主题) 后面部分讨论。
驱动程序可以注册的其他事件回调是 EvtDriverUnload、 EvtCleanupCallback 和 EvtDestroyCallback。
在模板代码中,客户端驱动程序注册两个事件: EvtDriverDeviceAdd 和 EvtCleanupCallback。 驱动程序指定指向其在 WDF_DRIVER_CONFIG 结构中的 EvtDriverDeviceAdd 实现的指针,并在 WDF_OBJECT_ATTRIBUTES结构中指定 EvtCleanupCallback 事件回调。
当 Windows 准备好释放 DRIVER_OBJECT 结构和卸载驱动程序时,框架将通过调用驱动程序的 EvtCleanupCallback 实现向客户端驱动程序报告该事件。 框架在删除框架驱动程序对象之前调用该回调。 客户端驱动程序可以释放在其 DriverEntry 中分配的所有全局资源。 例如,在模板代码中,客户端驱动程序停止在 DriverEntry 中激活的 WPP 跟踪。
以下代码示例演示客户端驱动程序的 EvtCleanupCallback 事件回调实现。
VOID MyUSBDriver_EvtDriverContextCleanup(
_In_ WDFDRIVER Driver
)
{
UNREFERENCED_PARAMETER(Driver);
PAGED_CODE ();
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
//
// Stop WPP Tracing
//
WPP_CLEANUP( WdfDriverWdmGetDriverObject(Driver) );
}
USB 驱动程序堆栈识别设备后,总线驱动程序 (设备的 PDO) 创建物理设备对象,并将 PDO 与设备节点相关联。 设备节点处于堆栈形式,其中 PDO 位于底部。 每个堆栈必须有一个 PDO,并且可以具有筛选器设备对象 (筛选器DO) ,并且其上方 (FDO) 函数设备对象。 有关详细信息,请参阅 设备节点和设备堆栈。
此图显示了模板驱动程序的设备堆栈,MyUSBDriver_.sys。
请注意名为“我的 USB 设备”的设备堆栈。 USB 驱动程序堆栈为设备堆栈创建 PDO。 在示例中,PDO 与 Usbhub3.sys 相关联,后者是 USB 驱动程序堆栈中包含的驱动程序之一。 作为设备的函数驱动程序,客户端驱动程序必须首先为设备创建 FDO,然后将其附加到设备堆栈的顶部。
对于基于 KMDF 的客户端驱动程序,框架代表客户端驱动程序执行这些任务。 为了表示设备的 FDO,框架会创建 一个框架设备对象。 但是,客户端驱动程序可以指定框架用于配置新对象的某些初始化参数。 当框架调用驱动程序的 EvtDriverDeviceAdd 实现时,会向客户端驱动程序提供此机会。 创建对象并将 FDO 附加到设备堆栈顶部后,框架为客户端驱动程序提供框架设备对象的 WDFDEVICE 句柄。 通过使用此句柄,客户端驱动程序可以执行各种与设备相关的操作。
下面的代码示例演示客户端驱动程序的 EvtDriverDeviceAdd 事件回调实现。
NTSTATUS
MyUSBDriver_EvtDeviceAdd(
_In_ WDFDRIVER Driver,
_Inout_ PWDFDEVICE_INIT DeviceInit
)
{
NTSTATUS status;
UNREFERENCED_PARAMETER(Driver);
PAGED_CODE();
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
status = MyUSBDriver_CreateDevice(DeviceInit);
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Exit");
return status;
}
在运行时,EvtDriverDeviceAdd 的实现使用 PAGED_CODE 宏检查在适合可分页代码的相应环境中调用例程。 请确保在声明所有变量后调用宏;否则,编译会失败,因为生成的源文件是 .c 文件,而不是 .cpp 文件。
客户端驱动程序的 EvtDriverDeviceAdd 实现调用 MyUSBDriver_CreateDevice 帮助程序函数来执行所需的任务。
下面的代码示例演示 MyUSBDriver_CreateDevice 帮助程序函数。 MyUSBDriver_CreateDevice在 Device.c 中定义。
NTSTATUS
MyUSBDriver_CreateDevice(
_Inout_ PWDFDEVICE_INIT DeviceInit
)
{
WDF_PNPPOWER_EVENT_CALLBACKS pnpPowerCallbacks;
WDF_OBJECT_ATTRIBUTES deviceAttributes;
PDEVICE_CONTEXT deviceContext;
WDFDEVICE device;
NTSTATUS status;
PAGED_CODE();
WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);
pnpPowerCallbacks.EvtDevicePrepareHardware = MyUSBDriver_EvtDevicePrepareHardware;
WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, &pnpPowerCallbacks);
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttributes, DEVICE_CONTEXT);
status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);
if (NT_SUCCESS(status)) {
//
// Get the device context and initialize it. WdfObjectGet_DEVICE_CONTEXT is an
// inline function generated by WDF_DECLARE_CONTEXT_TYPE macro in the
// device.h header file. This function will do the type checking and return
// the device context. If you pass a wrong object handle
// it will return NULL and assert if run under framework verifier mode.
//
deviceContext = WdfObjectGet_DEVICE_CONTEXT(device);
deviceContext->PrivateDeviceData = 0;
//
// Create a device interface so that applications can find and talk
// to us.
//
status = WdfDeviceCreateDeviceInterface(
device,
&GUID_DEVINTERFACE_MyUSBDriver_,
NULL // ReferenceString
);
if (NT_SUCCESS(status)) {
//
// Initialize the I/O Package and any Queues
//
status = MyUSBDriver_QueueInitialize(device);
}
}
return status;
}
EvtDriverDeviceAdd 具有两个参数:在上一次调用 DriverEntry 中创建的框架驱动程序对象的句柄,以及指向 WDFDEVICE_INIT 结构的指针。 框架分配 WDFDEVICE_INIT 结构,并为其传递指针,以便客户端驱动程序可以使用要创建的框架设备对象的初始化参数填充结构。
在 EvtDriverDeviceAdd 实现中,客户端驱动程序必须执行以下任务:
调用 WdfDeviceCreate 方法以检索新设备对象的 WDFDEVICE 句柄。
WdfDeviceCreate 方法使框架为 FDO 创建框架设备对象,并将其附加到设备堆栈的顶部。 在 WdfDeviceCreate 调用中,客户端驱动程序必须执行以下任务:
- 在框架指定的WDFDEVICE_INIT结构中,指定指向客户端驱动程序的即插即用 (PnP ) 电源回调例程的指针。 例程首先在 WDF_PNPPOWER_EVENT_CALLBACKS 结构中设置,然后通过调用 WdfDeviceInitSetPnpPowerEventCallbacks 方法与WDFDEVICE_INIT相关联。
Windows 组件、PnP 和电源管理器向驱动程序发送与设备相关的请求,以响应 PnP 状态 (更改,例如启动、停止和删除) 和电源状态 (,例如工作或暂停) 。 对于基于 KMDF 的驱动程序,框架会截获这些请求。 客户端驱动程序可以通过使用 WdfDeviceCreate 调用向框架注册名为 PnP 电源事件回调的回调例程来获取有关请求的通知。 当 Windows 组件发送请求时,如果客户端驱动程序已注册,框架会处理这些请求并调用相应的 PnP 电源事件回调。
客户端驱动程序必须实现的 PnP 电源事件回调例程之一是 EvtDevicePrepareHardware。 当 PnP 管理器启动设备时,将调用该事件回调。 下一节将讨论 EvtDevicePrepareHardware 的实现。
- 指定指向驱动程序的设备上下文结构的指针。 必须在通过调用 WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE 宏初始化的 WDF_OBJECT_ATTRIBUTES 结构 中设置指针 。
设备上下文 (有时称为设备扩展) 是客户端驱动程序 (定义的数据结构) ,用于存储有关特定设备对象的信息。 客户端驱动程序将指向其设备上下文的指针传递给框架。 框架根据结构的大小分配内存块,并将指向该内存位置的指针存储在框架设备对象中。 客户端驱动程序可以使用 指针访问设备上下文的成员并存储信息。 有关设备上下文的详细信息,请参阅 框架对象上下文空间。
WdfDeviceCreate 调用完成后,客户端驱动程序会收到新的框架设备对象的句柄,该句柄存储指向框架为设备上下文分配的内存块的指针。 客户端驱动程序现在可以通过调用 WdfObjectGet_DEVICE_CONTEXT 宏来获取指向设备上下文 的 指针。
通过调用 WdfDeviceCreateDeviceInterface 方法为客户端驱动程序注册设备接口 GUID。 应用程序可以通过使用此 GUID 与驱动程序通信。 GUID 常量在标头 public.h 中声明。
设置队列以便将 I/O 传输到设备。 模板代码定义 MyUSBDriver_QueueInitialize,这是用于设置队列的帮助程序例程,将在 队列源代码 部分中讨论。
设备源代码
设备对象表示在内存中为其加载客户端驱动程序的设备实例。 设备对象的完整源代码位于 Device.h 和 Device.c 中。
Device.h
Device.h 头文件包含 public.h,其中包含项目中所有文件使用的通用声明。
Device.h 中的下一个块声明客户端驱动程序的设备上下文。
typedef struct _DEVICE_CONTEXT
{
WDFUSBDEVICE UsbDevice;
ULONG PrivateDeviceData; // just a placeholder
} DEVICE_CONTEXT, *PDEVICE_CONTEXT;
WDF_DECLARE_CONTEXT_TYPE(DEVICE_CONTEXT)
DEVICE_CONTEXT结构由客户端驱动程序定义,并存储有关框架设备对象的信息。 它在 Device.h 中声明并包含两个成员:框架的 USB 目标设备对象的句柄 (稍后) 讨论和占位符。 此结构将在后面的练习中扩展。
Device.h 还包括 WdfObjectGet_DEVICE_CONTEXT 生成内联函数的 WDF_DECLARE_CONTEXT_TYPE 宏。 客户端驱动程序可以调用该函数,以从框架设备对象检索指向内存块的指针。
以下代码行声明MyUSBDriver_CreateDevice,这是一个帮助程序函数,用于检索 USB 目标设备对象的 WDFUSBDEVICE 句柄。
NTSTATUS
MyUSBDriver_CreateDevice(
_Inout_ PWDFDEVICE_INIT DeviceInit
);
USBCreate 将指向 WDFDEVICE_INIT 结构的指针作为其参数。 这是框架在调用客户端驱动程序的 EvtDriverDeviceAdd 实现时传递的同一指针。 基本上,MyUSBDriver_CreateDevice执行 EvtDriverDeviceAdd 的任务。 上一部分讨论了 EvtDriverDeviceAdd 实现的源代码。
Device.h 中的下一行声明 EvtDevicePrepareHardware 事件回调例程的函数角色类型声明。 事件回调由客户端驱动程序实现,并执行配置 USB 设备等任务。
EVT_WDF_DEVICE_PREPARE_HARDWARE MyUSBDriver_EvtDevicePrepareHardware;
Device.c
Device.c 实现文件包含以下代码块,这些代码块使用 alloc_text
杂注来指定 EvtDevicePrepareHardware 的驱动程序实现位于可分页内存中。
#ifdef ALLOC_PRAGMA
#pragma alloc_text (PAGE, MyUSBDriver_CreateDevice)
#pragma alloc_text (PAGE, MyUSBDriver_EvtDevicePrepareHardware)
#endif
在 EvtDevicePrepareHardware 的实现中,客户端驱动程序执行特定于 USB 的初始化任务。 这些任务包括注册客户端驱动程序、初始化特定于 USB 的 I/O 目标对象,以及选择 USB 配置。 下表显示了框架提供的专用 I/O 目标对象。 有关详细信息,请参阅 USB I/O 目标。
USB I/O 目标对象 (句柄) | 通过调用... 获取句柄... | 说明 |
---|---|---|
USB 目标设备对象 (WDFUSBDEVICE ) | WdfUsbTargetDeviceCreateWithParameters | 表示 USB 设备,并提供检索设备描述符和向设备发送控制请求的方法。 |
USB 目标接口对象 (WDFUSBINTERFACE ) | WdfUsbTargetDeviceGetInterface | 表示单个接口,并提供客户端驱动程序可以调用以选择备用设置和检索有关设置的信息的方法。 |
USB 目标管道对象 (WDFUSBPIPE) | WdfUsbInterfaceGetConfiguredPipe | 表示在接口的当前备用设置中配置的终结点的单个管道。 USB 驱动程序堆栈选择所选配置中的每个接口,并设置与接口内每个终结点的信道。 在 USB 术语中,该信道称为 管道。 |
此代码示例演示 EvtDevicePrepareHardware 的实现。
NTSTATUS
MyUSBDriver_EvtDevicePrepareHardware(
_In_ WDFDEVICE Device,
_In_ WDFCMRESLIST ResourceList,
_In_ WDFCMRESLIST ResourceListTranslated
)
{
NTSTATUS status;
PDEVICE_CONTEXT pDeviceContext;
WDF_USB_DEVICE_CREATE_CONFIG createParams;
WDF_USB_DEVICE_SELECT_CONFIG_PARAMS configParams;
UNREFERENCED_PARAMETER(ResourceList);
UNREFERENCED_PARAMETER(ResourceListTranslated);
PAGED_CODE();
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
status = STATUS_SUCCESS;
pDeviceContext = WdfObjectGet_DEVICE_CONTEXT(Device);
if (pDeviceContext->UsbDevice == NULL) {
//
// Specifying a client contract version of 602 enables us to query for
// and use the new capabilities of the USB driver stack for Windows 8.
// It also implies that we conform to rules mentioned in the documentation
// documentation for WdfUsbTargetDeviceCreateWithParameters.
//
WDF_USB_DEVICE_CREATE_CONFIG_INIT(&createParams,
USBD_CLIENT_CONTRACT_VERSION_602
);
status = WdfUsbTargetDeviceCreateWithParameters(Device,
&createParams,
WDF_NO_OBJECT_ATTRIBUTES,
&pDeviceContext->UsbDevice
);
if (!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE,
"WdfUsbTargetDeviceCreateWithParameters failed 0x%x", status);
return status;
}
//
// Select the first configuration of the device, using the first alternate
// setting of each interface
//
WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_MULTIPLE_INTERFACES(&configParams,
0,
NULL
);
status = WdfUsbTargetDeviceSelectConfig(pDeviceContext->UsbDevice,
WDF_NO_OBJECT_ATTRIBUTES,
&configParams
);
if (!NT_SUCCESS(status)) {
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE,
"WdfUsbTargetDeviceSelectConfig failed 0x%x", status);
return status;
}
}
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Exit");
return status;
}
下面详细介绍了模板代码实现的客户端驱动程序的任务:
指定客户端驱动程序的协定版本,准备将自身注册到 Windows 加载的基础 USB 驱动程序堆栈。
Windows 可以加载 USB 3.0 或 USB 2.0 驱动程序堆栈,具体取决于 USB 设备连接到的主控制器。 USB 3.0 驱动程序堆栈是 Windows 8 中的新增功能,支持 USB 3.0 规范定义的多项新功能,例如流功能。 新的驱动程序堆栈还实现了多项改进,例如更好地跟踪和处理 USB 请求块 (URB) ,这些可通过一组新的 URB 例程获得。 打算使用这些功能或调用新例程的客户端驱动程序必须指定USBD_CLIENT_CONTRACT_VERSION_602协定版本。 USBD_CLIENT_CONTRACT_VERSION_602客户端驱动程序必须遵守一组特定的规则。 有关这些规则的详细信息,请参阅 最佳做法:使用 URL。
若要指定协定版本,客户端驱动程序必须通过调用 WDF_USB_DEVICE_CREATE_CONFIG_INIT 宏,使用协定版本初始化 WDF_USB_DEVICE_CREATE_CONFIG 结构。
调用 WdfUsbTargetDeviceCreateWithParameters 方法。 方法需要一个框架设备对象的句柄,该对象是客户端驱动程序先前在 EvtDriverDeviceAdd 的驱动程序实现中通过调用 WdfDeviceCreate 获取的。 WdfUsbTargetDeviceCreateWithParameters 方法:
- 将客户端驱动程序注册到基础 USB 驱动程序堆栈。
- 检索框架创建的 USB 目标设备对象的 WDFUSBDEVICE 句柄。 模板代码将 USB 目标设备对象的句柄存储在其设备上下文中。 通过使用该句柄,客户端驱动程序可以获取有关设备的 USB 特定信息。
在以下情况下,必须调用 WdfUsbTargetDeviceCreate 而不是 WdfUsbTargetDeviceCreateWithParameters :
客户端驱动程序不会调用 Windows 8 版本的 WDK 中提供的新 URB 例程集。
如果客户端驱动程序调用 WdfUsbTargetDeviceCreateWithParameters,则 USB 驱动程序堆栈假定所有 URB 都是通过调用 WdfUsbTargetDeviceCreateUrb 或 WdfUsbTargetDeviceCreateIsochUrb 分配的。 由这些方法分配的 URB 具有不透明的 URB 上下文块,USB 驱动程序堆栈使用这些块来加快处理速度。 如果客户端驱动程序使用的 URB 不是由这些方法分配的,则 USB 驱动程序将生成 bug 检查。
有关 URB 分配的详细信息,请参阅 分配和生成 URL。
客户端驱动程序不打算遵守 最佳做法:使用 URL 中所述的规则集。
此类驱动程序不需要指定客户端协定版本,因此必须跳过步骤 1。
选择 USB 配置。
在模板代码中,客户端驱动程序选择 USB 设备中的 默认配置 。 默认配置包括设备的配置 0 和该配置中每个接口的备用设置 0。
若要选择默认配置,客户端驱动程序通过调用 WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_MULTIPLE_INTERFACES 函数来配置 WDF_USB_DEVICE_SELECT_CONFIG_PARAMS 结构。 函数将 Type 成员初始化为 WdfUsbTargetDeviceSelectConfigTypeMultiInterface ,以指示如果有多个接口可用,则必须选择每个接口中的备用设置。 由于调用必须选择默认配置,因此客户端驱动程序在 SettingPairs 参数中指定 NULL,在 NumberInterfaces 参数中指定 0。 完成后,WDF_USB_DEVICE_SELECT_CONFIG_PARAMS的 MultiInterface.NumberOfConfiguredInterfaces 成员指示为其选择了备用设置 0 的接口数。 不会修改其他成员。
注意 如果客户端驱动程序想要选择默认设置以外的其他设置,则驱动程序必须创建 WDF_USB_INTERFACE_SETTING_PAIR 结构的数组。 数组中的每个元素都指定要选择的备用设置的设备定义的接口号和索引。 该信息存储在设备的配置和接口描述符中,可以通过调用 WdfUsbTargetDeviceRetrieveConfigDescriptor 方法获取这些描述符。 然后,客户端驱动程序必须调用 WDF_USB_DEVICE_SELECT_CONFIG_PARAMS_INIT_MULTIPLE_INTERFACES 并将 WDF_USB_INTERFACE_SETTING_PAIR 数组传递给框架。
队列源代码
框架队列对象表示特定框架设备对象的 I/O 队列。 队列对象的完整源代码位于 Queue.h 和 Queue.c 中。
Queue.h
为框架的队列对象引发的事件声明事件回调例程。
Queue.h 中的第一个块声明队列上下文。
typedef struct _QUEUE_CONTEXT {
ULONG PrivateDeviceData; // just a placeholder
} QUEUE_CONTEXT, *PQUEUE_CONTEXT;
WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(QUEUE_CONTEXT, QueueGetContext)
与设备上下文类似,队列上下文是由客户端定义的一种数据结构,用于存储有关特定队列的信息。
下一行代码声明MyUSBDriver_QueueInitialize函数,函数是创建和初始化框架队列对象的帮助程序函数。
NTSTATUS
MyUSBDriver_QueueInitialize(
_In_ WDFDEVICE Device
);
下一个代码示例声明 EvtIoDeviceControl 事件回调例程的函数角色类型声明。 事件回调由客户端驱动程序实现,并在框架处理设备 I/O 控制请求时调用。
EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL MyUSBDriver_EvtIoDeviceControl;
Queue.c
实现文件 Queue.c 包含以下代码块,该代码块使用 alloc_text
杂注来指定驱动程序实现MyUSBDriver_QueueInitialize位于可分页内存中。
#ifdef ALLOC_PRAGMA
#pragma alloc_text (PAGE, MyUSBDriver_QueueInitialize)
#endif
WDF 提供框架队列对象来处理到客户端驱动程序的请求流。 当客户端驱动程序调用 WdfIoQueueCreate 方法时,框架会创建一个框架队列对象。 在该调用中,客户端驱动程序可以在框架创建队列之前指定某些配置选项。 这些选项包括队列是电源管理的、允许零长度的请求,还是驱动程序的默认队列。 单个框架队列对象可以处理多种类型的请求,例如读取、写入和设备 I/O 控制。 客户端驱动程序可以为其中每个请求指定事件回调。
客户端驱动程序还必须指定调度类型。 队列对象的调度类型确定框架如何将请求传送到客户端驱动程序。 传递机制可以是连续的、并行的,也可以由客户端驱动程序定义的自定义机制。 对于顺序队列,在客户端驱动程序完成上一个请求之前,不会传递请求。 在并行调度模式下,框架在请求从 I/O 管理器到达时立即转发请求。 这意味着客户端驱动程序可以在处理另一个请求时接收一个请求。 在自定义机制中,当驱动程序准备好处理该请求时,客户端会手动从框架队列对象中拉取下一个请求。
通常,客户端驱动程序必须在驱动程序的 EvtDriverDeviceAdd 事件回调中设置队列。 模板代码提供初始化框架队列对象的帮助程序例程MyUSBDriver_QueueInitialize。
NTSTATUS
MyUSBDriver_QueueInitialize(
_In_ WDFDEVICE Device
)
{
WDFQUEUE queue;
NTSTATUS status;
WDF_IO_QUEUE_CONFIG queueConfig;
PAGED_CODE();
//
// Configure a default queue so that requests that are not
// configure-fowarded using WdfDeviceConfigureRequestDispatching to goto
// other queues get dispatched here.
//
WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(
&queueConfig,
WdfIoQueueDispatchParallel
);
queueConfig.EvtIoDeviceControl = MyUSBDriver_EvtIoDeviceControl;
status = WdfIoQueueCreate(
Device,
&queueConfig,
WDF_NO_OBJECT_ATTRIBUTES,
&queue
);
if( !NT_SUCCESS(status) ) {
TraceEvents(TRACE_LEVEL_ERROR, TRACE_QUEUE, "WdfIoQueueCreate failed %!STATUS!", status);
return status;
}
return status;
}
若要设置队列,客户端驱动程序将执行以下任务:
- 指定 WDF_IO_QUEUE_CONFIG 结构中的队列配置选项。 模板代码使用 WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE 函数来初始化 结构。 函数将队列对象指定为默认队列对象,由电源管理,并并行接收请求。
- 为队列的 I/O 请求添加客户端驱动程序的事件回调。 在模板中,客户端驱动程序为设备 I/O 控制请求指定指向其事件回调的指针。
- 调用 WdfIoQueueCreate 以检索框架创建的框架队列对象的 WDFQUEUE 句柄。
队列机制的工作原理如下。 为了与 USB 设备通信,应用程序首先通过调用 SetDixxx 例程和 CreateHandle 打开设备的句柄。 通过使用此句柄,应用程序使用特定的控制代码调用 DeviceIoControl 函数。 根据控制代码的类型,应用程序可以在该调用中指定输入和输出缓冲区。 调用最终由 I/O 管理器接收,然后 I/O 管理器 (IRP) 创建请求并将其转发到客户端驱动程序。 框架截获请求,创建框架请求对象,并将其添加到框架队列对象。 在这种情况下,由于客户端驱动程序为设备 I/O 控制请求注册了其事件回调,因此框架会调用回调。 此外,由于队列对象是使用 WdfIoQueueDispatchParallel 标志创建的,因此一旦请求添加到队列中,就会立即调用回调。
VOID
MyUSBDriver_EvtIoDeviceControl(
_In_ WDFQUEUE Queue,
_In_ WDFREQUEST Request,
_In_ size_t OutputBufferLength,
_In_ size_t InputBufferLength,
_In_ ULONG IoControlCode
)
{
TraceEvents(TRACE_LEVEL_INFORMATION,
TRACE_QUEUE,
"!FUNC! Queue 0x%p, Request 0x%p OutputBufferLength %d InputBufferLength %d IoControlCode %d",
Queue, Request, (int) OutputBufferLength, (int) InputBufferLength, IoControlCode);
WdfRequestComplete(Request, STATUS_SUCCESS);
return;
}
当框架调用客户端驱动程序的事件回调时,它会将句柄传递给框架请求对象,该对象保存请求 (及其输入和输出缓冲区) 应用程序发送。 此外,它还向包含请求的框架队列对象发送句柄。 在事件回调中,客户端驱动程序会根据需要处理请求。 模板代码只是完成请求。 客户端驱动程序可以执行涉及更多任务。 例如,如果应用程序请求某些设备信息,在事件回调中,客户端驱动程序可以创建 USB 控制请求并将其发送到 USB 驱动程序堆栈以检索请求的设备信息。 USB 控制请求在 USB 控制传输中进行了讨论。