Accéder à un périphérique USB à l’aide des fonctions WinUSB

Cet article inclut une procédure pas à pas détaillée de l’utilisation des fonctions WinUSB pour communiquer avec un périphérique USB qui utilise Winusb.sys comme pilote de fonction.

Résumé

  • Ouverture de l’appareil et obtention du handle WinUSB.
  • Obtention d’informations sur l’appareil, la configuration et les paramètres d’interface de toutes les interfaces et de leurs points de terminaison.
  • Lecture et écriture de données dans des points de terminaison en bloc et interruption.

API importantes

Si vous utilisez Microsoft Visual Studio 2013, créez votre application squelette à l’aide du modèle WinUSB. Dans ce cas, ignorez les étapes 1 à 3 et passez à l’étape 4 de cet article. Le modèle ouvre un handle de fichier sur l’appareil et obtient le handle WinUSB requis pour les opérations suivantes. Ce handle est stocké dans la structure DEVICE_DATA définie par l’application dans device.h.

Pour plus d’informations sur le modèle, consultez Écrire une application de bureau Windows basée sur le modèle WinUSB.

Notes

Les fonctions WinUSB nécessitent Windows XP ou version ultérieure. Vous pouvez utiliser ces fonctions dans votre application C/C++ pour communiquer avec votre périphérique USB. Microsoft ne fournit pas d’API managée pour WinUSB.

Avant de commencer

Les éléments suivants s’appliquent à cette procédure pas à pas :

  • Ces informations s’appliquent aux versions Windows 8.1, Windows 8, Windows 7, Windows Server 2008 et Windows Vista.
  • Vous avez installé Winusb.sys en tant que pilote de fonction de l’appareil. Pour plus d’informations sur ce processus, consultez Installation de WinUSB (Winusb.sys).
  • Les exemples de cet article sont basés sur le périphérique OSR USB FX2 Learning Kit. Vous pouvez utiliser ces exemples pour étendre les procédures à d’autres périphériques USB.

Étape 1 : Créer une application squelette basée sur le modèle WinUSB

Pour accéder à un périphérique USB, commencez par créer une application squelette basée sur le modèle WinUSB inclus dans l’environnement intégré du Kit de pilotes Windows (WDK) (avec les outils de débogage pour Windows) et de Microsoft Visual Studio. Vous pouvez utiliser le modèle comme point de départ.

Pour plus d’informations sur le code du modèle et sur la création, la création, le déploiement et le débogage de l’application squelette, consultez Écrire une application de bureau Windows basée sur le modèle WinUSB.

Le modèle énumère les appareils à l’aide de routines SetupAPI , ouvre un handle de fichier pour l’appareil et crée un handle d’interface WinUSB requis pour les tâches suivantes. Pour obtenir un exemple de code qui obtient le handle d’appareil et ouvre l’appareil, consultez Discussion du code de modèle.

Étape 2 : Interroger l’appareil pour les descripteurs USB

Ensuite, interrogez l’appareil pour obtenir des informations spécifiques à l’USB, telles que la vitesse de l’appareil, les descripteurs d’interface, les points de terminaison associés et leurs canaux. La procédure est similaire à celle utilisée par les pilotes de périphérique USB. Toutefois, l’application termine les requêtes d’appareil en appelant WinUsb_GetDescriptor.

