了解 USB 客户端驱动程序代码结构 (KMDF)
在本主题中,你将了解基于 KMDF 的 USB 客户端驱动程序的源代码。 代码示例由 visual Studio 2019 Microsoft 随附的 USB 用户模式驱动程序模板生成。
这些部分提供有关模板代码的信息。
有关生成 KMDF 模板代码的说明,请参阅如何编写第一个 USB 客户端驱动程序(KMDF)。
驱动程序源代码
驱动程序 对象 表示 Windows 在内存中加载驱动程序后客户端驱动程序的实例。 驱动程序对象的完整源代码位于 Driver.h 和 Driver.c 中。
Driver.h
在讨论模板代码的详细信息之前,让我们看看与 KMDF 驱动程序开发相关的头文件(Driver.h)中的一些声明。
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"
对于 KMDF 驱动程序开发,始终包含 Ntddk.h 和 Wdf.h 头文件。 头文件包括需要编译 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
杂注指定 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结构中设置指针。
设备上下文(有时称为设备扩展)是用于存储有关特定设备对象的信息的数据结构(由客户端驱动程序定义)。 客户端驱动程序将指向其设备上下文的指针传递给框架。 框架根据结构的大小分配内存块,并将指向该内存位置的指针存储在框架设备对象中。 客户端驱动程序可以使用指针在设备上下文的成员中访问和存储信息。 有关设备上下文的详细信息,请参阅 Framework 对象上下文空间。
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 还包括生成内联函数的 WDF_DECLARE_CONTEXT_TYPE 宏,WdfObjectGet_DEVICE_CONTEXT。 客户端驱动程序可以调用该函数,从框架设备对象中检索指向内存块的指针。
以下代码行声明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客户端驱动程序必须遵循一组特定的规则。 有关这些规则的详细信息,请参阅 最佳做法:使用 URB。
若要指定协定版本,客户端驱动程序必须通过调用WDF_USB_DEVICE_CREATE_CONFIG_INIT宏来初始化协定版本WDF_USB_DEVICE_CREATE_CONFIG结构。
调用 WdfUsbTargetDeviceCreateWithParameters 方法。 该方法要求在驱动程序的 EvtDriverDeviceAdd 实现中调用 WdfDeviceCreate 获取的框架设备对象的框架设备对象。 WdfUsbTargetDeviceCreateWithParameters 方法:
- 将客户端驱动程序注册到基础 USB 驱动程序堆栈。
- 检索由框架创建的 USB 目标设备对象的 WDFUSBDEVICE 句柄。 模板代码将句柄存储在 USB 目标设备对象的设备上下文中。 通过使用该句柄,客户端驱动程序可以获取有关设备的 USB 特定信息。
如果满足以下条件,则必须调用 WdfUsbTargetDeviceCreate 而不是 WdfUsbTargetDeviceCreateWithParameters:
客户端驱动程序不会调用 WDK 的 Windows 8 版本可用的新 URB 例程集。
如果客户端驱动程序调用 WdfUsbTargetDeviceCreateWithParameters,USB 驱动程序堆栈假定所有 URB 都通过调用 WdfUsbTargetDeviceCreateUrb 或 WdfUsbTargetDeviceCreateIsochUrb 来分配。 这些方法分配的 URB 具有 USB 驱动程序堆栈用于更快处理的不透明 URB 上下文块。 如果客户端驱动程序使用这些方法未分配的 URB,USB 驱动程序将生成 bug 检查。
有关 URB 分配的详细信息,请参阅 分配和生成 URB。
客户端驱动程序不打算遵守最佳做法: 使用 URB 中所述的规则集。
此类驱动程序不需要指定客户端协定版本,因此必须跳过步骤 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 管理器接收,然后创建一个请求(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 控制传输中讨论。