USB 选择性暂停

注意

本文面向设备驱动程序开发人员。 如果 USB 设备遇到困难,请参阅 在 Windows 中修复 USB-C 问题

USB 选择性挂起功能允许中心驱动程序暂停单个端口,而不会影响中心上其他端口的操作。 USB 设备的选择性挂起在便携式计算机中特别有用,因为它有助于节省电池电量。 许多设备,如指纹读取器和其他种类的生物识别扫描仪,只需要间歇性电源。 当设备未使用时暂停此类设备可降低整体能耗。 更重要的是,任何没有选择性挂起的设备都可能会阻止 USB 主机控制器禁用其驻留在系统内存中的传输计划。 主机控制器向计划程序传输的直接内存访问(DMA)可以防止系统的处理器进入更深层的睡眠状态,例如 C3。

有两种不同的机制可用于选择性地挂起 USB 设备:空闲请求 IRP(IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION)和设置电源 IRP(IRP_MN_SET_POWER)。 要使用的机制取决于操作系统和设备类型:复合或非复合。

选择选择性挂起机制

客户端驱动程序(对于复合设备上的接口)启用远程唤醒的接口(IRP_MN_WAIT_WAKE),必须使用空闲请求 IRP (IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION) 机制选择性地暂停设备。

有关远程唤醒的信息,请参阅:

Windows 操作系统的版本确定非复合设备的驱动程序启用选择性挂起的方式。

  • Windows XP:在 Windows XP 上,所有客户端驱动程序都必须使用空闲请求 IRP(IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION)来关闭其设备。 客户端驱动程序不得使用 WDM 电源 IRP 选择性地挂起其设备。 这样做可防止其他设备有选择地挂起。
  • Windows Vista 和更高版本的 Windows:驱动程序编写器在 Windows Vista 和更高版本的 Windows 中有更多的选择来关闭设备。 尽管 Windows Vista 支持 Windows 空闲请求 IRP 机制,但驱动程序不需要使用它。

下表显示了需要使用空闲请求 IRP 和可以使用 WDM 电源 IRP 挂起 USB 设备的方案:

Windows 版本 复合设备上的功能,用于唤醒 复合设备上的功能,不武装唤醒 单接口 USB 设备
Windows 7 使用空闲请求 IRP 使用 WDM 电源 IRP 使用 WDM 电源 IRP
Windows Server 2008 使用空闲请求 IRP 使用 WDM 电源 IRP 使用 WDM 电源 IRP
Windows Vista 使用空闲请求 IRP 使用 WDM 电源 IRP 使用 WDM 电源 IRP
Windows Server 2003 使用空闲请求 IRP 使用空闲请求 IRP 使用空闲请求 IRP
Windows XP 使用空闲请求 IRP 使用空闲请求 IRP 使用空闲请求 IRP

本部分介绍 Windows 选择性挂起机制。

发送 USB 空闲请求 IRP

设备空闲时,客户端驱动程序通过发送空闲请求 IRP(IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION)通知总线驱动程序。 总线驱动程序确定设备处于低功率状态是安全的,它会调用客户端设备驱动程序使用空闲请求 IRP 向下传递堆栈的回调例程。

在回调例程中,客户端驱动程序必须取消所有挂起的 I/O 操作,并等待所有 USB I/O IRP 完成。 然后,它可以发出 IRP_MN_SET_POWER 请求,将 WDM 设备电源状态更改为 D2。 回调例程必须等待 D2 请求在返回之前完成。 有关空闲通知回调例程的详细信息,请参阅“USB 空闲通知回调例程”。

在调用空闲通知回调例程后,总线驱动程序未完成空闲请求 IRP。 相反,总线驱动程序会保留空闲请求 IRP 挂起,直到满足以下条件之一:

  • 收到IRP_MN_SUPRISE_REMOVALIRP_MN_REMOVE_DEVICE IRP。 收到其中一个 IRP 时,空闲请求 IRP 会完成STATUS_CANCELLED。
  • 总线驱动程序收到将设备置于工作电源状态(D0)的请求。 收到此请求总线驱动程序后,使用STATUS_SUCCESS完成挂起的空闲请求 IRP。