La liste suivante présente les fonctions WinUSB que vous pouvez appeler pour obtenir des informations spécifiques à USB :

  • Informations supplémentaires sur l’appareil.

    Appelez WinUsb_QueryDeviceInformation pour demander des informations aux descripteurs d’appareil pour l’appareil. Pour obtenir la vitesse de l’appareil, définissez DEVICE_SPEED (0x01) dans le paramètre InformationType . La fonction retourne LowSpeed (0x01) ou HighSpeed (0x03).

  • Descripteurs d’interface

    Appelez WinUsb_QueryInterfaceSettings et transmettez les handles d’interface de l’appareil pour obtenir les descripteurs d’interface correspondants. Le handle d’interface WinUSB correspond à la première interface. Certains périphériques USB, tels que l’appareil OSR Fx2, ne prennent en charge qu’une seule interface sans autre paramètre. Par conséquent, pour ces appareils, le paramètre AlternateSettingNumber est défini sur zéro et la fonction n’est appelée qu’une seule fois. WinUsb_QueryInterfaceSettings remplit la structure de USB_INTERFACE_DESCRIPTOR allouée par l’appelant (transmise dans le paramètre UsbAltInterfaceDescriptor ) avec des informations sur l’interface. Par exemple, le nombre de points de terminaison dans l’interface est défini dans le membre bNumEndpoints de USB_INTERFACE_DESCRIPTOR.

    Pour les appareils qui prennent en charge plusieurs interfaces, appelez WinUsb_GetAssociatedInterface pour obtenir des handles d’interface pour les interfaces associées en spécifiant les autres paramètres dans le paramètre AssociatedInterfaceIndex .

  • Points de terminaison

    Appelez WinUsb_QueryPipe pour obtenir des informations sur chaque point de terminaison sur chaque interface. WinUsb_QueryPipe remplit la structure de WINUSB_PIPE_INFORMATION allouée par l’appelant avec des informations sur le canal du point de terminaison spécifié. Les canaux des points de terminaison sont identifiés par un index de base zéro et doivent être inférieurs à la valeur du membre bNumEndpoints du descripteur d’interface récupéré dans l’appel précédent à **WinUsb_QueryInterfaceSettings. L’appareil OSR Fx2 a une interface qui a trois points de terminaison. Pour cet appareil, le paramètre AlternateInterfaceNumber de la fonction est défini sur 0 et la valeur du paramètre PipeIndex varie de 0 à 2.

    Pour déterminer le type de canal, examinez le membre PipeInfo de la structure WINUSB_PIPE_INFORMATION. Ce membre est défini sur l’une des valeurs d’énumération USBD_PIPE_TYPE : UsbdPipeTypeControl, UsbdPipeTypeIsochronous, UsbdPipeTypeBulk ou UsbdPipeTypeInterrupt. L’appareil OSR USB FX2 prend en charge un canal d’interruption, un canal en bloc et un canal de sortie en bloc. PipeInfo est donc défini sur UsbdPipeTypeInterrupt ou UsbdPipeTypeBulk. La valeur UsbdPipeTypeBulk identifie les canaux en bloc, mais ne fournit pas la direction du canal. Les informations de direction sont encodées dans le bit élevé de l’adresse du canal, qui est stocké dans le membre PipeId de la structure WINUSB_PIPE_INFORMATION. Le moyen le plus simple de déterminer la direction du canal consiste à passer la valeur PipeId à l’une des macros suivantes à partir d’Usb100.h :

    • La USB_ENDPOINT_DIRECTION_IN (PipeId) macro retourne TRUE si la direction est dans.
    • La USB_ENDPOINT_DIRECTION_OUT(PipeId) macro retourne TRUE si la direction est sortie.

    L’application utilise la valeur PipeId pour identifier le canal à utiliser pour le transfert de données dans les appels à des fonctions WinUSB, telles que WinUsb_ReadPipe (décrite dans la section « Demandes d’E/S d’émission » de cette rubrique), de sorte que l’exemple stocke les trois valeurs PipeId pour une utilisation ultérieure.

L’exemple de code suivant obtient la vitesse de l’appareil spécifiée par le handle d’interface 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;
}

L’exemple de code suivant interroge les différents descripteurs pour le périphérique USB spécifié par le handle d’interface WinUSB. L’exemple de fonction récupère les types de points de terminaison pris en charge et leurs identificateurs de canal. L’exemple stocke les trois valeurs PipeId pour une utilisation ultérieure.

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

Étape 3 : Envoyer le transfert de contrôle au point de terminaison par défaut

