Cómo gestionar dispositivos USB en aplicaciones de Windows — cómo elegir entre COM virtual, HID, WinUSB y SDK propietario
· Actualizado el: · Go Komura · USB, HID, WinUSB, Comunicación serie, Integración de equipos, Desarrollo en Windows, C#, Controladores de dispositivo
«Este equipo se conecta por USB, así que podré manejarlo desde la aplicación, ¿verdad?» — es la primera pregunta que surge en las consultas sobre integración de equipos. La respuesta es «depende de cómo se conecte», y en esa frase se esconde una diferencia de decenas de veces en el esfuerzo de desarrollo. Aunque hablemos siempre de «un equipo con conexión USB», según si se ve como un puerto COM, como un HID o si necesita un controlador propietario, cambian por completo el código que hay que escribir, la forma de distribuirlo y el tipo de problemas que aparecerán en el terreno.
Lo complicado es que esta decisión hay que tomarla antes de empezar a escribir la aplicación. Si se avanza con un «funcionó en cuanto instalé el SDK», el precio se paga más adelante, en forma de «no puedo compilar en 64 bits», «al conectar dos equipos no puedo distinguirlos» o «en el PC del cliente no se instala el controlador».
En este artículo repasamos los cuatro métodos para gestionar dispositivos USB desde una aplicación de Windows —puerto COM virtual, HID, WinUSB y SDK del fabricante—, sus criterios de selección, los puntos clave de implementación y el diseño común que exigen los cuatro métodos.
1. Conclusión general
- Lo primero que hay que comprobar es «en qué categoría, y como qué, aparece en el Administrador de dispositivos». Puertos (COM y LPT), dispositivos de interfaz humana, dispositivos de bus serie universal, una categoría propia… esto determina casi por completo el método a usar (capítulo 2).
- Los dispositivos que pertenecen a una clase estándar no necesitan controlador. Windows incluye de serie controladores de clase para audio, CDC, HID, almacenamiento masivo, impresión, etc., y los dispositivos correspondientes funcionan automáticamente. No se recomienda que un fabricante escriba un controlador para una clase estándar.1
- El orden de selección oficial es «de lo más simple a lo más complejo». (1) Si sirve un controlador de clase estándar, no escriba nada; (2) si no sirve pero el acceso lo hace una sola aplicación, use WinUSB; (3) si varias aplicaciones acceden a la vez, use un controlador UMDF; (4) si eso tampoco basta, un controlador KMDF. Este es el orden a seguir.2
- El COM virtual es lo mejor para portar y reutilizar código, y lo peor para identificar equipos. En los dispositivos CDC-ACM, a partir de Windows 10 usbser.sys se carga automáticamente y basta con
SerialPortpara escribir la aplicación. Pero el número de COM no es el identificador del dispositivo: es imprescindible resolverlo en tiempo de ejecución a partir del VID/PID y el número de serie (capítulo 3).3 - HID es el candidato oculto que permite comunicación bidireccional sin distribuir ni un solo controlador. Sin embargo, las colecciones equivalentes a ratón, teclado, pantalla táctil y lápiz las abre el sistema operativo en modo exclusivo y no se pueden tocar. La velocidad también está limitada por el ancho de banda de las transferencias de interrupción (capítulo 4).4
- WinUSB es para «hace falta velocidad con transferencias en bloque» o «protocolo propietario». La instalación automática sin INF solo es posible cuando el firmware informa el ID compatible
WINUSBmediante descriptores del sistema operativo de Microsoft, usado en Windows 8 o posterior. Si se trata de un dispositivo ya existente o hay que dar soporte a Windows 7 o anterior, en general hará falta un INF personalizado (capítulo 5).5 - El SDK del fabricante no se «elige»: se «asume». La bitness (versión de 32 o 64 bits), el modelo de hilos, el ciclo de vida y las condiciones de redistribución los decide otra empresa, así que hay que sacar a la luz esas restricciones del SDK como premisa del diseño de la aplicación desde el principio (capítulo 6).
- Sea cual sea el método, la identificación única del dispositivo, el seguimiento de la conexión/desconexión, los tiempos de espera y la gestión de energía son cuatro puntos que hay que diseñar uno mismo. Una aplicación de integración de equipos que se salte esto acabará, sin excepción, «fallando de vez en cuando» (capítulo 8).
- Si va a crear su propio controlador en modo kernel, a partir de Windows 10 1607 es obligatoria la firma de Microsoft. Calcule el coste de distribución de antemano, incluido el hecho de que abrir una cuenta en Partner Center exige un certificado EV (capítulo 10).6
2. La premisa básica — desde Windows, todo depende de «qué controlador se cargó» para el dispositivo USB
Empecemos mostrando en una sola imagen el árbol de decisión de los cuatro métodos. Es el orden de selección oficial mencionado en el capítulo 1 («de lo más simple a lo más complejo»), reordenado según la secuencia real de decisiones.2 El detalle de cada bifurcación corresponde a los capítulos 3 a 6.
flowchart TD
S["Quiero gestionar un dispositivo USB desde la aplicación"]
Q1["¿Cómo aparece en el Administrador de dispositivos?<br/>(conecte el dispositivo real y compruébelo primero)"]
Q2["¿Se puede modificar el firmware del dispositivo?<br/>(diseño propio, o se puede encargar al fabricante)"]
Q3["¿El ancho de banda necesario cabe en una transferencia de interrupción?<br/>(referencia: notificaciones de estado o comandos de decenas de KB/s o menos)"]
Q4["¿Varias aplicaciones acceden<br/>al mismo dispositivo a la vez?"]
A1["Método A: puerto COM virtual<br/>capítulo 3"]
A2["Método B: HID<br/>capítulo 4"]
A3["Método C: WinUSB<br/>capítulo 5"]
A4["Método D: SDK del fabricante<br/>capítulo 6"]
A5["Evaluar el desarrollo de un controlador UMDF<br/>y, si no basta, KMDF.<br/>El coste de distribución está en el capítulo 10"]
S --> Q1
Q1 -->|"Aparece como puerto (COM y LPT)"| A1
Q1 -->|"Aparece como dispositivo de interfaz humana"| A2
Q1 -->|"Tiene cargado un controlador del fabricante"| A4
Q1 -->|"Ninguno de los anteriores / dispositivo desconocido"| Q2
Q2 -->|"No se puede modificar"| A4
Q2 -->|"Se puede modificar"| Q3
Q3 -->|"Es suficiente"| A2
Q3 -->|"No es suficiente (grandes volúmenes de datos, protocolo propio)"| Q4
Q4 -->|"No"| A3
Q4 -->|"Sí"| A5
Figura 1: árbol de decisión de los cuatro métodos. El punto de partida es siempre «qué se ve en el Administrador de dispositivos»
Este diagrama tiene dos puntos clave: el punto de partida no es el catálogo del producto, sino el Administrador de dispositivos con el equipo real, y cuanto más abajo se baja, mayor es el coste de distribución. Si se puede resolver arriba, esa es la respuesta correcta; solo baje si tiene una razón de peso para hacerlo.
Sea lo que sea lo que hay al otro lado del cable USB, lo único que la aplicación puede ver es la interfaz que expone el controlador cargado sobre ese dispositivo. Sin tener esto claro, la discusión no llega a ninguna parte.
Al conectar el dispositivo, Windows lee los descriptores que este declara y decide, a partir del código de clase y el VID/PID, qué controlador cargar. Si corresponde a una clase estándar, se carga automáticamente el controlador de clase incluido con Windows.1
| Código de clase USB-IF | Controlador estándar de Windows | Cómo lo ve la aplicación |
|---|---|---|
| Audio (01h) | Usbaudio.sys | Dispositivo de audio |
| CDC (02h, subclase 02h) | Usbser.sys | Puerto COM |
| HID (03h) | Hidclass.sys / Hidusb.sys | Colección HID |
| Image (06h) | Usbscan.sys | Dispositivo WIA |
| Printer (07h) | Usbprint.sys | Impresora |
| Mass Storage (08h) | Usbstor.sys | Unidad |
| Video (0Eh) | Usbvideo.sys | Cámara (UVC) |
| Vendor Specific (FFh) | (ninguno) | Se recomienda WinUSB |
La última fila es la importante. Los dispositivos propietarios de un fabricante suelen declararse como FFh (Vendor Specific), y en ese caso la recomendación de Microsoft es WinUSB.1
Otro concepto que conviene conocer es el de dispositivo compuesto (composite device). Cuando un solo cable USB da acceso a varias funciones, Usbccgp.sys las despliega como dispositivos independientes, una por función. Esto explica que «un solo equipo aparezca como tres entradas en el Administrador de dispositivos»; por ejemplo, no es raro encontrar dispositivos con una configuración como «el control se hace por CDC (puerto COM) y la notificación de estado por HID». El método no se decide por dispositivo, sino por función (interfaz).
Lo primero que hay que hacer: mirar el dispositivo real en el Administrador de dispositivos
Antes de discutir nada, conecte el equipo real y compruebe lo siguiente. Se tarda cinco minutos y cambia todas las decisiones posteriores.
- En qué categoría del Administrador de dispositivos aparece y con qué nombre
- Propiedades → pestaña Detalles → ID de hardware (
USB\VID_xxxx&PID_yyyy&...) - Lo mismo → ID compatible (comprobar si aparece
USB\Class_02&SubClass_02oUSB\MS_COMP_WINUSB) - Lo mismo → el final de la ruta de instancia del dispositivo (si contiene un número de serie o es un valor generado con
&) - Pestaña Controlador → proveedor y archivos del controlador (si es de Microsoft o del fabricante)
Si en el punto 3 aparece USB\MS_COMP_WINUSB, ese dispositivo está diseñado como dispositivo WinUSB.5 El punto 4 se usa en el apartado 8.1 y es el material para decidir si es posible identificar el dispositivo de forma única.
3. Método A: puerto COM virtual — el más cómodo y el que más fácil se confunde
3.1 Qué está pasando en realidad
En los dispositivos que declaran la clase CDC (Communications and CDC Control) de USB, subclase 02h (ACM), el Usbser.sys estándar de Windows se carga automáticamente, sin distribuir ningún INF. Basta con que el descriptor de dispositivo declare clase 02 y subclase 02: el ID compatible resultante, USB\Class_02&SubClass_02, hace que el Usbser.inf estándar coincida y active el mecanismo.3
Ahora bien, esta carga automática es el comportamiento a partir de Windows 10.1 Si también hay que dar soporte a Windows 8.1 o anterior, el descriptor por sí solo no basta: hace falta preparar y distribuir un INF que haga referencia al controlador estándar (por ejemplo, un INF personalizado que remita a mdmcpq.inf). El caso de «en Windows 10 funcionó sin hacer nada, pero en el Windows 7 del cliente aparece como dispositivo desconocido» tiene aquí su origen.
Otra vía es el controlador VCP proporcionado por el fabricante del chip conversor USB-serie, como FTDI, Silicon Labs o Prolific. En este caso sí hace falta instalar el controlador, pero como estos fabricantes de chips también publican controladores firmados a través de Windows Update, en la práctica el resultado suele ser casi «se conecta y se instala solo».
En cualquiera de los dos casos, lo que ve la aplicación es un simple puerto COM. Esta es la mayor ventaja: se puede reutilizar tal cual todo el patrimonio, la experiencia y el software de terminal de pruebas heredados de la era RS-232.
3.2 La implementación se reduce a SerialPort, pero también hereda sus trampas
En .NET se usa System.IO.Ports.SerialPort (a partir de .NET 5 hace falta referenciar el paquete System.IO.Ports). Los puntos de atención de la implementación no son específicos de USB, sino de la comunicación serie en general —trama, tiempos de espera, reconexión, diseño de registro (logging)— y los hemos recopilado, incluidos estos aspectos, en «Trampas habituales de las aplicaciones de comunicación serie». En particular, el hecho de que Read(buffer, 0, 16) no garantiza leer exactamente 16 bytes sigue siendo cierto también por USB. Reciba los datos como flujo de bytes, acumúlelos en un búfer y extraiga las tramas con un analizador (parser) aparte.
3.3 No escriba el número de COM en el archivo de configuración
Esta es la causa número uno de que el método de COM virtual se rompa en el terreno.
- El número de COM es solo el número que Windows asignó en ese PC concreto; no es el identificador del dispositivo
- Puede cambiar si se cambia el puerto USB en el que se conecta
- Si se conectan dos equipos del mismo modelo, el número por sí solo no dice cuál es cuál
- Es normal que, al ir quedando ocupados COM3, etc., se pase a COM13 o COM27
La implementación correcta es resolver en tiempo de ejecución el número de COM a partir del VID/PID (y, si es posible, el número de serie). Se puede obtener a partir de la enumeración PnP.
# Sacar todos los puertos COM junto con su ID de hardware (lo más seguro es verlos todos sin filtrar todavía)
Get-CimInstance Win32_PnPEntity |
Where-Object { $_.PNPClass -eq 'Ports' } |
Select-Object Name, PNPDeviceID |
Format-List
# Ejemplo de salida:
# Name : USB シリアル デバイス (COM5) ← CDC-ACM (usbser.sys)
# PNPDeviceID : USB\VID_2341&PID_0043\85436323631351D0E1C1
# ^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
# VID/PID ID de instancia del dispositivo
#
# Name : USB Serial Port (COM7) ← controlador VCP de FTDI
# PNPDeviceID : FTDIBUS\VID_0403+PID_6001+A5XK3RJTA\0000
# ^^^^^^^ el enumerador es FTDIBUS, no USB
Filtrar aquí con PNPDeviceID -like 'USB\*' es un error garantizado. Como muestra el ejemplo anterior, el puerto COM que crea el controlador VCP de FTDI empieza por FTDIBUS\ y no coincide en absoluto con el filtro USB\. Otros fabricantes de chips, como Silicon Labs, también pueden tener su propio enumerador.
Una implementación segura es cualquiera de estas dos:
- Obtener primero todos los elementos de la clase
Portsy luego extraer con una expresión regular elVID_xxxx/PID_yyyy(oVID_xxxx+PID_yyyy) contenido enPNPDeviceID— es robusto porque no depende del nombre del enumerador - Poner en una lista blanca explícita los nombres de enumerador (
USB\,FTDIBUS\, etc.) — suficiente si los dispositivos objetivo son fijos
En cualquier caso, conecte de verdad el dispositivo objetivo, ejecute este comando y compruebe visualmente con qué PNPDeviceID aparece antes de escribir el filtro. El nombre del enumerador lo determina la combinación de dispositivo y controlador, así que no se puede decidir de antemano solo sobre el papel.
Desde C# se puede lanzar la misma consulta con ManagementObjectSearcher de System.Management, o construir un selector AQS con Windows.Devices.SerialCommunication.SerialDevice.GetDeviceSelectorFromUsbVidPid(vid, pid) y usar DeviceInformation.FindAllAsync (tenga cuidado de no confundirlo con GetDeviceSelector, que no recibe VID/PID y toma como argumento nada o un nombre de puerto). La primera opción tiene menos dependencias y suele ser más manejable en aplicaciones de escritorio.
Si se escribe de principio a fin, desde la enumeración hasta abrir el SerialPort, queda así. Es la comprobación anterior en PowerShell trasladada directamente a código (.NET 8; hace falta referenciar el paquete System.Management y es específico de Windows).
using System.Globalization;
using System.IO.Ports;
using System.Management;
using System.Text.RegularExpressions;
// Extrae el VID/PID de PNPDeviceID. Ojo: el separador puede ser & o +
// USB\VID_2341&PID_0043\... ← CDC-ACM (usbser.sys)
// FTDIBUS\VID_0403+PID_6001+... ← controlador VCP de FTDI
private static readonly Regex VidPidPattern = new(
@"VID[_+](?<vid>[0-9A-Fa-f]{4})[&+]PID[_+](?<pid>[0-9A-Fa-f]{4})",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex ComNamePattern = new(@"\((?<com>COM\d+)\)", RegexOptions.Compiled);
/// <summary>Enumera los nombres de puerto COM de un VID/PID dado (no depende del nombre del enumerador).</summary>
static IEnumerable<(string PortName, string PnpDeviceId)> FindComPorts(ushort vid, ushort pid)
{
// Se toman todos los que tienen PNPClass = 'Ports'. Filtrar por USB\ pierde los FTDIBUS\
using var searcher = new ManagementObjectSearcher(
"SELECT Name, PNPDeviceID FROM Win32_PnPEntity WHERE PNPClass = 'Ports'");
foreach (var device in searcher.Get().Cast<ManagementObject>())
{
using (device)
{
var name = device["Name"] as string;
var pnpId = device["PNPDeviceID"] as string;
if (name is null || pnpId is null) { continue; }
var ids = VidPidPattern.Match(pnpId);
if (!ids.Success) { continue; }
if (ushort.Parse(ids.Groups["vid"].Value, NumberStyles.HexNumber) != vid) { continue; }
if (ushort.Parse(ids.Groups["pid"].Value, NumberStyles.HexNumber) != pid) { continue; }
// Extrae el (COM5) de "USB シリアル デバイス (COM5)"
var com = ComNamePattern.Match(name);
if (!com.Success) { continue; }
yield return (com.Groups["com"].Value, pnpId);
}
}
}
// Dónde cae el número de serie dentro de la ruta de instancia del dispositivo depende del enumerador.
// USB\VID_2341&PID_0043\85436323631351D0E1C1 el número de serie es el último elemento
// FTDIBUS\VID_0403+PID_6001+A5XK3RJTA\0000 va incrustado en el elemento del medio;
// el final es \0000, y además FTDI añade
// al final un carácter que representa el puerto
// Si solo se comprueba "coincide con el último elemento", el VCP de FTDI no coincide nunca.
static bool MatchesSerial(string pnpDeviceId, string serial)
{
foreach (var part in pnpDeviceId.Split('\\'))
{
if (part.Equals(serial, StringComparison.OrdinalIgnoreCase)) { return true; }
// Último campo del formato FTDI VID_xxxx+PID_xxxx+<serie><carácter de puerto>
var fields = part.Split('+');
if (fields.Length < 3) { continue; }
var tail = fields[fields.Length - 1];
if (tail.Equals(serial, StringComparison.OrdinalIgnoreCase)) { return true; }
if (tail.Length == serial.Length + 1 &&
tail.StartsWith(serial, StringComparison.OrdinalIgnoreCase)) { return true; }
}
return false;
}
// Uso: lo que se abre es "el resultado de la resolución", no el número de COM del archivo de configuración
var candidates = FindComPorts(0x2341, 0x0043).ToList();
if (candidates.Count == 0) { throw new InvalidOperationException("No se encontró el dispositivo objetivo."); }
// Filtre siempre por número de serie, sin importar cuántos candidatos haya. Aunque solo se
// encuentre uno, no tiene por qué ser el equipo objetivo (puede ocurrir que el equipo
// objetivo esté desconectado y solo esté conectado otro equipo distinto). El orden de
// enumeración de WMI no garantiza la identidad del dispositivo, así que tomar
// candidates[0] tal cual provoca el accidente de "estaba operando el equipo de al lado".
var wanted = config.DeviceSerial; // recibe de la configuración o de un argumento "qué equipo es"
candidates = candidates.Where(c => MatchesSerial(c.PnpDeviceId, wanted)).ToList();
if (candidates.Count != 1)
{
throw new InvalidOperationException(
$"No se pudo identificar un único dispositivo con el número de serie '{wanted}' ({candidates.Count} coincidencias).");
}
using var port = new SerialPort(candidates[0].PortName, 115200)
{
ReadTimeout = 1000,
WriteTimeout = 1000,
};
port.Open();
Hay cuatro puntos clave: (1) tomar todos los elementos con PNPClass = 'Ports' y filtrar después por VID/PID (no filtrar por USB\), (2) el nombre de puerto que se abre se resuelve siempre a partir de esta enumeración (no escribir COM3 en el archivo de configuración), (3) el filtrado por número de serie se aplica siempre, no solo «cuando se encuentran varios», y (4) la forma de buscar el número de serie no debe depender del enumerador (cómo construir la clave se explica en el apartado 8.1).
Los puntos (3) y (4), si se hacen mal, producen el mismo síntoma: «estaba operando el equipo de al lado». La trampa del (3) es que, si solo se encuentra un candidato, parece que no hace falta comprobar nada más. Si el equipo objetivo está desconectado y solo hay conectado otro equipo distinto, aunque haya un único candidato, su contenido es otra cosa. El (4): si solo se mira el caso USB\ y se da por hecho que «el número de serie es el último elemento», con un formato como FTDIBUS\VID_0403+PID_6001+A5XK3RJTA\0000, donde va incrustado en el elemento del medio, no coincide ni un solo caso. En el código anterior, estos dos problemas se han encerrado dentro de MatchesSerial.
Si se puede usar como clave, es más fiable guardar en la configuración la ruta de instancia del dispositivo completa (apartado 8.1). Así deja de hacer falta el propio proceso de extraer el número de serie.
Extraer el número tomando el (COM5) del final de Name puede parecer poco riguroso, pero en la práctica es el método que funciona de forma más fiable. Para hacerlo con rigor, hay que leer el valor PortName bajo la clave del registro del dispositivo.
3.4 El identificador (handle) se corrompe al desconectar el cable
En un USB-serie, al desconectar el cable desaparece el propio puerto. Es un accidente clásico y conocido que, si se tiene abierto SerialPort y se desconecta, salte una excepción desde el hilo interno de recepción y se caiga toda la aplicación. Conviene tener preparado un orden de acciones seguro: al recibir la notificación PnP de retirada, cerrar (Close) el puerto lo primero de todo (apartado 8.2).
Reconectar no se resuelve con «volver a hacer Open()». Hay que diseñarlo como una regeneración completa de la sesión, que incluya invalidar la sesión anterior, dar por fallidas las solicitudes en curso, detener el lector/escritor, reabrir tras un tiempo de espera con retroceso exponencial (backoff) y volver a ejecutar la secuencia de inicialización del equipo.
3.5 Casos en los que conviene y en los que no
| Conviene | No conviene |
|---|---|
| Ya se dispone de un protocolo serie existente | Alto rendimiento a través de un chip conversor USB-UART |
| Equipos y aparatos de medida con respuesta a comandos de texto | Control con requisitos de latencia muy exigentes |
| Se quiere poder aislar problemas en el terreno con software de terminal | Muchas conexiones simultáneas del mismo modelo (la implementación de identificación se complica) |
| El desarrollador no tiene conocimientos de controladores | Desarrollo nuevo en el que se puede definir el protocolo por completo |
En cuanto al rendimiento, no lo meta todo en el mismo saco con un «como es COM virtual, es lento». En una configuración que pasa por un chip conversor USB-UART como los de FTDI, el techo lo marca la velocidad en baudios de la UART de destino (a 921,6 kbps, unos 92 KB/s aproximadamente). En cambio, en un dispositivo USB nativo cuyo microcontrolador implementa CDC-ACM directamente, la interfaz de datos usa transferencias en bloque de USB, sin la limitación de la UART, y con una conexión de alta velocidad puede llegar a varios MB/s. Aun así, en ese rango empieza a pesar la sobrecarga de la capa Usbser.sys y SerialPort, así que si el ancho de banda necesario supera unos cientos de KB/s, lo correcto es medirlo en el equipo real antes de decidir el método. Decidir «hace falta velocidad, así que WinUSB» sin haber medido nada supone cargar con una distribución de controlador innecesaria.
4. Método B: HID — comunicación bidireccional sin distribuir ningún controlador
4.1 HID no es solo para dispositivos de entrada
Al oír HID pensamos en ratones y teclados, pero según la especificación es un protocolo genérico que permite intercambiar cualquier secuencia de bytes (informes o “reports”) en ambos sentidos. Lectores de código de barras, lectores de tarjetas, cerraduras electrónicas, unidades de medida, SAI, cajas de E/S propietarias… la razón por la que dispositivos que “no quieren distribuir un controlador pero sí quieren intercambiar datos propios” se declaran como HID es que Windows incluye de serie Hidclass.sys y Hidusb.sys, así que funcionan sin distribuir absolutamente ningún INF ni controlador.1
Desde el punto de vista de Windows, la unidad de HID es la colección de nivel superior (top-level collection, TLC). Un mismo dispositivo físico puede tener varias TLC, y en ese caso cada una aparece como una interfaz de dispositivo independiente.4
4.2 HID al que se puede acceder y HID al que no
Esta es la restricción más importante. Windows abre algunas TLC en modo exclusivo. Es para evitar que otras aplicaciones puedan interceptar el estado de entrada global, y es el Raw Input Manager (RIM) el que abre esos dispositivos en modo exclusivo.4
| Usage Page / Usage | Uso | Modo de acceso |
|---|---|---|
| 0x0001 / 0x0001-0x0002 | Ratón | Exclusivo |
| 0x0001 / 0x0004-0x0005 | Mando de juego | Compartido |
| 0x0001 / 0x0006-0x0007 | Teclado / teclado numérico | Exclusivo |
| 0x000C / 0x0001 | Control de consumo | Compartido |
| 0x000D / 0x0001-0x0002 | Lápiz | Exclusivo |
| 0x000D / 0x0004-0x0005 | Pantalla táctil / panel táctil de precisión | Exclusivo |
| 0x0020 / varios | Sensores | Compartido |
| 0x008C / 0x0002 | Lector de código de barras | Compartido (obtener los datos decodificados es exclusivo) |
Es decir, aunque se intente leer datos directamente con la API de HID desde un lector de código de barras que emula teclado, no se podrá. Ese dispositivo está abierto en modo exclusivo como teclado. Para este tipo de dispositivos, si se quiere recibir los datos «como datos y no como pulsaciones de tecla», el camino correcto es cambiar, en la configuración del propio equipo, al modo de TLC definida por el fabricante en HID o al modo CDC.
Aun así, en un dispositivo abierto en modo exclusivo, si se abre el identificador (handle) sin solicitar permisos de lectura/escritura, sí es posible obtener atributos y cadenas con las funciones HidD_GetXxx.4 Para el caso de uso de «solo quiero comprobar si el dispositivo está conectado», esto es suficiente.
4.3 Implementación — equivocar la longitud del informe siempre provoca un fallo
El procedimiento de una aplicación en modo usuario está fijado: buscar la colección HID con SetupDi*, abrirla con CreateFile, obtener información con HidD_*, leer y escribir informes con ReadFile/WriteFile, e interpretarlos con HidP_*. Eso es todo.7
// Enumeración de dispositivos HID y obtención de la longitud del informe (declaraciones P/Invoke, extracto)
[DllImport("hid.dll")]
static extern void HidD_GetHidGuid(out Guid hidGuid);
[DllImport("hid.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.U1)]
static extern bool HidD_GetAttributes(SafeFileHandle device, ref HIDD_ATTRIBUTES attributes);
[DllImport("hid.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.U1)]
static extern bool HidD_GetPreparsedData(SafeFileHandle device, out IntPtr preparsedData);
[DllImport("hid.dll")]
static extern int HidP_GetCaps(IntPtr preparsedData, out HIDP_CAPS capabilities);
// El búfer que devuelve GetPreparsedData hay que liberarlo siempre. Si se olvida, se filtra memoria en el lado nativo
[DllImport("hid.dll")]
[return: MarshalAs(UnmanagedType.U1)]
static extern bool HidD_FreePreparsedData(IntPtr preparsedData);
[StructLayout(LayoutKind.Sequential)]
struct HIDD_ATTRIBUTES
{
public int Size; // hay que establecer siempre sizeof(HIDD_ATTRIBUTES)
public ushort VendorID;
public ushort ProductID;
public ushort VersionNumber;
}
// HIDP_CAPS no se debe declarar con "solo los campos que se usan".
// HidP_GetCaps escribe la longitud completa de la definición nativa (USHORT×32 = 64 bytes),
// así que si se pasa una estructura truncada, se destruye lo que haya a continuación en la pila
[StructLayout(LayoutKind.Sequential)]
struct HIDP_CAPS
{
public ushort Usage;
public ushort UsagePage;
public ushort InputReportByteLength; // longitud del búfer que se pasa a ReadFile
public ushort OutputReportByteLength; // longitud del búfer que se pasa a WriteFile
public ushort FeatureReportByteLength;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)]
public ushort[] Reserved; // área reservada. No se puede omitir
public ushort NumberLinkCollectionNodes;
public ushort NumberInputButtonCaps;
public ushort NumberInputValueCaps;
public ushort NumberInputDataIndices;
public ushort NumberOutputButtonCaps;
public ushort NumberOutputValueCaps;
public ushort NumberOutputDataIndices;
public ushort NumberFeatureButtonCaps;
public ushort NumberFeatureValueCaps;
public ushort NumberFeatureDataIndices;
}
Un accidente habitual en la declaración de la estructura es «escribir solo los campos que se usan y dejar el resto en un comentario». HidP_GetCaps escribe la longitud completa según la definición nativa (USHORT×32 = 64 bytes), así que si se le pasa una estructura con solo los cinco primeros campos (10 bytes), el marshaller escribe 54 bytes más allá del área reservada. Con suerte, saltará una AccessViolationException; con mala suerte, corromperá silenciosamente otra variable. Al declarar estructuras para P/Invoke, decláre siempre con el mismo tamaño y el mismo orden que en el lado nativo, incluidos los campos que no se usan.8
El búfer que devuelve HidD_GetPreparsedData está reservado en el lado nativo, así que hay que liberarlo siempre con HidD_FreePreparsedData cuando se termine de usar. En una aplicación que vuelve a enumerar todos los dispositivos HID cada vez que hay una conexión o desconexión, olvidar esto hace que se consuma memoria silenciosamente sin parar. Rodéelo con try/finally, o envuélvalo en una clase derivada de SafeHandle para evitar de forma estructural la fuga por olvido de liberación.
const int HIDP_STATUS_SUCCESS = 0x00110000;
// Compruebe siempre el valor de retorno. Si el dispositivo se desconectó justo después de enumerarlo, devuelve FALSE
if (!HidD_GetPreparsedData(handle, out IntPtr preparsed))
{
return null; // se omite este dispositivo. preparsed no es válido; no se toca
}
try
{
if (HidP_GetCaps(preparsed, out HIDP_CAPS caps) != HIDP_STATUS_SUCCESS)
{
return null;
}
// aquí sí es válido, por ejemplo, caps.InputReportByteLength
}
finally
{
HidD_FreePreparsedData(preparsed); // liberar solo si la obtención tuvo éxito
}
No ignore el valor de retorno de HidD_GetPreparsedData. Si el dispositivo se desconecta entre el momento de enumerarlo y el de abrir el identificador, devuelve FALSE, y preparsed no llega a ser un puntero válido. Si se pasa igualmente a HidP_GetCaps y HidD_FreePreparsedData, se acaba decidiendo la longitud del informe a partir de un caps de contenido desconocido. Es una condición de carrera que se dispara con más frecuencia cuanto más se conecta y desconecta el dispositivo, así que entre en el try solo cuando la obtención haya tenido éxito, y compruebe también el valor de retorno de HidP_GetCaps (si es HIDP_STATUS_SUCCESS).
El fallo más frecuente en la implementación es el manejo de la longitud del informe.
- El búfer que se pasa a
ReadFiledebe medir exactamenteInputReportByteLength. Si es más corto, falla; si es más largo, no se procesa correctamente - El primer byte del búfer es el identificador de informe (report ID). Si el dispositivo no usa identificadores de informe, ese byte contiene 0. Los datos reales empiezan en el byte 1
- De igual forma, el búfer de
WriteFiledebe medir exactamenteOutputReportByteLength, con el identificador de informe al principio
El 90% de los casos de «lo envié y el dispositivo no responde» se deben a que los datos están desplazados un byte por el identificador de informe, o a que la longitud del búfer no coincide. Si la documentación del dispositivo dice «el comando son 8 bytes» y OutputReportByteLength vale 9, significa 9 bytes incluyendo el identificador de informe.
Además de WriteFile, la ruta de envío de informes de salida también incluye HidD_SetOutputReport, y el uso de cada uno está definido oficialmente.9
| Uso | Qué usar |
|---|---|
| Enviar informes de salida de forma continuada | WriteFile (esta es la base) |
| Establecer el estado actual de la colección | HidD_SetOutputReport |
| Enviar un informe de función (Feature) | HidD_SetFeature |
Hay que tener cuidado porque la documentación oficial advierte de que «algunos dispositivos no admiten HidD_SetOutputReport y, al usarlo, pueden dejar de responder».9 Es decir, el cambio de «como WriteFile no funciona, paso a HidD_SetOutputReport» no es una alternativa incondicionalmente segura. Compruebe primero cuál usan las especificaciones del dispositivo y los ejemplos del fabricante, y si decide cambiar, verifique en el equipo real que el dispositivo no deja de responder.
Si el único objetivo es enumerar dispositivos, abra con dwDesiredAccess de CreateFile a 0. Así puede enumerar también los dispositivos abiertos en modo exclusivo, y obtener el VID/PID con HidD_GetAttributes o el número de serie con HidD_GetSerialNumberString.
Si no quiere escribir P/Invoke en bruto desde C#, también puede optar por una biblioteca como HidSharp. Aun así, al final hay que entender el manejo de la longitud y el identificador del informe, así que hacerlo una primera vez de la forma anterior agiliza la investigación posterior. En una aplicación empaquetada también se puede usar Windows.Devices.HumanInterfaceDevice.HidDevice, pero hace falta declarar la DeviceCapability correspondiente en el manifiesto.10
4.4 El techo de velocidad
HID usa transferencias de interrupción. En un dispositivo USB 2.0 a velocidad completa (full speed, 12 Mbps), el tamaño máximo de paquete del endpoint de interrupción es de 64 bytes, y el intervalo de sondeo (polling) es un valor que declara el firmware entre 1 y 255 ms. A alta velocidad (high speed, 480 Mbps), el máximo es de 1024 bytes, con un intervalo en unidades de 125 µs.11
Es decir, en un dispositivo HID a velocidad completa con sondeo de 1 ms y 64 bytes, el valor teórico ronda apenas los 64 KB/s. Si elige HID para usos que no caben en ese margen —imágenes, formas de onda, volcado masivo de registros—, más adelante no habrá vuelta atrás. Al contrario, para comandos de unas pocas decenas de bytes o notificaciones de estado, es un ancho de banda más que suficiente.
5. Método C: WinUSB — hablar directamente un protocolo propio
5.1 Qué papel cumple
Winusb.sys es un controlador USB genérico proporcionado por Microsoft; al cargarlo como controlador de función, las funciones que expone Winusb.dll en modo usuario permiten leer y escribir directamente en los endpoints. Es un mecanismo para manejar un protocolo propio sin escribir un controlador.2
Las condiciones oficiales para adoptar WinUSB son claras.2
- Que el dispositivo lo acceda una única aplicación
- Que tenga endpoints de tipo bulk, interrupt o isochronous (isochronous a partir de Windows 8.1)
- Que el objetivo incluya Windows XP SP2 o posterior
Al contrario, WinUSB no sirve para dispositivos a los que deben acceder varias aplicaciones a la vez. Ese es el terreno de los controladores UMDF.
| Función | WinUSB | UMDF | KMDF |
|---|---|---|---|
| Acceso simultáneo de varias aplicaciones | No | Sí | Sí |
| Transferencias bulk, interrupt y control | Sí | Sí | Sí |
| Transferencias isochronous | Sí (8.1 o posterior) | No | Sí |
| Apilar controladores de filtro | No | No | Sí |
| Suspensión selectiva | Sí | Sí | Sí |
5.2 Las condiciones bajo las que «no hace falta INF» es cierto
Este es el punto donde más se malinterpreta WinUSB. Que Winusb.sys se cargue automáticamente sin INF solo ocurre cuando el firmware del dispositivo tiene descriptores del sistema operativo de Microsoft e informa WINUSB como ID compatible.5
Además, esta coincidencia automática solo funciona a partir de Windows 8. El Winusb.inf incluido de serie empezó a admitir el ID compatible USB\MS_COMP_WINUSB en Windows 8; antes de eso era imprescindible un INF personalizado que especificara el ID de hardware.5 Como se indicaba en el apartado 5.1, WinUSB en sí mismo funciona desde Windows XP SP2, pero «funciona desde XP» y «se instala sin INF» son cosas distintas. Si también hay que dar soporte a Windows 7 o anterior, planifique partiendo de que habrá que distribuir un INF aunque haya descriptores del sistema operativo (en Windows 7 o anterior también puede coincidir si, a través de Windows Update, ya está instalado un Winusb.inf actualizado, pero eso no se puede tomar como premisa del plan de distribución).
En concreto, el dispositivo necesita la siguiente implementación. Tenga en cuenta que hay dos generaciones, la versión 1.0 (WCID) y la 2.0.
- Descriptor Microsoft OS 1.0 (común a todas las versiones)
- Tener un descriptor de cadena de sistema operativo en el índice de cadena
0xEEque devuelva el código de fabricante - Establecer
WINUSBencompatibleIDmediante el descriptor de función de ID compatible extendido (por función, en el caso de un dispositivo compuesto)
- Tener un descriptor de cadena de sistema operativo en el índice de cadena
- Descriptor Microsoft OS 2.0 (Windows 8.1 o posterior)
- Notificar la ubicación del conjunto de descriptores mediante el descriptor de función de plataforma del descriptor BOS. No se usa el descriptor de cadena
0xEE - Dentro de ese conjunto de descriptores, colocar el descriptor de función de ID compatible e informar
WINUSB. Notificar mediante BOS que «existe el conjunto» no basta por sí solo para que se seleccioneWinusb.sys. Quien decide la vinculación, igual que en la versión 1.0, es el ID compatible
Se diseñó para resolver las limitaciones y los problemas de fiabilidad de la versión 1.0, así que en un firmware de diseño nuevo esta es la primera opción12
- Notificar la ubicación del conjunto de descriptores mediante el descriptor de función de plataforma del descriptor BOS. No se usa el descriptor de cadena
El registro del GUID de interfaz de dispositivo cumple un papel distinto al anterior. Es el GUID con el que la aplicación encuentra el dispositivo; lo que decide si Winusb.sys puede vincularse o no es el ID compatible. El GUID está del lado del descubrimiento (discovery). Aun así, si no hay registrado un GUID propio, la búsqueda de dispositivos por parte de la aplicación se hace difícil de organizar, así que en la práctica se implementan ambas cosas juntas.
Aquí hay que tener cuidado porque existen dos nombres de propiedad en el registro, uno en singular y otro en plural.13
| Nombre | Tipo | Dónde se usa |
|---|---|---|
DeviceInterfaceGUID |
Cadena (REG_SZ) | El descriptor de propiedad extendida de Microsoft OS 1.0 especifica este nombre con wPropertyNameLength de 40 bytes5 |
DeviceInterfaceGUIDs |
Multicadena (REG_MULTI_SZ) | La forma estándar en un INF personalizado. El propio ejemplo de Microsoft también usa el plural: HKR,,DeviceInterfaceGUIDs,0x10000,"{...}"13 |
La documentación oficial, al describir el comportamiento de Winusb.sys, dice en plural: «lee la clave DeviceInterfaceGUIDs del registro y registra la interfaz de dispositivo con el GUID especificado en ella».13 El procedimiento manual de añadir la entrada al registro también indica que se coloque bajo Device Parameters una de las dos: DeviceInterfaceGUID en cadena, o DeviceInterfaceGUIDs en multicadena.13
Si usa el descriptor de propiedad de registro de OS 2.0, compruebe el nombre de propiedad y el tipo de dato en las especificaciones (en la práctica, lo habitual es el plural con REG_MULTI_SZ). Si traslada tal cual el singular de la 1.0 a la ruta de la 2.0, puede acabar escribiendo en un lugar que Windows no consulta, y quedarse en un estado de «la vinculación funciona, pero la aplicación no lo encuentra».
El método de verificación seguro es mirar el registro después de conectar el dispositivo. Comprobando en HKLM\SYSTEM\CurrentControlSet\Enum\USB\<ID de hardware>\<ID de instancia>\Device Parameters que están el nombre, el tipo y el valor previstos, se conoce el hecho con independencia de cómo se interprete el descriptor.
Además, si escribe un INF, use como clase de instalación USBDevice ({88BAE032-5A81-49f0-BC3D-A4FF138216D6}). La clase USB está reservada al controlador de host, los hubs y los dispositivos compuestos; usarla para un dispositivo propio está documentado como causa de problemas de fiabilidad y rendimiento.5
Si no se puede modificar el firmware de un dispositivo existente, la solución es preparar y distribuir usted mismo un INF personalizado que especifique el ID de hardware. En ese momento pasa a existir un «instalador que instala un controlador», y aparece el tema de la distribución (capítulo 10). Incluso un INF que no contenga ni un byte de un .sys propio y se limite a hacer referencia al winusb.sys de Microsoft necesita un catálogo firmado para poder entrar en un Windows de producción. No es «basta con escribir un INF», así que calcule el esfuerzo después de leer el capítulo 10.
Sustituir el controlador por WinUSB con una herramienta como Zadig durante el desarrollo es un método válido de verificación, pero es una operación que retira el controlador del fabricante. No lo convierta en el medio de distribución en producción: otras aplicaciones dejarían de poder usar el mismo dispositivo.
5.3 Puntos clave de la implementación
// Inicialización de WinUSB (se omite el manejo de errores)
HANDLE h = CreateFile(devicePath,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // lo asíncrono es casi obligatorio
NULL);
WINUSB_INTERFACE_HANDLE usb;
WinUsb_Initialize(h, &usb);
// Establezca siempre un tiempo de espera para la lectura (por defecto espera indefinidamente)
ULONG timeoutMs = 1000;
WinUsb_SetPipePolicy(usb, bulkInPipeId, PIPE_TRANSFER_TIMEOUT,
sizeof(timeoutMs), &timeoutMs);
// En modo asíncrono, pase NULL en LengthTransferred y tome la longitud transferida tras completarse.
// Compruebe siempre el valor de retorno. Si es FALSE y GetLastError no es ERROR_IO_PENDING, el fallo
// ya está decidido en este punto y no queda ninguna operación pendiente
BOOL started = WinUsb_ReadPipe(usb, bulkInPipeId, buffer, bufferLength, NULL, &overlapped);
if (!started && GetLastError() == ERROR_IO_PENDING) {
started = TRUE; // en curso. El resultado se espera más abajo
}
ULONG transferred = 0;
BOOL collected = FALSE; // si ya se recogió el resultado del OVERLAPPED (sin importar si tuvo éxito)
BOOL ok = FALSE; // si la lectura tuvo éxito
if (started) {
// Tanto si se completó de forma síncrona como si fue ERROR_IO_PENDING, el resultado se recibe aquí.
// Compruebe también aquí el valor de retorno. Con tiempo de espera agotado, cancelación o desconexión
// devuelve FALSE, y en ese caso transferred no es "la longitud transferida"
ok = WinUsb_GetOverlappedResult(usb, &overlapped, &transferred, TRUE);
collected = TRUE;
if (!ok) {
ReportError(GetLastError()); // tómelo inmediatamente. Si se intercala otra API, se sobrescribe
}
} else {
ReportError(GetLastError()); // desconexión, ID de pipe erróneo, identificador ya inválido, etc.
}
if (ok) {
Consume(buffer, transferred); // transferred solo es válido cuando se llega hasta aquí
}
// --- Limpieza. Se pasa por aquí en cada conexión/desconexión, así que si se olvida algo, se acumula ---
// En una aplicación real, la lectura anterior se ejecuta en bucle, así que a este punto se
// puede llegar con "puede que aún queden solicitudes sin recoger"
CancelIoEx(h, NULL); // detiene la E/S en curso, y
if (started && !collected) { // solo lo que no se haya recogido
WinUsb_GetOverlappedResult(usb, &overlapped, &transferred, TRUE); // recoge la finalización antes de
}
WinUsb_Free(usb); // liberar el identificador de interfaz, y
CloseHandle(h); // cerrar el identificador de archivo
Hay siete puntos que importan en el terreno.
- Abra con
FILE_FLAG_OVERLAPPEDy trabaje en modo asíncrono. Con E/S síncrona, si el dispositivo se queda callado, se bloquea todo el hilo - Establezca siempre
PIPE_TRANSFER_TIMEOUT. Por defecto, la lectura no vuelve nunca - Compruebe el valor de retorno de
WinUsb_ReadPipeantes de esperar. Cuando esFALSEyGetLastError()no esERROR_IO_PENDING, la propia solicitud no se ha aceptado. Justo después de una desconexión, con un ID de pipe equivocado, o con un identificador ya inválido: cualquiera de estos casos es habitual. En ese momento no hay ninguna operación pendiente, así que llamar a continuación aWinUsb_GetOverlappedResultequivale a esperar la finalización de una transferencia que ni siquiera empezó. El código de error original se sobrescribe y desaparece; lo que queda es «se completó con 0 bytes» o un error completamente distinto. El origen del problema desaparece antes de que empiece la investigación. Por eso en el código anterior se conservastarted, y la recogida en la limpieza final se rodea con la misma condición14 - Compruebe también el valor de retorno de
WinUsb_GetOverlappedResult. Incluso después de que la solicitud se haya aceptado, esta función devuelveFALSEante un tiempo de espera agotado (elPIPE_TRANSFER_TIMEOUTestablecido antes), unCancelIoEx, o una desconexión durante la transferencia. En ese caso,transferredno es «la longitud transferida». Si se usa sin comprobar el valor de retorno, el 0 con el que se inicializó fluye tal cual hacia abajo como si «se hubiera recibido un paquete vacío», y se traga un error que debería regenerar la sesión. Y como el síntoma es «un dispositivo que a veces no recibe nada», llegar a la causa lleva mucho tiempo. Si esFALSE, tomeGetLastError()en el acto (basta con intercalar una sola API más para que se sobrescriba) - En modo asíncrono, no pase un puntero en
LengthTransferred. La documentación oficial indica claramente que «siOverlappedno es NULL,LengthTransferredpuede ser NULL» y que «si se pasa un valor no nulo, el valor en el momento en queWinUsb_ReadPiperetorna carece de significado hasta que la operación se completa». La longitud transferida se obtiene conWinUsb_GetOverlappedResult.14 Pasar la dirección de una variable local no solo hace que el valor carezca de sentido: si esa variable sale de ámbito, se convierte en un puntero colgante (dangling pointer) - Empareje siempre
WinUsb_FreeconCloseHandle. Cada vez queWinUsb_Initializetiene éxito, se reserva un identificador de interfaz. Si, como en el apartado 8.2, se diseña para regenerar la sesión en cada conexión/desconexión, cada liberación que se olvide se acumula en cada reconexión. Asegúrese de que la limpieza se ejecuta en un único punto, no solo en la ruta de éxito, sino también en las rutas en las que falla algo a mitad de la inicialización (por ejemplo,WinUsb_Initializetuvo éxito pero falló la configuración del pipe). El orden es «detener la E/S en curso conCancelIoEx→ recoger la finalización conWinUsb_GetOverlappedResult→WinUsb_Free→CloseHandle». Si se cierra el identificador antes de recoger la finalización, se libera un búfer que el kernel todavía está tocando - Evite que la aplicación se inicie por duplicado. WinUSB no admite el acceso simultáneo de varias aplicaciones, así que incluya en las especificaciones una prevención de doble inicio (por ejemplo, con un mutex con nombre)
Para usar esto desde C#, como el backend de Windows de libusb está implementado sobre WinUSB, un envoltorio como LibUsbDotNet es una opción. En una aplicación empaquetada también se puede usar Windows.Devices.Usb.UsbDevice, pero tiene una restricción explícita: no se puede acceder a las clases de dispositivo Audio, HID, Image, Printer, Mass Storage, Smart Card, Audio/Video ni Wireless Controller.15
6. Método D: SDK del fabricante y controlador propietario — no se elige, se asume
Cámaras industriales, instrumentos de medida, periféricos de punto de venta (POS), autenticación por huella o vena, placas de E/S propietarias… en estos casos el fabricante proporciona el controlador y el SDK juntos, y de hecho no hay otra forma de usarlos. El método D no es tanto una opción como una condición previa que ya queda fijada en el momento en que se elige el equipo.
Por eso, sacar a la luz las restricciones del SDK en la fase de selección del equipo es, en sí mismo, el diseño. Estos son los puntos que hay que comprobar.
| Punto a comprobar | Qué pasa si se pasa por alto |
|---|---|
| Si es compatible con 32 y 64 bits | Una aplicación de 64 bits no puede llamar a una DLL exclusiva de 32 bits; hace falta separar procesos |
| Forma de la API (DLL en C / COM / .NET) | Cambia la forma de llamarla y el diseño del marshaling. Si es COM, se añaden restricciones de modelo de hilos |
| Restricciones de hilo (obligatorio STA, hilo de los callbacks) | Se bloquea el hilo de la interfaz de usuario, o se produce un interbloqueo |
| Objetos redistribuibles y condiciones de distribución | No se pueden incluir en el instalador; hace falta una instalación manual en el cliente |
| Estado de la firma del controlador incluido | No se puede instalar en compilaciones nuevas de Windows 11 ni en PC de planta |
| SO compatibles y fecha límite de soporte | Un cambio de SO obliga a rehacer la aplicación entera |
| Posibilidad de varias conexiones simultáneas y forma de identificarlas | Se rompe en cuanto se conecta el segundo equipo |
| Si hay código fuente de la aplicación de demostración | El coste de investigar comportamientos no documentados se dispara |
La bitness (ancho de bits) de la primera fila de la tabla se refiere a si la DLL del SDK está compilada como versión de 32 o de 64 bits. Que esto sea crítico en la práctica se debe a que un proceso de Windows no puede mezclar código de 32 y 64 bits dentro del mismo proceso. Un SDK que solo ofrece una DLL exclusiva de 32 bits no se puede llamar directamente desde una aplicación compilada en x64 (se producirá un BadImageFormatException o un fallo de LoadLibrary). Para evitarlo hay que compilar toda la aplicación en x86, o bien sacar solo la parte que llama al SDK a un proceso independiente de 32 bits y conectarlo mediante comunicación entre procesos. Ambas son decisiones que afectan a la estructura de la aplicación, así que deben conocerse desde la fase de selección del equipo.
En el plano de la implementación, la mayor defensa es no esparcir el SDK por toda la aplicación. Encierre las llamadas al SDK detrás de una única capa de abstracción delgada (una interfaz), y escriba el cuerpo de la aplicación contra esa abstracción. Así, el impacto de un cambio de modelo del equipo, una actualización mayor del SDK o un cambio de fabricante queda contenido en un único lugar, y además se puede escribir pruebas unitarias sin necesidad del dispositivo.
Si surge la necesidad de usar un SDK exclusivo de 32 bits desde una aplicación de 64 bits, la solución habitual es sacarlo a un proceso independiente y conectarlo mediante comunicación entre procesos. Si es por COM, «un caso real de puente COM para llamar a una DLL de 64 bits desde una aplicación de 32 bits» sigue la misma idea en sentido inverso; la forma de llamar a la DLL nativa en sí está recopilada en «Llamar a una DLL nativa desde C#: envoltorio C++/CLI frente a P/Invoke». Para la gestión del ciclo de vida del proceso hijo, consulte «Manejo seguro de procesos hijos».
7. Tabla comparativa de los cuatro métodos
| Aspecto | COM virtual | HID | WinUSB | SDK del fabricante |
|---|---|---|---|---|
| Distribución de controlador | No hace falta (CDC) / habitual (VCP) | No hace falta | Condicionalmente no hace falta; en muchos casos hace falta INF | Hace falta |
| Dificultad de implementación | Baja | Media | Media-alta | Depende del SDK (muy variable) |
| Rendimiento | Bajo-medio (depende del equipo; hay que medir) | Bajo | Alto | Alto |
| Latencia | Media | Media (depende del intervalo de sondeo) | Baja | Baja |
| Uso simultáneo por varias aplicaciones | No (puerto exclusivo) | Sí (si es una TLC compartida) | No | Depende del SDK |
| Identificación única del dispositivo | Hace falta implementarla (el número de COM no sirve) | Posible con VID/PID/número de serie | Posible con la ruta del dispositivo | Depende del SDK |
| Facilidad de aislar problemas en el terreno | Alta (software de terminal) | Media | Baja | Baja |
| Dependencia del firmware del equipo | Baja | Baja | Alta (descriptores del sistema operativo) | Total |
| Usos a los que se adapta | Equipos e instrumentos con respuesta a comandos | Notificación de estado, comandos pequeños | Grandes volúmenes de datos, protocolo propio | Cámaras, instrumentos de medida, equipos propietarios |
El orden de decisión es el siguiente.
- ¿El equipo ya aparece como puerto COM, HID o una clase estándar? → Si es así, úselo tal cual
- No aparece así, pero se puede modificar el firmware → decida entre HID (datos pequeños) o WinUSB (datos grandes) según el ancho de banda
- No se puede modificar el firmware, pero hay un SDK del fabricante → use el SDK. Antes, haga la comprobación del capítulo 6
- Nada de lo anterior encaja y hace falta acceso simultáneo de varias aplicaciones → evalúe el desarrollo de un controlador UMDF2
8. Cuatro cosas que hay que diseñar uno mismo sea cual sea el método
Aunque ya se haya decidido el método, hay cuatro cosas que hay que construir uno mismo. Una aplicación de integración de equipos que «a veces no funciona» casi siempre tiene alguna de estas cuatro cosas sin resolver.
8.1 Identificación única del dispositivo — agárrelo por el ID, no por el número
En la configuración solo debe guardarse el VID/PID + número de serie, o bien la ruta de interfaz de dispositivo. El número de COM o el orden en el que aparece en el Administrador de dispositivos no son identificadores.
Si el dispositivo tiene número de serie o no se sabe por el último elemento de la ruta de instancia del dispositivo.
USB\VID_2341&PID_0043\85436323631351D0E1C1 ← el equipo informa un número de serie (no cambia al moverlo)
USB\VID_0403&PID_6001\5&1a2b3c4d&0&2 ← no lo informa (valor generado por Windows a partir de la posición de conexión)
En un dispositivo compuesto, el VID/PID + número de serie por sí solos no bastan. Como se explicó en el capítulo 2, un mismo equipo puede tener varias funciones, y en ese caso todas comparten el mismo VID, PID y número de serie. En dispositivos con, por ejemplo, «dos CDC, uno de control y otro de mantenimiento» o «dos colecciones de nivel superior HID», este trío de datos coincide para varios candidatos a la vez, y cuál se abre queda librado al azar. Incluya en la clave también el elemento que distingue la función.
USB\VID_1234&PID_5678&MI_00\7&2a3b4c5d&0&0000 ← función 0 (por ejemplo, el CDC de control)
USB\VID_1234&PID_5678&MI_02\7&2a3b4c5d&0&0002 ← función 2 (por ejemplo, el CDC de mantenimiento)
mismo VID/PID, mismo dispositivo padre ↑ solo cambia MI_xx (número de interfaz USB)
En la práctica, la clave se construye por alguna de estas vías:
- Incluir el número de interfaz USB (
MI_xx) — la forma estándar de distinguir las funciones CDC/WinUSB de un dispositivo compuesto - En HID, combinarlo con Usage Page + Usage — así se distinguen varias colecciones de nivel superior de un mismo dispositivo (
UsagePage/UsagedeHidP_GetCaps) - Guardar tal cual la ruta de interfaz de dispositivo — la cadena que se obtiene al enumerar es única por función, así que usarla como clave es lo más fiable
Si su empresa puede decidir las especificaciones del equipo, asignar un número de interfaz distinto por función e informar siempre un número de serie en el firmware simplifica de forma drástica la lógica de identificación del lado del software.
Lo segundo (el que no informa el número de serie) contiene &, y su valor cambia si se conecta a otro puerto.
Ahora bien, este criterio no se puede aplicar a los nodos hijo de un dispositivo compuesto. En un dispositivo compuesto, el final del ID de instancia del PDO hijo enumerado como función CDC o colección HID es un valor generado por Usbccgp.sys, y contiene & aunque el dispositivo físico sí informe un número de serie. Juzgar «no tiene número de serie» mirando solo la ruta del hijo es un error. El número de serie está en el nodo del dispositivo USB padre, así que hay que subir hasta el padre con CM_Get_Parent (o DEVPKEY_Device_Parent) y mirar su ID de instancia.
USB\VID_1234&PID_5678\SN0001234 ← padre (dispositivo físico). Aquí está el número de serie
└ USB\VID_1234&PID_5678&MI_00\7&2a3b… ← hijo. Valor generado por Usbccgp, contiene & (no usar para el criterio)
Si en un entorno con varios equipos del mismo modelo se elige un equipo sin número de serie, el único medio de identificación que queda es «en qué puerto USB está conectado». En ese caso, hay que diseñar fijando el puerto del hub y llegando hasta el procedimiento operativo de pegar una etiqueta — esto es una parte que no se resuelve con tecnología, así que hay que darse cuenta en la fase de selección del equipo.
8.2 Seguimiento de la conexión y desconexión — no haga sondeo (polling)
Es habitual ver implementaciones que vuelven a enumerar cada segundo con un Timer, pero Windows tiene un mecanismo de notificación.
- Windows 8 o posterior: registre en
CM_Register_Notificationlos tiposCM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE(detección de llegada y eliminación) yCM_NOTIFY_FILTER_TYPE_DEVICEHANDLE(detección de que el dispositivo de un identificador abierto ha desaparecido)16 - Si también hay que dar soporte a Windows 7 o anterior: registre
DBT_DEVTYP_DEVICEINTERFACEconRegisterDeviceNotificationy proceseWM_DEVICECHANGE17
Hay dos puntos de atención que no se pueden pasar por alto en la implementación.16
CM_Register_Notificationno notifica «las interfaces que ya existían en el momento del registro». Regístrese primero, y después enumere lo ya existente conCM_Get_Device_Interface_List. Si se invierte el orden, se pierden los equipos conectados en ese intervalo- A cambio, hay que dar por hecho que aparecerán duplicados. Una interfaz activada entre el registro y la enumeración aparece a la vez en la notificación de llegada y en el listado. Si se envían ambas tal cual al procesamiento de llegada, se genera dos veces la sesión del mismo equipo, y la segunda apertura exclusiva falla, o se sobrescribe un estado ya establecido. Mantenga siempre un conjunto con clave en la ruta de interfaz de dispositivo, e ignore las rutas ya conocidas — esta eliminación de duplicados es imprescindible (este conjunto se limpia al retirar el dispositivo)
- No haga en el callback nada que pueda bloquear. El procesamiento que implique E/S debe delegarse a otro hilo. Esperar aquí atasca todo el procesamiento de eventos PnP
En una aplicación empaquetada, o en una configuración donde se pueda usar la API de WinRT, DeviceWatcher permite escribir lo mismo de forma más concisa.
// Vigila un dispositivo serie de un VID/PID concreto (WinRT)
string selector = SerialDevice.GetDeviceSelectorFromUsbVidPid(0x2341, 0x0043);
DeviceWatcher watcher = DeviceInformation.CreateWatcher(selector);
watcher.Added += (s, info) => OnDeviceArrived(info.Id);
watcher.Removed += (s, info) => OnDeviceRemoved(info.Id);
watcher.Start();
En una aplicación de escritorio tradicional (WinForms / WPF) donde no se puede usar WinRT, hay dos patrones posibles.
Patrón 1: RegisterDeviceNotification + WM_DEVICECHANGE (recomendado)
Se registra la notificación de la interfaz de dispositivo contra el identificador de ventana, y se recibe en WndProc. En WinForms, el punto de entrada es la sobrecarga de WndProc; en WPF, HwndSource.AddHook.
// Ejemplo en WinForms. En WPF, escriba el mismo procesamiento en HwndSource.AddHook
const int WM_DEVICECHANGE = 0x0219;
const int DBT_DEVICEARRIVAL = 0x8000;
const int DBT_DEVICEREMOVECOMPLETE = 0x8004;
const int DBT_DEVTYP_DEVICEINTERFACE = 0x00000005;
const int DEVICE_NOTIFY_WINDOW_HANDLE = 0x00000000;
// GUID de clase de interfaz para dispositivos USB
static readonly Guid GUID_DEVINTERFACE_USB_DEVICE =
new("A5DCBF10-6530-11D2-901F-00C04FB951ED");
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct DEV_BROADCAST_DEVICEINTERFACE
{
public int dbcc_size;
public int dbcc_devicetype;
public int dbcc_reserved;
public Guid dbcc_classguid;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
public char[] dbcc_name;
}
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern IntPtr RegisterDeviceNotification(IntPtr hRecipient, IntPtr filter, int flags);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool UnregisterDeviceNotification(IntPtr handle);
private IntPtr _notification = IntPtr.Zero;
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
var filter = new DEV_BROADCAST_DEVICEINTERFACE
{
dbcc_size = Marshal.SizeOf<DEV_BROADCAST_DEVICEINTERFACE>(),
dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE,
dbcc_classguid = GUID_DEVINTERFACE_USB_DEVICE,
dbcc_name = new char[1],
};
IntPtr buffer = Marshal.AllocHGlobal(filter.dbcc_size);
int error;
try
{
Marshal.StructureToPtr(filter, buffer, fDeleteOld: false);
_notification = RegisterDeviceNotification(Handle, buffer, DEVICE_NOTIFY_WINDOW_HANDLE);
error = Marshal.GetLastWin32Error(); // tómelo antes de llamar a FreeHGlobal
}
finally
{
Marshal.FreeHGlobal(buffer); // se copia al registrar, así que aquí ya se puede liberar
}
// Si falla no salta ninguna excepción; el valor de retorno simplemente es NULL. Si no se
// comprueba, se obtiene una aplicación que "arrancó bien pero WM_DEVICECHANGE nunca llega",
// y se pierde tiempo hasta descubrir que la causa de no seguir las conexiones y
// desconexiones está en el registro de la notificación
if (_notification == IntPtr.Zero)
{
// Win32Exception es de System.ComponentModel
throw new Win32Exception(error, "No se pudo registrar la notificación de dispositivo.");
}
// Enumere lo existente después de registrar. Si se invierte, se pierden los equipos
// conectados en el intervalo (la eliminación de duplicados es imprescindible)
ScanExistingDevices();
}
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_DEVICECHANGE)
{
switch ((int)m.WParam)
{
case DBT_DEVICEARRIVAL:
// Aquí solo se marca un indicador. La E/S se delega a otro hilo
QueueRescan();
break;
case DBT_DEVICEREMOVECOMPLETE:
QueueRescan();
break;
}
}
base.WndProc(ref m);
}
protected override void OnHandleDestroyed(EventArgs e)
{
if (_notification != IntPtr.Zero)
{
UnregisterDeviceNotification(_notification);
_notification = IntPtr.Zero;
}
base.OnHandleDestroyed(e);
}
También se ven implementaciones que solo miran WM_DEVICECHANGE sin registrar nada, pero en ese caso lo que llega es principalmente DBT_DEVNODES_CHANGED (un evento que solo informa de que «el árbol de dispositivos cambió»), y no se sabe qué equipo ha sido. Al final acaba siendo una reenumeración completa cada vez, así que si quiere limitarse a un objetivo concreto, registre la notificación como se ha mostrado.
Patrón 2: eventos de creación y eliminación de instancias de WMI
En un servicio sin ventana o en una aplicación de consola, suscribirse a eventos de WMI resulta cómodo.
using System.Management;
// WITHIN 2 significa "sondear cada 2 segundos". Cuanto menor sea el valor, mayor la carga
var arrival = new ManagementEventWatcher(new WqlEventQuery(
"SELECT * FROM __InstanceCreationEvent WITHIN 2 " +
"WHERE TargetInstance ISA 'Win32_PnPEntity'"));
var removal = new ManagementEventWatcher(new WqlEventQuery(
"SELECT * FROM __InstanceDeletionEvent WITHIN 2 " +
"WHERE TargetInstance ISA 'Win32_PnPEntity'"));
arrival.EventArrived += (s, e) =>
{
var target = (ManagementBaseObject)e.NewEvent["TargetInstance"];
var pnpId = target["PNPDeviceID"] as string; // aquí se decide el VID/PID
QueueRescan();
};
removal.EventArrived += (s, e) => QueueRescan();
arrival.Start();
removal.Start();
Pero esta consulta de WMI es un sondeo (polling). Con WITHIN 2, la detección puede retrasarse hasta 2 segundos, y cuanto más se acorte el intervalo, mayor será la carga sobre WMI. En una aplicación que necesite responder de inmediato a la conexión/desconexión, elija el patrón 1; considere WMI como opción cuando se trate de «un servicio en segundo plano donde un retraso de unos segundos es aceptable».
Además, sea cual sea el patrón que se use, el contenido de QueueRescan es común. La notificación no es más que una señal de «algo ha cambiado»; qué hay realmente conectado se confirma volviendo a enumerar. Si se escribe un procesamiento distinto para cada tipo de notificación, lo único que se consigue es multiplicar los huecos de duplicación y de pérdida que se describen a continuación.
Y lo importante es que la notificación PnP no debe ser el «único punto de entrada». Cuando se desconecta el cable, la E/S que estaba en curso puede completarse con un error de eliminación o cancelación antes que la notificación, o al mismo tiempo. El orden «al recibir la notificación, cerrar el identificador lo primero de todo» solo se cumple cuando la notificación llega antes, así que una implementación que dependa únicamente de eso sigue arrastrando el «se cae al desconectarlo» del apartado 3.4.
La forma correcta es tener dos puntos de entrada para el fin de sesión.
- En todas las rutas de finalización de lectura/escritura, trate los errores de tipo eliminación (
ERROR_DEVICE_NOT_CONNECTED/ERROR_DEVICE_REMOVED/ERROR_GEN_FAILURE, o en .NET laIOExceptioncorrespondiente) como «el dispositivo ha desaparecido», y desde ahí entre en la destrucción de la sesión - Trate la notificación PnP como una señal auxiliar. Es necesaria para capturar el caso de que se desconecte mientras no hay E/S en marcha, en espera, pero por sí sola no basta
Aquí hay algo que debe distinguirse sin falta: el caso de «lo he cancelado yo mismo». La interrupción por tiempo de espera agotado en la respuesta, la limpieza al cerrar la aplicación, la interrupción por una acción del usuario — cuando se usa CancelIoEx, CancellationToken o Dispose por estas razones, aparecen ERROR_OPERATION_ABORTED, OperationCanceledException u ObjectDisposedException aunque el funcionamiento sea normal. Si estos se interpretan sin condiciones como «el dispositivo ha desaparecido», se acaba con un bucle de mala calidad en el que el dispositivo sigue conectado, pero cada vez que hay un tiempo de espera se descarta la sesión y se reconecta.
// Distingue por estado si fue "yo mismo quien lo detuvo" o "el dispositivo desapareció".
// operationCts es el token de este único ciclo de lectura/escritura.
// El tiempo de espera de respuesta y la interrupción del usuario se cancelan con este
catch (OperationCanceledException ex) when (IsSelfCancelled(ex, operationCts.Token))
{
// Autocancelación. La sesión no está rota, así que no se destruye
}
catch (ObjectDisposedException) when (_shutdown.IsCancellationRequested)
{
// Caso en el que una E/S que estaba en vuelo vuelve después de que el
// proceso de cierre ya haya cerrado el identificador
}
catch (Exception ex) when (IsDeviceGone(ex))
{
TearDownSession(); // idempotente. No se ejecuta dos veces aunque lo llame la notificación PnP
}
// Determina "si en este momento soy yo quien está pidiendo la cancelación" comparándolo
// con el token que realmente se canceló
private bool IsSelfCancelled(OperationCanceledException ex, CancellationToken operation) =>
ex.CancellationToken == operation ||
ex.CancellationToken == _shutdown.Token ||
operation.IsCancellationRequested ||
_shutdown.IsCancellationRequested;
No basta con mirar solo el cierre de toda la aplicación. El tiempo de espera de respuesta y la interrupción por acción del usuario se cancelan con un token exclusivo de ese único ciclo de operación. En ese caso _shutdown no está activado, así que un catch que solo condicione por _shutdown.IsCancellationRequested lo deja pasar de largo. Ese OperationCanceledException que pasó de largo tampoco encaja en el siguiente IsDeviceGone (no hay que juzgar la autocancelación como «el dispositivo ha desaparecido»), así que sigue cayendo hasta el fondo y tira abajo todo el bucle de E/S. Es una forma de rotura en la que el hilo receptor muere cada vez que hay un tiempo de espera.
El criterio no se puede resolver solo con el tipo de excepción o el código de error. Solo se puede distinguir cotejándolo con el token del lado que hizo la cancelación. Dicho de otro modo, si su aplicación tiene una ruta de autocancelación, ese hecho debe quedar visible desde la capa de E/S. Escriba también IsDeviceGone de forma que no juzgue sin condiciones que OperationCanceledException y ObjectDisposedException significan «el dispositivo ha desaparecido».
Sea cual sea la ruta por la que se entre, haga que acabe en el mismo procesamiento de limpieza, idempotente, para que una llamada doble no rompa nada (por ejemplo, levantando un indicador con Interlocked.Exchange para que se ejecute una sola vez, la primera). Las excepciones que lanza una E/S sobre un identificador ya corrompido suelen llegar desde lugares difíciles de capturar — precisamente por eso hace falta un diseño que las atrape en el propio origen de la excepción y las conecte con la transición de estado.
8.3 Tiempos de espera y reconexión — con «un solo timeout» no basta
En la E/S de un dispositivo USB, tanto si se ha desconectado, como si se ha apagado, como si el firmware se ha colgado, el síntoma es el mismo: «no vuelve». Los tiempos de espera hay que tenerlos separados por significado.
| Tiempo de espera | A qué se aplica | Referencia |
|---|---|---|
| Tiempo de espera de apertura | Hasta abrir el dispositivo | Del orden de segundos |
| Tiempo de espera de respuesta | Desde que se emite el comando hasta que se completa la respuesta | El peor caso de la especificación del equipo × un factor de seguridad |
| Tiempo de espera entre bytes | Cuando, en medio de una trama, no llega la continuación | Calculado a partir de la velocidad de comunicación |
| Retroceso de reconexión | Intervalo de espera para reabrir | Retroceso exponencial (backoff) + límite superior |
Y trate el tiempo de espera no como «un seguro para cuando va lento», sino como «una regla que hace avanzar la transición de estados». El diseño solo está completo cuando se decide a qué estado se pasa al agotarse el tiempo de espera, cómo se da por fallida la solicitud en curso y qué se muestra en la interfaz de usuario. La forma de mostrarlo en la interfaz se trata en «Buenas prácticas para comprobar y mostrar el estado de un dispositivo externo» — no se conforme con un simple «conectando».
8.4 Gestión de energía — la verdadera causa de «responde lento sin haberlo desconectado»
La suspensión selectiva de USB es un mecanismo que pone en un estado de bajo consumo a un dispositivo inactivo. Como la recuperación tarda, es la causante habitual de síntomas como «solo la primera respuesta es lenta» o «si se deja en reposo un rato, se pierde el primer comando».
- En
Usbser.sys(COM virtual), está deshabilitada por defecto; se habilita y configura conIdleUsbSelectiveSuspendPolicyen el registro3 - En WinUSB, se controla con
DeviceIdleEnabled,DefaultIdleTimeout,UserSetDeviceIdleEnabled, etc., en el descriptor de función de propiedad extendida del sistema operativo (o en el INF)5
Lo primero que hay que comprobar en el terreno es la casilla «Permitir que el equipo apague este dispositivo para ahorrar energía» en las propiedades del dispositivo en cuestión (y del concentrador raíz USB) en el Administrador de dispositivos. En un PC de planta, hay fallos reales que se resuelven con solo desmarcar esta casilla. Incluyendo la configuración de ahorro de energía de un portátil, la verificación debe hacerse con el mismo plan de energía que en producción.
9. Estimación del rendimiento y la latencia
Poner cifras al ancho de banda y la latencia necesarios en la fase de selección del método evita tener que dar marcha atrás más adelante.
| Tipo de transferencia | Método que la usa | Características |
|---|---|---|
| Transferencia de control | Todos los métodos (uso interno) | Para configuración y comandos pequeños. Sin garantía de ancho de banda |
| Transferencia de interrupción | HID, WinUSB | Sondeo periódico. Baja latencia pero poca capacidad |
| Transferencia en bloque (bulk) | WinUSB, almacenamiento masivo | Orientada a grandes volúmenes. Sin garantía de ancho de banda; usa el hueco libre |
| Transferencia isócrona | UVC (cámara), UAC (audio), WinUSB (8.1 o posterior) | Con garantía de ancho de banda, sin reenvío |
Los endpoints de interrupción de USB 2.0 tienen, a velocidad completa, un máximo de 64 bytes por paquete con un intervalo de sondeo de 1 a 255 ms, y a alta velocidad, un máximo de 1024 bytes con un intervalo en unidades de 125 µs.11 Si va a elegir HID, compruebe que el ancho de banda necesario tiene, respecto a este límite, un margen de al menos un orden de magnitud.
Otro punto: Windows es un sistema operativo de propósito general, así que no hay garantías de latencia. Es poco realista pretender cumplir en la capa de la aplicación un requisito del tipo «debe responder siempre con un ciclo de 10 ms». Si el control periódico es una necesidad esencial, diseñe de forma que quede confinado en el microcontrolador del equipo, dejando que el PC se limite a mandar instrucciones y supervisar. Esta línea divisoria se trata en detalle en «Guía práctica para acercarse todo lo posible a un tiempo real por software en un Windows normal».
10. Distribución y operación — el coste cambia en el momento en que distribuye un controlador
Entre los métodos que «no necesitan controlador» (clase estándar, HID, dispositivo WinUSB) y los métodos que «distribuyen un controlador» hay un salto no de coste de desarrollo, sino de coste de distribución y mantenimiento.
- Es un error pensar que «como no escribo mi propio
.sys, no hace falta firma». En la instalación de dispositivos PnP, si el archivo de catálogo del paquete de controlador no está firmado, no se coloca en el Driver Store.18 Es un requisito que no depende del contenido del paquete, así que incluso un INF que se limite a referenciar elwinusb.sysde Microsoft, como en el apartado 5.2, necesita el proceso de generar y firmar un catálogo (.cat). Si calcula que «con escribir un INF y distribuirlo basta», se dará cuenta cuando el cliente lo rechace con un «el controlador de este dispositivo no está firmado». La firma del catálogo es una firma de publicación WHQL, o una firma con un certificado de publicación de terceros (SPC).18 - Firma de controladores en modo kernel. A partir de Windows 10 versión 1607, un controlador nuevo en modo kernel no se carga si no lo firma Microsoft a través del Dev Portal (Partner Center). Abrir una cuenta en Partner Center requiere un certificado de firma de código EV.6 Es un requisito de una capa distinta a la firma del catálogo anterior; si incluye binarios en modo kernel, hay que cumplir ambos.
- Hay dos vías de firma, y su alcance es distinto. La firma HLK tested / dashboard signed, que ha pasado las pruebas HLK, es válida incluso incluyendo desde Windows Vista hasta Windows Server, y Microsoft la recomienda como vía preferente. La otra vía, attestation signing, no necesita pruebas HLK a cambio, pero solo es válida desde Windows 10 de escritorio en adelante (no se acepta en Windows 7/8.1 ni en Windows Server 2016 en adelante). Además, no se puede distribuir a través de Windows Update para usuarios generales, y no obtiene la certificación Windows (Windows Certified). La propia documentación de Microsoft la describe como «con fines de prueba». 19 Usar attestation signing para un controlador propio distribuido con el instalador de su empresa es, de hecho, una práctica muy extendida, pero adóptela solo después de trasladar a la tabla de SO compatibles el hecho de que el objetivo queda limitado a Windows 10/11 de escritorio. Si el PC de planta ejecuta Windows Server o un LTSC antiguo, esta vía queda descartada desde el principio.
- No dé por sentadas las condiciones de excepción. Con Secure Boot deshabilitado, o con un certificado emitido antes del 29 de julio de 2015, un controlador con firma cruzada también funciona, pero basar un plan de distribución en esto se rompe en unos pocos años.6
- Consulte el coste y el plazo antes de empezar a escribir código. Si va a distribuir un controlador en modo kernel, la obtención del certificado de firma de código EV y la apertura de una cuenta en Partner Center son procesos previos.6 El importe y el plazo necesario varían según la autoridad de certificación, la época y el estado de los datos registrales de su empresa, así que no se fíe de las cifras de casos de otras empresas: confirme usted mismo, con el nombre de su propia empresa, (1) el coste anual del certificado EV (pida presupuesto a varias autoridades de certificación), (2) el plazo que exige la verificación de existencia de la organización, obligatoria para el certificado EV, y (3) el plazo necesario para abrir la cuenta en Partner Center. Esto es tiempo de trámites, no de tecnología, así que si no se avanza en paralelo con el calendario de desarrollo, se llega a la situación de tener el código terminado pero no poder distribuirlo.
- Diseño del instalador. Un instalador que incluya un controlador necesita permisos de administrador, y también hace falta verificar la instalación silenciosa. La forma de elegir el propio método de distribución se recoge en «Cómo elegir el método de distribución de una aplicación de Windows», y cómo identificar en qué casos hace falta el privilegio de administrador, en «Cuándo hace falta realmente el privilegio de administrador en Windows».
- En un PC de planta, el controlador y las actualizaciones del SO compiten entre sí. Si va a instalar un controlador del fabricante en un PC de planta con configuración LTSC, decida a la vez la fijación de la compilación del SO y la política de actualización del controlador. «Qué Windows conviene instalar en un PC industrial» puede servirle de referencia.
Como decisión de diseño, casi siempre es correcto «si existe un método que evita distribuir un controlador, elegirlo aunque la implementación sea algo más engorrosa». La fuerza silenciosa de HID se reduce, en el fondo, a este único punto.
11. Fallos habituales y cómo resolverlos
| Síntoma | Causa habitual | Solución |
|---|---|---|
| Funciona en la máquina de desarrollo pero no en la del cliente | El número de COM está fijado a mano en la configuración | Resolverlo en tiempo de ejecución a partir del VID/PID y el número de serie (8.1) |
| Al conectar el segundo equipo, funciona mal | El equipo no informa un número de serie | Revisar la selección del equipo. Si no es posible, fijar el puerto y llevar un procedimiento con etiquetas |
| Se agarra a un dispositivo distinto cada vez, aunque es un solo equipo | Es un dispositivo compuesto y la clave no distingue la función | Usar como clave MI_xx, el Usage de HID o la ruta de interfaz de dispositivo (8.1) |
| La aplicación se cae al desconectar el cable | E/S sobre un identificador corrompido; limpieza que solo depende de la notificación PnP | Tratar también los errores de tipo eliminación en la ruta de finalización de E/S como fin de sesión (8.2) |
| Solo la primera respuesta es lenta o se pierde | Recuperación desde la suspensión selectiva | Revisar o deshabilitar la configuración de gestión de energía (8.4) |
| Al enviar por HID, el equipo no responde | Los datos están desplazados por el identificador de informe | El búfer debe medir exactamente OutputReportByteLength, con el identificador de informe al principio (4.3) |
| No se puede leer ni un dato por HID | El objetivo es una TLC que el SO abre en modo exclusivo (teclado, etc.) | Cambiar el modo del equipo. Si solo se quiere enumerar, abrir con permiso 0 (4.2) |
| El Read de WinUSB no vuelve nunca | PIPE_TRANSFER_TIMEOUT sin establecer |
Establecer un tiempo de espera en la política de pipe (5.3) |
| Se reconecta cada vez que hay un tiempo de espera | Se confunde la autocancelación con una desconexión del dispositivo | Distinguirlo cotejando el estado con el token de cancelación, etc. (8.2) |
| Al repetir la conexión y desconexión, se vuelve cada vez más lento | Fugas de WinUsb_Free/CloseHandle |
Concentrar la limpieza en un único punto y asegurarse de pasar por ahí también en las rutas de fallo (5.3) |
| Después de una llamada P/Invoke se corrompe una variable sin relación | La estructura está declarada a medias | Declarar todos los campos con el mismo tamaño y el mismo orden que en el lado nativo (4.3) |
| En el cliente no se instala el controlador | Catálogo sin firmar (da igual si hay o no un .sys propio) |
Incluir la generación y firma del .cat en el plan de distribución (capítulo 10) |
| Al iniciar dos veces la aplicación, una de las dos falla | WinUSB no admite acceso simultáneo | Prevenir el doble inicio, o centralizarlo a través de un servicio en segundo plano |
| En una compilación de 64 bits no se puede cargar el SDK | DLL exclusiva de 32 bits | Separarla en un proceso independiente y conectarlo por IPC (capítulo 6) |
| No se sabe si el contenido de la comunicación realmente está llegando | No hay medio de observación | Analizador de protocolo USB, una traza equivalente a usbmon, implementar un registro de comunicación |
La última fila suele infravalorarse, pero es importante. Contar desde el principio con un medio para distinguir «quién tiene la culpa, la aplicación o el equipo» reduce de forma drástica el tiempo en el que la causa se desconoce. El método de COM virtual es fuerte en el terreno porque existe el software de terminal, una herramienta de aislamiento de problemas que cualquiera puede usar. Si elige HID o WinUSB, prepare usted mismo un registro y una CLI de pruebas que hagan ese papel.
12. Resumen
- La forma de tratar un dispositivo USB no la decide el equipo en sí, sino qué controlador se cargó sobre él. Se empieza mirando en el Administrador de dispositivos el ID de hardware, el ID compatible y la ruta de instancia del dispositivo.
- El orden de selección oficial es «de lo más simple a lo más complejo»: controlador de clase estándar → WinUSB (una sola aplicación) → UMDF (varias aplicaciones) → KMDF. Escribir un controlador propio es el último recurso.
- El COM virtual es fácil de implementar y de aislar problemas en el terreno, pero el número de COM no es un identificador. Resuélvalo en tiempo de ejecución a partir del VID/PID y el número de serie. El rendimiento cambia en órdenes de magnitud según sea a través de un conversor USB-UART o CDC nativo, así que mídalo en lugar de darlo por sentado.
- HID es una opción fuerte que permite comunicación bidireccional sin distribuir ningún controlador, pero las colecciones equivalentes a ratón, teclado, pantalla táctil y lápiz las abre el SO en modo exclusivo y no se pueden tocar, y el ancho de banda de las transferencias de interrupción marca el techo.
- WinUSB está pensado para grandes volúmenes de datos y protocolos propios. Ahora bien, que no haga falta INF solo se cumple si el equipo tiene descriptores del sistema operativo y se usa en Windows 8 o posterior, y no admite acceso simultáneo de varias aplicaciones. Para un firmware nuevo, el primer candidato es el descriptor OS 2.0.
- El SDK del fabricante no es una opción, es una condición previa. Saque a la luz la bitness, las restricciones de hilo, las condiciones de redistribución y la fecha límite de soporte en la fase de selección del equipo, y envuelva el SDK con una capa de abstracción delgada del lado de la aplicación.
- Sea cual sea el método, hay que diseñar uno mismo la identificación única, el seguimiento de conexión/desconexión, los tiempos de espera en varias capas y la gestión de energía. Ahí está el origen del «a veces no funciona». En un dispositivo compuesto, baje la identificación hasta el nivel de función, y capture la desconexión tanto por la notificación PnP como por los errores de E/S.
- Si va a distribuir un paquete de controlador, hace falta la firma del catálogo aunque no exista un
.syspropio. Si incluye binarios en modo kernel, además hace falta la firma de Microsoft posterior a la 1607 (y el certificado EV). Attestation signing no necesita HLK a cambio de quedar limitado a Windows 10 de escritorio en adelante, así que elíjalo solo después de cotejarlo con la tabla de SO compatibles. Si existe un método que evite distribuir un controlador, elegirlo es, en la práctica, casi siempre la respuesta correcta.
Artículos relacionados
- Trampas habituales de las aplicaciones de comunicación serie: hasta la reconexión y el diseño de registro
- Buenas prácticas para comprobar y mostrar el estado de un dispositivo externo: un diseño que no se conforma con «conectando»
- Llamar a la API de Win32 de forma segura desde C# — guía práctica de P/Invoke
- Llamar a una DLL nativa desde C#: envoltorio C++/CLI frente a P/Invoke
- Guía práctica para acercarse todo lo posible a un tiempo real por software en un Windows normal
- Cómo elegir el método de distribución de una aplicación de Windows — MSI/MSIX/ClickOnce/xcopy/actualizador propio
- Qué Windows conviene instalar en un PC industrial — guía práctica de Windows IoT Enterprise / LTSC
- Guía práctica de Process Monitor (ProcMon): identificar en 10 minutos «no se lee la configuración» o «ACCESS DENIED»
Áreas de consultoría relacionadas
KomuraSoft LLC se ocupa del diseño de la integración entre aplicaciones de Windows y equipos o instrumentos de medida conectados por USB, del envoltorio y la migración a 64 bits de SDK existentes, y de la investigación de causas y la corrección de aplicaciones de integración de equipos que se vuelven inestables con las conexiones, desconexiones o reconexiones.
- Desarrollo de aplicaciones de Windows
- Consultoría técnica y revisión de diseño
- Investigación y análisis de causas de fallos
- Contacto
Referencias
-
Microsoft Learn, USB device class drivers included in Windows. La lista de controladores de clase USB que Windows incluye de serie (Usbaudio.sys / Usbser.sys / Hidclass.sys y Hidusb.sys / Usbscan.sys / Usbprint.sys / Usbstor.sys / Usbvideo.sys, etc.), que un fabricante no debería escribir un controlador para las clases de dispositivo ya soportadas, que para una clase no clasificada, incluida Vendor Specific (FFh), se recomienda WinUSB (Winusb.sys), que en un dispositivo compuesto Usbccgp.sys genera un PDO por función, y el uso diferenciado entre la clase de instalación USBDevice ({88BAE032-5A81-49f0-BC3D-A4FF138216D6}) y la clase USB. También procede de esta página la mención, en la fila de CDC (02h), de que «en Windows 10, Usbser.inf carga automáticamente Usbser.sys como controlador de función», así como la vía de tratar la subclase 02h (ACM) con un INF personalizado que hace referencia a mdmcpq.inf. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Choose a driver model for developing a USB client driver. El orden de selección de «empezar por el método más simple» (controlador de clase estándar → WinUSB → UMDF → KMDF), que WinUSB es adecuado cuando el acceso lo hace una única aplicación, hay endpoints bulk/interrupt/isochronous y el objetivo incluye Windows XP SP2 o posterior, que WinUSB no sirve para acceso simultáneo de varias aplicaciones, y la tabla comparativa de funciones de WinUSB / UMDF / KMDF (la transferencia isochronous la admite WinUSB a partir de Windows 8.1; UMDF no la admite). ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, USB serial driver (Usbser.sys). Que declarar clase 02 y subclase 02 en el descriptor de dispositivo hace que, mediante el ID compatible (USB\Class_02&SubClass_02), coincida el Usbser.inf estándar y se cargue automáticamente Usbser.sys sin distribuir un INF propio; que si la subclase no es 02 no hay carga automática; que se puede comunicar con un dispositivo CDC desde el espacio de nombres Windows.Devices.SerialCommunication; y que la suspensión selectiva está deshabilitada por defecto y se puede configurar mediante IdleUsbSelectiveSuspendPolicy en el registro. ↩ ↩2 ↩3
-
Microsoft Learn, HID Architecture. Que el controlador de clase HID (hidclass.sys) abstrae la comunicación entre el cliente HID y el transporte; la lista de colecciones de nivel superior que admite Windows y su modo de acceso (ratón, teclado, lápiz, pantalla táctil y panel táctil de precisión son exclusivos; mando de juego, sensores, lector de código de barras, etc., son compartidos); que por motivos de seguridad el Raw Input Manager (RIM) abre esos dispositivos en modo exclusivo; y que, aun estando abiertos en modo exclusivo, si se abre el identificador sin solicitar permisos de lectura/escritura, se puede obtener información con HidD_GetXxx. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, WinUSB Device. Que un dispositivo WinUSB es un dispositivo USB cuyo firmware informa WINUSB como ID compatible mediante los descriptores de función del sistema operativo de Microsoft, y que en ese caso Winusb.sys se carga sin un INF personalizado; que antes de Windows 8 no existía la coincidencia automática por ID compatible y era imprescindible un INF personalizado (en Windows 8, el Winusb.inf incluido de serie pasó a admitir USB\MS_COMP_WINUSB, y para versiones anteriores se distribuye un INF actualizado a través de Windows Update); el mecanismo del descriptor de cadena de sistema operativo en el índice de cadena 0xEE y el código de fabricante; establecer WINUSB en compatibleID mediante el descriptor de ID compatible extendido; que registrar DeviceInterfaceGUID mediante el descriptor de propiedad extendida permite a la aplicación descubrir y operar el dispositivo; el uso de la clase de instalación USBDevice ({88BAE032-5A81-49f0-BC3D-A4FF138216D6}) en lugar de la clase USB; y la configuración de gestión de energía mediante DeviceIdleEnabled / DefaultIdleTimeout / UserSetDeviceIdleEnabled / SystemWakeEnabled. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Driver Signing Policy. Que a partir de Windows 10 versión 1607, un controlador nuevo en modo kernel que no esté firmado a través del Dev Portal no se carga; que registrarse en el programa Windows Hardware Dev Center requiere un certificado de firma de código EV; y las condiciones de excepción en las que se admite un controlador con firma cruzada (actualización a 1607, Secure Boot deshabilitado, certificado terminal emitido antes del 29 de julio de 2015). ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Opening HID collections. La secuencia por la que una aplicación en modo usuario identifica una colección HID con las funciones SetupDi*, la abre con CreateFile, obtiene los datos preparseados y la información con HidD_Xxx, lee los informes de entrada con ReadFile y envía los de salida con WriteFile, e interpreta los informes con HidP_Xxx. ↩
-
Microsoft Learn, HIDP_CAPS structure (hidpi.h). La definición completa de la estructura (Usage / UsagePage / InputReportByteLength / OutputReportByteLength / FeatureReportByteLength / Reserved[17], seguidos de los diez miembros «Number», en total USHORT×32), y que cada longitud de informe es un valor que incluye el byte del identificador de informe. ↩
-
Microsoft Learn, Sending HID Reports. Que una aplicación en modo usuario debe usar WriteFile para enviar informes de salida de forma continuada; que las rutinas HidD_SetXxx (HidD_SetOutputReport / HidD_SetFeature) también pueden enviar informes de salida o de función, pero HidD_SetXxx debería limitarse al uso de establecer el estado actual de la colección; y que se advierte de que «algunos dispositivos no admiten HidD_SetOutputReport, y usar esta rutina puede hacer que dejen de responder». Junto con HidD_SetOutputReport function, que ReportBufferLength lo determina el OutputReportByteLength de HIDP_CAPS, y que si no se usa un identificador de informe, el primer byte debe ser 0. ↩ ↩2
-
Microsoft Learn, HidDevice Class (Windows.Devices.HumanInterfaceDevice). Que HidDevice representa el dispositivo correspondiente a una colección de nivel superior; el flujo de crear un selector AQS con GetDeviceSelector a partir de usagePage / usageId / vendorId / productId y abrirlo con FromIdAsync; y que una aplicación que acceda a dispositivos HID con esta clase debe incluir los datos de DeviceCapability específicos en el nodo Capabilities del manifiesto. ↩
-
USB Implementers Forum, Universal Serial Bus Specification Revision 2.0. Que el tamaño máximo de paquete de un endpoint de interrupción es de 64 bytes a velocidad completa y de 1024 bytes a alta velocidad; que el intervalo de sondeo (bInterval) se expresa en milisegundos de 1 a 255 a velocidad completa, y en alta velocidad como 2^(bInterval-1) en unidades de 125 microsegundos (sección 9.6.6 Endpoint); y las características de ancho de banda de cada tipo de transferencia: control, en bloque, de interrupción e isócrona. ↩ ↩2
-
Microsoft Learn, Microsoft OS 2.0 Descriptors Specification. Que la versión 2.0 de los descriptores del sistema operativo de Microsoft se formuló para resolver las limitaciones y los problemas de fiabilidad de la versión 1.0, y que los SO objetivo son Windows 10 y la vista previa de Windows 8.1. ↩
-
Microsoft Learn, WinUSB (Winusb.sys) Installation for Developers. Que hay que añadir bajo la clave
Device Parametersdel dispositivo la entrada de cadenaDeviceInterfaceGUIDo la de multicadenaDeviceInterfaceGUIDspara establecer el GUID; que en elAddRegde un INF personalizado se escribeHKR,,DeviceInterfaceGUIDs,0x10000,"{...}"(0x10000 = REG_MULTI_SZ); que «cuando Winusb.sys se carga como controlador de función, lee la clave de registroDeviceInterfaceGUIDsy representa la interfaz de dispositivo con el GUID indicado» y que «Winusb.sys registra, en cada carga, una interfaz de dispositivo con la clase de interfaz de dispositivo especificada bajo la claveDeviceInterfaceGUIDs»; que del lado del modo usuario se enumeran las interfaces registradas conSetupDiGetClassDevsy se pasan aWinUsb_Initialize; y que el paquete de controlador necesita un archivo de catálogo firmado. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, WinUsb_ReadPipe function (winusb.h). Que al especificar Overlapped la función vuelve de inmediato y la operación se ejecuta de forma asíncrona; que en ese caso GetLastError devuelve ERROR_IO_PENDING y el éxito o fracaso se confirma con WinUsb_GetOverlappedResult; que en modo asíncrono (Overlapped no nulo) se puede establecer LengthTransferred a NULL; que si se pasa un LengthTransferred no nulo, el valor en el momento en que la función retorna carece de sentido (meaningless) hasta que se completa la operación superpuesta, y que el número real de bytes leídos hay que obtenerlo con WinUsb_GetOverlappedResult; y que en una llamada síncrona (Overlapped nulo) LengthTransferred debe ser no nulo. ↩ ↩2
-
Microsoft Learn, Windows.Devices.Usb Namespace. Que este espacio de nombres está dirigido a dispositivos WinUSB gestionados por el winusb.sys incluido de serie (ID compatible USB\MS_COMP_WINUSB); que no se puede acceder a las clases de dispositivo Audio (0x01) / HID (0x03) / Image (0x06) / Printer (0x07) / Mass Storage (0x08) / Smart Card (0x0B) / Audio/Video (0x10) / Wireless Controller (0xE0); que hace falta declarar la funcionalidad usb en el manifiesto, y que a partir de Windows 10 versión 1809 dejó de ser obligatorio especificar VendorId/ProductId; y que, en general, no se puede acceder a una pila de dispositivo que incluya controladores de filtro superiores o inferiores. ↩
-
Microsoft Learn, CM_Register_Notification function (cfgmgr32.h). Que está disponible a partir de Windows 8, y que si el objetivo incluye Windows 7 o anterior hay que usar RegisterDeviceNotification; los tipos de filtro CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE / DEVICEHANDLE / DEVICEINSTANCE; que los eventos PnP deben procesarse lo más rápido posible y que el procesamiento que pueda bloquear, como la E/S, debe hacerse de forma asíncrona en otro hilo; y que esta función no notifica las interfaces de dispositivo ya existentes, por lo que tras registrarse hay que llamar a CM_Get_Device_Interface_List, y las interfaces activadas en ese intervalo aparecen tanto en la notificación como en el listado. ↩ ↩2
-
Microsoft Learn, RegisterDeviceNotificationW function (winuser.h). Que es la función de registro para que una aplicación reciba notificaciones de dispositivo, y que devuelve un identificador de notificación de dispositivo al tener éxito y NULL al fallar. Junto con WM_DEVICECHANGE message y DBT_DEVICEARRIVAL event, que cuando se inserta un dispositivo o un medio y queda disponible, se difunde WM_DEVICECHANGE con wParam igual a DBT_DEVICEARRIVAL. ↩
-
Microsoft Learn, PnP Device Installation Signing Requirements. Que para colocar un paquete de controlador en el Driver Store hay que cumplir los requisitos de firma; que para que la instalación de dispositivo PnP lo considere «firmado», el archivo de catálogo del paquete de controlador debe estar firmado con WHQL o con un certificado de publicación de terceros (SPC, certificado de publicación comercial); que los requisitos de firma para cargar los binarios de un controlador en modo kernel se imponen aparte de esto; que en las versiones de 64 bits de Windows, la política de firma de código en modo kernel exige firma WHQL o SPC; y que algunas ediciones, como Windows 10 in S mode, solo aceptan catálogos con firma WHQL. ↩ ↩2
-
Microsoft Learn, Driver Signing Options. Que un controlador con firma dashboard que ha pasado las pruebas HLK funciona desde Windows Vista y las ediciones de Windows Server en adelante, y que por eso se recomienda, ya que se puede firmar para todas las versiones del SO; que el attestation signing se posiciona como «solo con fines de prueba» y no necesita pruebas HLK; que un controlador con firma attestation no se puede publicar a través de Windows Update para usuarios generales; que solo es válido a partir de Windows 10 de escritorio; que para versiones de Windows anteriores hace falta presentar los registros de pruebas HLK/HCK; que Windows Server 2016 en adelante no acepta envíos de firma attestation y solo carga controladores que han pasado HLK; y que la firma attestation requiere un certificado EV y, aun obteniéndola, no se logra la certificación Windows Certified. ↩
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Puntos débiles de las aplicaciones de comunicación serie - hasta el diseño de la reconexión y los registros
Puntos débiles habituales de las aplicaciones de comunicación serie en la integración con equipos e instrumentos de medición: framing, ti...
Usar WMI/CIM desde C# y PowerShell ── Guía práctica de obtención de información de hardware, monitorización de procesos y consultas remotas
WMI/CIM es la solución estándar para leer el número de serie, monitorizar el disco y detectar procesos. Cmdlets CIM, migración desde Get-...
Diseño de códigos en sistemas empresariales ── Cómo definir códigos de producto y cliente, y el dígito de control
Guía práctica para diseñar códigos de producto y cliente en sistemas empresariales: código significativo frente a secuencial, fórmulas de...
Cómo versionar el esquema de la base de datos de una aplicación empresarial — Migraciones que evitan que «cada cliente tenga una base de datos distinta»
Guía práctica para versionar el esquema de bases de datos de aplicaciones empresariales dispersas entre clientes: PRAGMA user_version, mi...
Suspensión, hibernación y Modern Standby: evitar con diseño que las apps de larga duración "se detengan de noche"
Analiza por qué las apps Windows de larga duración se detienen de noche, según las diferencias entre S3, hibernación y Modern Standby. Ex...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- Quiero usar un dispositivo USB desde mi aplicación. ¿Es necesario que escriba yo mismo el controlador?
- En la mayoría de los casos, no. La propia guía oficial de Microsoft indica explícitamente que hay que "empezar por el método más simple y avanzar hacia métodos más complejos solo cuando sea necesario". Si el dispositivo pertenece a una clase USB estándar (CDC, HID, almacenamiento masivo, etc.), el controlador de clase estándar de Windows se carga automáticamente, así que no hace falta ningún controlador. Si no pertenece a una clase estándar y solo va a acceder a él una única aplicación, puede usar WinUSB (winusb.sys) directamente como controlador de función. Solo cuando varias aplicaciones necesiten acceder al mismo tiempo entra en juego un controlador UMDF y, si eso tampoco es suficiente, un controlador KMDF. Escribir su propio controlador es el último recurso.
- ¿Cuál es el punto más débil del método de puerto COM virtual (USB serie)?
- Que el número de puerto COM no es el identificador del dispositivo. Aunque se trate del mismo dispositivo, el número de COM puede cambiar si se cambia el puerto USB en el que se conecta, y si se conectan varios equipos a la vez, no es posible distinguir cuál es cuál solo por el número. La práctica de escribir "COM3" en un archivo de configuración siempre acaba fallando en el terreno. En la práctica, lo correcto es resolver en tiempo de ejecución la correspondencia entre el VID/PID (y el número de serie) y el número de COM —por ejemplo a partir de Win32_PnPEntity— y abrir el puerto con ese resultado. Además, como la comunicación serie es un flujo de bytes que no garantiza los límites de los mensajes, hace falta también un analizador (parser) aparte que acumule los datos en un búfer de recepción y extraiga las tramas.
- Dicen que el método HID no necesita controlador y es cómodo, pero ¿en qué debo fijarme?
- En tres puntos. El primero es la velocidad: HID usa transferencias de interrupción, por lo que no es adecuado para transferencias continuas de grandes volúmenes de datos. El segundo es la longitud del informe (report): el búfer que se pasa a ReadFile debe medir exactamente lo que devuelve InputReportByteLength de HidP_GetCaps, y el primer byte es el identificador de informe (report ID). Equivocarse aquí produce el fallo clásico de "no se puede leer" o "se cae". El tercero es el control de exclusividad: las colecciones de nivel superior equivalentes a ratón, teclado, pantalla táctil y lápiz las abre en modo exclusivo el Raw Input Manager de Windows, así que la aplicación no puede leerlas ni escribir en ellas. Aun así, si se abre el identificador (handle) sin solicitar permisos de lectura/escritura, sí es posible obtener información mediante las funciones HidD_GetXxx.
- Quiero usar WinUSB. ¿Puedo aplicarlo tal cual a un dispositivo ya existente?
- En muchos casos no. Que winusb.sys se cargue automáticamente sin distribuir ningún archivo INF solo ocurre cuando el firmware del dispositivo tiene descriptores del sistema operativo de Microsoft e informa WINUSB como ID compatible (un "dispositivo WinUSB"), y se usa en Windows 8 o posterior. Como el Winusb.inf incluido de serie no admitió los ID compatibles hasta Windows 8, si también hay que dar soporte a Windows 7 o anterior, hay que partir de la base de que será necesario distribuir un INF. Incluso si el dispositivo existente no encaja como dispositivo WinUSB, es posible preparar y distribuir usted mismo un INF personalizado que especifique el ID de hardware. Sustituir el controlador con una herramienta como Zadig durante el desarrollo es válido como verificación, pero al retirar el controlador del fabricante no debe convertirse en el medio de distribución en producción. Si es posible modificar el firmware, lo más limpio es añadir los descriptores del sistema operativo, y para un diseño nuevo el primer candidato es el descriptor OS 2.0 (Windows 8.1 o posterior), que se notifica a través de BOS.
- ¿Cómo consigo que la aplicación siga la conexión y desconexión de un dispositivo USB?
- Suscribiéndose a notificaciones PnP en lugar de hacer sondeo (polling). En aplicaciones de escritorio para Windows 8 o posterior, lo habitual es especificar un filtro de interfaz de dispositivo en CM_Register_Notification; si también hay que dar soporte a Windows 7 o anterior, se usan RegisterDeviceNotification y WM_DEVICECHANGE. Un punto a tener en cuenta: CM_Register_Notification no notifica las interfaces que ya existían en el momento del registro, así que hay que registrar primero y, después, enumerar las existentes con CM_Get_Device_Interface_List (en el orden inverso se pierden dispositivos). Además, realizar dentro del callback un procesamiento que pueda bloquear, como E/S, es peligroso, así que hay que delegarlo a otro hilo. Dicho esto, lo importante es no convertir la notificación PnP en el único punto de entrada. Las E/S en curso pueden completarse con un error de eliminación o cancelación antes que la notificación, o al mismo tiempo que ella. Trate los errores de tipo eliminación como fin de sesión en todas las rutas de finalización de lectura/escritura, y use la notificación PnP como una señal auxiliar para detectar desconexiones mientras se está a la espera.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.