以下限制适用于使用空闲请求 IRP:

  • 发送空闲请求 IRP 时,驱动程序必须处于设备电源状态 D0
  • 驱动程序必须仅为每个设备堆栈发送一个空闲请求 IRP。

以下 WDM 示例代码演示了设备驱动程序发送 USB 空闲请求 IRP 所需的步骤。 以下代码示例中省略了错误检查。

  1. 分配和初始化 IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION IRP

    irp = IoAllocateIrp (DeviceContext->TopOfStackDeviceObject->StackSize, FALSE);
    nextStack = IoGetNextIrpStackLocation (irp);
    nextStack->MajorFunction = IRP_MJ_INTERNAL_DEVICE_CONTROL;
    nextStack->Parameters.DeviceIoControl.IoControlCode = IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION;
    nextStack->Parameters.DeviceIoControl.InputBufferLength =
    sizeof(struct _USB_IDLE_CALLBACK_INFO);
    
  2. 分配和初始化空闲请求信息结构(USB_IDLE_CALLBACK_INFO)。

    idleCallbackInfo = ExAllocatePool (NonPagedPool,
    sizeof(struct _USB_IDLE_CALLBACK_INFO));
    idleCallbackInfo->IdleCallback = IdleNotificationCallback;
    // Put a pointer to the device extension in member IdleContext
    idleCallbackInfo->IdleContext = (PVOID) DeviceExtension;  
    nextStack->Parameters.DeviceIoControl.Type3InputBuffer =
    idleCallbackInfo;
    
  3. 设置完成例程。

    客户端驱动程序必须将完成例程与空闲请求 IRP 相关联。 有关空闲通知完成例程和示例代码的详细信息,请参阅“USB 空闲请求 IRP 完成例程”。

    IoSetCompletionRoutine (irp,
        IdleNotificationRequestComplete,
        DeviceContext,
        TRUE,
        TRUE,
        TRUE);
    
  4. 将空闲请求存储在设备扩展中。

    deviceExtension->PendingIdleIrp = irp;
    
    
  5. 将空闲请求发送到父驱动程序。

    ntStatus = IoCallDriver (DeviceContext->TopOfStackDeviceObject, irp);
    

取消 USB 空闲请求

在某些情况下,设备驱动程序可能需要取消已提交到总线驱动程序的空闲请求 IRP。 如果设备被删除,在空闲并发送空闲请求后变为活动状态,或者整个系统正在转换为较低的系统电源状态,则可能会出现这种情况。

客户端驱动程序通过调用 IoCancelIrp 取消空闲的 IRP。 下表描述了取消空闲 IRP 的三种方案,并指定驱动程序必须执行的操作:

场景 空闲请求取消机制
客户端驱动程序已取消空闲 IRP,USB 驱动程序堆栈未调用“USB 空闲通知回调例程”。 USB 驱动程序堆栈完成空闲 IRP。 由于设备从未离开 D0,驱动程序不会更改设备状态。
客户端驱动程序已取消空闲 IRP,USB 驱动程序堆栈已调用 USB 空闲通知回调例程,并且尚未返回。 即使客户端驱动程序已在 IRP 上调用取消,也可能会调用 USB 空闲通知回调例程。 在这种情况下,客户端驱动程序的回调例程仍必须同步将设备发送到较低电源状态来关闭设备。

当设备处于较低电源状态时,客户端驱动程序可以发送 D0 请求。

或者,驱动程序可以等待 USB 驱动程序堆栈完成空闲 IRP,然后发送 D0 IRP。

