USB 配置描述符

USB 设备以一系列称为 USB 配置的接口的形式公开其功能。 每个接口由一个或多个备用设置组成,每个备用设置由一组终结点组成。 本主题介绍与 USB 配置关联的各种描述符。

配置描述符中描述了 USB 配置(请参阅 USB_CONFIGURATION_DESCRIPTOR 结构)。 配置描述符包含有关配置及其接口、备用设置及其终结点的信息。 每个接口描述符或备用设置都在 USB_INTERFACE_DESCRIPTOR 结构中介绍。 在配置中,每个接口描述符在内存中后面紧跟着该接口和备用设置的所有终结点描述符。 每个终结点描述符都存储在 USB_ENDPOINT_DESCRIPTOR 结构中。

例如,请考虑 USB 设备布局中所述的 USB 网络摄像头设备。 设备支持具有两个接口的配置,第一个接口(索引 0)支持两个备用设置。

以下示例显示了 USB 网络摄像头设备的配置描述符:

Configuration Descriptor:
wTotalLength:         0x02CA
bNumInterfaces:       0x02
bConfigurationValue:  0x01
iConfiguration:       0x00
bmAttributes:         0x80 (Bus Powered )
MaxPower:             0xFA (500 mA)

bConfigurationValue 字段指示设备固件中定义的配置编号。 客户端驱动程序使用该数字值来选择活动配置。 有关 USB 设备配置的详细信息,请参阅 如何为 USB 设备选择配置。 USB 配置还指示某些电源特征。 bmAttributes 包含一个位掩码,指示配置是否支持远程唤醒功能,以及设备是总线供电还是自供电。 MaxPower 字段指定设备可在设备为总线供电时从主机中提取的最大功率(以毫秒为单位)。 配置描述符还指示设备支持的接口总数(bNumInterfaces)。

以下示例显示了网络摄像头设备的备用设置 0 接口 0 的接口描述符:

Interface Descriptor:
bInterfaceNumber:     0x00
bAlternateSetting:    0x00
bNumEndpoints:        0x01
bInterfaceClass:      0x0E
bInterfaceSubClass:   0x02
bInterfaceProtocol:   0x00
iInterface:           0x02
0x0409: "Microsoft LifeCam VX-5000"
0x0409: "Microsoft LifeCam VX-5000"

在前面的示例中,请注意 bInterfaceNumberbAlternateSetting 字段值。 这些字段包含客户端驱动程序用于激活接口及其备用设置之一的索引值。 对于激活,驱动程序会将选择接口请求发送到 USB 驱动程序堆栈。 然后,驱动程序堆栈将生成标准控制请求(SET INTERFACE),并将其发送到设备。 记下 bInterfaceClass 字段。 接口描述符或其备用设置的描述符指定一个类代码、子类和协议。 0x0E的值指示接口适用于视频设备类。 另请注意 iInterface 字段。 该值指示在接口描述符后面追加了两个字符串描述符。 字符串描述符包含Unicode说明,这些说明在设备枚举期间用于标识功能。 有关字符串描述符的详细信息,请参阅 USB 字符串描述符

接口中的每个终结点描述设备的单个输入或输出流。 支持不同类型的函数流的设备具有多个接口。 支持多个与函数相关的流的设备可以在单个接口上支持多个终结点。

所有类型的终结点(默认终结点除外)都必须提供终结点描述符,以便主机可以获取有关终结点的信息。 终结点描述符包括其地址、类型、方向以及终结点可以处理的数据量等信息。 传输到终结点的数据基于该信息。

以下示例显示了网络摄像头设备的终结点描述符:

Endpoint Descriptor:
bEndpointAddress:   0x82  IN
bmAttributes:       0x01
wMaxPacketSize:     0x0080 (128)
bInterval:          0x01

