WinUSB 함수를 사용하여 USB 디바이스에 액세스

이 문서에는 WinUSB 함수 를 사용하여 함수 드라이버로 Winusb.sys 사용하는 USB 디바이스와 통신하는 방법에 대한 자세한 연습이 포함되어 있습니다.

요약

  • 디바이스를 열고 WinUSB 핸들을 가져옵니다.
  • 모든 인터페이스 및 해당 엔드포인트의 디바이스, 구성 및 인터페이스 설정에 대한 정보를 가져옵니다.
  • 대량 및 인터럽트 엔드포인트에 대한 데이터 읽기 및 쓰기.

중요 API

Microsoft Visual Studio 2013 사용하는 경우 WinUSB 템플릿을 사용하여 스켈레톤 앱을 만듭니다. 이 경우 1~3단계를 건너뛰고 이 문서의 4단계에서 진행합니다. 템플릿은 디바이스에 대한 파일 핸들을 열고 후속 작업에 필요한 WinUSB 핸들을 가져옵니다. 해당 핸들은 device.h의 앱 정의 DEVICE_DATA 구조에 저장됩니다.

템플릿에 대한 자세한 내용은 WinUSB 템플릿을 기반으로 Windows 데스크톱 앱 작성을 참조하세요.

참고

WinUSB 함수에는 Windows XP 이상이 필요합니다. C/C++ 애플리케이션에서 이러한 함수를 사용하여 USB 디바이스와 통신할 수 있습니다. Microsoft는 WinUSB용 관리되는 API를 제공하지 않습니다.

시작하기 전에

이 연습에는 다음 항목이 적용됩니다.

  • 이 정보는 Windows 8.1, Windows 8, Windows 7, Windows Server 2008, Windows Vista 버전의 Windows에 적용됩니다.
  • Winusb.sys 디바이스의 함수 드라이버로 설치했습니다. 이 프로세스에 대한 자세한 내용은 WinUSB(Winusb.sys) 설치를 참조하세요.
  • 이 문서의 예제는 OSR USB FX2 학습 키트 디바이스를 기반으로 합니다. 이러한 예제를 사용하여 절차를 다른 USB 디바이스로 확장할 수 있습니다.

1단계: WinUSB 템플릿을 기반으로 스켈레톤 앱 만들기

USB 디바이스에 액세스하려면 먼저 WDK(Windows 드라이버 키트)(Windows용 디버깅 도구 사용) 및 Microsoft Visual Studio의 통합 환경에 포함된 WinUSB 템플릿을 기반으로 기본 앱을 만듭니다. 템플릿을 시작점으로 사용할 수 있습니다.

템플릿 코드, 기본 앱을 만들고, 빌드하고, 배포하고, 디버그하는 방법에 대한 자세한 내용은 WinUSB 템플릿을 기반으로 Windows 데스크톱 앱 작성을 참조하세요.

템플릿은 SetupAPI 루틴을 사용하여 디바이스를 열거하고, 디바이스에 대한 파일 핸들을 열고, 후속 작업에 필요한 WinUSB 인터페이스 핸들을 만듭니다. 디바이스 핸들을 가져오고 디바이스를 여는 예제 코드는 템플릿 코드 토론을 참조하세요.

2단계: USB 설명자에 대한 디바이스 쿼리

다음으로 디바이스 속도, 인터페이스 설명자, 관련 엔드포인트 및 파이프와 같은 USB 관련 정보를 디바이스에 쿼리합니다. 이 절차는 USB 디바이스 드라이버에서 사용하는 절차와 비슷합니다. 그러나 애플리케이션은 WinUsb_GetDescriptor 호출하여 디바이스 쿼리를 완료합니다.