Ensuite, communiquez avec l’appareil en envoyant une demande de contrôle au point de terminaison par défaut.

Tous les périphériques USB ont un point de terminaison par défaut en plus des points de terminaison associés aux interfaces. L’objectif principal du point de terminaison par défaut est de fournir à l’hôte des informations qu’il peut utiliser pour configurer l’appareil. Toutefois, les appareils peuvent également utiliser le point de terminaison par défaut à des fins spécifiques de l’appareil. Par exemple, le périphérique USB FX2 OSR utilise le point de terminaison par défaut pour contrôler la barre d’éclairage et l’affichage numérique à sept segments.

Les commandes de contrôle se composent d’un paquet d’installation de 8 octets, qui comprend un code de requête qui spécifie la requête particulière et une mémoire tampon de données facultative. Les codes de requête et les formats de mémoire tampon sont définis par le fournisseur. Dans cet exemple, l’application envoie des données à l’appareil pour contrôler la barre d’éclairage. Le code permettant de définir la barre de lumière est 0xD8, qui est défini par souci de commodité comme SET_BARGRAPH_DISPLAY. Pour cette demande, l’appareil a besoin d’une mémoire tampon de données de 1 octet qui spécifie quels éléments doivent être éclairés en définissant les bits appropriés.

L’application peut fournir un ensemble de huit contrôles de boîte de case activée pour spécifier les éléments de la barre lumineuse à allumer. Les éléments spécifiés correspondent aux bits appropriés dans la mémoire tampon. Pour éviter le code de l’interface utilisateur, l’exemple de code de cette section définit les bits afin que d’autres lumières s’allument.

Pour émettre une demande de contrôle

  1. Allouez une mémoire tampon de données de 1 octet et chargez les données dans la mémoire tampon qui spécifie les éléments qui doivent être éclairés en définissant les bits appropriés.

  2. Construisez un paquet d’installation dans une structure de WINUSB_SETUP_PACKET allouée par l’appelant . Initialisez les membres pour représenter le type de requête et les données comme suit :

    • Le membre RequestType spécifie le sens de la requête. Il est défini sur 0, ce qui indique le transfert de données hôte à appareil. Pour les transferts appareil à hôte, définissez RequestType sur 1.
    • Le membre Request est défini sur le code défini par le fournisseur pour cette demande, 0xD8. Il est défini par commodité comme SET_BARGRAPH_DISPLAY.
    • Le membre Length est défini sur la taille de la mémoire tampon de données.
    • Les membres Index et Value ne sont pas requis pour cette requête, ils sont donc définis sur zéro.
  3. Appelez WinUsb_ControlTransfer pour transmettre la demande au point de terminaison par défaut en passant le handle d’interface WinUSB de l’appareil, le paquet d’installation et la mémoire tampon de données. La fonction reçoit le nombre d’octets qui ont été transférés vers l’appareil dans le paramètre LengthTransferred .

L’exemple de code suivant envoie une demande de contrôle au périphérique USB spécifié pour contrôler les lumières de la barre d’éclairage.

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

Étape 4 : Émettre des demandes d’E/S

Ensuite, envoyez des données aux points de terminaison en bloc et en bloc de l’appareil qui peuvent être utilisés pour les demandes de lecture et d’écriture, respectivement. Sur l’appareil OSR USB FX2, ces deux points de terminaison sont configurés pour le bouclage, de sorte que l’appareil déplace les données du point de terminaison en bloc vers le point de terminaison en bloc. Elle ne modifie pas la valeur des données ni n’ajoute de nouvelles données. Pour la configuration du bouclage, une demande de lecture lit les données qui ont été envoyées par la demande d’écriture la plus récente. WinUSB fournit les fonctions suivantes pour l’envoi de demandes d’écriture et de lecture :

