USB para desarrolladores de software: introducción a la escritura de controladores USB en espacio de usuario
(werwolv.net)- Aunque el desarrollo de controladores USB suele considerarse trabajo a nivel de kernel, en la práctica también puede implementarse en espacio de usuario con una dificultad similar a la programación con sockets
- Con libusb es posible realizar enumeración de dispositivos, transferencias de control y envío/recepción de datos sin escribir código de kernel
- La comunicación USB se compone de cuatro tipos de transferencia: Control, Bulk, Interrupt, Isochronous, además de la dirección IN/OUT; cada endpoint funciona como un canal unidireccional
- Usando el protocolo Fastboot de dispositivos Android como ejemplo, se muestra en código cómo intercambiar comandos y respuestas a través de endpoints Bulk
- Incluso en espacio de usuario es posible implementar un controlador USB completo, y todos los protocolos USB comparten la misma estructura básica
Introducción
- Los controladores para dispositivos USB suelen parecer difíciles porque existe la idea de que hay que trabajar con código de kernel, pero en realidad tienen una complejidad comparable a la de una aplicación que usa sockets
- Incluso desarrolladores sin mucha experiencia en hardware pueden aprender cómo manejar USB desde espacio de usuario
- Existen materiales que cubren el funcionamiento detallado de USB, pero para principiantes suelen ser poco accesibles
- Usar USB no requiere conocimientos de nivel de sistemas embebidos y puede abordarse de forma similar a los sockets de red
Dispositivos USB
- Como ejemplo se usa un smartphone Android en modo bootloader
- Es fácil de conseguir, el protocolo es simple y, como el sistema operativo no incluye un controlador predeterminado, resulta ideal para experimentar
- La forma de entrar al modo bootloader varía según el dispositivo, pero por lo general se logra con una combinación del botón de encendido y los botones de volumen
Enumeración manual del dispositivo
- La enumeración (Enumeration) es el proceso mediante el cual el host solicita información del dispositivo para identificarlo, y se realiza automáticamente al conectarlo
- Los dispositivos estándar cargan controladores automáticamente según su clase USB, mientras que los dispositivos específicos del fabricante usan
VID(Vendor ID) yPID(Product ID) - En Linux se puede consultar la información del dispositivo con el comando
lsusb- Ejemplo:
ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot) 18d1es el VID de Google y4ee0es el PID del bootloader de Nexus/Pixel
- Ejemplo:
- Con el comando
lsusb -tse puede revisar la clase y el estado del controlador- Si aparece
Class=Vendor Specific Class,Driver=[none], significa que el sistema operativo no cargó ningún controlador
- Si aparece
- En Windows se puede verificar la misma información con Device Manager o USB Device Tree Viewer
Enumeración del dispositivo con libusb
- La biblioteca libusb permite comunicarse con dispositivos USB desde espacio de usuario sin escribir código de kernel
- Con
libusb_hotplug_register_callback()se puede configurar un callback para ejecutarse cuando se conecte un dispositivo con una combinación específica deVID:PID - Al conectar el dispositivo después de ejecutar el programa, se muestra el mensaje
"Device plugged in!" - En Linux funciona de forma predeterminada y, si hace falta, se puede desacoplar el controlador del kernel con
libusb_detach_kernel_driver() - En Windows se necesita el controlador
Winusb.sys; si no está disponible, puede reemplazarse manualmente con la herramienta Zadig
Comunicación con el dispositivo
- La primera comunicación con un dispositivo USB se realiza mediante el endpoint de Control (dirección 0x00)
- Con
libusb_control_transfer()se envía una solicitud estándar (GET_STATUS) para leer el estado del dispositivo- Ejemplo de respuesta:
01 00→ el primer byte indica Self-Powered, y el segundo que no es compatible con Remote Wakeup
- Ejemplo de respuesta:
- Después se puede obtener el descriptor del dispositivo mediante una solicitud GET_DESCRIPTOR
- Los datos devueltos incluyen información como
idVendor,idProduct,bDeviceClass, entre otros
- Los datos devueltos incluyen información como
- Con el comando
lsusb -vse pueden consultar en detalle todos los descriptores (dispositivo, configuración, interfaz, endpoint, etc.)- Ejemplo: la interfaz
Android Fastboottiene endpoints Bulk IN (0x81) y Bulk OUT (0x02)
- Ejemplo: la interfaz
Endpoints
- Un endpoint es un concepto similar a un puerto de red: un canal por el que el dispositivo envía y recibe datos
- En el descriptor se define el tipo y la dirección de cada endpoint
-
Tipo de transferencia Control
- Todos los dispositivos tienen uno y su dirección siempre es
0x00 - Se usa para la configuración inicial y para solicitar información del dispositivo
- No pertenece a una interfaz, sino que existe como parte del propio dispositivo
- Todos los dispositivos tienen uno y su dirección siempre es
-
Tipo de transferencia Bulk
- Se usa para transferencia de grandes volúmenes de datos no sensibles al tiempo real
- Ejemplos: Mass Storage, CDC-ACM (serial), RNDIS (Ethernet)
- Tiene gran ancho de banda, pero baja prioridad
-
Tipo de transferencia Interrupt
- Se usa para transferencias de poca cantidad de datos y baja latencia
- En teclados y ratones, por ejemplo, sirve para sondear rápidamente la entrada de botones
- No es una interrupción de hardware real; el host hace solicitudes periódicas
-
Tipo de transferencia Isochronous
- Se usa para datos voluminosos sensibles al tiempo (streaming de audio y video)
- Si hay latencia, la pérdida de calidad se nota de inmediato
- En libusb se maneja de forma asíncrona
-
Dirección IN / OUT
- USB tiene una arquitectura centrada en el host, por lo que el dispositivo no transmite datos hasta recibir una solicitud
IN: dirección en la que el host recibe datosOUT: dirección en la que el host envía datos- Si el bit más significativo (MSB) de la dirección del endpoint es
1, es IN; si es0, es OUT - Se pueden usar hasta 127 endpoints definidos por el usuario (
0x00está reservado para Control) - Los endpoints son unidireccionales y suelen organizarse en pares IN/OUT, como en la interfaz Fastboot
Protocolo Fastboot
- Fastboot es el protocolo de comunicación del bootloader de Android; su estructura consiste en enviar una cadena de comando y recibir un código de estado de 4 bytes junto con datos
- Ejemplos:
Host: "getvar:version"→Client: "OKAY0.4"Host: "getvar:nonexistant"→Client: "OKAY"
- Ejemplos:
- Se muestra un ejemplo de código que envía comandos Fastboot usando libusb
- Se toma control de la interfaz 0 con
libusb_claim_interface() - El comando
"getvar:version"se envía al endpoint Bulk OUT (0x02) - La respuesta se recibe desde el endpoint Bulk IN (0x81)
- Ejemplo de salida:
Request: getvar:version Response: OKAY0.4 OKAYindica éxito y0.4es la versión de Fastboot
- Se toma control de la interfaz 0 con
Cierre
- Es posible implementar un controlador USB completo desde espacio de usuario sin escribir código de kernel
- Todos los controladores USB siguen los mismos principios básicos; lo que cambia es el protocolo
- Incluso protocolos complejos (como MTP) comparten la misma estructura básica y pueden abordarse con una idea similar a la comunicación por sockets
1 comentarios
Comentarios en Hacker News
Justo llegó en el momento perfecto. Pronto voy a recoger un MOTU MIDI Express XT en el Guitar Center local
Como es equipo usado, por ley tienen que retenerlo cierto tiempo, así que estoy esperando. El problema es que este equipo no usa MIDI-over-USB estándar, sino un protocolo propietario, así que no puedo usarlo directamente por USB en mis sistemas como Linux, OpenBSD o Haiku
Por ahora está bien porque solo necesito enrutar entre módulos de sintetizador y controladores, pero estaría bueno lograr que también funcione del lado de la PC
Sí existe un driver de Linux, pero no está clara su estabilidad ni si soporta el XT. Dicen que ya se resolvió el problema de kernel panic, pero siguen quedando issues
Así que estoy pensando en hacer yo mismo un driver en espacio de usuario basado en LibUSB. Si expone puertos MIDI y además agrega herramientas de routing, podría quedar bastante útil
Si quieres probar algo así en Go, hice la librería go-usb, que permite acceso USB sin cgo
Con eso también desarrollé go-uvc para manejar dispositivos UVC
Yo también estoy implementando recientemente un sistema usbip de forma parecida en una Macbook M3
Pero en las versiones recientes de macOS hay limitaciones. Para dispositivos USB que el sistema reconoce, no puedes construir un driver en espacio de usuario basado en libusb a menos que desactives manualmente funciones de seguridad
Este enfoque al final hace que el driver USB también cumpla el papel de código de aplicación. O sea, se parece más a una librería + programa que a un driver
Por ejemplo, me pregunto cómo harías para conectar un dispositivo USB-Ethernet como adaptador de red del sistema operativo
Si hubiera leído este artículo hace unos años, me habría sido mucho más fácil hacer ingeniería inversa de funciones de una laptop. En especial, el programa de control de LEDs del teclado sigue siendo uno de mis proyectos favoritos
Fue una introducción realmente útil. Trabajar con APIs de hardware de bajo nivel es difícil, pero muy gratificante. Las capas de abstracción de los sistemas operativos modernos lo han hecho más fácil, pero igual sigue siendo importante entender lo que hay debajo
El código en C++ se veía raro. Nunca he visto un teclado donde puedas escribir directamente el carácter de flecha
->. Es la sintaxis de trailing return type de C++ moderno"->". La fuente solo lo renderiza como una flechaMe preguntaba si un dispositivo USB soporta DMA. Quería saber si solo es posible a través del host o si el dispositivo también puede acceder directamente a la memoria
Hace tiempo intenté crear un dispositivo USB sencillo, pero casi no había información sobre cómo escribir descriptores (descriptors). Casi todo era del estilo “busca un dispositivo parecido, cópialo y ajústalo”. Me hizo dudar de si USB de verdad es un estándar tan bueno
Si me pidieran “escribe tú mismo el driver de este dispositivo USB”, yo devolvería el dispositivo y primero vería si no se puede resolver con un puerto COM virtual