如何编写第一个 USB 客户端驱动程序 (UMDF)

在本文中,将使用 Microsoft Visual Studio 2022 提供的用户模式驱动程序,USB (UMDF V2) 模板来编写基于用户模式驱动程序框架 (UMDF) 的客户端驱动程序。 生成并安装客户端驱动程序后,你将在设备管理器中查看客户端驱动程序,并在调试器中查看驱动程序输出。

UMDF(在本文中被称为框架)基于组件对象模型 (COM)。 默认情况下,每个框架对象都必须实现 IUnknown 及其方法 QueryInterfaceAddRefReleaseAddRefRelease 方法管理对象的生存期,因此客户端驱动程序无需维护引用计数。 通过 QueryInterface 方法,客户端驱动程序可以获取 Windows 驱动程序框架 (WDF) 对象模型中其他框架对象的接口指针。 框架对象执行复杂的驱动程序任务并与 Windows 进行交互。 某些框架对象会公开接口,使客户端驱动程序能够与框架进行交互。

基于 UMDF 的客户端驱动程序是作为进程内 COM 服务器 (DLL) 实现的,而 C++ 是编写 USB 设备客户端驱动程序的首选语言。 通常情况下,客户端驱动程序会实现框架公开的多个接口。 本文将实现框架接口的客户端驱动程序定义的类称为回调类。 在这些类被实例化后,生成的回调对象会与特定的框架对象配对。 这种合作关系让客户端驱动程序有机会对框架报告的设备或系统相关事件做出响应。 每当 Windows 通知框架某些事件时,框架都会调用客户端驱动程序的回调(如有)。 否则,框架将按照默认方式处理事件。 模板代码定义了驱动程序、设备和队列回调类。

有关模板生成的源代码的说明,请参阅了解 USB 客户端驱动程序的 UMDF 模板代码

开始之前

要开发、调试和安装用户模式驱动程序,需要使用两台计算机:

  • 运行 Windows 10 或更高版本 Windows 操作系统的主计算机。 主计算机是开发环境,可在这里编写和调试驱动程序。
  • 运行要测试驱动程序的操作系统版本的目标计算机,例如,Windows 11,版本 22H2。 目标计算机上有要调试的用户模式驱动程序和其中一个调试器。

在某些情况下,当主计算机和目标计算机运行相同版本的 Windows 时,可以只让一台计算机运行 Windows 10 或更高版本的 Windows。 本文假定使用两台计算机来开发、调试和安装用户模式驱动程序。

开始之前,请确保符合以下要求:

软件要求

  • 主计算机安装了 Visual Studio 2022。

  • 主计算机安装了最新的 Windows 11 驱动程序工具包 (WDK),版本为 22H2。

    该工具包包括开发、生成和调试 USB 客户端驱动程序所需的标头、库、工具、文档和调试工具。 可以通过如何获取 WDK 获取最新版本的 WDK。

  • 主计算机已安装最新版本的 Windows 调试工具。 可以从 WDK 获取最新版本,也可以下载并安装 Windows 调试工具

  • 如果使用两台计算机,则必须将主计算机和目标计算机配置为用户模式调试。 有关详细信息,请参阅在 Visual Studio 中设置用户模式调试

硬件要求

获取一个 USB 设备,为其编写客户端驱动程序。 在大多数情况下,你会收到一个 USB 设备及其硬件规格。 该规范描述了设备功能和支持的供应商命令。 使用规范确定 USB 驱动程序的功能和相关设计决策。

如果还不熟悉 USB 驱动程序开发,请使用 OSR USB FX2 学习工具包来研究 WDK 中包含的 USB 示例。 它包含 USB FX2 设备和实施客户端驱动程序所需的所有硬件规格。

步骤 1:生成驱动程序代码

有关编写 UMDF 驱动程序代码的详细信息,请参阅基于模板编写 UMDF 驱动程序

对于 USB 专用代码,请在 Visual Studio 2022 中选择以下选项

  1. 在“新建项目”对话框中,在顶部的搜索框中输入 USB
  2. 在中间窗格中,选择“用户模式驱动程序,USB (UMDF V2)”
  3. 选择下一步
  4. 输入项目名称,选择保存位置,然后选择“创建”。

以下屏幕截图显示了 USB 用户模式驱动程序模板的“新建项目”对话框。

Visual Studio 创建项目选项的屏幕截图。

Visual Studio 创建项目配置屏幕的屏幕截图。

本文假定项目名称为 MyUSBDriver_UMDF_。 它包含以下文件:

