UDE 클라이언트 드라이버 작성

이 문서에서는 UDE(USB 디바이스 에뮬레이션) 클래스 확장의 동작과 클라이언트 드라이버가 에뮬레이트된 호스트 컨트롤러 및 연결된 디바이스에 대해 수행해야 하는 작업에 대해 설명합니다. 클래스 드라이버 및 클래스 확장이 루틴 및 콜백 함수 집합을 통해 각각과 통신하는 방법에 대한 정보를 제공합니다. 또한 클라이언트 드라이버가 구현해야 하는 기능에 대해서도 설명합니다.

요약

  • 클래스 확장 및 클라이언트 드라이버에서 사용하는 UDE 개체 및 핸들입니다.
  • 컨트롤러 기능을 쿼리하고 컨트롤러를 다시 설정하는 기능을 사용하여 에뮬레이트된 호스트 컨트롤러를 만듭니다.
  • 가상 USB 디바이스를 만들고, 엔드포인트를 통한 전원 관리 및 데이터 전송을 위해 설정합니다.

중요 API

시작하기 전에

  • 개발 컴퓨터의 최신 WDK(Windows 드라이버 키트)를 설치합니다. 키트에는 UDE 클라이언트 드라이버를 작성하는 데 필요한 헤더 파일 및 라이브러리가 있습니다. 특히 다음이 필요합니다.
    • 스텁 라이브러리(Udecxstub.lib)입니다. 라이브러리는 클라이언트 드라이버의 호출을 변환하고 UdeCx에 전달합니다.
    • 헤더 파일 Udecx.h입니다.
  • 대상 컴퓨터에 Windows 10 설치합니다.
  • UDE에 익숙해지세요. 아키텍처: USB 디바이스 에뮬레이션(UDE)을 참조하세요.
  • WDF(Windows 드라이버 파운데이션)에 대해 잘 알고 있습니다. 권장 읽기: 페니 오윅과 가이 스미스가 쓴 Windows 드라이버 파운데이션을 사용하여 드라이버 개발.

UDE 개체 및 핸들

UDE 클래스 확장 및 클라이언트 드라이버는 디바이스와 호스트 간에 데이터를 전송하는 데 사용되는 엔드포인트 및 URL을 포함하여 에뮬레이트된 호스트 컨트롤러와 가상 디바이스를 나타내는 특정 WDF 개체를 사용합니다. 클라이언트 드라이버는 개체 만들기를 요청하고 개체의 수명은 클래스 확장에 의해 관리됩니다.

  • 에뮬레이트된 호스트 컨트롤러 개체(WDFDEVICE)

    에뮬레이트된 호스트 컨트롤러를 나타내며 UDE 클래스 확장과 클라이언트 드라이버 간의 기본 핸들입니다.

  • UDE 디바이스 개체(UDECXUSBDEVICE)

    에뮬레이트된 호스트 컨트롤러의 포트에 연결된 가상 USB 디바이스를 나타냅니다.

  • UDE 엔드포인트 개체(UDECXUSBENDPOINT)

    USB 디바이스의 순차적 데이터 파이프를 나타냅니다. 엔드포인트에서 데이터를 보내거나 받기 위한 소프트웨어 요청을 받는 데 사용됩니다.

에뮬레이트된 호스트 컨트롤러 초기화

