使用虚拟 HID 框架 (VHF) 编写 HID 源驱动程序

该主题解释了如何:

  • (KMDF 编写 Kernel-Mode 驱动程序框架,) HID 源驱动程序向 Windows 提交 HID 读取报告。
  • 将 VHF 驱动程序作为较低筛选器加载到虚拟 HID 设备堆栈中的 HID 源驱动程序。

了解如何编写向操作系统报告 HID 数据的 HID 源驱动程序。

HID 输入设备(如键盘、鼠标、笔、触摸或按钮)向操作系统发送各种报告,以便它可以了解设备的用途并采取必要的操作。 报表采用 HID 集合HID 用法的形式。 设备通过各种传输方式发送这些报告,其中一些报告受 Windows 支持,例如 I2C 上的 HIDUSB 上的 HID。 在某些情况下,Windows 可能不支持传输,或者报表可能无法直接映射到实际硬件。 它可以是 HID 格式的数据流,由另一个软件组件为虚拟硬件发送,例如,对于非 GPIO 按钮或传感器。 例如,假设手机的加速计数据作为游戏控制器以无线方式发送到电脑。 在另一个示例中,计算机可以使用 UIBC 协议从 Miracast 设备接收远程输入。

在以前版本的 Windows 中,若要支持新的传输 (实际硬件或软件) ,必须编写 HID 传输微型驱动程序 并将其绑定到 Microsoft 提供的内置类驱动程序,Hidclass.sys。 类/微型驱动程序对向高级驱动程序和用户模式应用程序提供 HID 集合,例如 顶级集合 。 在该模型中,挑战在于编写微型驱动程序,这可以是一项复杂的任务。

从 Windows 10 开始,新的虚拟 HID 框架 (VHF) 无需编写传输微型驱动程序。 相反,可以使用 KMDF 或 WDM 编程接口编写 HID 源驱动程序。 该框架由 Microsoft 提供的静态库组成,该库公开驱动程序使用的编程元素。 它还包括 Microsoft 提供的内置驱动程序,该驱动程序枚举一个或多个子设备,并继续生成和管理虚拟 HID 树。

注意

在此版本中,VHF 仅在内核模式下支持 HID 源驱动程序。

本主题介绍框架的体系结构、虚拟 HID 设备树和配置方案。

虚拟 HID 设备树

在此图中,设备树显示驱动程序及其关联的设备对象。

虚拟 HID 设备树的示意图。

HID 源驱动程序 (驱动程序)

HID 源驱动程序链接到 Vhfkm.lib,并在其生成项目中包括 Vhf.h。 可以使用 Windows 驱动程序模型 (WDM) 或 Kernel-Mode 驱动程序框架 (KMDF) (属于 Windows 驱动程序框架 (WDF) )编写驱动程序 。 驱动程序可以作为筛选器驱动程序或函数驱动程序加载到设备堆栈中。

VHF 静态库 (vhfkm.lib)

静态库包含在适用于Windows 10的 Windows 驱动程序工具包 (WDK) 中。 库公开 HID 源驱动程序使用的编程接口,例如例程和回调函数。 当驱动程序调用函数时,静态库会将请求转发到处理该请求的 VHF 驱动程序。

VHF 驱动程序 (Vhf.sys)

Microsoft 提供的内置驱动程序。 此驱动程序必须作为较低筛选器驱动程序加载到 HID 源设备堆栈中的驱动程序下方。 VHF 驱动程序动态枚举子设备, (PDO) 为 HID 源驱动程序指定的一个或多个 HID 设备创建物理设备对象。 它还实现枚举子设备的 HID 传输微型驱动程序功能。

HID 类驱动程序对 (Hidclass.sys,Mshidkmdf.sys)

Hidclass/Mshidkmdf 对枚举 顶级集合 (TLC) 类似于它枚举真实 HID 设备的这些集合的方式。 HID 客户端可以继续请求和使用 TLC,就像真正的 HID 设备一样。 此驱动程序对作为函数驱动程序安装在设备堆栈中。

注意

在某些情况下,HID 客户端可能需要标识 HID 数据的源。 例如,系统具有内置传感器,并从相同类型的远程传感器接收数据。 系统可能希望选择一个传感器来更可靠。 为了区分连接到系统的两个传感器,HID 客户端会查询 TLC 的容器 ID。 在这种情况下,HID 源驱动程序可以提供容器 ID,VHF 将其报告为虚拟 HID 设备的容器 ID。

HID 客户端 (应用程序)

查询和使用 HID 设备堆栈报告的 TLC。

标头和库要求