文件 说明
Driver.h; Driver.c 包含驱动程序模块入口点的实现。 DriverEntry 和 WDFDRIVER 相关功能和回调。
Device.h; Device.c 与 WDFDEVICE 和 WDFUSBDEVICE 相关的功能和回调。
Queue.h; Queue.c 与 WDFQUEUE 相关的功能和回调。
Trace.h 定义设备接口 GUID。 它还声明了跟踪函数和宏。
<Project name>.inf 在目标计算机上安装客户端驱动程序所需的 INF 文件。

步骤 2:添加有关设备的信息

在生成驱动程序之前,必须添加设备相关信息,特别是硬件 ID。 提供硬件 ID:

  1. 在“解决方案资源管理器”窗口中,右键单击 MyUSBDriver_UMDF_ 并选择“属性”。
  2. MyUSBDriver_UMDF_ Property Pages 窗口中,转到配置属性 > 驱动程序安装 > 部署,如下所示。 Visual Studio 2022 属性页面窗口的屏幕截图。
  3. 选中“部署前删除以前的驱动程序版本”
  4. 对于“目标设备名称”,请选择配置用于测试和调试的计算机名。
  5. 选择“硬件 ID 驱动程序更新”,然后输入驱动程序的硬件 ID。 在本练习中,硬件 ID 是 Root\MyUSBDriver_UMDF_。 选择“确定”

注意

在本练习中,硬件 ID 不标识真实的硬件。 它标识了虚构设备,该设备位于设备树中,作为根节点的子节点。 对于实际硬件,请勿选择“硬件 ID 驱动程序更新”。 请改为选择“安装和验证”。 可以在驱动程序的信息 (INF) 文件中看到硬件 ID。 在“解决方案资源管理器”窗口中,转到 MyUSBDriver_UMDF_ > 驱动程序文件,然后双击 MyUSBDriver_UMDF_.inf。 硬件 ID 位于 [Standard.NT$ARCH$] 下。

所有基于 UMDF 的 USB 客户端驱动程序都需要 Microsoft 提供的两个驱动程序,即反射器和 WinUSB。

  • 反射器:如果驱动程序加载成功,则反射器将作为内核模式堆栈中最顶层的驱动程序进行加载。 反射器必须是内核模式堆栈中的顶级驱动程序。 为满足这一要求,模板的 INF 文件将反射器指定为服务,并在 INF 中将 WinUSB 指定为低级筛选驱动程序:

    [MyDevice_Install.NT.Services]
    AddService=WUDFRd,0x000001fa,WUDFRD_ServiceInstall  ; flag 0x2 sets this as the service for the device
    AddService=WinUsb,0x000001f8,WinUsb_ServiceInstall  ; this service is installed because its a filter.
    
  • WinUSB:安装包必须包含 Winusb.sys 的 Cointaller,因为对于客户端驱动程序而言,WinUSB 是内核模式 USB 驱动程序堆栈的网关。 另一个被加载的组件是客户端驱动程序主机进程 (Wudfhost.exe) 中名为 WinUsb.dll 的用户模式 DLL。 Winusb.dll 公开了 WinUSB 函数,可简化客户端驱动程序与 WinUSB 之间的通信过程。

步骤 3:生成 USB 客户端驱动程序代码

要生成驱动程序,请执行以下操作:

  1. 在 Visual Studio 2022 中,打开驱动程序项目或解决方案。
  2. 在“解决方案资源管理器”中右键单击解决方案,然后选择“配置管理器”。
  3. 从“配置管理器”中,选择与你感兴趣的生成类型相对应的“活动解决方案配置”(例如“调试”或“发布”)和“活动解决方案平台”(例如 x64)。
  4. 验证设备接口 GUID 在整个项目中的准确性。
    • 设备接口 GUID 在 Trace.h 中定义,并在 Device.c 中从 MyUSBDriverUMDFCreateDevice 引用。 在以 MyUSBDriver_UMDF_ 的名称创建项目时,Visual Studio 2022 会以 GUID_DEVINTERFACE_MyUSBDriver_UMDF_ 的名称定义设备接口 GUID,但会以错误的参数 &GUID_DEVINTERFACE_MyUSBDriverUMDF 来调用 WdfDeviceCreateDeviceInterface。 使用 Trace.h 中定义的名称替换不正确的参数,以确保驱动程序的正确生成。
  5. 从“生成”菜单中,选择“生成解决方案”。

有关详细信息,请参阅生成驱动程序

步骤 4:配置用于测试和调试的计算机

