Acceso a un dispositivo USB mediante funciones de WinUSB

En este artículo se incluye un tutorial detallado sobre cómo usar funciones de WinUSB para comunicarse con un dispositivo USB que usa Winusb.sys como controlador de funciones.

Resumen

  • Abra el dispositivo y obtenga el identificador WinUSB.
  • Obtener información sobre el dispositivo, la configuración y la configuración de interfaz de todas las interfaces y sus puntos de conexión.
  • Leer y escribir datos en puntos de conexión masivos e interrumpirlos.

API importantes

Si usa Microsoft Visual Studio 2013, cree la aplicación esqueleto mediante la plantilla WinUSB. En ese caso, omita los pasos del 1 al 3 y continúe desde el paso 4 de este artículo. La plantilla abre un identificador de archivo en el dispositivo y obtiene el identificador winUSB necesario para las operaciones posteriores. Ese identificador se almacena en la estructura de DEVICE_DATA definida por la aplicación en device.h.

Para obtener más información sobre la plantilla, consulte Escritura de una aplicación de escritorio de Windows basada en la plantilla de WinUSB.

Nota:

Las funciones winUSB requieren Windows XP o posterior. Puede usar estas funciones en la aplicación de C/C++ para comunicarse con el dispositivo USB. Microsoft no proporciona una API administrada para WinUSB.

Antes de empezar

Los siguientes elementos se aplican a este tutorial:

  • Esta información se aplica a Windows 8.1, Windows 8, Windows 7, Windows Server 2008, versiones de Windows Vista de Windows.
  • Ha instalado Winusb.sys como controlador de función del dispositivo. Para obtener más información sobre este proceso, consulte Instalación de WinUSB (Winusb.sys).
  • Los ejemplos de este artículo se basan en el dispositivo OSR USB FX2 Learning Kit. Puede usar estos ejemplos para ampliar los procedimientos a otros dispositivos USB.

Paso 1: Creación de una aplicación esqueleto basada en la plantilla de WinUSB

Para acceder a un dispositivo USB, empiece por crear una aplicación esqueleto basada en la plantilla de WinUSB incluida en el entorno integrado del Kit de controladores de Windows (WDK) (con Herramientas de depuración para Windows) y Microsoft Visual Studio. Puede usar la plantilla como punto de partida.

Para obtener información sobre el código de plantilla, cómo crear, compilar, implementar y depurar la aplicación esqueleto, consulte Escritura de una aplicación de escritorio de Windows basada en la plantilla de WinUSB.

La plantilla enumera los dispositivos mediante rutinas setupAPI , abre un identificador de archivo para el dispositivo y crea un identificador de interfaz de WinUSB necesario para las tareas posteriores. Para obtener código de ejemplo que obtiene el identificador del dispositivo y abre el dispositivo, consulte Discusión sobre el código de plantilla.

Paso 2: Consulta del dispositivo para descriptores USB

A continuación, consulte el dispositivo para obtener información específica de USB, como la velocidad del dispositivo, los descriptores de interfaz, los puntos de conexión relacionados y sus canalizaciones. El procedimiento es similar al que usan los controladores de dispositivo USB. Sin embargo, la aplicación completa las consultas de dispositivo llamando a WinUsb_GetDescriptor.

