如何在复合驱动程序中实现函数挂起

本文概述了通用串行总线 (USB) 3.0 多功能设备 (复合设备) 的功能暂停和功能远程唤醒功能。 本文介绍如何在控制复合设备的驱动程序中实现这些功能。 本文适用于替换 Usbccgp.sys 的复合驱动程序。

通用串行总线 (USB) 3.0 规范定义了一项称为 函数挂起的新功能。 该功能使复合设备的单个功能能够独立于其他功能进入低功耗状态。 考虑一个复合设备,它为键盘定义一个函数,为鼠标定义另一个函数。 用户使键盘功能保持工作状态,但在一段时间内不移动鼠标。 鼠标的客户端驱动程序可以检测函数的空闲状态,并在键盘功能保持工作状态时发送函数挂起状态。

无论设备中任何功能的电源状态如何,整个设备都可以转换为挂起状态。 如果某个特定函数和整个设备进入挂起状态,则当设备处于挂起状态时,以及在整个设备的暂停进入和退出过程中,函数的挂起状态将保留。

与 USB 2.0 设备的远程唤醒功能类似 (请参阅 USB 设备的远程唤醒) ,USB 3.0 复合设备中的单个功能可以从低功耗状态唤醒,而不会影响其他功能的电源状态。 此功能称为 函数远程唤醒。 主机通过发送协议请求在设备固件中设置远程唤醒位来显式启用该功能。 此过程称为 武装 函数进行远程唤醒。 有关远程唤醒相关位的信息,请参阅官方 USB 规范中的图 9-6。

如果某个函数已准备好进行远程唤醒,则当处于挂起状态时函数 (,) 在物理设备上发生用户事件时保留足够的电源来生成唤醒 恢复信号 。 由于该恢复信号,客户端驱动程序随后可以退出关联函数的挂起状态。 在复合设备中的鼠标功能示例中,当用户摆动处于空闲状态的鼠标时,鼠标函数会向主机发送恢复信号。 在主机上,USB 驱动程序堆栈检测哪个函数唤醒,并将通知传播到相应函数的客户端驱动程序。 然后,客户端驱动程序可以唤醒函数并进入工作状态。

对于客户端驱动程序,发送函数以挂起状态并唤醒函数的步骤类似于将整个设备发送到挂起状态的单函数设备驱动程序。 以下过程总结了这些步骤。

  1. 检测关联函数何时处于空闲状态。
  2. (IRP) 发送空闲 I/O 请求数据包。
  3. 通过 (IRP) 发送等待唤醒 I/O 请求数据包,提交请求以武装其函数进行远程唤醒。
  4. 通过将 Dx 电源 IRP (D2D3) 将函数转换为低功耗状态。

有关上述步骤的详细信息,请参阅 USB 选择性挂起中的“发送 USB 空闲请求 IRP”。 复合驱动程序为复合设备中的每个功能创建一个物理设备对象 (PDO) ,并处理客户端驱动程序 (函数设备堆栈) 的 FDO 发送的电源请求。 为了使客户端驱动程序成功进入和退出其函数的挂起状态,复合驱动程序必须支持函数挂起和远程唤醒功能,并处理收到的电源请求。

在 Windows 8 中,USB 3.0 设备的 USB 驱动程序堆栈支持这些功能。 此外,函数挂起和函数远程唤醒实现已添加到 Microsoft 提供的 USB 泛型父驱动程序 (Usbccgp.sys) (Windows 默认复合驱动程序)。 如果要编写自定义复合驱动程序,则驱动程序必须按照以下过程处理与函数挂起和远程唤醒请求相关的请求。

步骤 1:确定 USB 驱动程序堆栈是否支持函数挂起

在复合驱动程序的 start-device 例程 (IRP_MN_START_DEVICE) 中,执行以下步骤:

  1. 调用 USBD_QueryUsbCapability 例程以确定基础 USB 驱动程序堆栈是否支持函数挂起功能。 调用需要你在上一次调用 USBD_CreateHandle 例程中获得的有效 USBD 句柄。

