Udostępnij za pośrednictwem


Uzyskiwanie dostępu do urządzenia USB przy użyciu funkcji WinUSB

Ten artykuł zawiera szczegółowy przewodnik dotyczący używania funkcji WinUSB do komunikowania się z urządzeniem USB, które używa Winusb.sys jako sterownika funkcji.

Streszczenie

  • Otwieranie urządzenia i uzyskiwanie uchwytu WinUSB.
  • Uzyskiwanie informacji o ustawieniach urządzenia, konfiguracji i interfejsu wszystkich interfejsów oraz ich punktów końcowych.
  • Odczytywanie i zapisywanie danych w punktach końcowych typu bulk i typu interrupt.

Ważne interfejsy API

Jeśli używasz programu Microsoft Visual Studio 2013, utwórz szkielet aplikacji przy użyciu szablonu WinUSB. W takim przypadku pomiń kroki od 1 do 3 i przejdź z kroku 4 w tym artykule. Szablon otwiera dojście do pliku na urządzeniu i uzyskuje dojście WinUSB wymagane do kolejnych operacji. Ten uchwyt jest przechowywany w strukturze DEVICE_DATA zdefiniowanej przez aplikację w pliku device.h.

Aby uzyskać więcej informacji na temat szablonu, zobacz Pisanie aplikacji klasycznej systemu Windows na podstawie szablonu WinUSB.

Notatka

Funkcje WinUSB wymagają systemu Windows XP lub nowszego. Te funkcje można używać w aplikacji C/C++ do komunikowania się z urządzeniem USB. Firma Microsoft nie udostępnia zarządzanego interfejsu API dla winUSB.

Przed rozpoczęciem

Następujące elementy mają zastosowanie do tego przewodnika:

  • Te informacje dotyczą systemów Windows 8.1, Windows 8, Windows 7, Windows Server 2008, Windows Vista w wersjach systemu Windows.
  • Winusb.sys jest instalowany jako sterownik funkcji urządzenia. Aby uzyskać więcej informacji na temat tego procesu, zobacz WinUSB (Winusb.sys) Instalacja.
  • Przykłady w tym artykule są oparte na urządzeniu OSR USB FX2 Learning Kit. Możesz użyć tych przykładów, aby rozszerzyć procedury na inne urządzenia USB.

Krok 1. Tworzenie szkieletowej aplikacji na podstawie szablonu WinUSB

Aby uzyskać dostęp do urządzenia USB, zacznij od utworzenia szkieletowej aplikacji opartej na szablonie WinUSB dołączonym do zintegrowanego środowiska zestawu Windows Driver Kit (WDK) (z narzędziami debugowania dla systemu Windows) i programem Microsoft Visual Studio. Szablon można użyć jako punktu wyjścia.

Aby uzyskać informacje o kodzie szablonu, sposobie tworzenia, kompilowania, wdrażania i debugowania szkieletowej aplikacji, zobacz Pisanie aplikacji klasycznej systemu Windows na podstawie szablonu WinUSB.

Szablon wylicza urządzenia przy użyciu SetupAPI procedur, otwiera dojście do plików dla urządzenia i tworzy uchwyt interfejsu WinUSB wymagany do kolejnych zadań. Aby zobaczyć przykładowy kod pobierający uchwyt urządzenia i otwierający urządzenie, przejdź do sekcji Omówienie kodu szablonu.

Krok 2: Zapytanie urządzenia o deskryptory USB

Następnie wykonaj zapytanie dotyczące urządzenia w kontekście informacji specyficznych dla USB, takich jak szybkość urządzenia, deskryptory interfejsu, odpowiednie punkty końcowe i ich kanały. Procedura jest podobna do procedury używanej przez sterowniki urządzeń USB. Jednak aplikacja wykonuje zapytania dotyczące urządzeń, wywołując WinUsb_GetDescriptor.