要测试和调试驱动程序,需要在主计算机上运行调试器,在目标计算机上运行驱动程序。 到目前为止,你已经在主计算机上使用 Visual Studio 生成了驱动程序。 接下来需要配置目标计算机。 要配置目标计算机,请按照为驱动程序部署和测试配置计算机中的说明执行操作。

步骤 5:启用内核调试跟踪

模板代码包含多条跟踪消息 (TraceEvents),可帮助对函数调用进行跟踪。 源代码中的所有函数都包含跟踪消息,用于标记例程的进入和退出。 对于错误,跟踪消息包含错误代码和一个有意义的字符串。 由于驱动程序项目启用了 WPP 跟踪,因此在生成过程中创建的 PDB 符号文件包含了跟踪消息格式说明。 如果为 WPP 跟踪配置了主计算机和目标计算机,驱动程序就可以向文件或调试器发送跟踪消息。

配置主计算机以便进行 WPP 跟踪

  1. 从 PDB 符号文件中提取跟踪消息格式说明,以便创建跟踪消息格式 (TMF) 文件。

    可以使用 Tracepdb.exe 来创建 TMF 文件。 该工具位于 WDK 的 <安装文件夹>Windows Kits\10\bin\<architecture> 文件夹中。 以下命令将为驱动程序项目创建 TMF 文件。

    tracepdb -f <PDBFiles> -p <TMFDirectory>
    

    -f 选项指定 PDB 符号文件的位置和名称。 -p 选项指定由 Tracepdb 创建的 TMF 文件的位置。 有关详细信息,请参阅 Tracepdb 命令

    在指定位置有三个文件,项目中的每个 C 代码文件一个。 它们会被赋予 GUID 文件名。

  2. 在调试器中键入以下命令:

    .load Wmitrace
    .chain
    !wmitrace.searchpath + <TMF file location>
    

这些命令会:

  • 加载 Wmitrace.dll 扩展。
  • 验证调试器扩展是否已加载。
  • 将 TMF 文件的位置添加到调试器扩展的搜索路径中。

输出如下所示:

Trace Format search path is: 'C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE;c:\drivers\tmf

配置目标计算机以便进行 WPP 跟踪

  1. 确保在目标计算机上安装了 Tracelog 工具。 该工具位于 WDK 的 <安装文件夹>Windows Kits\10\Tools\<arch> 文件夹中。 有关详细信息,请参阅 Tracelog 命令语法

  2. 打开命令窗口并以管理员身份运行。

  3. 输入以下命令:

    tracelog -start MyTrace -guid \#c918ee71-68c7-4140-8f7d-c907abbcb05d -flag 0xFFFF -level 7-rt -kd
    

该命令将启动名为 MyTrace 的跟踪会话。

guid 参数指定跟踪提供程序(即客户端驱动程序)的 GUID。 GUID 可以从 Visual Studio 2022 项目中的 Trace.h 获取。 另一种方法是键入以下命令,在 .guid 文件中指定 GUID。 文件包含连字符格式的 GUID:

tracelog -start MyTrace -guid c:\\drivers\\Provider.guid -flag 0xFFFF -level 7-rt -kd

键入以下命令即可停止跟踪会话:

tracelog -stop MyTrace

步骤 6:在目标计算机上部署驱动程序

  1. 在“解决方案资源管理器”窗口中,右键单击项目名称 (MyUSBDriver_UMDF_),然后选择“属性”。
  2. 在左窗格中,导航到配置属性>驱动程序安装>部署
  3. 在“目标设备名称”中,请指定目标计算机的名称。
  4. 选择“安装/重新安装和验证”。
  5. 选择“确定”。
  6. 在“调试”菜单上,选择“开始调试”或按键盘上的 F5

注意

不要在“硬件 ID 驱动程序更新”下指定设备的硬件 ID。 硬件 ID 只能在驱动程序信息 (INF) 文件中指定。

步骤 7:在“设备管理器”中查看驱动程序

  1. 输入以下命令,打开“设备管理器”。

    devmgmt
    
  2. 验证“设备管理器”是否显示了以下节点。

    USB 设备

    MyUSBDriver_UMDF_Device

步骤 8:在调试器中查看输出

验证跟踪信息是否出现在主计算机上的调试器即时窗口中。

输出应如下所示:

[0]0744.05F0::00/00/0000-00:00:00.000 [MyUSBDriver_UMDF_]CMyDevice::OnPrepareHardware Entry
[0]0744.05F0::00/00/0000-00:00:00.000 [MyUSBDriver_UMDF_]CMyDevice::OnPrepareHardware Exit
[1]0744.05F0::00/00/0000-00:00:00.000 [MyUSBDriver_UMDF_]CMyDevice::CreateInstanceAndInitialize Entry
[1]0744.05F0::00/00/0000-00:00:00.000 [MyUSBDriver_UMDF_]CMyDevice::Initialize Entry
[1]0744.05F0::00/00/0000-00:00:00.000 [MyUSBDriver_UMDF_]CMyDevice::Initialize Exit
[1]0744.05F0::00/00/0000-00:00:00.000 [MyUSBDriver_UMDF_]CMyDevice::CreateInstanceAndInitialize Exit
[1]0744.05F0::00/00/0000-00:00:00.000 [MyUSBDriver_UMDF_]CMyDevice::Configure Entry
[1]0744.05F0::00/00/0000-00:00:00.000 [MyUSBDriver_UMDF_]CMyIoQueue::CreateInstanceAndInitialize Entry
[1]0744.05F0::00/00/0000-00:00:00.000 [MyUSBDriver_UMDF_]CMyIoQueue::Initialize Entry
[1]0744.05F0::00/00/0000-00:00:00.000 [MyUSBDriver_UMDF_]CMyIoQueue::Initialize Exit
[1]0744.05F0::00/00/0000-00:00:00.000 [MyUSBDriver_UMDF_]CMyIoQueue::CreateInstanceAndInitialize Exit
[1]0744.05F0::00/00/0000-00:00:00.000 [MyUSBDriver_UMDF_]CMyDevice::Configure Exit

注解

让我们来看看框架和客户端驱动程序如何协同工作,与 Windows 交互并处理发送到 USB 设备的请求。 此图显示了系统中加载的基于 UMDF 的 USB 客户端驱动程序的模块。

用户模式客户端驱动程序体系结构示意图。

下面介绍了每个模块的用途:

  • 应用程序 - 发出 I/O 请求以便与 USB 设备通信的用户模式进程。
  • I/O 管理器 - 一个 Windows 组件,用于创建 I/O 请求数据包 (IRP) 以表示接收到的应用程序请求,并将其转发到目标设备的内核模式设备堆栈顶部。
  • 反射器 - Microsoft 提供的一个内核模式驱动程序,安装在内核模式设备栈的顶部 (WUDFRd.sys)。 反射器会将从 I/O 管理器接收到的 IRP 重定向到客户端驱动程序主机进程。 在收到请求后,框架和客户端驱动程序会处理请求。
  • 主机进程 - 运行用户模式驱动程序 (Wudfhost.exe) 的进程。 它还会托管框架和 I/O 调度程序。
  • 客户端驱动程序 - USB 设备的用户模式功能驱动程序。
  • UMDF - 框架模块,代表客户端驱动程序处理与 Windows 的大部分交互。 它公开了用户模式设备驱动程序接口 (DDI),而客户端驱动程序可利用这些接口执行常见的驱动程序任务。
  • 调度程序 - 在主机进程中运行的调度机制;决定在用户模式驱动程序处理请求并到达用户模式堆栈底部后,如何将请求转发到内核模式。 在图例中,调度程序将请求转发给用户模式 DLL Winusb.dll。
  • Winusb.dll - Microsoft 提供的一个用户模式 DLL,用于公开 WinUSB 函数,可简化客户端驱动程序与 WinUSB(Winusb.sys,在内核模式下加载)之间的通信过程。
  • Winusb.sys - Microsoft提供的一个驱动程序,USB 设备的所有 UMDF 客户端驱动程序都需要使用该驱动程序。 该驱动程序必须安装在反射器下方,并充当内核模式下 USB 驱动程序堆栈的网关。 有关详细信息,请参阅 面向开发人员的 WinUSB 简介。
  • USB 驱动程序堆栈 - Microsoft 提供的一套驱动程序,用于处理与 USB 设备的协议级通信。 有关详细信息,请参阅 Windows 中的 USB 主机端驱动程序

每当应用程序向 USB 驱动程序堆栈发出请求时,Windows I/O 管理器就会将请求发送给反射器,而反射器又会将请求导向用户模式下的客户端驱动程序。 客户端驱动程序通过调用特定的 UMDF 方法来处理请求,这些方法会在内部调用 WinUSB 函数,以便将请求发送到 WinUSB。 收到请求后,WinUSB 要么处理该请求,要么将其转发给 USB 驱动程序堆栈。