本文介绍如何执行以下操作:
- 编写 Kernel-Mode 驱动程序框架(KMDF)HID 源驱动程序,该驱动程序将 HID 读取报告提交到 Windows。
- 将 VHF 驱动程序作为较低筛选器加载到虚拟 HID 设备堆栈中的 HID 源驱动程序。
了解如何编写向作系统报告 HID 数据的 HID 源驱动程序。
HID 输入设备(例如键盘、鼠标、笔、触摸或按钮)向作系统发送各种报告,以便它可以了解设备的用途并采取必要的作。 报告采用 HID 集合 和 HID 用法的形式。 设备通过各种传输(其中一些 Windows 支持)发送这些报告,例如 通过 I2C 的 HID 和 USB 上的 HID。 在某些情况下,Windows 不支持传输,或者报表不会直接映射到实际硬件。 它可以是另一个软件组件为虚拟硬件(如非GPIO按钮或传感器)发送的HID格式数据流。 例如,考虑来自作为游戏控制器的手机的加速计数据,以无线方式发送到电脑。 在另一个示例中,计算机可以使用 UIBC 协议从 Miracast 设备接收远程输入。
在早期版本的 Windows 中,若要支持新传输(真正的硬件或软件),必须编写 HID 传输微型驱动程序 并将其绑定到Microsoft提供的内置类驱动程序,Hidclass.sys。 类驱动程序/微型驱动程序对提供了 HID 集合,如 Top-Level集合,给高级驱动程序和用户模式应用程序。 在该模型中,挑战是编写微型驱动程序,这可以是一项复杂的任务。
从 Windows 10 开始,新的虚拟 HID 框架(VHF)无需编写传输微型驱动程序。 相反,可以使用 KMDF 或 WDM 编程接口编写 HID 源驱动程序。 该框架由Microsoft提供的静态库组成,该库公开驱动程序使用的编程元素。 它还包括一个Microsoft提供的内置驱动程序,用于枚举一个或多个子设备,并继续生成和管理虚拟 HID 树。
注释
在此版本中,VHF 仅支持内核模式下的 HID 源驱动程序。
本文介绍框架的体系结构、虚拟 HID 设备树和配置方案。
虚拟 HID 设备树
在此图像中,设备树显示驱动程序及其关联的设备对象。
HID 源驱动程序(您的驱动程序)
HID 源驱动程序链接到 Vhfkm.lib,并在其生成项目中包括 Vhf.h。 驱动程序可以使用 Windows 驱动程序模型 (WDM)或属于 Windows 驱动程序框架(WDF)的 Kernel-Mode 驱动程序框架(KMDF)编写。 驱动程序可以作为筛选器驱动程序或设备堆栈中的函数驱动程序加载。
VHF 静态库 (vhfkm.lib)
静态库包含在 Windows 10 的 Windows 驱动程序工具包(WDK)中。 该库公开编程接口,例如 HID 源驱动程序使用的例程和回调函数。 当驱动程序调用函数时,静态库会将请求转发到处理请求的 VHF 驱动程序。
VHF 驱动程序(Vhf.sys)
Microsoft提供的内置驱动程序。 此驱动程序必须作为较低筛选器驱动程序加载,在 HID 源设备堆栈中位于您的驱动程序下方。 VHF 驱动程序动态枚举子设备,并为 HID 源驱动程序指定的一个或多个 HID 设备创建物理设备对象(PDO)。 它还实现了已枚举子设备的 HID 传输迷你驱动程序功能。
HID 类驱动程序对(Hidclass.sys,Mshidkmdf.sys)
Hidclass/Mshidkmdf 枚举 Top-Level 集合(TLC),类似于它枚举真实 HID 设备集合的方式。 HID 客户端可以继续请求和使用 TLC,就像真正的 HID 设备一样。 此驱动程序对安装为设备堆栈中的功能驱动程序。
注释
在某些情况下,HID 客户端可能需要标识 HID 数据源。 例如,系统具有内置传感器,并从相同类型的远程传感器接收数据。 系统可能想要选择一个传感器来更可靠。 为了区分连接到系统的两个传感器,HID 客户端会查询 TLC 的容器 ID。 在这种情况下,HID 源驱动程序可以提供一个容器 ID,该 ID 会被 VHF 报告为虚拟 HID 设备的容器 ID。
HID 客户端 (应用程序)
查询和处理 HID 设备堆栈报告的 TLC。
标头和库要求
此过程介绍如何编写向作系统报告头戴显示设备按钮的 HID 源驱动程序。 在这种情况下,实现此代码的驱动程序可以是现有的已修改 KMDF 音频驱动程序,作为使用 VHF 的 HID 源来报告耳机按钮的状态。
包括 WDK 中的 Vhf.h。
WDK 中包含的 vhfkm.lib 的链接。
创建设备将要向操作系统报告的 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:
- IOCTL_HID_GET_STRING
- IOCTL_HID_GET_DEVICE_ATTRIBUTES
- IOCTL_HID_GET_DEVICE_DESCRIPTOR
- IOCTL_HID_GET_REPORT_DESCRIPTOR
如果请求为 GetFeature、 SetFeature、 WriteReport 或GetInputReport,并且 HID 源驱动程序注册了相应的回调函数,VHF 将调用回调函数。 在该函数中,HID 源驱动程序可以获取或设置 HID 虚拟设备的 HID 数据。 如果驱动程序未注册回调,VHF 将以状态 STATUS_NOT_SUPPORTED 完成请求。
VHF 为这些 IOCTL 调用由 HID 源驱动程序实现的事件回调函数:
-
如果驱动程序希望在提交缓冲区以获取 HID 输入报告时处理缓冲策略,则必须实现 EvtVhfReadyForNextReadReport 并在 EvtVhfAsyncOperationGetInputReport 成员中指定指针。 有关详细信息,请参阅 提交 HID 输入报告。
IOCTL_HID_GET_FEATURE 或 IOCTL_HID_SET_FEATURE
如果驱动程序希望异步获取或设置 HID 功能报告,驱动程序必须实现 EvtVhfAsyncOperation 函数,并指定指向 VHF_CONFIGEvtVhfAsyncOperationGetFeature 或 EvtVhfAsyncOperationSetFeature 成员中的 get 或 set 实现函数的指针。
-
如果驱动程序希望异步获取 HID 输入报告,驱动程序必须实现 EvtVhfAsyncOperation 函数,并指定指向 VHF_CONFIGEvtVhfAsyncOperationGetInputReport 成员中的函数的指针。
-
如果驱动程序希望异步写入 HID 输入报告,驱动程序必须实现 EvtVhfAsyncOperation 函数,并在 VHF_CONFIG 的EvtVhfAsyncOperationWriteReport 成员中指定指向该函数的指针。
对于任何其他 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 进行调用。 删除虚拟 HID 设备后,VhfDelete 将返回。 如果驱动程序异步调用 VhfDelete ,它将立即返回,并且 VHF 在删除作完成后调用 EvtVhfCleanup 。 该方法最多可以在DISPATCH_LEVEL级别调用。 在这种情况下,驱动程序在以前调用 VhfCreate 时需要注册并实现 EvtVhfCleanup 回调函数。 下面是 HID 源驱动程序想要删除虚拟 HID 设备时的事件序列:
- HID 源驱动程序停止对 VHF 的调用。
- HID 源调用 VhfDelete ,Wait 设置为 FALSE。
- VHF 停止调用 HID 源驱动程序实现的回调函数。
- VHF 开始向 PnP 管理器报告设备已丢失。 此时,VhfDelete 调用可能会返回。
- 当设备被报告为缺失设备时,若HID源驱动程序已注册其实现,VHF将调用EvtVhfCleanup。
- 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"