bEndpointAddress 字段指定包含终结点编号(Bits 3..0)和终结点方向(Bit 7)的唯一终结点地址。 通过读取前面的示例中的这些值,我们可以确定描述符描述其终结点编号为 2 的 IN 终结点。 bmAttributes 属性指示终结点类型是同步的。 wMaxPacketSizefield 指示终结点可以在单个事务中发送和接收的最大字节数。 位 12..11 表示每个微帧可以发送的事务总数。 bInterval 指示终结点可以发送或接收数据的频率。

如何获取配置描述符

配置描述符是通过标准设备请求(GET_DESCRIPTOR)从设备获取的,该请求通过 USB 驱动程序堆栈作为控制传输发送。 USB 客户端驱动程序可以通过以下方式之一启动请求:

  • 如果设备仅支持一个配置,最简单的方法是调用框架提供的 WdfUsbTargetDeviceRetrieveConfigDescriptor 方法。

  • 对于支持多个配置的设备,如果客户端驱动程序想要获取除第一个配置以外的配置的描述符,驱动程序必须提交 URB。 若要提交 URB,驱动程序必须分配、设置格式,然后将 URB 提交到 USB 驱动程序堆栈。

    若要分配 URB,客户端驱动程序必须调用 WdfUsbTargetDeviceCreateUrb 方法。 该方法接收指向 USB 驱动程序堆栈分配的 URB 的指针。

    若要设置 URB 的格式,客户端驱动程序可以使用 UsbBuildGetDescriptorRequest 宏。 宏会在 URB 中填写所有必要信息,例如设备定义的用于检索描述符的配置编号。 URB 函数设置为URB_FUNCTION_GET_DESCRIPTOR_FROM_DEVICE(请参阅 _URB_CONTROL_DESCRIPTOR_REQUEST),描述符的类型设置为USB_CONFIGURATION_DESCRIPTOR_TYPE。 通过使用 URB 中包含的信息,USB 驱动程序堆栈将生成标准控制请求并将其发送到设备。

    若要提交 URB,客户端驱动程序必须使用 WDF 请求对象。 若要以异步方式将请求对象发送到 USB 驱动程序堆栈,驱动程序必须调用 **WdfRequestSend**方法。 若要同步发送,请调用 WdfUsbTargetDeviceSendUrbSynchronously 方法。

    WDM 驱动程序: Windows 驱动程序模型(WDM)客户端驱动程序只能通过提交 URB 来获取配置描述符。 若要分配 URB,驱动程序必须调用 USBD_UrbAllocate 例程。 若要设置 URB 的格式,驱动程序必须调用 UsbBuildGetDescriptorRequest 宏。 若要提交 URB,驱动程序必须将 URB 与 IRP 相关联,并将 IRP 提交到 USB 驱动程序堆栈。 有关详细信息,请参阅 如何提交 URB

在 USB 配置中,接口数及其备用设置是可变的。 因此,很难预测保存配置描述符所需的缓冲区大小。 客户端驱动程序必须在两个步骤中收集所有这些信息。 首先,确定保存所有配置描述符所需的大小缓冲区,然后发出检索整个描述符的请求。 客户端驱动程序可以通过以下方法之一获取大小:

若要通过调用 WdfUsbTargetDeviceRetrieveConfigDescriptor 获取配置描述符,请执行以下步骤:

  1. 通过调用 WdfUsbTargetDeviceRetrieveConfigDescriptor 获取保存所有配置信息所需的缓冲区大小。 驱动程序必须在缓冲区中传递 NULL,以及用于保存缓冲区大小的变量。
  2. 根据从之前的 WdfUsbTargetDeviceRetrieveConfigDescriptor 调用中接收到的大小分配一个更大的缓冲区。
  3. 再次调用 WdfUsbTargetDeviceRetrieveConfigDescriptor ,并指定指向步骤 2 中分配的新缓冲区的指针。
 NTSTATUS RetrieveDefaultConfigurationDescriptor (
    _In_  WDFUSBDEVICE  UsbDevice,
    _Out_ PUSB_CONFIGURATION_DESCRIPTOR *ConfigDescriptor 
    )
{
    NTSTATUS ntStatus = -1;

    USHORT sizeConfigDesc;

    PUSB_CONFIGURATION_DESCRIPTOR fullConfigDesc = NULL;

    PAGED_CODE();

    *ConfigDescriptor  = NULL;

    ntStatus = WdfUsbTargetDeviceRetrieveConfigDescriptor (
        UsbDevice, 
        NULL,
        &sizeConfigDesc);

    if (sizeConfigDesc == 0)
    {
        TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE, 
            "%!FUNC! Could not retrieve the configuration descriptor size.");

        goto Exit;
    }
    else
    {
        fullConfigDesc = (PUSB_CONFIGURATION_DESCRIPTOR) ExAllocatePoolWithTag (
            NonPagedPool, 
            sizeConfigDesc,
            USBCLIENT_TAG);

        if (!fullConfigDesc)
        {
            ntStatus = STATUS_INSUFFICIENT_RESOURCES;
            goto Exit;
        }  
    }

    RtlZeroMemory (fullConfigDesc, sizeConfigDesc);

    ntStatus = WdfUsbTargetDeviceRetrieveConfigDescriptor (
        UsbDevice, 
        fullConfigDesc,
        &sizeConfigDesc);

    if (!NT_SUCCESS(ntStatus))
    {           
        TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE, 
            "%!FUNC! Could not retrieve the configuration descriptor.");

        goto Exit;
    }

    *ConfigDescriptor = fullConfigDesc;

Exit:

    return ntStatus;   
}

若要通过提交 URB 来获取配置描述符,请执行以下步骤:

  1. 通过调用 WdfUsbTargetDeviceCreateUrb 方法分配 URB
  2. 通过调用 UsbBuildGetDescriptorRequest 宏设置 URB 的格式。 URB 的传输缓冲区必须指向足够大的缓冲区,才能容纳 USB_CONFIGURATION_DESCRIPTOR 结构。
  3. 通过调用 WdfRequestSendWdfUsbTargetDeviceSendUrbSynchronously 将 URB 作为 WDF 请求对象提交。
  4. 请求完成后,检查USB_CONFIGURATION_DESCRIPTORwTotalLength 成员。 该值指示包含完整配置描述符所需的缓冲区大小。
  5. 根据 wTotalLength 中检索到的大小分配更大的缓冲区。
  6. 对较大的缓冲区发出相同的请求。

以下示例代码演示调用 UsbBuildGetDescriptorRequest 来请求获取第 i 个配置的配置信息:

