USB 选择性挂起

注释

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

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

在默认设置下,已启用选择性挂起。 Microsoft强烈建议 不要禁用 选择性挂起功能。

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

若要选择性地暂停 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 选择性挂起机制。

发送 USB 空闲请求包 (IRP)

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

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

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

  • 收到 IRP_MN_SURPRISE_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 驱动程序堆栈完成空闲 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 可能导致死锁。

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

状态代码 DESCRIPTION
状态_成功 指示设备不应再挂起。 但是,驱动程序应验证其设备是否已启用,并将它们置于 D0 中(如果它们尚未在 D0 中)。
状态_已取消 在以下任一情况下,总线驱动程序在闲置请求 IRP 中完成使用 STATUS_CANCELLED 操作:
  • 设备驱动程序取消了 IRP。
  • 需要系统电源状态更改。
状态_电源_状态_无效 指示设备驱动程序请求其设备进入 D3 电源状态。 发生此请求时,总线驱动程序将以STATUS_POWER_STATE_INVALID状态完成所有挂起的空闲 IRP。
设备状态忙碌 指示总线驱动程序已保留设备挂起的空闲请求 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 睡眠状态。

集线器驱动程序和 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 设备。

全局挂起的条件

USB 集线器驱动程序选择性地挂起所有连接的设备处于 D1D2D3 设备电源状态的任何集线器。 所有 USB 集线器选择性暂停后,整个总线将进入全局挂起。 每当设备处于 D1、D2D3 的 WDM 设备状态时,USB 驱动程序堆栈都将设备视为空闲。