如果回调例程由于内存不足而无法将设备置于低功率状态,无法分配电源 IRP,则应取消空闲的 IRP 并立即退出。 在回调例程返回之前,不会完成空闲 IRP;因此,回调例程不应阻止等待已取消的空闲 IRP 完成。
设备已处于低功率状态。 如果设备已处于低功率状态,客户端驱动程序可以发送 D0 IRP。 USB 驱动程序堆栈使用 STATUS_SUCCESS完成空闲请求 IRP。

或者,驱动程序可以取消空闲的 IRP,等待 USB 驱动程序堆栈完成空闲 IRP,然后发送 D0 IRP。

USB 空闲请求 IRP 完成例程

在许多情况下,总线驱动程序可能会调用驱动程序的空闲请求 IRP 完成例程。 如果发生这种情况,客户端驱动程序必须检测总线驱动程序完成 IRP 的原因。 返回的状态代码可以提供此信息。 如果未STATUS_POWER_STATE_INVALID状态代码,驱动程序应将其设备置于 D0(如果设备尚未在 D0)。 如果设备仍处于空闲状态,驱动程序可以提交另一个空闲请求 IRP。

注意

空闲请求 IRP 完成例程不应阻止等待 D0 电源请求完成。 可以在中心驱动程序的电源 IRP 上下文中调用完成例程,在完成例程中的另一个电源 IRP 上阻止可能会导致死锁。

以下列表指示空闲请求的完成例程如何解释一些常见的状态代码:

状态代码 说明
STATUS_SUCCESS 指示设备不应再挂起。 但是,驱动程序应验证其设备是否已启用,并将它们置于 D0 中(如果它们尚未在 D0)。
STATUS_CANCELLED 在以下任一情况下,总线驱动程序使用STATUS_CANCELLED完成空闲请求 IRP:
  • 设备驱动程序取消了 IRP。
  • 需要系统电源状态更改。
  • 在 Windows XP 上,其中一个连接的 USB 设备的设备驱动程序在执行空闲请求回调例程时未能将其设备 置于 D2 中。 因此,总线驱动程序已完成所有挂起的空闲请求 IRP。
STATUS_POWER_STATE_INVALID 指示设备驱动程序为其设备请求 了 D3 电源状态。 发生这种情况时,总线驱动程序将完成所有挂起的空闲 IRP,并STATUS_POWER_STATE_INVALID。
STATUS_DEVICE_BUSY 指示总线驱动程序已保留设备挂起的空闲请求 IRP。 给定设备一次只能挂起一个空闲 IRP。 提交多个空闲请求 IRP 是电源策略所有者的一个错误,应由驱动程序编写器解决。

下面的代码示例演示了空闲请求完成例程的示例实现。

/*Routine Description:

  Completion routine for idle notification IRP

Arguments:

    DeviceObject - pointer to device object
    Irp - I/O request packet
    DeviceExtension - pointer to device extension

Return Value:

    NT status value

--*/