NTSTATUS FX3_RetrieveConfigurationDescriptor (
    _In_ WDFUSBDEVICE  UsbDevice,
    _In_ PUCHAR ConfigurationIndex,
    _Out_ PUSB_CONFIGURATION_DESCRIPTOR *ConfigDescriptor 
    )
{
    NTSTATUS ntStatus = STATUS_SUCCESS;

    USB_CONFIGURATION_DESCRIPTOR configDesc;
    PUSB_CONFIGURATION_DESCRIPTOR fullConfigDesc = NULL;

    PURB urb = NULL;

    WDFMEMORY urbMemory = NULL;

    PAGED_CODE();

    RtlZeroMemory (&configDesc, sizeof(USB_CONFIGURATION_DESCRIPTOR));
    *ConfigDescriptor = NULL;

    // Allocate an URB for the get-descriptor request. 
    // WdfUsbTargetDeviceCreateUrb returns the address of the 
    // newly allocated URB and the WDFMemory object that 
    // contains the URB.

    ntStatus = WdfUsbTargetDeviceCreateUrb (
        UsbDevice,
        NULL,
        &urbMemory,
        &urb);

    if (!NT_SUCCESS (ntStatus))
    {
        TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE, 
            "%!FUNC! Could not allocate URB for an open-streams request.");

        goto Exit;
    }

       // Format the URB.
    UsbBuildGetDescriptorRequest (
        urb,                                                        // Points to the URB to be formatted
        (USHORT) sizeof( struct _URB_CONTROL_DESCRIPTOR_REQUEST ),  // Size of the URB.
        USB_CONFIGURATION_DESCRIPTOR_TYPE,                          // Type of descriptor
        *ConfigurationIndex,                                        // Index of the configuration
        0,                                                          // Not used for configuration descriptors
        &configDesc,                                                // Points to a USB_CONFIGURATION_DESCRIPTOR structure
        NULL,                                                       // Not required because we are providing a buffer not MDL
        sizeof(USB_CONFIGURATION_DESCRIPTOR),                       // Size of the USB_CONFIGURATION_DESCRIPTOR structure.
        NULL                                                        // Reserved.
        );

       // Send the request synchronously.
    ntStatus = WdfUsbTargetDeviceSendUrbSynchronously (
        UsbDevice,
        NULL,
        NULL,
        urb);

    if (configDesc.wTotalLength == 0)
    {
        TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE, 
            "%!FUNC! Could not retrieve the configuration descriptor size.");

        ntStatus = USBD_STATUS_INAVLID_CONFIGURATION_DESCRIPTOR;

        goto Exit;
    }

    // Allocate memory based on the retrieved size. 
       // The allocated memory is released by the caller.
    fullConfigDesc = (PUSB_CONFIGURATION_DESCRIPTOR) ExAllocatePoolWithTag (
        NonPagedPool, 
        configDesc.wTotalLength,
        USBCLIENT_TAG);

    RtlZeroMemory (fullConfigDesc, configDesc.wTotalLength);

    if (!fullConfigDesc)
    {
        ntStatus = STATUS_INSUFFICIENT_RESOURCES;

        goto Exit;
    }

       // Format the URB.
    UsbBuildGetDescriptorRequest (
        urb,                                                        
        (USHORT) sizeof( struct _URB_CONTROL_DESCRIPTOR_REQUEST ),  
        USB_CONFIGURATION_DESCRIPTOR_TYPE,                          
        *ConfigurationIndex,                                         
        0,                                                          
        fullConfigDesc,                                                 
        NULL,                                                       
        configDesc.wTotalLength,                       
        NULL                                                        
        );

       // Send the request again.
    ntStatus = WdfUsbTargetDeviceSendUrbSynchronously (
        UsbDevice,
        NULL,
        NULL,
        urb);

    if ((fullConfigDesc->wTotalLength == 0) || !NT_SUCCESS (ntStatus))
    {
        TraceEvents(TRACE_LEVEL_ERROR, TRACE_DEVICE, 
            "%!FUNC! Could not retrieve the configuration descriptor.");

        ntStatus = USBD_STATUS_INAVLID_CONFIGURATION_DESCRIPTOR;

        goto Exit;
    }

       // Return to the caller.
    *ConfigDescriptor = fullConfigDesc;

Exit:

    if (urbMemory)
    {
        WdfObjectDelete (urbMemory);
    }

    return ntStatus;
}

当设备返回配置描述符时,请求缓冲区将填充所有备用设置的接口描述符,以及特定备用设置内所有终结点的终结点描述符。 对于 USB 设备布局中所述的设备,下图说明了如何在内存中布局配置信息。

配置描述符布局的结构图。

USB_INTERFACE_DESCRIPTOR 的 bInterfaceNumber 成员在配置中用于区分接口,并从零开始。 对于给定接口,从零开始的 bAlternateSetting 成员区分接口的备用设置。 设备按 bInterfaceNumber 值的顺序返回接口描述符,然后按 bAlternateSetting 值的顺序返回。

若要在配置中搜索给定的接口描述符,客户端驱动程序可以调用 USBD_ParseConfigurationDescriptorEx。 在调用中,客户端驱动程序在配置中提供起始位置。 (可选)驱动程序可以指定接口号、备用设置、类、子类或协议。 例程返回指向下一个匹配接口描述符的指针。

若要检查终结点或字符串描述符的配置描述符,请使用 USBD_ParseDescriptors 例程。 调用方在配置和描述符类型(如USB_STRING_DESCRIPTOR_TYPE或USB_ENDPOINT_DESCRIPTOR_TYPE)中提供起始位置。 例程返回指向下一个匹配描述符的指针。