成功调用 USBD_QueryUsbCapability 可确定基础 USB 驱动程序堆栈是否支持函数挂起。 调用可能会返回错误代码,指示 USB 驱动程序堆栈不支持函数挂起或连接的设备不是 USB 3.0 多功能设备。

  1. 如果 USBD_QueryUsbCapability 调用指示支持函数挂起,请将复合设备注册到基础 USB 驱动程序堆栈。 若要注册复合设备,必须发送 IOCTL_INTERNAL_USB_REGISTER_COMPOSITE_DEVICE I/O 控制请求。 有关此请求的详细信息,请参阅 如何注册复合设备

注册请求使用 REGISTER_COMPOSITE_DEVICE 结构来指定有关复合驱动程序的信息。 请确保将 CapabilityFunctionSuspend 设置为 1 以指示复合驱动程序支持函数挂起。

有关演示如何确定 USB 驱动程序堆栈是否支持函数挂起的代码示例,请参阅 USBD_QueryUsbCapability

步骤 2:处理空闲 IRP

客户端驱动程序可以发送空闲的 IRP, (查看 IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION) 。 客户端驱动程序检测到函数的空闲状态后发送请求。 IRP 包含指向回调完成例程的指针, (客户端驱动程序实现的称为 空闲回调) 。 在空闲回调中,客户端在将函数发送到挂起状态之前执行任务,例如取消挂起的 I/O 传输。

注意

对于 USB 3.0 设备的客户端驱动程序,空闲 IRP 机制是可选的。 但是,大多数客户端驱动程序都编写为支持 USB 2.0 和 USB 3.0 设备。 若要支持 USB 2.0 设备,驱动程序必须发送空闲 IRP,因为复合驱动程序依赖于该 IRP 来跟踪每个函数的电源状态。 如果所有函数都处于空闲状态,复合驱动程序会将整个设备发送到挂起状态。

从客户端驱动程序接收空闲 IRP 后,复合驱动程序必须立即调用空闲回调,以通知客户端驱动程序,客户端驱动程序可能会将函数发送到挂起状态。

步骤 3:发送远程唤醒通知的请求

客户端驱动程序可以通过提交将次要函数代码设置为 IRP_MN_WAIT_WAKE ( 等待唤醒 IRP) 的IRP_MJ_POWER IRP 来提交请求,以为其函数提供远程唤醒。 仅当驱动程序因用户事件而进入工作状态时,客户端驱动程序才会提交此请求。

收到等待唤醒 IRP 后,复合驱动程序必须将 IOCTL_INTERNAL_USB_REQUEST_REMOTE_WAKE_NOTIFICATION I/O 控制请求发送到 USB 驱动程序堆栈。 请求使 USB 驱动程序堆栈能够在堆栈收到有关恢复信号的通知时通知复合驱动程序。 IOCTL_INTERNAL_USB_REQUEST_REMOTE_WAKE_NOTIFICATION使用 REQUEST_REMOTE_WAKE_NOTIFICATION 结构来指定请求参数。 复合驱动程序必须指定的值之一是用于远程唤醒的函数的函数句柄。 复合驱动程序在上一个请求中获取该句柄,以将复合设备注册到 USB 驱动程序堆栈。 有关复合驱动程序注册请求的详细信息,请参阅 如何注册复合设备

在请求的 IRP 中,复合驱动程序提供指向 (远程唤醒) 完成例程的指针,该例程由复合驱动程序实现。

以下示例代码演示如何发送远程唤醒请求。

/*++

Description:
    This routine sends a IOCTL_INTERNAL_USB_REQUEST_REMOTE_WAKE_NOTIFICATION request
    to the USB driver stack. The IOCTL is completed by the USB driver stack
    when the function wakes up from sleep.

    Parameters:
    parentFdoExt: The device context associated with the FDO for the
    composite driver.

    functionPdoExt: The device context associated with the PDO (created by
    the composite driver) for the client driver.
--*/

VOID
SendRequestForRemoteWakeNotification(
    __inout PPARENT_FDO_EXT parentFdoExt,
    __inout PFUNCTION_PDO_EXT functionPdoExt
)