Na poniższej liście przedstawiono funkcje WinUSB, które można wywołać w celu uzyskania informacji specyficznych dla portu USB:

  • Więcej informacji o urządzeniu.

    Wywołaj WinUsb_QueryDeviceInformation, aby zażądać informacji z deskryptorów urządzenia. Aby uzyskać szybkość urządzenia, ustaw DEVICE_SPEED (0x01) w parametrze InformationType. Funkcja zwraca wartość LowSpeed (0x01) lub HighSpeed (0x03).

  • Deskryptory interfejsu

    Wywołaj WinUsb_QueryInterfaceSettings i przekaż uchwyty interfejsu urządzenia, aby uzyskać odpowiednie deskryptory interfejsu. Uchwyt interfejsu WinUSB odpowiada pierwszemu interfejsowi. Niektóre urządzenia USB, takie jak urządzenie OSR Fx2, obsługują tylko jeden interfejs bez żadnego alternatywnego ustawienia. W związku z tym dla tych urządzeń parametr AlternateSettingNumber jest ustawiony na zero, a funkcja jest wywoływana tylko raz. WinUsb_QueryInterfaceSettings wypełnia przydzieloną przez wywołującego strukturę USB_INTERFACE_DESCRIPTOR (przekazaną w parametrze UsbAltInterfaceDescriptor) informacjami o interfejsie. Na przykład liczba punktów końcowych w interfejsie jest ustawiona w bNumEndpoints elementu członkowskiego USB_INTERFACE_DESCRIPTOR.

    W przypadku urządzeń obsługujących wiele interfejsów, wywołaj WinUsb_GetAssociatedInterface, aby uzyskać uchwyty dla skojarzonych interfejsów, podając ustawienia alternatywne w parametrze AssociatedInterfaceIndex.

  • Punkty końcowe

    Wywołaj WinUsb_QueryPipe, aby uzyskać informacje o każdym punkcie końcowym w każdym interfejsie. WinUsb_QueryPipe wypełnia strukturę WINUSB_PIPE_INFORMATION, która została przydzielona przez wywołującego, informacjami o potoku określonego punktu końcowego. Indeks zaczynający się od zera identyfikuje potoki punktów końcowych i musi być mniejszy niż wartość w bNumEndpoints członka deskryptora interfejsu pobranego w poprzednim wywołaniu **WinUsb_QueryInterfaceSettings. Urządzenie OSR Fx2 ma jeden interfejs, który ma trzy punkty końcowe. W przypadku tego urządzenia parametr AlternateInterfaceNumber funkcji ma wartość 0, a wartość parametru PipeIndex wynosi od 0 do 2.

    Aby określić typ potoku, zbadaj element członkowski struktury WINUSB_PIPE_INFORMATION. Ten członek jest ustawiony na jedną z wartości wyliczenia USBD_PIPE_TYPE: UsbdPipeTypeControl, UsbdPipeTypeIsochronous, UsbdPipeTypeBulk lub UsbdPipeTypeInterrupt. Urządzenie OSR USB FX2 obsługuje potok przerwania, potok zbiorczy i potok wyjściowy, więc PipeInfo jest ustawiony na UsbdPipeTypeInterrupt lub UsbdPipeTypeBulk. Wartość UsbdPipeTypeBulk identyfikuje potoki zbiorcze, ale nie zapewnia kierunku potoku. Informacje o kierunku są kodowane w najwyższym bicie adresu potoku, który jest przechowywany w elemencie PipeId struktury WINUSB_PIPE_INFORMATION. Najprostszym sposobem określenia kierunku rury jest przekazanie wartości PipeId do jednego z następujących makr z Usb100.h:

    • Makro USB_ENDPOINT_DIRECTION_IN (PipeId) zwraca TRUE, jeśli kierunek jest do wewnątrz.
    • Makro USB_ENDPOINT_DIRECTION_OUT(PipeId) zwraca TRUE, jeśli kierunek jest na zewnątrz.

    Aplikacja używa wartości PipeId, aby zidentyfikować, który potok użyć do transferu danych w wywołaniach funkcji WinUSB, takich jak WinUsb_ReadPipe (opisane w sekcji "Realizacja żądań I/O" w tym temacie), więc przykład przechowuje wszystkie trzy wartości PipeId do późniejszego użycia.

Poniższy przykładowy kod pobiera szybkość urządzenia określonego przez uchwyt interfejsu 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;
}

Poniższy przykładowy kod wykonuje zapytania dotyczące różnych deskryptorów dla urządzenia USB określonego przez uchwyt interfejsu WinUSB. Przykładowa funkcja pobiera typy obsługiwanych punktów końcowych i ich identyfikatory potoku. W przykładzie są przechowywane wszystkie trzy wartości PipeId do późniejszego użycia.

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;
}