此过程介绍如何编写向操作系统报告头戴显示设备按钮的简单 HID 源驱动程序。 在这种情况下,实现此代码的驱动程序可以是现有的 KMDF 音频驱动程序,该驱动程序已修改为使用 VHF 充当 HID 源报告头戴显示设备按钮。

  1. 包括 WDK for Windows 10 中包含的 Vhf.h。

  2. 链接到 WDK 中包含的 vhfkm.lib。

  3. 创建设备要向操作系统报告的 HID 报告描述符。 在此示例中,HID 报告描述符描述头戴显示设备按钮。 报告指定 HID 输入报告,大小为 8 位 (1 字节) 。 前三位用于头戴显示设备中间、调高音量和调低音量按钮。 其余位未使用。

    UCHAR HeadSetReportDescriptor[] = {
        0x05, 0x01,         // USAGE_PAGE (Generic Desktop Controls)
        0x09, 0x0D,         // USAGE (Portable Device Buttons)
        0xA1, 0x01,         // COLLECTION (Application)
        0x85, 0x01,         //   REPORT_ID (1)
        0x05, 0x09,         //   USAGE_PAGE (Button Page)
        0x09, 0x01,         //   USAGE (Button 1 - HeadSet : middle button)
        0x09, 0x02,         //   USAGE (Button 2 - HeadSet : volume up button)
        0x09, 0x03,         //   USAGE (Button 3 - HeadSet : volume down button)
        0x15, 0x00,         //   LOGICAL_MINIMUM (0)
        0x25, 0x01,         //   LOGICAL_MAXIMUM (1)
        0x75, 0x01,         //   REPORT_SIZE (1)
        0x95, 0x03,         //   REPORT_COUNT (3)
        0x81, 0x02,         //   INPUT (Data,Var,Abs)
        0x95, 0x05,         //   REPORT_COUNT (5)
        0x81, 0x03,         //   INPUT (Cnst,Var,Abs)
        0xC0,               // END_COLLECTION
    };
    

创建虚拟 HID 设备

通过调用 VHF_CONFIG_INIT 宏,然后调用 VhfCreate 方法初始化 VHF_CONFIG 结构。 在 WdfDeviceCreate 调用之后,驱动程序必须在 PASSIVE_LEVEL 调用 VhfCreate,通常在驱动程序的 EvtDriverDeviceAdd 回调函数中调用。

VhfCreate 调用中,驱动程序可以指定某些配置选项,例如必须异步处理的操作或设置设备信息 (供应商/产品 ID) 。

例如,应用程序请求 TLC。 当 HID 类驱动程序对收到该请求时,该对将确定请求的类型,并创建适当的 HID 微型驱动程序 IOCTL 请求并将其转发到 VHF。 收到 IOCTL 请求后,VHF 可以处理请求、依赖 HID 源驱动程序来处理请求,或者使用STATUS_NOT_SUPPORTED完成请求。

VHF 处理以下 IOCTL:

如果请求为 GetFeatureSetFeatureWriteReportGetInputReport,并且 HID 源驱动程序注册了相应的回调函数,则 VHF 将调用回调函数。 在该函数中,HID 源驱动程序可以获取或设置 HID 虚拟设备的 HID 数据。 如果驱动程序未注册回调,VHF 会以状态STATUS_NOT_SUPPORTED完成请求。

VHF 为以下 IOCTL 调用 HID 源驱动程序实现的事件回调函数:

对于任何其他 HID 微型驱动程序 IOCTL,VHF 使用 STATUS_NOT_SUPPORTED 完成请求。

通过调用 VhfDelete 删除虚拟 HID 设备。 如果驱动程序为虚拟 HID 设备分配了资源,则需要 EvtVhfCleanup 回调。 驱动程序必须实现 EvtVhfCleanup 函数,并在 VHF_CONFIGEvtVhfCleanup 成员中指定指向该函数的指针。 在 VhfDelete 调用完成之前调用 EvtVhfCleanup 。 有关详细信息,请参阅 删除虚拟 HID 设备

注意

异步操作完成后,驱动程序必须调用 VhfAsyncOperationComplete 来设置操作的结果。 可以从事件回调调用 方法,也可以在从回调返回后稍后调用方法。

NTSTATUS
VhfSourceCreateDevice(
_Inout_ PWDFDEVICE_INIT DeviceInit
)

{
    WDF_OBJECT_ATTRIBUTES   deviceAttributes;
    PDEVICE_CONTEXT deviceContext;
    VHF_CONFIG vhfConfig;
    WDFDEVICE device;
    NTSTATUS status;

    PAGED_CODE();

    WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttributes, DEVICE_CONTEXT);
    deviceAttributes.EvtCleanupCallback = VhfSourceDeviceCleanup;

    status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);

    if (NT_SUCCESS(status))
    {
        deviceContext = DeviceGetContext(device);

        VHF_CONFIG_INIT(&vhfConfig,
            WdfDeviceWdmGetDeviceObject(device),
            sizeof(VhfHeadSetReportDescriptor),
            VhfHeadSetReportDescriptor);

        status = VhfCreate(&vhfConfig, &deviceContext->VhfHandle);

        if (!NT_SUCCESS(status)) {
            TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE, "VhfCreate failed %!STATUS!", status);
            goto Error;
        }

        status = VhfStart(deviceContext->VhfHandle);
        if (!NT_SUCCESS(status)) {
            TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE, "VhfStart failed %!STATUS!", status);
            goto Error;
        }

    }

