Introducción
En un mundo perfecto, los dispositivos y hubs USB se comportarían correctamente y sabrían arreglar cualquier problema que se les presente. En la realidad pasan todo tipo de cosas raras, y hay que hacer algo al respecto. La solución habitual es desenchufar el USB y volver a conectarlo. ¿Y si hace falta que esto ocurra automáticamente?
La solución más drástica se presenta en otra página web: provoca la detención y el reinicio de los controladores (drivers) del hub USB. El efecto es casi equivalente a desconectar y volver a conectar todos los dispositivos USB del ordenador. Es el martillo más pesado, y a veces hace falta.
En esta página se presentan dos métodos para reiniciar un dispositivo USB concreto. Empezaré explicando cómo llevar a cabo cada método, con unas pocas explicaciones mínimas. Después entraré en muchos detalles técnicos.
Todo el análisis se limita a Linux, aunque el segundo método que se presenta aquí probablemente también sea posible en otras plataformas.
Gran parte de la información procede de la Universal Serial Bus Specification Revision 2.0. El nombre de archivo de este documento suele ser usb_20.pdf.
Método 1: Pedir al kernel de Linux que reinicie el dispositivo
Hay varias herramientas disponibles para hacerlo. La más avanzada forma parte de usbutils. Descarga usbreset.c y compílalo con gcc:
$ gcc -O3 -Wall -g usbreset.c -o usbreset
Y luego:
$ ./usbreset Usage: usbreset PPPP:VVVV - reset by product and vendor id usbreset BBB/DDD - reset by bus and device number usbreset "Product" - reset by product name Devices: Number 001/004 ID 045e:07b2 Microsoft® Nano Transceiver v1.0 Number 001/002 ID 04f3:0103 $ ./usbreset 001/004 Resetting Microsoft® Nano Transceiver v1.0 ... can't open [Permission denied] $ sudo ./usbreset 001/004 Resetting Microsoft® Nano Transceiver v1.0 ... ok $ sudo ./usbreset 045e:07b2 Resetting Microsoft® Nano Transceiver v1.0 ... ok
En respuesta a los reinicios, en el registro del kernel aparece lo siguiente:
usb 1-6: reset full-speed USB device number 4 using xhci_hcd
Lo que demuestra esta sesión:
- Sin argumentos, usbreset responde con una lista de dispositivos USB. Esa misma información se obtiene con lsusb.
- Hay que ser root para reiniciar de verdad un dispositivo (de ahí el sudo).
- El dispositivo puede elegirse por su dirección de bus (número de bus y número de dispositivo). Nótese que la dirección de bus cambia cada vez que se conecta el dispositivo.
- También puede elegirse por los IDs de Vendor / Product. Este es el método preferido cuando solo hay un dispositivo USB de ese tipo.
- El dispositivo no cambió su dirección en el bus USB (no se re-enumeró como consecuencia del reinicio).
¿Qué hace usbreset, entonces? Se reduce a este comando sobre el archivo de dispositivo del USB:
ioctl(fd, USBDEVFS_RESET, 0)
Esto acaba en una llamada a la función usb_reset_device() del kernel, que hace mucho más que reiniciar el dispositivo. La idea es que el proceso sea limpio: esta función avisa al controlador (driver) del dispositivo antes y después del reinicio, si así se le pide. Desvincula el controlador antes del reinicio, y lo vuelve a vincular al terminar. También se vuelve a cargar la configuración del dispositivo después del reinicio. Sin esto, el dispositivo no sabría su dirección de bus ni estaría listo para intercambiar datos.
Es casi como volver a conectar el dispositivo, pero sin pedirle que se identifique (porque la información ya se conoce) y sin asignarle una dirección nueva.
En USB 3.x esto es un hot reset, que es la variante menos eficaz. Hablaremos más sobre el hot reset más abajo.
Método 2: Cambiar el estado de alimentación del puerto USB
También hay varias herramientas para esto. Voy a demostrar el método con hubpower. Descarga hubpower.c y compila:
$ gcc -O3 -Wall -g hubpower.c -o hubpower
Usar esta herramienta es algo más delicado, porque los comandos no se envían al propio dispositivo USB, sino al hub USB al que está conectado.
El primer paso es, por tanto, averiguar de qué hub se trata. Primero, busquemos la dirección del dispositivo USB:
$ lsusb Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 001 Device 004: ID 045e:07b2 Microsoft Corp. Bus 001 Device 002: ID 04f3:0103 Elan Microelectronics Corp. ActiveJet K-2024 Multimedia Keyboard Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
El dispositivo está, pues, en el bus número 1 y es el dispositivo número 4. Nótese que el número de dispositivo cambia cada vez que se enumera el dispositivo USB.
¿A qué hub está conectado el dispositivo USB, entonces? ¿Y a qué puerto?
$ lsusb -t /: Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/6p, 5000M /: Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/12p, 480M |__ Port 6: Dev 4, If 0, Class=Human Interface Device, Driver=usbhid, 12M |__ Port 6: Dev 4, If 1, Class=Human Interface Device, Driver=usbhid, 12M |__ Port 6: Dev 4, If 2, Class=Human Interface Device, Driver=usbhid, 12M |__ Port 11: Dev 2, If 0, Class=Human Interface Device, Driver=usbhid, 1.5M |__ Port 11: Dev 2, If 1, Class=Human Interface Device, Driver=usbhid, 1.5M
El dispositivo número 4 está conectado al puerto n.º 6. El bus del hub es el número 1 y su dispositivo el número 1 (es el controlador USB de la placa base, que actúa como hub raíz).
Veamos qué dice hubpower al respecto:
$ sudo ./hubpower 1:1 status
Port 1 status: 0100 Power-On
Port 2 status: 0100 Power-On
Port 3 status: 0100 Power-On
Port 4 status: 0100 Power-On
Port 5 status: 0100 Power-On
Port 6 status: 0103 Power-On Enabled Connected
Port 7 status: 0100 Power-On
Port 8 status: 0100 Power-On
Port 9 status: 0100 Power-On
Port 10 status: 0100 Power-On
Port 11 status: 0303 Low-Speed Power-On Enabled Connected
Port 12 status: 0100 Power-On
Observa que la parte «1:1» es la dirección de bus del hub. Si se trata de un hub externo, esta dirección cambia cada vez que el hub se conecta al ordenador.
Pero los números de los puertos no cambian nunca (siempre que el dispositivo siga conectado al mismo puerto físico).
Una vez que sabemos el número de puerto del dispositivo, vamos a reiniciarlo:
$ sudo ./hubpower 1:1 power 6 off Port 6 status: 0000 Power-Off $ sudo ./hubpower 1:1 power 6 on Port 6 status: 0100 Power-On
Después del primer comando, el registro del kernel informará de que el dispositivo se ha desconectado:
usb 1-6: USB disconnect, device number 4
Después del segundo comando, el ordenador se comporta como si el dispositivo se hubiera conectado físicamente:
usb 1-6: new full-speed USB device number 5 using xhci_hcd
usb 1-6: New USB device found, idVendor=045e, idProduct=07b2, bcd Device= 7.04
usb 1-6: New USB device strings: Mfr=1, Product=2, SerialNumber=0
usb 1-6: Product: Microsoft® Nano Transceiver v1.0
usb 1-6: Manufacturer: Microsoft
[ ... ]
Debido a esta re-enumeración, se asigna una dirección de bus nueva al dispositivo:
$ lsusb
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 005: ID 045e:07b2 Microsoft Corp.
Bus 001 Device 002: ID 04f3:0103 Elan Microelectronics Corp. ActiveJet K-2024 Multimedia Keyboard
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
¿Es esto como desconectar físicamente el dispositivo USB y volver a conectarlo? La respuesta es: depende. En la mayoría de los casos, los comandos de hubpower realmente no cortan la alimentación del dispositivo USB. El dispositivo, por tanto, sigue recibiendo la tensión de VBUS (5 V). Muy pocos hubs USB cortan la alimentación de verdad. Más sobre esto abajo.
Con la gran mayoría de los hubs, el primer comando («power off») solo pone el puerto en un estado que hace que ignore al dispositivo. El segundo comando («power on») devuelve el puerto a su estado natural. El resultado es que el dispositivo se detecta y comienza su procedimiento de inicialización. Entre otras cosas, el dispositivo se reinicia y se enumera, y su controlador lo inicializa.
Por tanto, la desconexión física suele ser más eficaz que estos comandos, sobre todo si el dispositivo recibe su alimentación del propio conector USB. Pero si el hub corta de verdad VBUS, hubpower es igual de eficaz.
Observa que en este ejemplo los comandos se enviaron al controlador USB de la placa base (el hub raíz). La razón es que el dispositivo USB estaba conectado directamente al ordenador. Si este dispositivo se conecta al ordenador mediante un hub externo, los comandos deben enviarse a ese hub externo. En ambos casos, hubpower se utiliza de la misma manera.
Desgraciadamente, hubpower no soporta USB 3.0 (SuperSpeed). Más sobre esto más abajo.
Diferencias entre los dos métodos
¿En qué se diferencia el segundo método (con hubpower) del primero (con usbreset)? A la hora de resolver un problema con un dispositivo, ambos métodos hacen esencialmente lo mismo: envían un comando de reinicio. La forma en que ocurre es drásticamente distinta, pero al final es ese comando de reinicio el que probablemente resolverá el problema, si es que lo resuelve algo.
Pero hay algunas diferencias importantes:
- hubpower funciona incluso si el dispositivo no es reconocido en el bus. Por ejemplo, si el ordenador intentó enumerar el dispositivo varias veces y luego se rindió. En ese caso, el dispositivo puede estar físicamente conectado al ordenador, pero usbreset no sirve de nada porque el dispositivo no tiene dirección de bus.
- Solo hubpower puede ayudar cuando el puerto USB queda deshabilitado: a veces el ordenador ignora por completo un puerto USB después de varios errores en ese puerto. hubpower debería solucionarlo.
- hubpower no funciona con SuperSpeed (USB 3.x). Probablemente sea cuestión de añadirle soporte. Más sobre SuperSpeed abajo.
- hubpower también puede controlar la alimentación del dispositivo USB (aunque normalmente no lo hace). Es una ventaja si el dispositivo necesita que se le corte y restablezca la alimentación para solucionar el problema.
- hubpower es más difícil de manejar: hay que averiguar a cuál de los puertos del hub está conectado el dispositivo USB, y también la dirección de bus del hub.
¿Controlar aparatos eléctricos?
Aunque el tema principal de esta página es cómo solucionar un problema con un dispositivo USB, también hay un efecto secundario interesante: a veces es posible controlar la alimentación de 5 V del puerto USB. Es decir, un hub USB sencillo y barato puede utilizarse para encender y apagar una fuente de alimentación capaz de manejar 2,5 vatios.
Con eso hay más que suficiente para controlar un relé electromecánico. Solo hace falta añadir un único componente para controlar un aparato eléctrico que funcione a 110 V / 220 V. Bueno, también es buena idea añadir un diodo sencillo, para proteger el hub USB de posibles daños. Y ya está.
Por desgracia, la capacidad de controlar la alimentación es opcional: según la sección 11.11 de la especificación USB 2.0, un hub puede tener interruptores de alimentación que cortan la alimentación de 5 V de un puerto cuando este se encuentra en el estado Powered-Off. Alternativamente, puede haber un interruptor que controle la alimentación de varios puertos a la vez (lo que se denomina «ganged power switching», conmutación de alimentación en grupo). La finalidad de controlar las alimentaciones es, principalmente, apagar los dispositivos USB que consumen demasiada corriente, para que los puertos restantes puedan seguir funcionando con normalidad.
Pero, como ya se ha dicho, el control físico de la alimentación es opcional. Lo que hubpower hace en realidad es cambiar el valor del atributo PORT_POWER del puerto (en las especificaciones USB se llama «feature», o característica). Este cambio se refiere a dos aspectos distintos del puerto del hub:
- Obligatorio: el estado lógico del puerto. Cuando PORT_POWER es cero, el puerto solo puede estar en el estado Powered-Off (o Not Configured, es decir, no configurado). En otras palabras, el hub debe comportarse como si no hubiera tensión en el puerto y, por tanto, ignorar cualquier dispositivo conectado, si lo hay.
- Opcional: la presencia de VBUS (la tensión de 5 V) en los cables de alimentación del puerto. Si el hub no soporta esta característica, la tensión puede estar presente incluso cuando PORT_POWER es cero.
PORT_POWER se explica con más detalle más abajo.
¿Controla la tensión mi hub?
¿Cómo saber si tu hub corta de verdad la tensión? La única forma de estar seguro es probarlo. Conecta algo que no sea un dispositivo USB, pero que consuma corriente del puerto USB. Los dispositivos USB reales pueden inducir a confusión. Por ejemplo, un ratón óptico normalmente apaga su LED en respuesta al comando «power off», aunque la tensión de 5 V siga presente.
Cada hub declara si controla la tensión y con qué granularidad en la información que se muestra con «lsusb -v» (en el Hub Descriptor, definido en la sección 11.23.2.1 de la especificación). El hub puede declarar que no controla la tensión, que la controla por grupos de puertos (llamados «gangs» en inglés), o que controla la tensión de cada puerto individualmente. Según la sección 11.11 de la especificación USB 2.0, «un hub con interruptores de alimentación puede conmutar la alimentación de todos los puertos como un grupo/gang, de cada puerto individualmente, o tener un número arbitrario de grupos (gangs) de uno o más puertos».
Sin embargo, esta información no es fiable. Me he encontrado con varios hubs que declaran que controlan la tensión, pero ninguno lo hacía.
Por ejemplo, así se ve una salida de «lsusb -v»:
Bus 001 Device 073: ID 0bda:5411 Realtek Semiconductor Corp. Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.10 bDeviceClass 9 Hub bDeviceSubClass 0 Unused bDeviceProtocol 2 TT per port bMaxPacketSize0 64 idVendor 0x0bda Realtek Semiconductor Corp. idProduct 0x5411 bcdDevice 1.23 iManufacturer 1 Generic iProduct 2 4-Port USB 2.0 Hub iSerial 0 bNumConfigurations 1 Configuration Descriptor: [ ... ] Hub Descriptor: bLength 9 bDescriptorType 41 nNbrPorts 4 wHubCharacteristic 0x00a9 Per-port power switching Per-port overcurrent protection TT think time 16 FS bits Port indicators bPwrOn2PwrGood 0 * 2 milli seconds bHubContrCurrent 100 milli Ampere DeviceRemovable 0x00 PortPwrCtrlMask 0xff Hub Port Status: Port 1: 0000.0503 highspeed power enable connect Port 2: 0000.0503 highspeed power enable connect Port 3: 0000.0100 power Port 4: 0000.0100 power
Tiene buen aspecto, ¿verdad? En realidad, este hub no controla las tensiones en absoluto.
En cambio, este es el hub normal de una placa base:
Bus 005 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 9 Hub bDeviceSubClass 0 Unused bDeviceProtocol 1 Single TT bMaxPacketSize0 64 idVendor 0x1d6b Linux Foundation idProduct 0x0002 2.0 root hub bcdDevice 4.15 iManufacturer 3 Linux 4.15.0-20-generic xhci-hcd iProduct 2 xHCI Host Controller iSerial 1 0000:06:00.0 bNumConfigurations 1 Configuration Descriptor: [ ... ] Hub Descriptor: bLength 9 bDescriptorType 41 nNbrPorts 2 wHubCharacteristic 0x000a No power switching (usb 1.0) Per-port overcurrent protection TT think time 8 FS bits bPwrOn2PwrGood 10 * 2 milli seconds bHubContrCurrent 0 milli Ampere DeviceRemovable 0x00 PortPwrCtrlMask 0xff Hub Port Status: Port 1: 0000.0100 power Port 2: 0000.0100 power Device Status: 0x0001 Self Powered
Este hub, por tanto, admite que no soporta ningún control de tensión.
Observa que, además de la información sobre la conmutación de la alimentación, también hay información de estado de cada puerto con el epígrafe «Hub Port Status». Estos bits de estado se definen en la tabla 11-21 de la especificación USB 2.0 (sección 11.24.2.7.1). Es la misma información que obtiene hubpower.
La herramienta uhubctl
La idea de controlar un relé con un hub USB es la motivación de muchas iniciativas. Merece la pena echarle un vistazo a uhubctl, sobre todo porque este proyecto mantiene una lista de hubs USB que controlan la tensión.
Esta herramienta está claramente pensada únicamente para el control de tensión, y no para resolver problemas con un dispositivo USB. Por ejemplo, por defecto uhubctl ignora los hubs que no declaran tener capacidad de controlar la tensión individualmente en cada puerto.
Estos comandos son equivalentes a los comandos de hubpower mostrados antes:
$ sudo ./uhubctl -f -l 1 -p 6 -a 0 Current status for hub 1 [1d6b:0002 Linux 5.16.0 xhci-hcd xHCI Host Controller 0000:00:14.0, USB 2.00, 12 ports, nops] Port 6: 0103 power enable connect [045e:07b2 Microsoft Microsoft? Nano Transceiver v1.0] Sent power off request New status for hub 1 [1d6b:0002 Linux 5.16.0 xhci-hcd xHCI Host Controller 0000:00:14.0, USB 2.00, 12 ports, nops] Port 6: 0000 off $ sudo ./uhubctl -f -l 1 -p 6 -a 1 Current status for hub 1 [1d6b:0002 Linux 5.16.0 xhci-hcd xHCI Host Controller 0000:00:14.0, USB 2.00, 12 ports, nops] Port 6: 0000 off Sent power on request New status for hub 1 [1d6b:0002 Linux 5.16.0 xhci-hcd xHCI Host Controller 0000:00:14.0, USB 2.00, 12 ports, nops] Port 6: 0100 power
La sintaxis del comando es mucho menos cómoda. La explico brevemente más abajo.
uhubctl está basado en libusb, así que esta herramienta también funciona en otros sistemas operativos. En cambio, hubpower accede directamente al archivo de dispositivo del hub en /dev/bus/usb/ (o /proc/bus/usb/), lo que solo funciona en Linux.
uhubctl también soporta USB 3.x (ver este commit y su continuación). Sin embargo, parece que a nadie le importó lo que ocurre cuando hay un dispositivo USB real conectado al hub: mis intentos de reiniciar un dispositivo SuperSpeed dieron resultados extraños. Cuando se restablecía la alimentación, el dispositivo se quedaba en el estado Polling y no se enumeraba. Además, mientras la alimentación estaba cortada, «lsusb -v» se quedaba colgado. Así que allí ocurría algo malo.
Estos son los comandos para cortar y volver a dar alimentación en un puerto SuperSpeed. Más sobre SuperSpeed abajo.
# ./uhubctl -f -e -l 2 -p 4 -a 0 # ./uhubctl -f -e -l 2 -p 4 -a 1
- «-f» significa trabajar con un hub aunque este informe de que no soporta el control de tensión individual por puerto.
- «-e» viene de «exact position» (posición exacta), es decir, contar un puerto SuperSpeed como dos hubs.
- «-l 2» indica el segundo hub (el primero era el hub USB 2.0 de mi ordenador).
- «-p 4» indica el puerto número cuatro.
- «-a 0» significa apagar la alimentación (acción n.º 0).
- «-a 1» significa encender la alimentación (acción n.º 1).
PORT_POWER explicado
Cuando llega un comando del host para poner PORT_POWER a cero, el puerto entra incondicionalmente en el estado Powered-off. Esto es cierto aunque el hub siga alimentando al dispositivo con sus 5 V. Hay otros motivos por los que PORT_POWER puede pasar a cero, en particular una condición de sobrecorriente en el puerto (el dispositivo consume demasiada corriente).
La única forma de poner PORT_POWER a «1» es mediante un comando del host. Esto coloca el puerto en el estado Disconnected. Los puertos USB que no tienen nada conectado normalmente están en este estado. Cuando se detecta un dispositivo en el puerto, el estado cambia a Disabled tras una breve demora. Si el dispositivo ya está conectado y PORT_POWER pasa a «1», esto ocurre inmediatamente.
Desde este estado, la única vía para activar el dispositivo es reiniciando el puerto (con PORT_RESET; ver más abajo). Solo el host puede hacerlo. Por tanto, todo lo que puede hacer el hub es notificar al host que hay un dispositivo conectado que necesita atención.
Aquí entra en juego uno de los bits de estado del puerto: PORT_CONNECTION. Este bit debe ser cero cuando el puerto está en el estado Powered-off o en el estado Disconnected. PORT_CONNECTION pasa a «1» cuando el puerto transita del estado Disconnected al estado Disabled. Un cambio en PORT_CONNECTION genera un evento de hub, de modo que se notifica al controlador del hub (es decir, se hace una llamada a la función port_event() en el hub.c del kernel con USB_PORT_FEAT_C_CONNECTION activado). El controlador responde reiniciando el puerto (poniendo el bit PORT_RESET del puerto a «1») y enumerando el dispositivo conectado.
Todo esto se refiere a USB 2.0. La figura 11-10 de la especificación USB 2.0 muestra cómo cambia de estado el puerto de un hub.
Controlar PORT_RESET y PORT_ENABLE directamente
hubpower también tiene la capacidad de cambiar directamente otros dos atributos: PORT_RESET y PORT_ENABLE. De hecho, añadí esta capacidad a mi propio fork de esta herramienta. Ten en cuenta, sin embargo, que con esto lo único que puedes hacer es provocar deliberadamente un fallo en el dispositivo USB (y así conseguir que el ordenador reinicie el dispositivo para arreglarlo). Ahora con más detalle:
El host debe poner PORT_RESET a «1» para iniciar un reinicio de puerto, según la sección 11.5.1.5 de la especificación USB 2.0. El hub devuelve PORT_RESET a «0» al terminar el reinicio. El hub nunca inicia un reinicio de puerto por iniciativa propia, y al host no le está permitido escribir un «0» en este atributo (sección 11.24.2.7.1.5).
El hub ignorará PORT_RESET si el puerto está en el estado Powered-off o en el estado Disconnected.
En cuanto a PORT_ENABLE: cuando este atributo cambia a «0», el puerto pasa al estado Disabled. Esto puede ocurrir por una petición del host, así como por la desconexión del dispositivo USB, por encontrarse el puerto en un estado Powered-off, o por un error durante el proceso de reinicio.
PORT_ENABLE solo puede pasar a «1» como resultado de una petición de reinicio de puerto por parte del host (sección 11.24.2.7.1.2 de la especificación USB 2.0).
Por tanto, el host no puede cambiar PORT_RESET a «0» ni PORT_ENABLE a «1». Si se intenta, el hub responde con un error (el comando ioctl() correspondiente devolverá un estado de error).
hubpower puede solicitar un reinicio directamente, poniendo el PORT_RESET del puerto a «1». Esto reiniciará el dispositivo USB y, como resultado, el dispositivo olvidará su dirección de bus. Por tanto, el dispositivo ya no será accesible.
Este comando se envía directo al hub, de modo que el controlador del hub en el kernel de Linux no se enterará de que ha ocurrido. De hecho, el ordenador no notará que nada ha cambiado hasta que intente acceder al dispositivo USB. Lo que ocurra después dependerá del controlador del dispositivo. Este tratará el dispositivo como si tuviera algún error de hardware y, en consecuencia, tomará algún tipo de medida correctiva. Lo más probable es que incluya un reinicio.
Así que usar hubpower para provocar un reinicio directo probablemente logrará el resultado deseado, pero con mucho drama innecesario. PORT_POWER hace esto con más elegancia. La única ventaja posible de un PORT_RESET directo es que el controlador podría decir «oye, a este dispositivo le pasa algo de verdad; hagamos algo drástico para arreglarlo». Y eso puede ayudar.
En cuanto a manipular PORT_ENABLE, ocurrirá lo mismo: el dispositivo desaparecerá de repente. Aun así, el ordenador no sabrá inmediatamente que ha pasado algo: según la sección 11.24.2.7.2.2 de la especificación USB 2.0, la notificación de cambio en PORT_ENABLE (es decir, C_PORT_ENABLE) solo se dispara si el puerto se deshabilita debido a un error en el enlace. La especificación dice explícitamente que esta notificación no se genera por ningún otro motivo.
Por tanto, cambiar PORT_ENABLE a cero tendrá más o menos el mismo efecto que cambiar PORT_RESET directamente, con una desventaja: según la sección 10.14.2.6.1 de la especificación USB 3.0, PORT_ENABLE «no está soportado por los hubs SuperSpeed».
SuperSpeed (USB 3.x)
Ante todo: si estás leyendo esto porque tienes un problema con un dispositivo USB 3.x, pregúntate si de verdad necesitas la velocidad de datos que ofrece USB 3.x. Si la respuesta es no, prueba a conectar el dispositivo al ordenador mediante un hub USB que no soporte USB 3.x (o un cable USB 2.0 corto). Puede que solo con eso se resuelva el problema.
USB SuperSpeed (que es lo mismo que USB 3.x) convive en paralelo con USB 2.0. Todo dispositivo SuperSpeed consta en la práctica de dos dispositivos: uno para SuperSpeed y otro para USB 2.0. Cada una de estas dos versiones de USB utiliza hilos separados en el cable USB, y son mutuamente independientes, tanto electrónica como conceptualmente.
Las especificaciones USB exigen que todo dispositivo SuperSpeed conste de estos dos dispositivos, aunque en la práctica no sea necesario. Dicho de otro modo, un dispositivo SuperSpeed que no soporte USB 2.0 funciona correctamente cuando se conecta a un puerto SuperSpeed.
Cuando se conecta un dispositivo SuperSpeed a un puerto SuperSpeed, el primer intento es establecer la conexión a través de la interfaz SuperSpeed. Si eso falla, se intenta conectar a través de USB 2.0. En la práctica (y según la especificación), un dispositivo USB nunca se conecta a través de ambas versiones a la vez. Sin embargo, es posible, y eso haría que el dispositivo USB se comportase como si fueran dos dispositivos separados.
Un hub SuperSpeed consta de dos hubs en paralelo: uno para USB 2.0 y otro para SuperSpeed. Cuando se conecta un hub USB SuperSpeed externo a un ordenador, se añaden dos hubs al sistema y aparecen como dos dispositivos separados. A un dispositivo USB normal no se le permite usar ambas versiones en paralelo, pero un hub debe hacerlo.
Si un hub SuperSpeed se conecta a un puerto USB 2.0, se comporta como un hub USB 2.0 corriente.
En general, cada uno de estos dos hubs funciona con independencia del otro. Cada hub tiene sus propios puertos, y cada uno de estos puertos funciona de forma independiente. En particular, si se cambian los parámetros de un puerto en uno de estos hubs en paralelo, eso no tiene ningún efecto sobre los puertos del otro hub.
Otra conclusión es que si «lsusb -t» muestra que un dispositivo está conectado al ordenador a través del hub raíz SuperSpeed, funciona como dispositivo SuperSpeed. En otras palabras, su velocidad de datos es de 5 Gbit/s o más. Del mismo modo, si el dispositivo está conectado mediante el hub raíz de USB 2.0, su velocidad de datos es de 480 Mbit/s o menos.
SuperSpeed y PORT_POWER
Recuerda que lo que hubpower hacía en realidad era cambiar el atributo PORT_POWER del puerto.
Pero un hub SuperSpeed consta de dos hubs en paralelo. Cada uno de esos hubs tiene su propio atributo PORT_POWER independiente para cada puerto. Entonces, ¿cuándo debe el hub cortar la alimentación de VBUS? Cada interruptor físico de alimentación depende de dos atributos PORT_POWER, uno de cada uno de los hubs en paralelo.
La tabla 10-2 de la especificación USB 3.0 ofrece la tabla de verdad de si el hub debe activar o no la alimentación. Se puede resumir así: si el hub se comporta solo como hub USB 2.0 (p. ej., conectado a un ordenador que no soporta SuperSpeed), sigue el PORT_POWER del hub USB 2.0. Si está conectado como hub SuperSpeed (o si ambos hubs en paralelo están conectados), VBUS solo se corta si los dos PORT_POWER son cero.
Si te ha parecido complicado, en realidad esa era la parte fácil: la parte SuperSpeed del hub tiene una máquina de estados (state machine) interna distinta. Es bastante lógico, porque el entrenamiento del enlace (link training) se hace de otra manera. Pero esta máquina de estados tiene tres estados distintos (en lugar de uno, como USB 2.0) que son posibles cuando PORT_POWER es cero:
- DSPORT.Powered-off, que significa que el puerto queda completamente inactivo.
- DSPORT.Powered-off-detect, que significa que el puerto intenta detectar un link partner (dispositivo en el otro extremo del enlace) SuperSpeed.
- DSPORT.Powered-off-reset, que significa que el puerto realiza un warm reset del link partner (más sobre el warm reset más abajo).
El propósito de los dos últimos estados es garantizar que, si un dispositivo SuperSpeed con alimentación propia se conecta al puerto, la conexión no se degrade a USB 2.0. Eso ocurriría porque el dispositivo no tendría ninguna manera de reconocer que está conectado a un puerto SuperSpeed.
¿Qué aprendemos de todo esto? Principalmente, que reiniciar un dispositivo cambiando PORT_POWER en un hub SuperSpeed no es tan sencillo como en un hub USB 2.0.
SuperSpeed: warm reset y hot reset
USB 2.0 tiene una forma sencilla de reiniciar el dispositivo: el hub conecta ambos cables de datos a masa (SE0) durante 10 ms. Los dispositivos SuperSpeed, en cambio, tienen dos formas de hacer un reinicio: warm reset y hot reset. No deben confundirse con PowerOn Reset e Inband Reset, que son términos que definen el motivo del reinicio. Inband Reset significa que el reinicio se produce a causa de una petición del host. Esto puede dar lugar a un warm reset o a un hot reset, según el tipo de petición (más sobre esto justo debajo).
Es importante distinguir entre warm reset y hot reset: en particular, un warm reset implica detener el flujo de datos y volver a ponerlo en marcha desde el principio. Un hot reset se envía dentro del propio flujo de datos y mantiene ese flujo en marcha. El hot reset es, por tanto, mucho más rápido, pero si hay un problema con el flujo de datos que se pueda arreglar reiniciándolo, es necesario un warm reset. Un ejemplo de este tipo de problema es que haya errores de bit en la capa física. Interrumpir el flujo de bits (bitstream) y empezar de nuevo puede arreglarlo, quizá porque corrige un ajuste subóptimo del ecualizador (equalizer).
Recuerda que usbreset ejecuta un ioctl() con USBDEVFS_RESET. Entre todas las demás cosas que ocurren, esto cambia PORT_RESET a cero, sin importar la versión USB del dispositivo (por lo menos en el kernel de Linux v5.16).
Según la sección 7.4.2 de la especificación USB 3.0, una petición PORT_RESET da lugar a un Hot Reset. Esto significa que si el flujo de datos está activo, no se interrumpe, sino que el comando de reinicio se envía a través de ese flujo. La especificación también define BH_PORT_RESET (número de feature 28), que fuerza un warm reset (salvo que el puerto esté deshabilitado). Este reinicio es más profundo: detiene el flujo de datos y vuelve a iniciar el procedimiento para ponerlo en marcha de nuevo (mediante señalización LFPS). La diferencia importante es que si el flujo de datos necesita reiniciarse, BH_PORT_RESET lo consigue, pero PORT_RESET no.
Conectar un dispositivo SuperSpeed nuevo conlleva un Warm Reset. Así que es una pena que al parecer no haya ninguna herramienta que pueda hacer lo mismo con PORT_POWER en un puerto SuperSpeed. Como se ha mencionado antes, uhubctl puede hacerlo técnicamente, pero el dispositivo acaba en un estado bastante caótico.
Notas sueltas
Son fragmentos sueltos de información que pueden resultar útiles, pero que no tienen un contexto claro.
- Los códigos de comandos como PORT_POWER aparecen como feature selectors (selectores de características) de la clase Hub en la tabla 11-17 de la especificación USB 2.0 y en la tabla 10-8 de la especificación USB 3.0.
- La función del kernel que activa PORT_RESET es hub_port_reset(). Pero para reinicializar de verdad un puerto está usb_reset_device(), que además prepara al controlador asignado al dispositivo para el reinicio, y después llama a usb_reset_and_verify_device(). Todas estas funciones están definidas en drivers/usb/core/hub.c, y solo usb_reset_device() se exporta.
- En drivers/usb/core/devio.c, usb_reset_device() es llamada por proc_resetdevice(), que puede activarse mediante un ioctl() USBDEVFS_RESET sobre el archivo de dispositivo correspondiente. Esto es lo que hace usbreset (ver más arriba).
- Esto equivale a la función op_reset_device() de libusb en linux_usbfs.c (ahí se usa IOCTL_USBFS_RESET en lugar de USBDEVFS_RESET, pero es igual a USBDEVFS_RESET; ambos valen 20, como puede compararse en include/uapi/linux/usbdevice_fs.h del kernel). La función de libusb también libera (unclaims) las interfaces. proc_resetdevice(), en cambio, se negará educadamente a hacerlo si alguna de las interfaces del dispositivo ha sido reclamada.
- En hub.c, hub_event() llama a port_event(). port_event() es la que detecta cambios en los bits del puerto. hub_event() es un elemento de trabajo (work item) que se pone en marcha mediante kick_hub_wq() (más concretamente desde hub_irq(), que «se dispara con los cambios de estado del puerto y varios fallos», en respuesta a las notificaciones de cambio de estado que llegan por el endpoint IN del hub, designado para este propósito).