En la lista siguiente se muestran las funciones de WinUSB a las que puede llamar para obtener información específica de USB:

  • Más información del dispositivo.

    Llame a WinUsb_QueryDeviceInformation para solicitar información de los descriptores de dispositivo para el dispositivo. Para obtener la velocidad del dispositivo, establezca DEVICE_SPEED (0x01) en el parámetro InformationType . La función devuelve LowSpeed (0x01) o HighSpeed (0x03).

  • Descriptores de interfaz

    Llame a WinUsb_QueryInterfaceSettings y pase los identificadores de interfaz del dispositivo para obtener los descriptores de interfaz correspondientes. El identificador de interfaz winUSB corresponde a la primera interfaz. Algunos dispositivos USB, como el dispositivo OSR Fx2, solo admiten una interfaz sin ninguna configuración alternativa. Por lo tanto, para estos dispositivos, el parámetro AlternateSettingNumber se establece en cero y la función se llama solo una vez. WinUsb_QueryInterfaceSettings rellena la estructura de USB_INTERFACE_DESCRIPTOR asignada por el autor de la llamada (pasada en el parámetro UsbAltInterfaceDescriptor ) con información sobre la interfaz. Por ejemplo, el número de puntos de conexión de la interfaz se establece en el miembro bNumEndpoints de USB_INTERFACE_DESCRIPTOR.

    Para los dispositivos que admiten varias interfaces, llame a WinUsb_GetAssociatedInterface para obtener identificadores de interfaz para las interfaces asociadas especificando la configuración alternativa en el parámetro AssociatedInterfaceIndex .

  • Puntos de conexión

    Llame a WinUsb_QueryPipe para obtener información sobre cada punto de conexión en cada interfaz. WinUsb_QueryPipe rellena la estructura de WINUSB_PIPE_INFORMATION asignada por el autor de la llamada con información sobre la canalización del punto de conexión especificado. Las canalizaciones de los puntos de conexión se identifican mediante un índice de base cero y deben ser menores que el valor del miembro bNumEndpoints del descriptor de interfaz que se recupera en la llamada anterior a **WinUsb_QueryInterfaceSettings. El dispositivo OSR Fx2 tiene una interfaz que tiene tres puntos de conexión. Para este dispositivo, el parámetro AlternateInterfaceNumber de la función se establece en 0 y el valor del parámetro PipeIndex varía de 0 a 2.

    Para determinar el tipo de canalización, examine el miembro PipeInfo de la estructura WINUSB_PIPE_INFORMATION. Este miembro se establece en uno de los valores de enumeración de USBD_PIPE_TYPE : UsbdPipeTypeControl, UsbdPipeTypeIsochronous, UsbdPipeTypeBulk o UsbdPipeTypeInterrupt. El dispositivo OSR USB FX2 admite una canalización de interrupción, una canalización masiva y una canalización de salida masiva, por lo que PipeInfo se establece en UsbdPipeTypeInterrupt o UsbdPipeTypeBulk. El valor UsbdPipeTypeBulk identifica canalizaciones masivas, pero no proporciona la dirección de la canalización. La información de dirección se codifica en el bit alto de la dirección de canalización, que se almacena en el miembro PipeId de la estructura WINUSB_PIPE_INFORMATION. La manera más sencilla de determinar la dirección de la canalización es pasar el valor PipeId a una de las siguientes macros de Usb100.h:

    • La USB_ENDPOINT_DIRECTION_IN (PipeId) macro devuelve TRUE si la dirección está en.
    • La USB_ENDPOINT_DIRECTION_OUT(PipeId) macro devuelve TRUE si la dirección está fuera.

    La aplicación usa el valor PipeId para identificar la canalización que se va a usar para la transferencia de datos en llamadas a funciones winUSB, como WinUsb_ReadPipe (que se describe en la sección "Solicitudes de E/S de problemas" de este tema), por lo que el ejemplo almacena los tres valores de PipeId para su uso posterior.

El código de ejemplo siguiente obtiene la velocidad del dispositivo especificado por el identificador de interfaz de 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;
}

En el código de ejemplo siguiente se consultan los distintos descriptores para el dispositivo USB especificado por el identificador de interfaz winUSB. La función de ejemplo recupera los tipos de puntos de conexión admitidos y sus identificadores de canalización. En el ejemplo se almacenan los tres valores pipeid para su uso posterior.

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

Paso 3: Enviar transferencia de control al punto de conexión predeterminado

A continuación, comunique con el dispositivo mediante la emisión de la solicitud de control al punto de conexión predeterminado.

Todos los dispositivos USB tienen un punto de conexión predeterminado además de los puntos de conexión asociados a las interfaces. El propósito principal del punto de conexión predeterminado es proporcionar al host información que puede usar para configurar el dispositivo. Sin embargo, los dispositivos también pueden usar el punto de conexión predeterminado para fines específicos del dispositivo. Por ejemplo, el dispositivo OSR USB FX2 usa el punto de conexión predeterminado para controlar la barra de luz y la pantalla digital de siete segmentos.

Los comandos de control constan de un paquete de instalación de 8 bytes, que incluye un código de solicitud que especifica la solicitud concreta y un búfer de datos opcional. Los códigos de solicitud y los formatos de búfer se definen por el proveedor. En este ejemplo, la aplicación envía datos al dispositivo para controlar la barra de luz. El código para establecer la barra de luz es 0xD8, que se define para mayor comodidad como SET_BARGRAPH_DISPLAY. Para esta solicitud, el dispositivo requiere un búfer de datos de 1 byte que especifica qué elementos se deben iluminar estableciendo los bits adecuados.

La aplicación podría proporcionar un conjunto de ocho controles de casilla para especificar qué elementos de la barra de luz se deben iluminar. Los elementos especificados corresponden a los bits adecuados del búfer. Para evitar el código de interfaz de usuario, el código de ejemplo de esta sección establece los bits para que las luces alternativas se iluminan.

Para emitir una solicitud de control

  1. Asigne un búfer de datos de 1 byte y cargue los datos en el búfer que especifica los elementos que se deben iluminar estableciendo los bits adecuados.

  2. Construya un paquete de instalación en una estructura de WINUSB_SETUP_PACKET asignada por el autor de la llamada. Inicialice los miembros para representar el tipo de solicitud y los datos de la siguiente manera:

    • El miembro RequestType especifica la dirección de la solicitud. Se establece en 0, lo que indica la transferencia de datos de host a dispositivo. Para las transferencias de dispositivo a host, establezca RequestType en 1.
    • El miembro Request se establece en el código definido por el proveedor para esta solicitud, 0xD8. Se define por comodidad como SET_BARGRAPH_DISPLAY.
    • El miembro Length se establece en el tamaño del búfer de datos.
    • Los miembros Index y Value no son necesarios para esta solicitud, por lo que se establecen en cero.
  3. Llame a WinUsb_ControlTransfer para transmitir la solicitud al punto de conexión predeterminado pasando el identificador de interfaz WinUSB del dispositivo, el paquete de instalación y el búfer de datos. La función recibe el número de bytes transferidos al dispositivo en el parámetro LengthTransferred .