{
    PIRP                                irp;
    REQUEST_REMOTE_WAKE_NOTIFICATION    remoteWake;
    PIO_STACK_LOCATION                  nextStack;
    NTSTATUS                            status;

    // Allocate an IRP
    irp =  IoAllocateIrp(parentFdoExt->topDevObj->StackSize, FALSE);

    if (irp)
    {

        //Initialize the USBDEVICE_REMOTE_WAKE_NOTIFICATION structure
        remoteWake.Version = 0;
        remoteWake.Size = sizeof(REQUEST_REMOTE_WAKE_NOTIFICATION);
        remoteWake.UsbdFunctionHandle = functionPdoExt->functionHandle;
        remoteWake.Interface = functionPdoExt->baseInterfaceNumber;

        nextStack = IoGetNextIrpStackLocation(irp);

        nextStack->MajorFunction = IRP_MJ_INTERNAL_DEVICE_CONTROL;
        nextStack->Parameters.DeviceIoControl.IoControlCode = IOCTL_INTERNAL_USB_REQUEST_REMOTE_WAKE_NOTIFICATION;

        nextStack->Parameters.Others.Argument1 = &remoteWake;

        // Caller's completion routine will free the IRP when it completes.

        SetCompletionRoutine(functionPdoExt->debugLog,
                             parentFdoExt->fdo,
                             irp,
                             CompletionRemoteWakeNotication,
                             (PVOID)functionPdoExt,
                             TRUE, TRUE, TRUE);

        // Pass the IRP
        IoCallDriver(parentFdoExt->topDevObj, irp);

    }

    return;
}

IOCTL_INTERNAL_USB_REQUEST_REMOTE_WAKE_NOTIFICATION请求在唤醒过程中由 USB 驱动程序堆栈完成,当它收到有关恢复信号的通知时。 在此期间,USB 驱动程序堆栈还会调用远程唤醒完成例程。

复合驱动程序必须使等待-唤醒 IRP 保持挂起状态,并将其排队以供以后处理。 当 USB 驱动程序堆栈调用驱动程序的远程唤醒完成例程时,复合驱动程序必须完成该 IRP。

步骤 4:发送请求以武装函数进行远程唤醒

若要将函数发送到低功耗状态,客户端驱动程序会提交 IRP_MN_SET_POWER IRP,请求将 Windows 驱动程序模型 (WDM) 设备电源状态更改为 D2D3。 通常,如果驱动程序提前发送等待唤醒 IRP 以请求远程唤醒,则客户端驱动程序会发送 D2 IRP。 否则,客户端驱动程序会发送 D3 IRP。

收到 D2 IRP 后,复合驱动程序必须首先确定等待唤醒 IRP 是否正在等待客户端驱动程序发送的请求。 如果该 IRP 处于挂起状态,则复合驱动程序必须武装函数进行远程唤醒。 为此,复合驱动程序必须将SET_FEATURE控制请求发送到函数的第一个接口,使设备能够发送恢复信号。 若要发送控制请求,请通过调用 USBD_UrbAllocate 例程来分配 URB 结构,并调用 UsbBuildFeatureRequest 宏以格式化SET_FEATURE请求的 URB。 在调用中,将 URB_FUNCTION_SET_FEATURE_TO_INTERFACE 指定为操作代码,将 USB_FEATURE_FUNCTION_SUSPEND 指定为功能选择器。 在 Index 参数中,设置最有效字节的 位 1 。 该值将复制到传输的设置数据包中的 wIndex 字段。

以下示例演示如何发送SET_FEATURE控制请求。

/*++

Routine Description:

Sends a SET_FEATURE for REMOTE_WAKEUP to the device using a standard control request.

Parameters:
parentFdoExt: The device context associated with the FDO for the
composite driver.

functionPdoExt: The device context associated with the PDO (created by
the composite driver) for the client driver.

Returns:

NTSTATUS code.

--*/
VOID
    NTSTATUS SendSetFeatureControlRequestToSuspend(
    __inout PPARENT_FDO_EXT parentFdoExt,
    __inout PFUNCTION_PDO_EXT functionPdoExt,
    )