다음 목록에서는 USB 관련 정보를 가져오기 위해 호출할 수 있는 WinUSB 함수를 보여 줍니다.

  • 추가 디바이스 정보입니다.

    WinUsb_QueryDeviceInformation 호출하여 디바이스에 대한 디바이스 설명자에서 정보를 요청합니다. 디바이스의 속도를 얻으려면 InformationType 매개 변수에서 DEVICE_SPEED(0x01)을 설정합니다. 함수는 LowSpeed(0x01) 또는 HighSpeed(0x03)를 반환합니다.

  • 인터페이스 설명자

    WinUsb_QueryInterfaceSettings 호출하고 디바이스의 인터페이스 핸들을 전달하여 해당 인터페이스 설명자를 가져옵니다. WinUSB 인터페이스 핸들은 첫 번째 인터페이스에 해당합니다. OSR Fx2 디바이스와 같은 일부 USB 디바이스는 대체 설정 없이 하나의 인터페이스만 지원합니다. 따라서 이러한 디바이스의 경우 AlternateSettingNumber 매개 변수는 0으로 설정되고 함수는 한 번만 호출됩니다. WinUsb_QueryInterfaceSettings 호출자가 할당한 USB_INTERFACE_DESCRIPTOR 구조체( UsbAltInterfaceDescriptor 매개 변수에 전달됨)를 인터페이스에 대한 정보로 채웁니다. 예를 들어 인터페이스의 엔드포인트 수는 USB_INTERFACE_DESCRIPTORbNumEndpoints 멤버에 설정됩니다.

    여러 인터페이스를 지원하는 디바이스의 경우 WinUsb_GetAssociatedInterface 를 호출하여 AssociatedInterfaceIndex 매개 변수에서 대체 설정을 지정하여 연결된 인터페이스에 대한 인터페이스 핸들을 가져옵니다.

  • 엔드포인트

    WinUsb_QueryPipe 호출하여 각 인터페이스의 각 엔드포인트에 대한 정보를 가져옵니다. WinUsb_QueryPipe 호출자가 할당한 WINUSB_PIPE_INFORMATION 구조체를 지정된 엔드포인트의 파이프에 대한 정보로 채웁니다. 엔드포인트의 파이프는 0부터 시작하는 인덱스로 식별되며** WinUsb_QueryInterfaceSettings 대한 이전 호출에서 검색된 인터페이스 설명자의 bNumEndpoints 멤버 값보다 작아야 합니다. OSR Fx2 디바이스에는 3개의 엔드포인트가 있는 하나의 인터페이스가 있습니다. 이 디바이스의 경우 함수의 AlternateInterfaceNumber 매개 변수는 0으로 설정되고 PipeIndex 매개 변수의 값은 0에서 2로 다릅니다.

    파이프 유형을 확인하려면 WINUSB_PIPE_INFORMATION 구조체의 PipeInfo 멤버를 검사합니다. 이 멤버는 USBD_PIPE_TYPE 열거형 값 중 하나로 설정됩니다. UsbdPipeTypeControl, UsbdPipeTypeIsochronous, UsbdPipeTypeBulk 또는 UsbdPipeTypeInterrupt입니다. OSR USB FX2 디바이스는 인터럽트 파이프, 대량 파이프 및 대량 출력 파이프를 지원하므로 PipeInfo 는 UsbdPipeTypeInterrupt 또는 UsbdPipeTypeBulk로 설정됩니다. UsbdPipeTypeBulk 값은 대량 파이프를 식별하지만 파이프의 방향을 제공하지는 않습니다. 방향 정보는 파이프 주소의 상위 비트로 인코딩되며 WINUSB_PIPE_INFORMATION 구조체의 PipeId 멤버에 저장됩니다. 파이프 방향을 결정하는 가장 간단한 방법은 PipeId 값을 Usb100.h에서 다음 매크로 중 하나에 전달하는 것입니다.

    • 방향이 있으면 매크로가 USB_ENDPOINT_DIRECTION_IN (PipeId)TRUE 를 반환합니다.
    • 방향이 꺼진 경우 매크로는 USB_ENDPOINT_DIRECTION_OUT(PipeId)TRUE 를 반환합니다.

    애플리케이션은 PipeId 값을 사용하여 winUSB 함수 호출에서 데이터 전송에 사용할 파이프(예: WinUsb_ReadPipe (이 항목의 "문제 I/O 요청" 섹션에 설명됨)를 식별하므로 예제에서는 나중에 사용할 수 있도록 세 개의 PipeId 값을 모두 저장합니다.

다음 예제 코드는 WinUSB 인터페이스 핸들에 지정된 디바이스의 속도를 가져옵니다.

BOOL GetUSBDeviceSpeed(WINUSB_INTERFACE_HANDLE hDeviceHandle, UCHAR* pDeviceSpeed)
{
  if (!pDeviceSpeed || hDeviceHandle==INVALID_HANDLE_VALUE)
  {
    return FALSE;
  }

  BOOL bResult = TRUE;
  ULONG length = sizeof(UCHAR);

  bResult = WinUsb_QueryDeviceInformation(hDeviceHandle, DEVICE_SPEED, &length, pDeviceSpeed);

  if(!bResult)
  {
    printf("Error getting device speed: %d.\n", GetLastError());
    goto done;
  }

  if(*pDeviceSpeed == LowSpeed)
  {
    printf("Device speed: %d (Low speed).\n", *pDeviceSpeed);
    goto done;
  }

  if(*pDeviceSpeed == FullSpeed)
  {
    printf("Device speed: %d (Full speed).\n", *pDeviceSpeed);
    goto done;
  }

  if(*pDeviceSpeed == HighSpeed)
  {
    printf("Device speed: %d (High speed).\n", *pDeviceSpeed);
    goto done;
  }

done:
  return bResult;
}

다음 예제 코드는 WinUSB 인터페이스 핸들로 지정된 USB 디바이스에 대한 다양한 설명자를 쿼리합니다. 예제 함수는 지원되는 엔드포인트 유형과 해당 파이프 식별자를 검색합니다. 이 예제에서는 나중에 사용할 수 있는 세 개의 PipeId 값을 모두 저장합니다.

struct PIPE_ID
{
  UCHAR  PipeInId;
  UCHAR  PipeOutId;
};

BOOL QueryDeviceEndpoints (WINUSB_INTERFACE_HANDLE hDeviceHandle, PIPE_ID* pipeid)
{
  if (hDeviceHandle==INVALID_HANDLE_VALUE)
  {
    return FALSE;
  }

  BOOL bResult = TRUE;

  USB_INTERFACE_DESCRIPTOR InterfaceDescriptor;
  ZeroMemory(&InterfaceDescriptor, sizeof(USB_INTERFACE_DESCRIPTOR));

  WINUSB_PIPE_INFORMATION  Pipe;
  ZeroMemory(&Pipe, sizeof(WINUSB_PIPE_INFORMATION));

  bResult = WinUsb_QueryInterfaceSettings(hDeviceHandle, 0, &InterfaceDescriptor);

  if (bResult)
  {
    for (int index = 0; index < InterfaceDescriptor.bNumEndpoints; index++)
    {
      bResult = WinUsb_QueryPipe(hDeviceHandle, 0, index, &Pipe);

      if (bResult)
      {
        if (Pipe.PipeType == UsbdPipeTypeControl)
        {
          printf("Endpoint index: %d Pipe type: Control Pipe ID: %d.\n", index, Pipe.PipeType, Pipe.PipeId);
        }

        if (Pipe.PipeType == UsbdPipeTypeIsochronous)
        {
          printf("Endpoint index: %d Pipe type: Isochronous Pipe ID: %d.\n", index, Pipe.PipeType, Pipe.PipeId);
        }

        if (Pipe.PipeType == UsbdPipeTypeBulk)
        {
          if (USB_ENDPOINT_DIRECTION_IN(Pipe.PipeId))
          {
            printf("Endpoint index: %d Pipe type: Bulk Pipe ID: %c.\n", index, Pipe.PipeType, Pipe.PipeId);
            pipeid->PipeInId = Pipe.PipeId;
          }

          if (USB_ENDPOINT_DIRECTION_OUT(Pipe.PipeId))
          {
            printf("Endpoint index: %d Pipe type: Bulk Pipe ID: %c.\n", index, Pipe.PipeType, Pipe.PipeId);
            pipeid->PipeOutId = Pipe.PipeId;
          }
        }

        if (Pipe.PipeType == UsbdPipeTypeInterrupt)
        {
          printf("Endpoint index: %d Pipe type: Interrupt Pipe ID: %d.\n", index, Pipe.PipeType, Pipe.PipeId);
        }
      }
      else
      {
        continue;
      }
    }
  }

done:
  return bResult;
}

3단계: 기본 엔드포인트로 제어 전송 보내기

다음으로, 기본 엔드포인트에 제어 요청을 실행하여 디바이스와 통신합니다.

모든 USB 디바이스에는 인터페이스와 연결된 엔드포인트 외에 기본 엔드포인트가 있습니다. 기본 엔드포인트의 주요 목적은 디바이스를 구성하는 데 사용할 수 있는 정보를 호스트에 제공하는 것입니다. 그러나 디바이스는 디바이스별 용도로 기본 엔드포인트를 사용할 수도 있습니다. 예를 들어 OSR USB FX2 디바이스는 기본 엔드포인트를 사용하여 라이트바 및 7 세그먼트 디지털 디스플레이를 제어합니다.

제어 명령은 특정 요청을 지정하는 요청 코드와 선택적 데이터 버퍼를 포함하는 8 바이트 설정 패킷으로 구성됩니다. 요청 코드 및 버퍼 형식은 공급업체가 정의합니다. 이 예제에서 애플리케이션은 라이트바를 제어하기 위해 디바이스에 데이터를 보냅니다. 라이트바를 설정하는 코드는 0xD8 SET_BARGRAPH_DISPLAY 편의를 위해 정의됩니다. 이 요청의 경우 디바이스에는 적절한 비트를 설정하여 조명해야 하는 요소를 지정하는 1 바이트 데이터 버퍼가 필요합니다.

애플리케이션은 조명 표시줄의 요소를 지정하는 8개의 검사 상자 컨트롤 집합을 제공할 수 있습니다. 지정된 요소는 버퍼의 적절한 비트에 해당합니다. UI 코드를 방지하기 위해 이 섹션의 예제 코드는 대체 조명이 켜지도록 비트를 설정합니다.

제어 요청을 실행하려면

  1. 1 바이트 데이터 버퍼를 할당하고 적절한 비트를 설정하여 조명해야 하는 요소를 지정하는 버퍼에 데이터를 로드합니다.

  2. 호출자가 할당한 WINUSB_SETUP_PACKET 구조에서 설치 패킷을 생성합니다. 다음과 같이 요청 형식 및 데이터를 나타내도록 멤버를 초기화합니다.

    • RequestType 멤버는 요청 방향을 지정합니다. 호스트-디바이스 데이터 전송을 나타내는 0으로 설정됩니다. 디바이스-호스트 전송의 경우 RequestType을 1로 설정합니다.
    • 요청 멤버는 0xD8 이 요청에 대한 공급업체 정의 코드로 설정됩니다. 편의상 SET_BARGRAPH_DISPLAY 정의됩니다.
    • Length 멤버는 데이터 버퍼의 크기로 설정됩니다.
    • 이 요청에 는 IndexValue 멤버가 필요하지 않으므로 0으로 설정됩니다.
  3. WinUsb_ControlTransfer 호출하여 디바이스의 WinUSB 인터페이스 핸들, 설정 패킷 및 데이터 버퍼를 전달하여 요청을 기본 엔드포인트로 전송합니다. 함수는 LengthTransferred 매개 변수에서 디바이스로 전송된 바이트 수를 받습니다.

다음 코드 예제에서는 지정된 USB 디바이스에 컨트롤 요청을 보내 조명 표시줄의 조명을 제어합니다.

BOOL SendDatatoDefaultEndpoint(WINUSB_INTERFACE_HANDLE hDeviceHandle)
{
  if (hDeviceHandle==INVALID_HANDLE_VALUE)
  {
    return FALSE;
  }

  BOOL bResult = TRUE;

  UCHAR bars = 0;

  WINUSB_SETUP_PACKET SetupPacket;
  ZeroMemory(&SetupPacket, sizeof(WINUSB_SETUP_PACKET));
  ULONG cbSent = 0;

  //Set bits to light alternate bars
  for (short i = 0; i < 7; i+= 2)
  {
    bars += 1 << i;
  }

  //Create the setup packet
  SetupPacket.RequestType = 0;
  SetupPacket.Request = 0xD8;
  SetupPacket.Value = 0;
  SetupPacket.Index = 0; 
  SetupPacket.Length = sizeof(UCHAR);

  bResult = WinUsb_ControlTransfer(hDeviceHandle, SetupPacket, &bars, sizeof(UCHAR), &cbSent, 0);

  if(!bResult)
  {
    goto done;
  }

  printf("Data sent: %d \nActual data transferred: %d.\n", sizeof(bars), cbSent);

done:
  return bResult;
}

4단계: I/O 요청 발급

다음으로 읽기 및 쓰기 요청에 각각 사용할 수 있는 디바이스의 대량 및 대량 엔드포인트로 데이터를 보냅니다. OSR USB FX2 디바이스에서 이러한 두 엔드포인트는 루프백에 대해 구성되므로 디바이스는 대량 엔드포인트에서 대량 엔드포인트로 데이터를 이동합니다. 데이터의 값을 변경하거나 새 데이터를 추가하지 않습니다. 루프백 구성의 경우 읽기 요청은 가장 최근의 쓰기 요청에서 보낸 데이터를 읽습니다. WinUSB는 쓰기 및 읽기 요청을 보내기 위한 다음 함수를 제공합니다.

쓰기 요청을 보내려면

  1. 버퍼를 할당하고 디바이스에 쓰려는 데이터로 채웁니다. 애플리케이션이 파이프의 정책 유형으로 RAW_IO 설정하지 않으면 버퍼 크기에 제한이 없습니다. WinUSB는 필요한 경우 버퍼를 적절한 크기의 청크로 나눕니다. RAW_IO 설정된 경우 버퍼의 크기는 WinUSB에서 지원하는 최대 전송 크기로 제한됩니다.
  2. WinUsb_WritePipe 호출하여 디바이스에 버퍼를 씁니다. 디바이스에 대한 WinUSB 인터페이스 핸들, 대량 출력 파이프의 파이프 식별자(이 문서의 USB 설명자에 대한 디바이스 쿼리 섹션에 설명된 대로) 및 버퍼를 전달합니다. 함수는 bytesWritten 매개 변수에서 디바이스에 기록되는 바이트 수를 반환합니다. Overlapped 매개 변수는 동기 작업을 요청하기 위해 NULL로 설정됩니다. 비동기 쓰기 요청을 수행하려면 OVERLAPPED 구조체에 대한 포인터로 Overlapped를 설정합니다.

길이가 0인 데이터가 포함된 쓰기 요청은 USB 스택 아래로 전달됩니다. 전송 길이가 최대 전송 길이보다 큰 경우 WinUSB는 요청을 최대 전송 길이의 더 작은 요청으로 나누고 직렬로 제출합니다. 다음 코드 예제에서는 문자열을 할당하고 디바이스의 대량 엔드포인트로 보냅니다.

BOOL WriteToBulkEndpoint(WINUSB_INTERFACE_HANDLE hDeviceHandle, UCHAR* pID, ULONG* pcbWritten)
{
  if (hDeviceHandle==INVALID_HANDLE_VALUE || !pID || !pcbWritten)
  {
    return FALSE;
  }

  BOOL bResult = TRUE;

  UCHAR szBuffer[] = "Hello World";
  ULONG cbSize = strlen(szBuffer);
  ULONG cbSent = 0;

  bResult = WinUsb_WritePipe(hDeviceHandle, *pID, szBuffer, cbSize, &cbSent, 0);

  if(!bResult)
  {
    goto done;
  }

  printf("Wrote to pipe %d: %s \nActual data transferred: %d.\n", *pID, szBuffer, cbSent);
  *pcbWritten = cbSent;

done:
  return bResult;
}

읽기 요청을 보내려면

  • WinUsb_ReadPipe 호출하여 디바이스의 대량 엔드포인트에서 데이터를 읽습니다. 디바이스의 WinUSB 인터페이스 핸들, 대량 엔드포인트의 파이프 식별자 및 적절한 크기의 빈 버퍼를 전달합니다. 함수가 반환되면 버퍼에는 디바이스에서 읽은 데이터가 포함됩니다. 읽은 바이트 수는 함수의 bytesRead 매개 변수에 반환됩니다. 읽기 요청의 경우 버퍼는 최대 패킷 크기의 배수여야 합니다.

길이가 0인 읽기 요청은 성공으로 즉시 완료되며 스택 아래로 전송되지 않습니다. 전송 길이가 최대 전송 길이보다 큰 경우 WinUSB는 요청을 최대 전송 길이의 더 작은 요청으로 나누고 직렬로 제출합니다. 전송 길이가 엔드포인트의 MaxPacketSize 배수가 아닌 경우 WinUSB는 전송 크기를 MaxPacketSize의 다음 배수로 늘입니다. 디바이스가 요청된 것보다 더 많은 데이터를 반환하는 경우 WinUSB는 초과 데이터를 저장합니다. 데이터가 이전 읽기 요청에서 남아 있는 경우 WinUSB는 데이터를 다음 읽기 요청의 시작 부분에 복사하고 필요한 경우 요청을 완료합니다. 다음 코드 예제에서는 디바이스의 대량 엔드포인트에서 데이터를 읽습니다.

BOOL ReadFromBulkEndpoint(WINUSB_INTERFACE_HANDLE hDeviceHandle, UCHAR* pID, ULONG cbSize)
{
  if (hDeviceHandle==INVALID_HANDLE_VALUE)
  {
    return FALSE;
  }

  BOOL bResult = TRUE;

  UCHAR* szBuffer = (UCHAR*)LocalAlloc(LPTR, sizeof(UCHAR)*cbSize);
  ULONG cbRead = 0;

  bResult = WinUsb_ReadPipe(hDeviceHandle, *pID, szBuffer, cbSize, &cbRead, 0);

  if(!bResult)
  {
    goto done;
  }

  printf("Read from pipe %d: %s \nActual data read: %d.\n", *pID, szBuffer, cbRead);

done:
  LocalFree(szBuffer);
  return bResult;
}

5단계: 디바이스 핸들 해제

디바이스에 필요한 모든 호출을 완료한 후 다음 함수를 호출하여 디바이스에 대한 파일 핸들 및 WinUSB 인터페이스 핸들을 해제합니다.

  • 1단계에서 설명한 대로 CreateFile에서 만든 핸들을 해제하는 CloseHandle입니다.
  • WinUsb_Free **WinUsb_Initialize 반환되는 디바이스에 대한 WinUSB 인터페이스 핸들을 해제합니다.

6단계: 기본 함수 구현

다음 코드 예제에서는 콘솔 애플리케이션의 기본 함수를 보여줍니다.

디바이스 핸들을 가져오고 디바이스를 여는 예제 코드(이 예제에서는 GetDeviceHandleGetWinUSBHandle )는 템플릿 코드 토론을 참조하세요.

int _tmain(int argc, _TCHAR* argv[])
{

  GUID guidDeviceInterface = OSR_DEVICE_INTERFACE; //in the INF file
  BOOL bResult = TRUE;
  PIPE_ID PipeID;
  HANDLE hDeviceHandle = INVALID_HANDLE_VALUE;
  WINUSB_INTERFACE_HANDLE hWinUSBHandle = INVALID_HANDLE_VALUE;
  UCHAR DeviceSpeed;
  ULONG cbSize = 0;

  bResult = GetDeviceHandle(guidDeviceInterface, &hDeviceHandle);

  if(!bResult)
  {
    goto done;
  }

  bResult = GetWinUSBHandle(hDeviceHandle, &hWinUSBHandle);

  if(!bResult)
  {
    goto done;
  }

  bResult = GetUSBDeviceSpeed(hWinUSBHandle, &DeviceSpeed);

  if(!bResult)
  {
    goto done;
  }

  bResult = QueryDeviceEndpoints(hWinUSBHandle, &PipeID);

  if(!bResult)
  {
    goto done;
  }

  bResult = SendDatatoDefaultEndpoint(hWinUSBHandle);

  if(!bResult)
  {
    goto done;
  }

  bResult = WriteToBulkEndpoint(hWinUSBHandle, &PipeID.PipeOutId, &cbSize);

  if(!bResult)
  {
    goto done;
  }

  bResult = ReadFromBulkEndpoint(hWinUSBHandle, &PipeID.PipeInId, cbSize);

  if(!bResult)
  {
    goto done;
  }

  system("PAUSE");

done:
  CloseHandle(hDeviceHandle);
  WinUsb_Free(hWinUSBHandle);

  return 0;
}

다음 단계

디바이스가 등시 엔드포인트를 지원하는 경우 WinUSB 함수를 사용하여 전송을 보낼 수 있습니다. 이 기능은 Windows 8.1 지원됩니다. 자세한 내용은 WinUSB 데스크톱 앱에서 USB 등시 전송 보내기를 참조하세요.

추가 정보