En el ejemplo de código siguiente se envía una solicitud de control al dispositivo USB especificado para controlar las luces de la barra de luz.

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

Paso 4: Emitir solicitudes de E/S

A continuación, envíe datos a los puntos de conexión masivos y de salida masiva del dispositivo que se pueden usar para las solicitudes de lectura y escritura, respectivamente. En el dispositivo OSR USB FX2, estos dos puntos de conexión están configurados para bucle invertido, por lo que el dispositivo mueve los datos del punto de conexión de entrada masiva al punto de conexión de salida masiva. No cambia el valor de los datos ni agrega datos nuevos. Para la configuración de bucle invertido, una solicitud de lectura lee los datos enviados por la solicitud de escritura más reciente. WinUSB proporciona las siguientes funciones para enviar solicitudes de escritura y lectura:

Para enviar una solicitud de escritura

  1. Asigne un búfer y llene con los datos que desea escribir en el dispositivo. No hay ninguna limitación en el tamaño del búfer si la aplicación no establece RAW_IO como tipo de directiva de la canalización. WinUSB divide el búfer en fragmentos de tamaño adecuado, si es necesario. Si se establece RAW_IO, el tamaño del búfer está limitado por el tamaño máximo de transferencia admitido por WinUSB.
  2. Llame a WinUsb_WritePipe para escribir el búfer en el dispositivo. Pase el identificador de interfaz winUSB para el dispositivo, el identificador de canalización de la canalización masiva (como se describe en la sección Consulta del dispositivo para descriptores USB de este artículo) y el búfer. La función devuelve el número de bytes que se escriben en el dispositivo en el parámetro bytesWritten . El parámetro Superpuesto se establece en NULL para solicitar una operación sincrónica. Para realizar una solicitud de escritura asincrónica, establezca Superpuesto en un puntero a una estructura SUPERPUESTA .

Las solicitudes de escritura que contienen datos de longitud cero se reenvieron hacia abajo en la pila USB. Si la longitud de la transferencia es mayor que una longitud máxima de transferencia, WinUSB divide la solicitud en solicitudes más pequeñas de longitud máxima de transferencia y las envía en serie. En el ejemplo de código siguiente se asigna una cadena y se envía al punto de conexión de salida masiva del dispositivo.

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

Para enviar una solicitud de lectura

  • Llame a WinUsb_ReadPipe para leer datos del punto de conexión de forma masiva del dispositivo. Pase el identificador de la interfaz WinUSB del dispositivo, el identificador de canalización para el punto de conexión masivo y un búfer vacío de tamaño adecuado. Cuando se devuelve la función, el búfer contiene los datos leídos del dispositivo. El número de bytes leídos se devuelve en el parámetro bytesRead de la función. Para las solicitudes de lectura, el búfer debe ser un múltiplo del tamaño máximo del paquete.

Las solicitudes de lectura de longitud cero se completan inmediatamente con éxito y no se envían a la pila. Si la longitud de la transferencia es mayor que una longitud máxima de transferencia, WinUSB divide la solicitud en solicitudes más pequeñas de longitud máxima de transferencia y las envía en serie. Si la longitud de la transferencia no es un múltiplo de MaxPacketSize del punto de conexión, WinUSB aumenta el tamaño de la transferencia al siguiente múltiplo de MaxPacketSize. Si un dispositivo devuelve más datos de los solicitados, WinUSB guarda los datos sobrantes. Si los datos permanecen de una solicitud de lectura anterior, WinUSB lo copia al principio de la siguiente solicitud de lectura y completa la solicitud, si es necesario. En el ejemplo de código siguiente se leen los datos del punto de conexión de entrada masiva del dispositivo.

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

Paso 5: Liberar los identificadores del dispositivo

Después de completar todas las llamadas necesarias al dispositivo, libere el identificador de archivo y el identificador de interfaz winUSB para el dispositivo mediante una llamada a las siguientes funciones:

  • CloseHandle para liberar el identificador creado por CreateFile, como se describe en el paso 1.
  • WinUsb_Free liberar el identificador de interfaz winUSB para el dispositivo, que devuelve **WinUsb_Initialize.

Paso 6: Implementación de la función principal

En el ejemplo de código siguiente se muestra la función principal de la aplicación de consola.

Para obtener el código de ejemplo que obtiene el identificador del dispositivo y abre el dispositivo (GetDeviceHandle y GetWinUSBHandle en este ejemplo), consulte Discusión del código de plantilla.

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

Pasos siguientes

Si el dispositivo admite puntos de conexión isócronos, puede usar funciones winUSB para enviar transferencias. Esta característica solo se admite en Windows 8.1. Para obtener más información, consulte Envío de transferencias isócrónicas USB desde una aplicación de escritorio winUSB.

Consulte también