Error:
    return status;
}

提交 HID 输入报告

通过调用 VhfReadReportSubmit 提交 HID 输入报告。

通常,HID 设备通过中断发送输入报告来发送有关状态更改的信息。 例如,当按钮的状态发生更改时,头戴显示设备可能会发送报告。 在这种情况下,将调用驱动程序的中断服务例程 (ISR) 。 在该例程中,驱动程序可能会计划延迟过程调用 (DPC) 处理输入报告并将其提交到 VHF,后者将信息发送到操作系统。 默认情况下,VHF 缓冲报表,HID 源驱动程序可以在输入报告时开始提交 HID 输入报告。 这样就无需 HID 源驱动程序实现复杂的同步。

HID 源驱动程序可以通过为挂起的报表实现缓冲策略来提交输入报告。 为了避免重复缓冲,HID 源驱动程序可以实现 EvtVhfReadyForNextReadReport 回调函数,并跟踪 VHF 是否调用了此回调。 如果以前调用过,HID 源驱动程序可以调用 VhfReadReportSubmit 来提交报告。 它必须等待 EvtVhfReadyForNextReadReport 被调用,然后才能再次调用 VhfReadReportSubmit

VOID
MY_SubmitReadReport(
    PMY_CONTEXT  Context,
    BUTTON_TYPE  ButtonType,
    BUTTON_STATE ButtonState
    )
{
    PDEVICE_CONTEXT deviceContext = (PDEVICE_CONTEXT)(Context);

    if (ButtonState == ButtonStateUp) {
        deviceContext->VhfHidReport.ReportBuffer[0] &= ~(0x01 << ButtonType);
    } else {
        deviceContext->VhfHidReport.ReportBuffer[0] |=  (0x01 << ButtonType);
    }

    status = VhfReadReportSubmit(deviceContext->VhfHandle, &deviceContext->VhfHidReport);

    if (!NT_SUCCESS(status)) {
        TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE,"VhfReadReportSubmit failed %!STATUS!", status);
    }
}

删除虚拟 HID 设备

通过调用 VhfDelete 删除虚拟 HID 设备。

可以通过指定 Wait 参数同步或异步调用 VhfDelete。 对于同步调用,必须在PASSIVE_LEVEL调用 方法,例如从设备对象的 EvtCleanupCallback 调用。 VhfDelete 在删除虚拟 HID 设备后返回。 如果驱动程序以异步方式调用 VhfDelete ,它将立即返回 ,并且 VHF 在删除操作完成后调用 EvtVhfCleanup 。 可以在最大DISPATCH_LEVEL调用 方法。 在这种情况下,驱动程序必须在之前调用 VhfCreate 时注册并实现了 EvtVhfCleanup 回调函数。 下面是 HID 源驱动程序想要删除虚拟 HID 设备时的事件序列:

  1. HID 源驱动程序停止对 VHF 的调用。
  2. HID 源调用 VhfDelete将 Wait 设置为 FALSE。
  3. VHF 停止调用 HID 源驱动程序实现的回调函数。
  4. VHF 开始向 PnP 管理器报告设备缺失。 此时,VhfDelete 调用可能会返回。
  5. 当设备报告为缺失设备时,如果 HID 源驱动程序注册了其实现,VHF 将调用 EvtVhfCleanup
  6. EvtVhfCleanup 返回后,VHF 将执行其清理。
VOID
VhfSourceDeviceCleanup(
_In_ WDFOBJECT DeviceObject
)
{
    PDEVICE_CONTEXT deviceContext;

    PAGED_CODE();

    TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DEVICE, "%!FUNC! Entry");

    deviceContext = DeviceGetContext(DeviceObject);

    if (deviceContext->VhfHandle != WDF_NO_HANDLE)
    {
        VhfDelete(deviceContext->VhfHandle, TRUE);
    }

}

安装 HID 源驱动程序

在安装 HID 源驱动程序的 INF 文件中,请确保使用 AddReg 指令将 Vhf.sys 声明为 HID 源驱动程序的较低筛选器驱动程序。

[HIDVHF_Inst.NT.HW]
AddReg = HIDVHF_Inst.NT.AddReg

[HIDVHF_Inst.NT.AddReg]
HKR,,"LowerFilters",0x00010000,"vhf"