USB 选择性暂停

注意

本文面向设备驱动程序开发人员。 如果在使用 USB 设备时遇到问题,请参阅 排查常见 USB 问题

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

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

选择选择性挂起机制

对于复合设备上的接口,使用等待唤醒 IRP (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 电源状态。 发生这种情况时,总线驱动程序使用STATUS_POWER_STATE_INVALID完成所有挂起的空闲 IRP。
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 参数设置为 wdm.h 中定义的枚举器值 PowerDeviceD2 (调用 PoRequestPowerIrp,将设备置于 WDM 睡眠状态;ntddk.h) 。 在 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 集线器选择性挂起后,整个总线进入全局挂起。 每当设备处于 D1、D2D3 的 WDM 设备状态时,Windows 7 USB 驱动程序堆栈会将设备视为空闲。

Windows Vista 中全局挂起的条件

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

具体而言,每当设备处于 D1、D2D3 的 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。 设备 3 对于 Windows XP 中的全局挂起目的,不符合空闲设备的条件,因为它的父级没有空闲请求 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

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