Krok 3: Wysyłanie transferu kontrolnego do domyślnego punktu końcowego

Następnie skomunikuj się z urządzeniem, wysyłając żądanie kontroli do domyślnego punktu końcowego.

Wszystkie urządzenia USB mają domyślny punkt końcowy oprócz punktów końcowych skojarzonych z interfejsami. Podstawowym celem domyślnego punktu końcowego jest dostarczenie hostowi informacji, których może użyć do skonfigurowania urządzenia. Jednak urządzenia mogą również używać domyślnego punktu końcowego do celów specyficznych dla urządzenia. Na przykład urządzenie OSR USB FX2 używa domyślnego punktu końcowego do sterowania paskiem świetlnym i siedmiosegmentowym wyświetlaczem cyfrowym.

Polecenia sterujące składają się z 8-bajtowego pakietu instalacyjnego, który zawiera kod żądania określający określone żądanie i opcjonalny bufor danych. Kody żądań i formaty buforu są definiowane przez dostawcę. W tym przykładzie aplikacja wysyła dane do urządzenia w celu kontrolowania paska światła. Kod ustawiający pasek świetlny jest 0xD8, który jest zdefiniowany dla wygody jako SET_BARGRAPH_DISPLAY. W przypadku tego żądania urządzenie wymaga buforu danych 1-bajtowego, który określa, które elementy powinny być oświetlone przez ustawienie odpowiednich bitów.

Aplikacja może udostępnić zestaw ośmiu kontrolek pól wyboru, aby określić, które elementy paska światła powinny być oświetlone. Określone elementy odpowiadają odpowiednim bitom w buforze. Aby uniknąć kodu UI, w przykładowym kodzie w tej sekcji bity są ustawione tak, aby świeciły się na przemian lampki.

Aby wydać żądanie kontroli

  1. Przydziel bufor danych 1-bajtowy i załaduj dane do buforu, który określa elementy, które powinny być oświetlone przez ustawienie odpowiednich bitów.

  2. Konstruowanie pakietu instalacyjnego w strukturze WINUSB_SETUP_PACKET przydzielonej przez obiekt wywołujący. Zainicjuj członków, aby reprezentować typ żądania i dane w następujący sposób:

    • Członek RequestType określa kierunek żądania. właściwość RequestType jest ustawiona na 0, co wskazuje transfer danych z hosta do urządzenia. W przypadku transferów z urządzenia do hosta ustaw RequestType na 1.
    • Członek żądania jest ustawiony na kod zdefiniowany przez dostawcę dla tego żądania, 0xD8. Żądanie jest definiowane jako SET_BARGRAPH_DISPLAY dla wygody.
    • Element członkowski długości jest ustawiony na rozmiar buforu danych.
    • Członkowie indeksu i wartości nie są wymagani w tym żądaniu, więc są ustawione na zero.
  3. Wywołaj WinUsb_ControlTransfer, aby przesłać żądanie do domyślnego punktu końcowego, przekazując dojście interfejsu WinUSB urządzenia, pakiet instalacyjny i bufor danych. Funkcja odbiera liczbę bajtów przesłanych do urządzenia w parametrze LengthTransferred.

Poniższy przykład kodu wysyła żądanie sterowania do określonego urządzenia USB w celu sterowania światłami na pasku świetlnym.

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;
}

Krok 4: Wydawaj żądania we/wy

Następnie wyślij dane do końcówek 'bulk-in' i 'bulk-out' urządzenia, które mogą być używane odpowiednio do żądań odczytu i zapisu. Na urządzeniu OSR USB FX2 te dwa punkty końcowe są skonfigurowane do pętli zwrotnej, dzięki czemu urządzenie przenosi dane z punktu końcowego wejściowego zbiorczego do punktu końcowego wyjściowego zbiorczego. Nie zmienia wartości danych ani nie dodaje żadnych nowych danych. W przypadku konfiguracji sprzężenia zwrotnego żądanie odczytu odczytuje dane wysyłane przez najnowsze żądanie zapisu. Usługa WinUSB udostępnia następujące funkcje wysyłania żądań zapisu i odczytu:

Aby wysłać żądanie zapisu

  1. Przydziel bufor i wypełnij go danymi, które chcesz zapisać na urządzeniu. Rozmiar buforu nie jest ograniczony, jeśli aplikacja nie ustawia RAW_IO jako typ zasad potoku. WinUSB dzieli bufor na odpowiednio rozmiarowane fragmenty, jeśli to konieczne. Jeśli RAW_IO jest ustawiona, rozmiar buforu jest ograniczony przez maksymalny rozmiar transferu obsługiwany przez winUSB.
  2. Wywołaj WinUsb_WritePipe, aby zapisać bufor na urządzeniu. Przekaż uchwyt interfejsu WinUSB dla urządzenia, identyfikator potoku dla potoku zbiorczego (zgodnie z opisem w Zapytanie o urządzenie dla deskryptorów USB sekcji tego artykułu) i buforu. Funkcja zwraca liczbę bajtów zapisywanych na urządzeniu w parametrze bytesWritten. Parametr Overlapped jest ustawiony na wartość NULL, aby zażądać operacji synchronicznej. Aby wykonać asynchroniczne żądanie zapisu, ustaw Overlapped na wskaźnik na strukturę OVERLAPPED.

Żądania zapisu zawierające dane o zerowej długości są przekazywane w dół stosu USB. Jeśli długość transferu jest większa niż maksymalna długość transferu, winUSB dzieli żądanie na mniejsze żądania o maksymalnej długości transferu i przesyła je szeregowo. Poniższy przykład kodu przydziela ciąg i wysyła go do zbiorczego punktu końcowego urządzenia.

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;
}

Aby wysłać żądanie odczytu

  • Wywołaj WinUsb_ReadPipe, aby odczytać dane z punktu końcowego bulk-in urządzenia. Podaj uchwyt interfejsu WinUSB urządzenia, identyfikator potoku dla punktu końcowego wejściowego typu bulk oraz odpowiednio rozmiar pustego buforu. Po powrocie funkcji bufor zawiera dane odczytane z urządzenia. Liczba odczytanych bajtów jest zwracana w parametrze bytesRead funkcji. W przypadku żądań odczytu bufor musi być wielokrotnym maksymalnym rozmiarem pakietu.

Żądania odczytu o zerowej długości są natychmiast zakończone sukcesem i nie są przesyłane dalej w stosie. Jeśli długość transferu jest większa niż maksymalna długość transferu, winUSB dzieli żądanie na mniejsze żądania o maksymalnej długości transferu i przesyła je szeregowo. Jeśli długość transferu nie jest wielokrotnością MaxPacketSize punktu końcowego, WinUSB zwiększa rozmiar transferu do kolejnej wielokrotności MaxPacketSize. Jeśli urządzenie zwraca więcej danych niż zażądano, usługa WinUSB zapisuje nadmiar danych. Jeśli dane pozostają z poprzedniego żądania odczytu, WinUSB kopiuje je na początku następnego żądania odczytu i kończy żądanie, jeśli to konieczne. Poniższy przykład kodu odczytuje dane z punktu końcowego typu bulk-in urządzenia.

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;
}

Krok 5: Zwolnij uchwyty urządzenia

Po zakończeniu wszystkich wymaganych wywołań do urządzenia, zwolnij uchwyt do pliku oraz uchwyt interfejsu WinUSB dla urządzenia, wywołując następujące funkcje:

  • CloseHandle, aby zwolnić dojście, które zostało utworzone przez CreateFile, zgodnie z opisem w kroku 1.
  • WinUsb_Free, aby zwolnić uchwyt interfejsu WinUSB dla urządzenia, który jest zwracany przez **WinUsb_Initialize.

Krok 6. Implementowanie funkcji main

Poniższy przykład kodu przedstawia główną funkcję aplikacji konsolowej.

Na przykład kod, który pobiera uchwyt urządzenia i otwiera urządzenie (GetDeviceHandle i GetWinUSBHandle w tym przykładzie), zobacz Omówienie kodu szablonu.

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;
}

Następne kroki

Jeśli urządzenie obsługuje izochroniczne punkty końcowe, możesz użyć funkcji WinUSB do wysyłania transferów. Ta funkcja jest obsługiwana tylko w systemie Windows 8.1. Aby uzyskać więcej informacji, zobacz Wysyłanie izochronicznych transferów USB z aplikacji klasycznej WinUSB.

Zobacz też