Pour envoyer une demande d’écriture

  1. Allouez une mémoire tampon et remplissez-la avec les données que vous souhaitez écrire sur l’appareil. La taille de la mémoire tampon n’est pas limitée si l’application ne définit pas RAW_IO comme type de stratégie du canal. WinUSB divise la mémoire tampon en blocs de taille appropriée, si nécessaire. Si RAW_IO est défini, la taille de la mémoire tampon est limitée par la taille de transfert maximale prise en charge par WinUSB.
  2. Appelez WinUsb_WritePipe pour écrire la mémoire tampon sur l’appareil. Transmettez le handle d’interface WinUSB pour l’appareil, l’identificateur du canal de sortie en bloc (comme décrit dans la section Interroger le périphérique pour les descripteurs USB de cet article) et la mémoire tampon. La fonction retourne le nombre d’octets écrits sur l’appareil dans le paramètre bytesWritten . Le paramètre Chevauchement est défini sur NULL pour demander une opération synchrone. Pour effectuer une demande d’écriture asynchrone, définissez Chevauchement sur un pointeur vers une structure OVERLAPPED .

Les demandes d’écriture qui contiennent des données de longueur nulle sont transférées vers le bas de la pile USB. Si la longueur de transfert est supérieure à une longueur de transfert maximale, WinUSB divise la requête en demandes plus petites de la longueur maximale de transfert et les envoie en série. L’exemple de code suivant alloue une chaîne et l’envoie au point de terminaison en bloc de l’appareil.

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

Pour envoyer une demande de lecture

  • Appelez WinUsb_ReadPipe pour lire les données à partir du point de terminaison en bloc de l’appareil. Transmettez le handle d’interface WinUSB de l’appareil, l’identificateur de canal pour le point de terminaison en bloc et une mémoire tampon vide de taille appropriée. Lorsque la fonction retourne, la mémoire tampon contient les données lues à partir de l’appareil. Le nombre d’octets lus est retourné dans le paramètre bytesRead de la fonction. Pour les demandes de lecture, la mémoire tampon doit être un multiple de la taille maximale des paquets.

Les demandes de lecture de longueur nulle se terminent immédiatement avec succès et ne sont pas envoyées dans la pile. Si la longueur de transfert est supérieure à une longueur de transfert maximale, WinUSB divise la requête en demandes plus petites de la longueur maximale de transfert et les envoie en série. Si la longueur de transfert n’est pas un multiple de MaxPacketSize du point de terminaison, WinUSB augmente la taille du transfert au multiple suivant de MaxPacketSize. Si un appareil retourne plus de données que demandé, WinUSB enregistre les données excédentaires. Si des données restent d’une demande de lecture précédente, WinUSB les copie au début de la demande de lecture suivante et termine la demande, si nécessaire. L’exemple de code suivant lit les données du point de terminaison en bloc de l’appareil.

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

Étape 5 : Libérer les handles d’appareil

Une fois que vous avez effectué tous les appels requis à l’appareil, relâchez le handle de fichier et le handle d’interface WinUSB pour l’appareil en appelant les fonctions suivantes :

  • CloseHandle pour libérer le handle créé par CreateFile, comme décrit à l’étape 1.
  • WinUsb_Free libérer le handle d’interface WinUSB pour l’appareil, qui est retourné par **WinUsb_Initialize.

Étape 6 : Implémenter la fonction main

L’exemple de code suivant montre la fonction main de votre application console.

Pour obtenir un exemple de code qui obtient le handle d’appareil et ouvre l’appareil (GetDeviceHandle et GetWinUSBHandle dans cet exemple), consultez Discussion sur le code de modèle.

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

Étapes suivantes

Si votre appareil prend en charge les points de terminaison isochrone, vous pouvez utiliser les fonctions WinUSB pour envoyer des transferts. Cette fonctionnalité est uniquement prise en charge dans Windows 8.1. Pour plus d’informations, consultez Envoyer des transferts isochrones USB à partir d’une application de bureau WinUSB.

Voir aussi