클라이언트 드라이버가 에뮬레이트된 호스트 컨트롤러에 대한 WDFDEVICE 핸들을 검색하는 시퀀스의 요약은 다음과 같습니다. 드라이버가 EvtDriverDeviceAdd 콜백 함수에서 이러한 작업을 수행하는 것이 좋습니다.

  1. 프레임워크에서 전달된 WDFDEVICE_INIT 대한 참조를 전달하여 UdecxInitializeWdfDeviceInit를 호출합니다.

  2. 이 디바이스가 다른 USB 호스트 컨트롤러와 유사하게 표시되도록 설정 정보를 사용하여 WDFDEVICE_INIT 구조를 초기화합니다. 예를 들어 FDO 이름 및 기호 링크를 할당하려면 애플리케이션이 디바이스에 대한 핸들을 열 수 있도록 디바이스 인터페이스 GUID로 Microsoft 제공 GUID_DEVINTERFACE_USB_HOST_CONTROLLER GUID에 디바이스 인터페이스를 등록합니다.

  3. WdfDeviceCreate를 호출하여 프레임워크 디바이스 개체를 만듭니다.

  4. UdecxWdfDeviceAddUsbDeviceEmulation을 호출하고 클라이언트 드라이버의 콜백 함수를 등록합니다.

    다음은 UDE 클래스 확장에 의해 호출되는 호스트 컨트롤러 개체와 연결된 콜백 함수입니다. 이러한 함수는 클라이언트 드라이버에서 구현해야 합니다.

    
    EVT_WDF_DRIVER_DEVICE_ADD                 Controller_WdfEvtDeviceAdd;
    
    #define BASE_DEVICE_NAME                  L"\\Device\\USBFDO-"
    #define BASE_SYMBOLIC_LINK_NAME           L"\\DosDevices\\HCD"
    
    #define DeviceNameSize                    sizeof(BASE_DEVICE_NAME)+MAX_SUFFIX_SIZE
    #define SymLinkNameSize                   sizeof(BASE_SYMBOLIC_LINK_NAME)+MAX_SUFFIX_SIZE
    
    NTSTATUS
    Controller_WdfEvtDeviceAdd(
        _In_
            WDFDRIVER Driver,
        _Inout_
            PWDFDEVICE_INIT WdfDeviceInit
        )
    {
        NTSTATUS                            status;
        WDFDEVICE                           wdfDevice;
        WDF_PNPPOWER_EVENT_CALLBACKS        wdfPnpPowerCallbacks;
        WDF_OBJECT_ATTRIBUTES               wdfDeviceAttributes;
        WDF_OBJECT_ATTRIBUTES               wdfRequestAttributes;
        UDECX_WDF_DEVICE_CONFIG             controllerConfig;
        WDF_FILEOBJECT_CONFIG               fileConfig;
        PWDFDEVICE_CONTEXT                  pControllerContext;
        WDF_IO_QUEUE_CONFIG                 defaultQueueConfig;
        WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS
                                            idleSettings;
        UNICODE_STRING                      refString;
        ULONG instanceNumber;
        BOOLEAN isCreated;
    
        DECLARE_UNICODE_STRING_SIZE(uniDeviceName, DeviceNameSize);
        DECLARE_UNICODE_STRING_SIZE(uniSymLinkName, SymLinkNameSize);
    
        UNREFERENCED_PARAMETER(Driver);
    
        ...
    
        WdfDeviceInitSetPnpPowerEventCallbacks(WdfDeviceInit, &wdfPnpPowerCallbacks);
    
        WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&wdfRequestAttributes, REQUEST_CONTEXT);
        WdfDeviceInitSetRequestAttributes(WdfDeviceInit, &wdfRequestAttributes);
    
    // To distinguish I/O sent to GUID_DEVINTERFACE_USB_HOST_CONTROLLER, we will enable
    // enable interface reference strings by calling WdfDeviceInitSetFileObjectConfig
    // with FileObjectClass WdfFileObjectWdfXxx.
    
    WDF_FILEOBJECT_CONFIG_INIT(&fileConfig,
                                WDF_NO_EVENT_CALLBACK,
                                WDF_NO_EVENT_CALLBACK,
                                WDF_NO_EVENT_CALLBACK // No cleanup callback function
                                );
    
    ...
    
    WdfDeviceInitSetFileObjectConfig(WdfDeviceInit,
                                        &fileConfig,
                                        WDF_NO_OBJECT_ATTRIBUTES);
    
    ...
    
    // Do additional setup required for USB controllers.
    
    status = UdecxInitializeWdfDeviceInit(WdfDeviceInit);
    
    ...
    
    WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&wdfDeviceAttributes, WDFDEVICE_CONTEXT);
    wdfDeviceAttributes.EvtCleanupCallback = _ControllerWdfEvtCleanupCallback;
    
    // Call WdfDeviceCreate with a few extra compatibility steps to ensure this device looks
    // exactly like other USB host controllers.
    
    isCreated = FALSE;
    
    for (instanceNumber = 0; instanceNumber < ULONG_MAX; instanceNumber++) {
    
        status = RtlUnicodeStringPrintf(&uniDeviceName,
                                        L"%ws%d",
                                        BASE_DEVICE_NAME,
                                        instanceNumber);
    
        ...
    
        status = WdfDeviceInitAssignName(*WdfDeviceInit, &uniDeviceName);
    
        ...
    
        status = WdfDeviceCreate(WdfDeviceInit, WdfDeviceAttributes, WdfDevice);
    
        if (status == STATUS_OBJECT_NAME_COLLISION) {
    
            // This is expected to happen at least once when another USB host controller
            // already exists on the system.
    
        ...
    
        } else if (!NT_SUCCESS(status)) {
    
        ...
    
        } else {
    
            isCreated = TRUE;
            break;
        }
    }
    
    if (!isCreated) {
    
        ...
    }
    
    // Create the symbolic link (also for compatibility).
    status = RtlUnicodeStringPrintf(&uniSymLinkName,
                                    L"%ws%d",
                                    BASE_SYMBOLIC_LINK_NAME,
                                    instanceNumber);
    ...
    
    status = WdfDeviceCreateSymbolicLink(*WdfDevice, &uniSymLinkName);
    
    ...
    
    // Create the device interface.
    
    RtlInitUnicodeString(&refString,
                         USB_HOST_DEVINTERFACE_REF_STRING);
    
    status = WdfDeviceCreateDeviceInterface(wdfDevice,
                                            (LPGUID)&GUID_DEVINTERFACE_USB_HOST_CONTROLLER,
                                            &refString);
    
    ...
    
    UDECX_WDF_DEVICE_CONFIG_INIT(&controllerConfig, Controller_EvtUdecxWdfDeviceQueryUsbCapability);
    
    status = UdecxWdfDeviceAddUsbDeviceEmulation(wdfDevice,
                                               &controllerConfig);
    
    // Create default queue. It only supports USB controller IOCTLs. (USB I/O will come through
    // in separate USB device queues.)
    // Shown later in this topic.
    
    WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&defaultQueueConfig, WdfIoQueueDispatchSequential);
    defaultQueueConfig.EvtIoDeviceControl = ControllerEvtIoDeviceControl;
    defaultQueueConfig.PowerManaged = WdfFalse;
    
    status = WdfIoQueueCreate(wdfDevice,
                              &defaultQueueConfig,
                              WDF_NO_OBJECT_ATTRIBUTES,
                              &pControllerContext->DefaultQueue);
    
    ...
    
    // Initialize virtual USB device software objects.
    // Shown later in this topic.
    
    status = Usb_Initialize(wdfDevice);
    
    ...
    
    exit:
    
        return status;
    }
    ```1.
    
    

호스트 컨트롤러로 전송된 사용자 모드 IOCTL 요청 처리

초기화 중에 UDE 클라이언트 드라이버는 GUID_DEVINTERFACE_USB_HOST_CONTROLLER 디바이스 인터페이스 GUID를 노출합니다. 이렇게 하면 드라이버가 해당 GUID를 사용하여 디바이스 핸들을 여는 애플리케이션에서 IOCTL 요청을 받을 수 있습니다. IOCTL 제어 코드 목록은 디바이스 인터페이스 GUID가 있는 USB IOCTL : GUID_DEVINTERFACE_USB_HOST_CONTROLLER 참조하세요.

이러한 요청을 처리하기 위해 클라이언트 드라이버는 EvtIoDeviceControl 이벤트 콜백을 등록합니다. 구현에서는 요청을 처리하는 대신 드라이버가 처리를 위해 요청을 UDE 클래스 확장으로 전달하도록 선택할 수 있습니다. 요청을 전달하려면 드라이버가 UdecxWdfDeviceTryHandleUserIoctl을 호출해야 합니다. 수신된 IOCTL 제어 코드가 디바이스 설명자 검색과 같은 표준 요청에 해당하는 경우 클래스 확장은 요청을 처리하고 성공적으로 완료합니다. 이 경우 UdecxWdfDeviceTryHandleUserIoctl 이 반환 값으로 TRUE로 완료됩니다. 그렇지 않으면 호출이 FALSE를 반환하고 드라이버는 요청을 완료하는 방법을 결정해야 합니다. 가장 간단한 구현에서 드라이버는 WdfRequestComplete를 호출하여 적절한 오류 코드로 요청을 완료할 수 있습니다.


EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL        Controller_EvtIoDeviceControl;

VOID
Controller_EvtIoDeviceControl(
    _In_
        WDFQUEUE Queue,
    _In_
        WDFREQUEST Request,
    _In_
        size_t OutputBufferLength,
    _In_
        size_t InputBufferLength,
    _In_
        ULONG IoControlCode
)
{
    BOOLEAN handled;
    NTSTATUS status;
    UNREFERENCED_PARAMETER(OutputBufferLength);
    UNREFERENCED_PARAMETER(InputBufferLength);

    handled = UdecxWdfDeviceTryHandleUserIoctl(WdfIoQueueGetDevice(Queue),
                                                Request);

    if (handled) {

        goto exit;
    }

    // Unexpected control code.
    // Fail the request.

    status = STATUS_INVALID_DEVICE_REQUEST;

    WdfRequestComplete(Request, status);

exit:

    return;
}

호스트 컨트롤러의 기능 보고

상위 계층 드라이버가 USB 호스트 컨트롤러의 기능을 사용하려면 먼저 드라이버가 해당 기능을 컨트롤러에서 지원하는지 여부를 결정해야 합니다. 드라이버는 WdfUsbTargetDeviceQueryUsbCapabilityUSBD_QueryUsbCapability 호출하여 이러한 쿼리를 수행합니다. 이러한 호출은 USB UDE(Device Emulation) 클래스 확장으로 전달됩니다. 요청을 받으면 클래스 확장은 클라이언트 드라이버의 EVT_UDECX_WDF_DEVICE_QUERY_USB_CAPABILITY 구현을 호출합니다. 이 호출은 EvtDriverDeviceAdd 가 완료된 후에만 수행되며, 일반적으로 EvtDevicePrepareHardware 에서는 이고 EvtDeviceReleaseHardware 이후에는 수행되지 않습니다. 콜백 함수가 필요합니다.

구현에서 클라이언트 드라이버는 요청된 기능을 지원하는지 여부를 보고해야 합니다. 특정 기능은 정적 스트림과 같은 UDE에서 지원되지 않습니다.

NTSTATUS
Controller_EvtControllerQueryUsbCapability(
    WDFDEVICE     UdeWdfDevice,
    PGUID         CapabilityType,
    ULONG         OutputBufferLength,
    PVOID         OutputBuffer,
    PULONG        ResultLength
)

{
    NTSTATUS status;

    UNREFERENCED_PARAMETER(UdeWdfDevice);
    UNREFERENCED_PARAMETER(OutputBufferLength);
    UNREFERENCED_PARAMETER(OutputBuffer);

    *ResultLength = 0;

    if (RtlCompareMemory(CapabilityType,
                         &GUID_USB_CAPABILITY_CHAINED_MDLS,
                         sizeof(GUID)) == sizeof(GUID)) {

        //
        // TODO: Is GUID_USB_CAPABILITY_CHAINED_MDLS supported?
        // If supported, status = STATUS_SUCCESS
        // Otherwise, status = STATUS_NOT_SUPPORTED
    }

    else {

        status = STATUS_NOT_IMPLEMENTED;
    }

    return status;
}

가상 USB 디바이스 만들기

가상 USB 디바이스는 USB 디바이스와 유사하게 작동합니다. 여러 인터페이스가 있는 구성을 지원하며 각 인터페이스는 대체 설정을 지원합니다. 각 설정에는 데이터 전송에 사용되는 엔드포인트가 하나 더 있을 수 있습니다. 디바이스가 실제 USB 디바이스와 유사한 정보를 보고할 수 있도록 모든 설명자(디바이스, 구성, 인터페이스, 엔드포인트)는 UDE 클라이언트 드라이버에 의해 설정됩니다.

참고

UDE 클라이언트 드라이버는 외부 허브를 지원하지 않습니다.

클라이언트 드라이버가 UDE 디바이스 개체에 대한 UDECXUSBDEVICE 핸들을 만드는 시퀀스의 요약은 다음과 같습니다. 드라이버는 에뮬레이트된 호스트 컨트롤러에 대한 WDFDEVICE 핸들을 검색한 후 이러한 단계를 수행해야 합니다. 드라이버가 EvtDriverDeviceAdd 콜백 함수에서 이러한 작업을 수행하는 것이 좋습니다.

  1. UdecxUsbDeviceInitAllocate를 호출하여 디바이스를 만드는 데 필요한 초기화 매개 변수에 대한 포인터를 가져옵니다. 이 구조체는 UDE 클래스 확장에 의해 할당됩니다.

  2. UDECX_USB_DEVICE_STATE_CHANGE_CALLBACKS 멤버를 설정한 다음 UdecxUsbDeviceInitSetStateChangeCallbacks를 호출하여 이벤트 콜백 함수를 등록합니다. 다음은 UDE 클래스 확장에서 호출하는 UDE 디바이스 개체와 연결된 콜백 함수입니다.

    이러한 함수는 엔드포인트를 만들거나 구성하기 위해 클라이언트 드라이버에 의해 구현됩니다.

  3. UdecxUsbDeviceInitSetSpeed를 호출하여 USB 디바이스 속도와 디바이스 유형, USB 2.0 또는 SuperSpeed 디바이스를 설정합니다.

  4. UdecxUsbDeviceInitSetEndpointsType을 호출하여 디바이스에서 지원하는 엔드포인트 유형(단순 또는 동적)을 지정합니다. 클라이언트 드라이버가 간단한 엔드포인트를 만들도록 선택하는 경우 드라이버는 디바이스에 연결하기 전에 모든 엔드포인트 개체를 만들어야 합니다. 디바이스에는 하나의 구성만 있어야 하며 인터페이스당 하나의 인터페이스 설정만 있어야 합니다. 동적 엔드포인트의 경우 드라이버는 EVT_UDECX_USB_DEVICE_ENDPOINTS_CONFIGURE 이벤트 콜백을 수신할 때 디바이스에 연결한 후 언제든지 엔드포인트를 만들 수 있습니다. 동적 엔드포인트 만들기를 참조하세요.

  5. 이러한 메서드를 호출하여 디바이스에 필요한 설명자를 추가합니다.

    • UdecxUsbDeviceInitAddDescriptor

    • UdecxUsbDeviceInitAddDescriptorWithIndex

    • UdecxUsbDeviceInitAddStringDescriptor

    • UdecxUsbDeviceInitAddStringDescriptorRaw

      UDE 클래스 확장이 이전 메서드 중 하나를 사용하여 초기화 중에 클라이언트 드라이버가 제공한 표준 설명자에 대한 요청을 받으면 클래스 확장이 자동으로 요청을 완료합니다. 클래스 확장은 해당 요청을 클라이언트 드라이버에 전달하지 않습니다. 이 디자인은 드라이버가 제어 요청을 처리해야 하는 요청 수를 줄입니다. 또한 드라이버가 설치 패킷을 광범위하게 구문 분석하고 wLengthTransferBufferLength 를 올바르게 처리해야 하는 설명자 논리를 구현할 필요가 없습니다. 이 목록에는 표준 요청이 포함됩니다. 클라이언트 드라이버는 이러한 요청에 대해 검사 필요가 없습니다(설명자를 추가하기 위해 이전 메서드가 호출된 경우에만).

    • USB_REQUEST_GET_DESCRIPTOR

    • USB_REQUEST_SET_CONFIGURATION

    • USB_REQUEST_SET_INTERFACE

    • USB_REQUEST_SET_ADDRESS

    • USB_REQUEST_SET_FEATURE

    • USB_FEATURE_FUNCTION_SUSPEND

    • USB_FEATURE_REMOTE_WAKEUP

    • USB_REQUEST_CLEAR_FEATURE

    • USB_FEATURE_ENDPOINT_STALL

    • USB_REQUEST_SET_SEL

    • USB_REQUEST_ISOCH_DELAY

      그러나 인터페이스, 클래스별 또는 공급업체 정의 설명자에 대한 요청은 UDE 클래스 확장이 클라이언트 드라이버에 전달합니다. 드라이버는 이러한 GET_DESCRIPTOR 요청을 처리해야 합니다.

  6. UdecxUsbDeviceCreate를 호출하여 UDE 디바이스 개체를 만들고 UDECXUSBDEVICE 핸들을 검색합니다.

  7. UdecxUsbEndpointCreate를 호출하여 정적 엔드포인트를 만듭니다. 간단한 엔드포인트 만들기를 참조하세요.

  8. UdecxUsbDevicePlugIn을 호출하여 디바이스가 연결되어 있고 엔드포인트에서 I/O 요청을 받을 수 있음을 UDE 클래스 확장에 나타냅니다. 이 호출 후 클래스 확장은 엔드포인트 및 USB 디바이스에서 콜백 함수를 호출할 수도 있습니다. 참고 런타임에 USB 디바이스를 제거해야 하는 경우 클라이언트 드라이버는 UdecxUsbDevicePlugOutAndDelete를 호출할 수 있습니다. 드라이버가 디바이스를 사용하려는 경우 UdecxUsbDeviceCreate를 호출하여 만들어야 합니다.

이 예제에서 설명자 선언은 다음과 같이 HID 디바이스에 대해 여기에 표시된 것처럼 선언된 전역 변수로 간주됩니다.

const UCHAR g_UsbDeviceDescriptor[] = {
    // Device Descriptor
    0x12, // Descriptor Size
    0x01, // Device Descriptor Type
    0x00, 0x03, // USB 3.0
    0x00, // Device class
    0x00, // Device sub-class
    0x00, // Device protocol
    0x09, // Maxpacket size for EP0 : 2^9
    0x5E, 0x04, // Vendor ID
    0x39, 0x00, // Product ID
    0x00, // LSB of firmware version
    0x03, // MSB of firmware version
    0x01, // Manufacture string index
    0x03, // Product string index
    0x00, // Serial number string index
    0x01 // Number of configurations
};

다음은 클라이언트 드라이버가 콜백 함수를 등록하고, 디바이스 속도를 설정하고, 엔드포인트 유형을 나타내고, 마지막으로 일부 디바이스 설명자를 설정하여 초기화 매개 변수를 지정하는 예제입니다.


NTSTATUS
Usb_Initialize(
    _In_
        WDFDEVICE WdfDevice
    )
{
    NTSTATUS                                status;
    PUSB_CONTEXT                            usbContext;    //Client driver declared context for the host controller object
    PUDECX_USBDEVICE_CONTEXT                deviceContext; //Client driver declared context for the UDE device object
    UDECX_USB_DEVICE_STATE_CHANGE_CALLBACKS callbacks;
    WDF_OBJECT_ATTRIBUTES                   attributes;

    UDECX_USB_DEVICE_PLUG_IN_OPTIONS        pluginOptions;

    usbContext = WdfDeviceGetUsbContext(WdfDevice);

    usbContext->UdecxUsbDeviceInit = UdecxUsbDeviceInitAllocate(WdfDevice);

    if (usbContext->UdecxUsbDeviceInit == NULL) {

        ...
        goto exit;
    }

    // State changed callbacks

    UDECX_USB_DEVICE_CALLBACKS_INIT(&callbacks);
#ifndef SIMPLEENDPOINTS
    callbacks.EvtUsbDeviceDefaultEndpointAdd = UsbDevice_EvtUsbDeviceDefaultEndpointAdd;
    callbacks.EvtUsbDeviceEndpointAdd = UsbDevice_EvtUsbDeviceEndpointAdd;
    callbacks.EvtUsbDeviceEndpointsConfigure = UsbDevice_EvtUsbDeviceEndpointsConfigure;
#endif
    callbacks.EvtUsbDeviceLinkPowerEntry = UsbDevice_EvtUsbDeviceLinkPowerEntry;
    callbacks.EvtUsbDeviceLinkPowerExit = UsbDevice_EvtUsbDeviceLinkPowerExit;
    callbacks.EvtUsbDeviceSetFunctionSuspendAndWake = UsbDevice_EvtUsbDeviceSetFunctionSuspendAndWake;

    UdecxUsbDeviceInitSetStateChangeCallbacks(usbContext->UdecxUsbDeviceInit, &callbacks);

    // Set required attributes.

    UdecxUsbDeviceInitSetSpeed(usbContext->UdecxUsbDeviceInit, UdecxUsbLowSpeed);

#ifdef SIMPLEENDPOINTS
    UdecxUsbDeviceInitSetEndpointsType(usbContext->UdecxUsbDeviceInit, UdecxEndpointTypeSimple);
#else
    UdecxUsbDeviceInitSetEndpointsType(usbContext->UdecxUsbDeviceInit, UdecxEndpointTypeDynamic);
#endif

    // Add device descriptor
    //
    status = UdecxUsbDeviceInitAddDescriptor(usbContext->UdecxUsbDeviceInit,
                                           (PUCHAR)g_UsbDeviceDescriptor,
                                           sizeof(g_UsbDeviceDescriptor));

    if (!NT_SUCCESS(status)) {

        goto exit;
    }

#ifdef USB30

    // Add BOS descriptor for a SuperSpeed device

    status = UdecxUsbDeviceInitAddDescriptor(pUsbContext->UdecxUsbDeviceInit,
                                           (PUCHAR)g_UsbBOSDescriptor,
                                           sizeof(g_UsbBOSDescriptor));

    if (!NT_SUCCESS(status)) {

        goto exit;
    }
#endif

    // String descriptors

    status = UdecxUsbDeviceInitAddDescriptorWithIndex(usbContext->UdecxUsbDeviceInit,
                                                    (PUCHAR)g_LanguageDescriptor,
                                                    sizeof(g_LanguageDescriptor),
                                                    0);

    if (!NT_SUCCESS(status)) {

        goto exit;
    }

    status = UdecxUsbDeviceInitAddStringDescriptor(usbContext->UdecxUsbDeviceInit,
                                                 &g_ManufacturerStringEnUs,
                                                 g_ManufacturerIndex,
                                                 US_ENGLISH);

    if (!NT_SUCCESS(status)) {

        goto exit;
    }

    WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, UDECX_USBDEVICE_CONTEXT);

    status = UdecxUsbDeviceCreate(&usbContext->UdecxUsbDeviceInit,
                                &attributes,
                                &usbContext->UdecxUsbDevice);

    if (!NT_SUCCESS(status)) {

        goto exit;
    }

#ifdef SIMPLEENDPOINTS
   // Create the default control endpoint
   // Shown later in this topic.

    status = UsbCreateControlEndpoint(WdfDevice);

    if (!NT_SUCCESS(status)) {

        goto exit;
    }

#endif

    UDECX_USB_DEVICE_PLUG_IN_OPTIONS_INIT(&pluginOptions);
#ifdef USB30
    pluginOptions.Usb30PortNumber = 2;
#else
    pluginOptions.Usb20PortNumber = 1;
#endif
    status = UdecxUsbDevicePlugIn(usbContext->UdecxUsbDevice, &pluginOptions);

exit:

    if (!NT_SUCCESS(status)) {

        UdecxUsbDeviceInitFree(usbContext->UdecxUsbDeviceInit);
        usbContext->UdecxUsbDeviceInit = NULL;

    }

    return status;
}

USB 디바이스의 전원 관리

UDE 클래스 확장은 디바이스를 저전력 상태로 보내거나 작업 상태로 되돌리라는 요청을 받으면 클라이언트 드라이버의 콜백 함수를 호출합니다. 이러한 콜백 함수는 절 해제를 지원하는 USB 디바이스에 필요합니다. 클라이언트 드라이버는 UdecxUsbDeviceInitSetStateChangeCallbacks에 대한 이전 호출에서 에 의해 구현을 등록했습니다.

자세한 내용은 USB 디바이스 전원 상태를 참조하세요.

USB 3.0 디바이스를 사용하면 개별 함수가 더 낮은 전원 상태로 들어갈 수 있습니다. 각 함수는 절전 모드 해제 신호를 보낼 수도 있습니다. UDE 클래스 확장은 EVT_UDECX_USB_DEVICE_SET_FUNCTION_SUSPEND_AND_WAKE 호출하여 클라이언트 드라이버에 알 수 있습니다. 이 이벤트는 함수 전원 상태 변경을 나타내고 함수가 새 상태에서 해제할 수 있는지 여부를 클라이언트 드라이버에 알릴 수 있습니다. 함수에서 클래스 확장은 해제 중인 함수의 인터페이스 번호를 전달합니다.

클라이언트 드라이버는 낮은 링크 전원 상태, 함수 일시 중단 또는 둘 다에서 자체 절전 모드 해제를 시작하는 가상 USB 디바이스의 동작을 시뮬레이션할 수 있습니다. USB 2.0 디바이스의 경우 드라이버가 최신 EVT_UDECX_USB_DEVICE_D0_EXIT 디바이스에서 절전 모드 해제를 사용하도록 설정한 경우 드라이버는 UdecxUsbDeviceSignalWake를 호출해야 합니다. USB 3.0 디바이스의 경우 USB 3.0 절전 모드 해제 기능이 기능별로 사용되므로 드라이버는 UdecxUsbDeviceSignalFunctionWake 를 호출해야 합니다. 전체 디바이스가 저전력 상태이거나 이러한 상태를 입력하는 경우 UdecxUsbDeviceSignalFunctionWake 는 디바이스를 절전 모드에서 해제합니다.

간단한 엔드포인트 만들기

클라이언트 드라이버는 USB 디바이스 간 데이터 전송을 처리하는 UDE 엔드포인트 개체를 만듭니다. 드라이버는 UDE 디바이스를 만든 후 디바이스를 플러그 인으로 보고하기 전에 간단한 엔드포인트를 만듭니다.

클라이언트 드라이버가 UDE 엔드포인트 개체에 대한 UDECXUSBENDPOINT 핸들을 만드는 시퀀스의 요약은 다음과 같습니다. 드라이버는 가상 USB 디바이스에 대한 UDECXUSBDEVICE 핸들을 검색한 후 이러한 단계를 수행해야 합니다. 드라이버가 EvtDriverDeviceAdd 콜백 함수에서 이러한 작업을 수행하는 것이 좋습니다.

  1. UdecxUsbSimpleEndpointInitAllocate를 호출하여 클래스 확장에 의해 할당된 초기화 매개 변수에 대한 포인터를 가져옵니다.

  2. UdecxUsbEndpointInitSetEndpointAddress를 호출하여 초기화 매개 변수에서 엔드포인트 주소를 설정합니다.

  3. UdecxUsbEndpointInitSetCallbacks를 호출하여 클라이언트 드라이버 구현 콜백 함수를 등록합니다.

    이러한 함수는 엔드포인트에서 큐 및 요청을 처리하기 위해 클라이언트 드라이버에 의해 구현됩니다.

  4. UdecxUsbEndpointCreate를 호출하여 엔드포인트 개체를 만들고 UDECXUSBENDPOINT 핸들을 검색합니다.

  5. UdecxUsbEndpointSetWdfIoQueue를 호출하여 프레임워크 큐 개체를 엔드포인트와 연결합니다. 해당하는 경우 적절한 특성을 설정하여 엔드포인트 개체를 큐의 WDF 부모 개체로 설정할 수 있습니다.

    모든 엔드포인트 개체에는 전송 요청을 처리하기 위한 프레임워크 큐 개체가 있습니다. 클래스 확장이 수신하는 각 전송 요청에 대해 프레임워크 요청 개체를 큐에 대기합니다. 큐의 상태(시작, 제거됨)는 UDE 클래스 확장에 의해 관리되며 클라이언트 드라이버는 해당 상태를 변경하지 않아야 합니다. 각 요청 개체에는 전송 세부 정보가 포함된 URB(USB 요청 블록)가 포함되어 있습니다.

이 예제에서 클라이언트 드라이버는 기본 컨트롤 엔드포인트를 만듭니다.

EVT_WDF_IO_QUEUE_IO_INTERNAL_DEVICE_CONTROL IoEvtControlUrb;
EVT_UDECX_USB_ENDPOINT_RESET UsbEndpointReset;
EVT_UDECX_USB_ENDPOINT_PURGE UsEndpointEvtPurge;
EVT_UDECX_USB_ENDPOINT_START UsbEndpointEvtStart;

NTSTATUS
UsbCreateControlEndpoint(
    _In_
        WDFDEVICE WdfDevice
    )
{
    NTSTATUS                      status;
    PUSB_CONTEXT                  pUsbContext;
    WDF_IO_QUEUE_CONFIG           queueConfig;
    WDFQUEUE                      controlQueue;
    UDECX_USB_ENDPOINT_CALLBACKS  callbacks;
    PUDECXUSBENDPOINT_INIT        endpointInit;

    pUsbContext = WdfDeviceGetUsbContext(WdfDevice);
    endpointInit = NULL;

    WDF_IO_QUEUE_CONFIG_INIT(&queueConfig, WdfIoQueueDispatchSequential);

    queueConfig.EvtIoInternalDeviceControl = IoEvtControlUrb;

    status = WdfIoQueueCreate (Device,
                               &queueConfig,
                               WDF_NO_OBJECT_ATTRIBUTES,
                               &controlQueue);

    if (!NT_SUCCESS(status)) {

        goto exit;
    }

    endpointInit = UdecxUsbSimpleEndpointInitAllocate(pUsbContext->UdecxUsbDevice);

    if (endpointInit == NULL) {

        status = STATUS_INSUFFICIENT_RESOURCES;
        goto exit;
    }

    UdecxUsbEndpointInitSetEndpointAddress(endpointInit, USB_DEFAULT_ENDPOINT_ADDRESS);

    UDECX_USB_ENDPOINT_CALLBACKS_INIT(&callbacks, UsbEndpointReset);
    UdecxUsbEndpointInitSetCallbacks(endpointInit, &callbacks);

    callbacks.EvtUsbEndpointStart = UsbEndpointEvtStart;
    callbacks.EvtUsbEndpointPurge = UsEndpointEvtPurge;

    status = UdecxUsbEndpointCreate(&endpointInit,
        WDF_NO_OBJECT_ATTRIBUTES,
        &pUsbContext->UdecxUsbControlEndpoint);

    if (!NT_SUCCESS(status)) {
        goto exit;
    }

    UdecxUsbEndpointSetWdfIoQueue(pUsbContext->UdecxUsbControlEndpoint,
        controlQueue);

exit:

    if (endpointInit != NULL) {

        NT_ASSERT(!NT_SUCCESS(status));
        UdecxUsbEndpointInitFree(endpointInit);
        endpointInit = NULL;
    }

    return status;
}

동적 엔드포인트 만들기

클라이언트 드라이버는 허브 드라이버 및 클라이언트 드라이버를 대신하여 UDE 클래스 확장의 요청에 따라 동적 엔드포인트를 만들 수 있습니다. 클래스 확장은 다음 콜백 함수를 호출하여 요청을 수행합니다.

*EVT_UDECX_USB_DEVICE_DEFAULT_ENDPOINT_ADD 클라이언트 드라이버는 기본 제어 엔드포인트를 만듭니다(엔드포인트 0).

*EVT_UDECX_USB_DEVICE_ENDPOINT_ADD 클라이언트 드라이버는 동적 엔드포인트를 만듭니다.

*EVT_UDECX_USB_DEVICE_ENDPOINTS_CONFIGURE 클라이언트 드라이버는 대체 설정을 선택하거나, 현재 엔드포인트를 사용하지 않도록 설정하거나, 동적 엔드포인트를 추가하여 구성을 변경합니다.

클라이언트 드라이버는 UdecxUsbDeviceInitSetStateChangeCallbacks를 호출하는 동안 이전 콜백을 등록했습니다. 가상 USB 디바이스 만들기를 참조하세요. 이 메커니즘을 사용하면 클라이언트 드라이버가 디바이스의 USB 구성 및 인터페이스 설정을 동적으로 변경할 수 있습니다. 예를 들어 엔드포인트 개체가 필요하거나 기존 엔드포인트 개체를 해제해야 하는 경우 클래스 확장은 EVT_UDECX_USB_DEVICE_ENDPOINTS_CONFIGURE 호출합니다.

다음은 클라이언트 드라이버가 콜백 함수 구현에서 엔드포인트 개체에 대한 UDECXUSBENDPOINT 핸들을 만드는 시퀀스의 요약입니다.

  1. UdecxUsbEndpointInitSetEndpointAddress를 호출하여 초기화 매개 변수에서 엔드포인트 주소를 설정합니다.

  2. UdecxUsbEndpointInitSetCallbacks를 호출하여 클라이언트 드라이버 구현 콜백 함수를 등록합니다. 간단한 엔드포인트와 마찬가지로 드라이버는 다음과 같은 콜백 함수를 등록할 수 있습니다.

  3. UdecxUsbEndpointCreate를 호출하여 엔드포인트 개체를 만들고 UDECXUSBENDPOINT 핸들을 검색합니다.

  4. UdecxUsbEndpointSetWdfIoQueue를 호출하여 프레임워크 큐 개체를 엔드포인트와 연결합니다.

이 예제 구현에서 클라이언트 드라이버는 동적 기본 컨트롤 엔드포인트를 만듭니다.

NTSTATUS
UsbDevice_EvtUsbDeviceDefaultEndpointAdd(
    _In_
        UDECXUSBDEVICE            UdecxUsbDevice,
    _In_
        PUDECXUSBENDPOINT_INIT    UdecxUsbEndpointInit
)
{
    NTSTATUS                    status;
    PUDECX_USBDEVICE_CONTEXT    deviceContext;
    WDFQUEUE                    controlQueue;
    WDF_IO_QUEUE_CONFIG         queueConfig;
    UDECX_USB_ENDPOINT_CALLBACKS  callbacks;

    deviceContext = UdecxDeviceGetContext(UdecxUsbDevice);

    WDF_IO_QUEUE_CONFIG_INIT(&queueConfig, WdfIoQueueDispatchSequential);

    queueConfig.EvtIoInternalDeviceControl = IoEvtControlUrb;

    status = WdfIoQueueCreate (deviceContext->WdfDevice,
                               &queueConfig,
                               WDF_NO_OBJECT_ATTRIBUTES,
                               &controlQueue);

    if (!NT_SUCCESS(status)) {

        goto exit;
    }

    UdecxUsbEndpointInitSetEndpointAddress(UdecxUsbEndpointInit, USB_DEFAULT_DEVICE_ADDRESS);

    UDECX_USB_ENDPOINT_CALLBACKS_INIT(&callbacks, UsbEndpointReset);
    UdecxUsbEndpointInitSetCallbacks(UdecxUsbEndpointInit, &callbacks);

    status = UdecxUsbEndpointCreate(UdecxUsbEndpointInit,
        WDF_NO_OBJECT_ATTRIBUTES,
        &deviceContext->UdecxUsbControlEndpoint);

    if (!NT_SUCCESS(status)) {
        goto exit;
    }

    UdecxUsbEndpointSetWdfIoQueue(deviceContext->UdecxUsbControlEndpoint,
        controlQueue);

exit:

    return status;
}

엔드포인트를 다시 설정하여 오류 복구 수행

때때로 엔드포인트의 중단 조건과 같은 다양한 이유로 인해 데이터 전송이 실패할 수 있습니다. 전송에 실패한 경우 엔드포인트는 오류 조건을 지울 때까지 요청을 처리할 수 없습니다. UDE 클래스 확장에서 데이터 전송에 실패하면 클라이언트 드라이버의 EVT_UDECX_USB_ENDPOINT_RESET 콜백 함수를 호출합니다. 이 함수는 이전 UdecxUsbEndpointInitSetCallbacks 호출에서 드라이버가 등록했습니다. 구현에서 드라이버는 파이프의 HALT 상태를 지우고 오류 조건을 지우는 데 필요한 다른 단계를 수행할 수 있습니다.

이 호출은 비동기적입니다. 클라이언트가 다시 설정 작업을 완료한 후 드라이버는 WdfRequestComplete를 호출하여 적절한 오류 코드로 요청을 완료해야 합니다. 이 호출은 UDE 클라이언트 확장에 상태 다시 설정 작업의 완료에 대해 알 수 있습니다.

참고 오류 복구에 복잡한 솔루션이 필요한 경우 클라이언트 드라이버에는 호스트 컨트롤러를 다시 설정하는 옵션이 있습니다. 이 논리는 드라이버가 UdecxWdfDeviceAddUsbDeviceEmulation 호출에 등록된 EVT_UDECX_WDF_DEVICE_RESET 콜백 함수에서 구현할 수 있습니다. 해당하는 경우 드라이버는 호스트 컨트롤러 및 모든 다운스트림 디바이스를 다시 설정할 수 있습니다. 클라이언트 드라이버가 컨트롤러를 다시 설정할 필요가 없지만 모든 다운스트림 디바이스를 다시 설정하는 경우 드라이버는 등록하는 동안 구성 매개 변수에 UdeWdfDeviceResetActionResetEachUsbDevice 를 지정해야 합니다. 이 경우 클래스 확장은 연결된 각 디바이스에 대해 EVT_UDECX_WDF_DEVICE_RESET 호출합니다.

큐 상태 관리 구현

UDE 엔드포인트 개체와 연결된 프레임워크 큐 개체의 상태는 UDE 클래스 확장에서 관리됩니다. 그러나 클라이언트 드라이버가 엔드포인트 큐에서 다른 내부 큐로 요청을 전달하는 경우 클라이언트는 엔드포인트의 I/O 흐름에서 변경 내용을 처리하는 논리를 구현해야 합니다. 이러한 콜백 함수는 UdecxUsbEndpointInitSetCallbacks에 등록됩니다.

엔드포인트 제거 작업

엔드포인트당 하나의 큐가 있는 UDE 클라이언트 드라이버는 다음 예제와 같이 EVT_UDECX_USB_ENDPOINT_PURGE 구현할 수 있습니다.

EVT_UDECX_USB_ENDPOINT_PURGE 구현에서 클라이언트 드라이버는 엔드포인트의 큐에서 전달된 모든 I/O가 완료되었는지 확인해야 하며 클라이언트 드라이버의 EVT_UDECX_USB_ENDPOINT_START 호출될 때까지 새로 전달된 I/O도 실패합니다. 이러한 요구 사항은 UdecxUsbEndpointPurgeComplete를 호출하여 충족되며, 전달된 모든 I/O가 완료되고 향후 전달된 I/O가 실패하는지 확인합니다.

엔드포인트 시작 작업

EVT_UDECX_USB_ENDPOINT_START 구현에서 클라이언트 드라이버는 엔드포인트의 큐 및 엔드포인트에 대해 전달된 I/O를 수신하는 모든 큐에서 I/O 처리를 시작해야 합니다. 엔드포인트를 만든 후에는 이 콜백 함수가 반환될 때까지 I/O를 받지 않습니다. 이 콜백은 EVT_UDECX_USB_ENDPOINT_PURGE 완료된 후 엔드포인트를 처리 I/O 상태로 반환합니다.

URL(데이터 전송 요청) 처리

클라이언트 디바이스의 엔드포인트로 전송된 USB I/O 요청을 처리하려면 큐를 엔드포인트와 연결할 때 UdecxUsbEndpointInitSetCallbacks와 함께 사용되는 큐 개체에서 EVT_WDF_IO_QUEUE_IO_INTERNAL_DEVICE_CONTROL 콜백을 가로채야 합니다. 해당 콜백에서 IOCTL_INTERNAL_USB_SUBMIT_URB IoControlCode에 대한 I/O 를 처리합니다(URB 처리 메서드 아래의 샘플 코드 참조).

URB 처리 메서드

가상 디바이스의 엔드포인트와 연결된 큐의 IOCTL_INTERNAL_USB_SUBMIT_URB 통해 URL을 처리하는 과정의 일환으로 UDE 클라이언트 드라이버는 다음 방법을 사용하여 I/O 요청의 전송 버퍼에 대한 포인터를 가져올 수 있습니다.

이러한 함수는 엔드포인트에서 큐 및 요청을 처리하기 위해 클라이언트 드라이버에 의해 구현됩니다.

UdecxUrbRetrieveControlSetupPacket 지정된 프레임워크 요청 개체에서 USB 컨트롤 설정 패킷을 검색합니다.

UdecxUrbRetrieveBuffer 엔드포인트 큐로 전송된 지정된 프레임워크 요청 개체에서 URB의 전송 버퍼를 검색합니다.

UdecxUrbSetBytesCompleted 프레임워크 요청 개체 내에 포함된 URB에 대해 전송되는 바이트 수를 설정합니다.

UdecxUrbComplete USB 관련 완료 상태 코드로 URB 요청을 완료합니다.

UdecxUrbCompleteWithNtStatus NTSTATUS 코드를 사용하여 URB 요청을 완료합니다.

다음은 USB OUT 전송의 URB에 대한 일반적인 I/O 처리 흐름입니다.

static VOID
IoEvtSampleOutUrb(
    _In_ WDFQUEUE Queue,
    _In_ WDFREQUEST Request,
    _In_ size_t OutputBufferLength,
    _In_ size_t InputBufferLength,
    _In_ ULONG IoControlCode
)
{
    PENDPOINTQUEUE_CONTEXT pEpQContext;
    NTSTATUS status = STATUS_SUCCESS;
    PUCHAR transferBuffer;
    ULONG transferBufferLength = 0;

    UNREFERENCED_PARAMETER(OutputBufferLength);
    UNREFERENCED_PARAMETER(InputBufferLength);

    // one possible way to get context info
    pEpQContext = GetEndpointQueueContext(Queue);

    if (IoControlCode != IOCTL_INTERNAL_USB_SUBMIT_URB)
    {
        LogError(TRACE_DEVICE, "WdfRequest %p Incorrect IOCTL %x, %!STATUS!",
            Request, IoControlCode, status);
        status = STATUS_INVALID_PARAMETER;
        goto exit;
    }

    status = UdecxUrbRetrieveBuffer(Request, &transferBuffer, &transferBufferLength);
    if (!NT_SUCCESS(status))
    {
        LogError(TRACE_DEVICE, "WdfRequest %p unable to retrieve buffer %!STATUS!",
            Request, status);
        goto exit;
    }

    if (transferBufferLength >= 1)
    {
        //consume one byte of output data
        pEpQContext->global_storage = transferBuffer[0];
    }

exit:
    // writes never pended, always completed
    UdecxUrbSetBytesCompleted(Request, transferBufferLength);
    UdecxUrbCompleteWithNtStatus(Request, status);
    return;
}

클라이언트 드라이버는 DPC를 사용하여 별도의 I/O 요청을 완료할 수 있습니다. 다음 모범 사례를 따릅니다.

  • 기존 USB 드라이버와의 호환성을 보장하려면 UDE 클라이언트가 DISPATCH_LEVEL WdfRequestComplete 를 호출해야 합니다.
  • URB가 엔드포인트의 큐에 추가되고 드라이버가 호출 드라이버의 스레드 또는 DPC에서 동기적으로 처리를 시작하는 경우 요청을 동기적으로 완료하면 안 됩니다. WdfDpcEnqueue를 호출하여 드라이버가 큐에 대기하는 용도로는 별도의 DPC가 필요합니다.
  • UDE 클래스 확장이 EvtIoCanceledOnQueue 또는 EvtRequestCancel을 호출하는 경우 클라이언트 드라이버는 호출자의 스레드 또는 DPC와 별도의 DPC에서 수신된 URB를 완료해야 합니다. 이렇게 하려면 드라이버는 URB 큐에 대해 EvtIoCanceledOnQueue 콜백을 제공해야 합니다.