NTSTATUS
IdleNotificationRequestComplete(
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP Irp,
    IN PDEVICE_EXTENSION DeviceExtension
    )
{
    NTSTATUS                ntStatus;
    POWER_STATE             powerState;
    PUSB_IDLE_CALLBACK_INFO idleCallbackInfo;

    ntStatus = Irp->IoStatus.Status;

    if(!NT_SUCCESS(ntStatus) && ntStatus != STATUS_NOT_SUPPORTED)
    {

        //Idle IRP completes with error.

        switch(ntStatus)
        {

        case STATUS_INVALID_DEVICE_REQUEST:

            //Invalid request.

            break;

        case STATUS_CANCELLED:

            //1. The device driver canceled the IRP.
            //2. A system power state change is required.

            break;

        case STATUS_POWER_STATE_INVALID:

            // Device driver requested a D3 power state for its device
            // Release the allocated resources.

            goto IdleNotificationRequestComplete_Exit;

        case STATUS_DEVICE_BUSY:

            //The bus driver already holds an idle IRP pending for the device.

            break;

        default:
            break;

        }


        // If IRP completes with error, issue a SetD0

        //Increment the I/O count because
        //a new IRP is dispatched for the driver.
        //This call is not shown.

        powerState.DeviceState = PowerDeviceD0;

        // Issue a new IRP
        PoRequestPowerIrp (
            DeviceExtension->PhysicalDeviceObject,
            IRP_MN_SET_POWER,
            powerState,
            (PREQUEST_POWER_COMPLETE) PoIrpCompletionFunc,
            DeviceExtension,
            NULL);
    }

IdleNotificationRequestComplete_Exit:

    idleCallbackInfo = DeviceExtension->IdleCallbackInfo;

    DeviceExtension->IdleCallbackInfo = NULL;

    DeviceExtension->PendingIdleIrp = NULL;

    InterlockedExchange(&DeviceExtension->IdleReqPend, 0);

    if(idleCallbackInfo)
    {
        ExFreePool(idleCallbackInfo);
    }

    DeviceExtension->IdleState = IdleComplete;

    // Because the IRP was created using IoAllocateIrp,
    // the IRP needs to be released by calling IoFreeIrp.
    // Also return STATUS_MORE_PROCESSING_REQUIRED so that
    // the kernel does not reference this.

    IoFreeIrp(Irp);

    KeSetEvent(&DeviceExtension->IdleIrpCompleteEvent, IO_NO_INCREMENT, FALSE);

    return STATUS_MORE_PROCESSING_REQUIRED;
}

USB 空闲通知回调例程

总线驱动程序(中心驱动程序的实例或通用父驱动程序)确定何时可以安全地暂停其设备的子级。 如果是,则调用每个子客户端驱动程序提供的空闲通知回调例程。

USB_IDLE_CALLBACK的函数原型如下所示:

typedef VOID (*USB_IDLE_CALLBACK)(__in PVOID Context);

设备驱动程序必须在其空闲通知回调例程中执行以下操作:

  • 如果需要为设备提供远程唤醒,请为设备请求 IRP_MN_WAIT_WAKE IRP。
  • 取消所有 I/O 并准备设备以进入较低电源状态。
  • 通过将 PowerState 参数设置为枚举器值 PowerDeviceD2(在 wdm.h; ntddk.h 中定义)调用 PoRequestPowerIrp,使设备处于 WDM 睡眠状态。 在 Windows XP 中,驱动程序不得将其设备置于 PowerDeviceD3 中,即使设备没有进行远程唤醒。

在 Windows XP 中,驱动程序必须依赖空闲通知回调例程来有选择地挂起设备。 如果 Windows XP 中运行的驱动程序直接将设备置于较低电源状态,而无需使用空闲通知回调例程,这可能会阻止 USB 设备树中的其他设备挂起。

中心驱动程序和 USB 通用父驱动程序(Usbccgp.sys) 都调用 IRQL = PASSIVE_LEVEL的空闲通知回调例程。 这允许回调例程在等待电源状态更改请求完成时阻止。

仅在系统处于 S0 且设备位于 D0 中时调用回调例程。

以下限制适用于空闲请求通知回调例程:

  • 设备驱动程序可以在空闲通知回调例程中启动从 D0D2 的设备电源状态转换,但不允许其他电源状态转换。 具体而言,驱动程序在执行回调例程时不得尝试将其设备更改为 D0
  • 设备驱动程序不得从空闲通知回调例程内请求多个电源 IRP。

在空闲通知回调例程中为唤醒武装设备

空闲通知回调例程应确定其设备是否具有 挂起IRP_MN_WAIT_WAKE 请求。 如果没有IRP_MN_WAIT_WAKE请求挂起,回调例程应在暂停设备之前提交IRP_MN_WAIT_WAKE请求。 有关等待唤醒机制的详细信息,请参阅 支持具有唤醒功能的设备。