{
    PURB                            urb
    PIRP                            irp;
    PIO_STACK_LOCATION              nextStack;
    NTSTATUS                        status;

    status = USBD_UrbAllocate(parentFdoExt->usbdHandle, &urb);

    if (!NT_SUCCESS(status))
    {
        //USBD_UrbAllocate failed.
        goto Exit;
    }

    //Format the URB structure.
    UsbBuildFeatureRequest (
        urb,
        URB_FUNCTION_SET_FEATURE_TO_INTERFACE, // Operation code
        USB_FEATURE_FUNCTION_SUSPEND,          // feature selector
        functionPdoExt->firstInterface,           // first interface of the function
        NULL);

    irp =  IoAllocateIrp(parentFdoExt->topDevObj->StackSize, FALSE);

    if (!irp)
    {
        // IoAllocateIrp failed.
        status = STATUS_INSUFFICIENT_RESOURCES;

        goto Exit;
    }

    nextStack = IoGetNextIrpStackLocation(irp);

    nextStack->MajorFunction = IRP_MJ_INTERNAL_DEVICE_CONTROL;

    nextStack->Parameters.DeviceIoControl.IoControlCode = IOCTL_INTERNAL_USB_SUBMIT_URB;

    //  Attach the URB to the IRP.
    USBD_AssignUrbToIoStackLocation(nextStack, (PURB)urb);

    // Caller's completion routine will free the IRP when it completes.
    SetCompletionRoutine(functionPdoExt->debugLog,
        parentFdoExt->fdo,
        irp,
        CompletionForSuspendControlRequest,
        (PVOID)functionPdoExt,
        TRUE, TRUE, TRUE);


    // Pass the IRP
    IoCallDriver(parentFdoExt->topDevObj, irp);


Exit:
    if (urb)
    {
        USBD_UrbFree( parentFdoExt->usbdHandle, urb);
    }

    return status;

}

然后,复合驱动程序将 D2 IRP 向下发送到 USB 驱动程序堆栈。 如果所有其他函数都处于挂起状态,USB 驱动程序堆栈将通过操作控制器上的某些端口寄存器来挂起端口。

注解

在鼠标功能示例中,由于远程唤醒功能已启用 (请参阅步骤 4) ,因此当用户摆动鼠标时,鼠标函数会在上游主控制器的线路上生成恢复信号。 然后,控制器通过发送包含唤醒函数相关信息的通知数据包来通知 USB 驱动程序堆栈。 有关函数唤醒通知的信息,请参阅 USB 3.0 规范中的图 8-17。

收到通知数据包后,USB 驱动程序堆栈将完成挂起 的IOCTL_INTERNAL_USB_REQUEST_REMOTE_WAKE_NOTIFICATION 请求 (请参阅步骤 3) ,并调用 (请求中指定的并由复合驱动程序实现的远程唤醒) 完成回调例程。 当通知到达复合驱动程序时,它会通过完成客户端驱动程序先前发送的等待唤醒 IRP,通知相应的客户端驱动程序函数已进入工作状态。

在 (远程唤醒) 完成例程中,复合驱动程序应将工作项排队以完成挂起的等待唤醒 IRP。 对于 USB 3.0 设备,复合驱动程序仅唤醒发送恢复信号的函数,并使其他函数处于挂起状态。 对工作项进行排队可确保与 USB 2.0 设备的功能驱动程序的现有实现兼容。 有关对工作项进行排队的信息,请参阅 IoQueueWorkItem

工作线程完成等待-唤醒 IRP 并调用客户端驱动程序的完成例程。 然后,完成例程发送 D0 IRP 以进入工作状态的函数。 在完成等待-唤醒 IRP 之前,复合驱动程序应调用 PoSetSystemWake ,将等待唤醒 IRP 标记为导致系统从挂起状态中唤醒的 IRP。 Power Manager 记录 Windows (ETW) 事件跟踪, (全局系统通道) 中查看,其中包含有关唤醒系统的设备的信息。