USB 全局挂起

USB 2.0 规范将全局挂起定义为 USB 主机控制器后面的整个总线挂起,方法是停止总线上的所有 USB 流量,包括帧启动数据包。 尚未挂起的下游设备在其上游端口上检测到空闲状态,并自行进入暂停状态。 Windows 不会以这种方式实现全局挂起。 在停止总线上的所有 USB 流量之前,Windows 始终有选择地挂起 USB 主机控制器后面的每个 USB 设备。

Windows 7 中的全局挂起条件

与 Windows Vista 相比,Windows 7 更积极地暂停 USB 中心。 Windows 7 USB 中心驱动程序将有选择地挂起其所有附加设备都处于 D1D2D3 设备电源状态的任何中心。 所有 USB 中心都选择性挂起后,整个总线将进入全局挂起。 每当设备处于 D1D2D3WDM 设备状态时,Windows 7 USB 驱动程序堆栈都将设备视为空闲。

Windows Vista 中的全局挂起条件

在 Windows Vista 中执行全局暂停的要求比在 Windows XP 中更灵活。

具体而言,每当设备处于 D1D2D3 的 WDM 设备状态时,USB 堆栈都将设备视为 Windows Vista 中的空闲状态。

下图演示了 Windows Vista 中可能发生的方案。

说明 Windows Vista 中的全局挂起的关系图。

此图演示了类似于“Windows XP 中的全局挂起条件”部分中描述的情况。 但是,在这种情况下,设备 3 限定为空闲设备。 由于所有设备都处于空闲状态,因此总线驱动程序能够调用与挂起的空闲请求 IRP 关联的空闲通知回调例程。 每个驱动程序都会暂停其设备,总线驱动程序在安全执行此操作后立即暂停 USB 主机控制器。

在 Windows Vista 上,所有非中心 USB 设备都必须位于 D1D2D3 中,然后才能启动全局挂起,此时所有 USB 中心(包括根中心)都会暂停。 这意味着任何不支持选择性挂起的 USB 客户端驱动程序都阻止总线进入全局挂起。

Windows XP 中的全局挂起条件

为了最大程度地节省 Windows XP 上的电源,重要的是每个设备驱动程序都使用空闲请求 IRP 来暂停其设备。 如果一个驱动程序使用 IRP_MN_SET_POWER 请求(而不是空闲请求 IRP)暂停其设备,则它可能会阻止其他设备挂起。

下图演示了 Windows XP 中可能发生的方案。

说明 Windows XP 中的全局挂起的关系图。

在此图中,设备 3 处于电源状态 D3,并且没有空闲请求 IRP 挂起。 对于 Windows XP 中的全局挂起,设备 3 不符合空闲设备的条件,因为它没有其父级挂起的空闲请求 IRP。 这可以防止总线驱动程序调用与树中其他设备的驱动程序关联的空闲请求回调例程。

启用选择性挂起

已禁用选择性挂起,以便升级 Microsoft Windows XP 的版本。 它已启用 Windows XP、Windows Vista 和更高版本的 Windows 的干净安装。

若要为给定的根中心及其子设备启用选择性暂停支持,请选中设备管理器中 USB 根中心的电源管理选项卡上的复选框

或者,可以通过在 USB 端口驱动程序的软件密钥下设置 HcDisableSelectiveSuspend 的值来启用或禁用选择性挂起。 值为 1 会禁用选择性挂起。 值为 0 可启用选择性挂起。

例如,Usbport.inf 中的以下行禁用 Hydra OHCI 控制器的选择性挂起:

[OHCI_NOSS.AddReg.NT]
HKR,,"HcDisableSelectiveSuspend",0x00010001,1

客户端驱动程序不应尝试确定是否在发送空闲请求之前启用了选择性挂起。 每当设备处于空闲状态时,它们都应提交空闲请求。 如果空闲请求失败,客户端驱动程序应重置空闲计时器并重试。