Introduction
Dans un monde parfait, les périphériques USB et les hubs se comportent bien et parviennent à résoudre les problèmes qu'ils rencontrent. Dans la réalité, toutes sortes de choses bizarres arrivent, et il faut bien faire quelque chose. La solution habituelle consiste à débrancher le câble USB puis à rebrancher le périphérique. Mais que faire quand cette opération doit se faire automatiquement ?
La solution la plus radicale est présentée sur une autre page web : elle provoque l'arrêt et le redémarrage des pilotes du hub USB. L'effet est presque équivalent à débrancher puis rebrancher tous les périphériques USB de l'ordinateur. C'est le marteau le plus lourd, et il est parfois nécessaire.
Cette page présente deux méthodes pour réinitialiser un périphérique USB bien précis. Je commence par montrer comment mettre en œuvre chaque méthode, avec un minimum d'explications. Ensuite, j'entre dans beaucoup de détails techniques.
Toute la discussion est limitée à Linux, même si la seconde méthode présentée ici est probablement possible sur d'autres plateformes également.
Une grande partie des informations de cette page provient de la spécification Universal Serial Bus Revision 2.0. Le nom de fichier de ce document est en général usb_20.pdf.
Méthode n°1 : demander au noyau Linux de réinitialiser le périphérique
Plusieurs outils permettent de faire cela. L'outil le plus abouti fait partie d'usbutils. Téléchargez usbreset.c et compilez-le avec gcc :
$ gcc -O3 -Wall -g usbreset.c -o usbreset
Et ensuite :
$ ./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
À la suite de ces réinitialisations, on voit apparaître ceci dans le journal du noyau :
usb 1-6: reset full-speed USB device number 4 using xhci_hcd
Ce que cette session démontre :
- Sans argument, usbreset affiche une liste des périphériques USB. La même information s'obtient avec lsusb.
- Il faut être root pour réellement réinitialiser un périphérique (d'où sudo).
- Le périphérique peut être choisi par son adresse de bus (numéro de bus et numéro de périphérique). Notez que l'adresse de bus change chaque fois que le périphérique est connecté.
- Le périphérique peut aussi être choisi par ses identifiants Vendor / Product. C'est la méthode à privilégier lorsqu'on n'a qu'un seul périphérique USB de ce type.
- Le périphérique n'a pas changé d'adresse sur le bus USB (il n'a pas été réénuméré à la suite du reset).
Alors, que fait exactement usbreset ? Tout se résume à cette commande sur le fichier représentant le périphérique USB :
ioctl(fd, USBDEVFS_RESET, 0)
Cela aboutit à un appel à la fonction usb_reset_device() du noyau, qui fait bien plus qu'un simple reset du périphérique. L'idée est de rendre tout le processus fluide : cette fonction informe le pilote du périphérique du reset avant et après, si celui-ci le demande. Elle détache le pilote avant le reset et le rattache ensuite. La configuration du périphérique est également rechargée après le reset. Sans cela, le périphérique ne connaîtrait pas son adresse de bus et ne serait pas prêt à échanger des données.
C'est donc presque comme si on rebranchait le périphérique, sauf qu'on ne lui demande pas de s'identifier (car l'information est déjà connue) et qu'on ne lui attribue pas de nouvelle adresse.
Pour l'USB 3.x, il s'agit d'un hot reset, ce qui est le type de reset le moins efficace. Plus de détails sur le hot reset plus bas.
Méthode n°2 : changer l'état d'alimentation du port USB
Il existe également plusieurs outils pour cela. Je vais démontrer cette méthode avec hubpower. Téléchargez hubpower.c et compilez-le :
$ gcc -O3 -Wall -g hubpower.c -o hubpower
Cet outil est un peu plus délicat à utiliser, car les commandes ne sont pas envoyées au périphérique USB lui-même. Les requêtes sont envoyées au hub USB auquel le périphérique est connecté.
La première étape consiste donc à trouver quel hub c'est. Commençons par trouver l'adresse du périphérique 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
Le périphérique se trouve donc sur le bus numéro 1 et le périphérique numéro 4. Notez que le numéro de périphérique change à chaque énumération du périphérique USB.
À quel hub le périphérique USB est-il connecté, alors ? Et à quel port ?
$ 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
Le périphérique numéro 4 est donc connecté sur le port n°6. Le numéro de bus du hub est 1 et son numéro de périphérique est 1 (c'est le contrôleur USB de la carte mère, qui fait office de hub racine).
Voyons ce que hubpower a à dire à ce sujet :
$ 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
Remarquez que la partie « 1:1 » est l'adresse de bus du hub. S'il s'agit d'un hub externe, cette adresse changera chaque fois que le hub est connecté à l'ordinateur.
En revanche, les numéros de port ne changent jamais (tant que le périphérique reste connecté sur le même port physique).
Maintenant que nous connaissons le numéro de port du périphérique, réinitialisons-le :
$ 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
Après la première commande, le périphérique est signalé comme déconnecté dans le journal du noyau :
usb 1-6: USB disconnect, device number 4
Après la seconde commande, l'ordinateur se comporte comme si le périphérique venait d'être branché physiquement :
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
[ ... ]
En raison de cette réénumération, une nouvelle adresse de bus est attribuée au périphérique :
$ 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
Est-ce donc équivalent à débrancher physiquement le périphérique USB puis à le rebrancher ? La réponse est : pas forcément. Dans la plupart des cas, les commandes de hubpower ne coupent pas réellement l'alimentation du périphérique USB. Le périphérique continue donc de recevoir sa tension VBUS (5 V). Très peu de hubs USB coupent réellement l'alimentation. Plus de détails ci-dessous.
Avec l'immense majorité des hubs, la première commande (« power off ») se contente de mettre le port dans un état qui l'amène à ignorer le périphérique. La seconde commande (« power on ») rend au port son état naturel. Le résultat, c'est que le périphérique est détecté et que sa procédure d'initialisation démarre. Entre autres, le périphérique est réinitialisé et énuméré, puis son pilote l'initialise.
Une déconnexion physique est donc généralement plus efficace que ces commandes, en particulier si le périphérique tire son alimentation de la prise USB. Mais si le hub coupe réellement le VBUS, hubpower est tout aussi efficace.
Remarquez que dans cet exemple, les commandes ont été envoyées au contrôleur USB de la carte mère (le hub racine). La raison est que le périphérique USB était directement connecté à l'ordinateur. Si ce périphérique est connecté à l'ordinateur par l'intermédiaire d'un hub externe, les commandes doivent être envoyées à ce hub externe. Dans les deux cas, hubpower s'utilise de la même façon.
hubpower ne prend malheureusement pas en charge l'USB 3.0 (SuperSpeed). Plus de détails là-dessus plus bas.
Différences entre les deux méthodes
En quoi la seconde méthode (avec hubpower) diffère-t-elle de la première (avec usbreset) ? Pour résoudre un problème avec un périphérique, les deux méthodes font essentiellement la même chose : elles envoient une commande de reset. La manière dont cela se produit est radicalement différente, mais c'est toujours cette commande de reset qui, si quelque chose peut résoudre le problème, a le plus de chances de le faire.
Il y a tout de même quelques différences importantes :
- hubpower fonctionne même si le périphérique n'est pas reconnu sur le bus. Par exemple, si l'ordinateur a essayé d'énumérer le périphérique plusieurs fois puis a abandonné. Dans ce cas, le périphérique peut être physiquement connecté à l'ordinateur, mais usbreset est inutile car le périphérique n'a pas d'adresse de bus.
- Seul hubpower peut aider lorsque le port USB est désactivé : il arrive que l'ordinateur ignore complètement un port USB après plusieurs erreurs sur ce port. hubpower devrait résoudre cela.
- hubpower ne fonctionne pas avec le SuperSpeed (USB 3.x). C'est probablement une question d'ajout de prise en charge. Voir plus de détails sur le SuperSpeed ci-dessous.
- hubpower peut aussi commander l'alimentation du périphérique USB (mais ce n'est généralement pas le cas). C'est un avantage lorsque le périphérique a besoin d'une coupure puis d'une remise sous tension pour résoudre le problème.
- hubpower est plus difficile à utiliser : il faut trouver le port du hub auquel le périphérique USB est connecté, ainsi que l'adresse de bus du hub.
Piloter un équipement électrique (?)
Même si le sujet principal de cette page est de réparer un problème avec un périphérique USB, il y a aussi un effet secondaire intéressant : il est parfois possible de piloter l'alimentation 5 V du port USB. En d'autres termes, un hub USB simple et bon marché peut servir à allumer et éteindre une alimentation capable de fournir 2,5 watts.
C'est amplement suffisant pour commander un relais électromécanique. Il ne reste donc qu'un seul composant à ajouter pour piloter un appareil qui fonctionne en 110 V / 220 V. Bon, c'est aussi une bonne idée d'ajouter une simple diode, afin de protéger le hub USB contre d'éventuels dégâts. Mais c'est tout.
Malheureusement, la possibilité de couper l'alimentation reste optionnelle : selon la section 11.11 de la spécification USB 2.0, un hub peut avoir des interrupteurs d'alimentation qui coupent la tension 5 V d'un port lorsque celui-ci est dans l'état Powered-Off. Autrement, un interrupteur d'alimentation peut être utilisé pour couper l'alimentation de plusieurs ports à la fois (c'est le « ganged power switching »). Le but de la commande d'alimentation est surtout de pouvoir éteindre les périphériques USB qui consomment trop de courant, afin que les autres ports puissent continuer à fonctionner normalement.
Mais comme déjà mentionné, la coupure physique de l'alimentation est optionnelle. Ce que hubpower fait réellement, c'est changer la valeur de l'attribut PORT_POWER du port (ce qu'on appelle une « feature » dans les spécifications USB). Cette modification touche deux aspects différents du port du hub :
- Obligatoire : l'état logique du port. Lorsque PORT_POWER est à zéro, le port ne peut se trouver que dans l'état Powered-Off (ou Not Configured). En d'autres termes, le hub doit se comporter comme si aucune tension n'était présente sur le port et donc ignorer tout périphérique connecté, le cas échéant.
- Optionnel : l'existence de VBUS (la tension de 5 V) sur les fils d'alimentation du port. Si le hub ne prend pas en charge cette fonctionnalité, la tension peut rester présente même lorsque PORT_POWER est à zéro.
PORT_POWER est expliqué plus en détail ci-dessous.
Est-ce que mon hub coupe réellement la tension ?
Comment savoir si votre hub coupe réellement la tension ? Le seul moyen d'en être sûr, c'est de tester. Branchez quelque chose qui n'est pas un périphérique USB, mais qui consomme du courant sur le port USB. De vrais périphériques USB peuvent prêter à confusion. Par exemple, une souris optique éteint généralement sa LED en réponse à la commande « power off », même si la tension 5 V reste présente.
Chaque hub déclare, dans les informations visibles avec « lsusb -v » (dans le Hub Descriptor, défini à la section 11.23.2.1 de la spécification), s'il coupe la tension et à quelle granularité. Le hub peut déclarer qu'il ne coupe pas la tension, qu'il la coupe par groupes de ports (« gangs »), ou qu'il la coupe individuellement pour chaque port. Selon la section 11.11 de la spécification USB 2.0, « un hub avec interrupteurs d'alimentation peut couper l'alimentation de tous les ports en un seul groupe, de chaque port individuellement, ou avoir un nombre quelconque de groupes d'un ou plusieurs ports ».
Cependant, cette information n'est pas fiable. J'ai rencontré plusieurs hubs qui déclarent couper la tension, mais aucun ne le faisait réellement.
Par exemple, voici un extrait de la sortie 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
Cela semble optimiste, non ? En réalité, ce hub ne coupe pas du tout les tensions.
À l'inverse, voici un hub de carte mère tout ce qu'il y a de plus ordinaire :
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
Ce hub admet donc qu'il ne prend en charge aucun contrôle de tension.
Remarquez qu'en plus des informations sur la commutation d'alimentation, il y a aussi l'état de chaque port sous la rubrique « Hub Port Status ». Ces bits d'état sont définis dans la table 11-21 de la spécification USB 2.0 (section 11.24.2.7.1). C'est la même information que celle récupérée par hubpower.
L'outil uhubctl
L'idée de piloter un relais avec un hub USB est à l'origine de nombreuses initiatives. Cela vaut le coup de regarder uhubctl, principalement parce que ce projet tient une liste de hubs USB qui coupent réellement la tension.
Cet outil est clairement destiné uniquement au contrôle de la tension, et non à la résolution de problèmes avec un périphérique USB. Par exemple, par défaut, uhubctl ignore les hubs qui ne déclarent pas être capables de couper individuellement la tension de chaque port.
Ces commandes sont équivalentes aux commandes hubpower ci-dessus :
$ 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 syntaxe de la commande est beaucoup moins pratique. Elle est expliquée brièvement ci-dessous.
uhubctl est basé sur libusb, ce qui fait que cet outil fonctionne aussi sur d'autres systèmes d'exploitation. En revanche, hubpower accède directement au fichier du hub dans /dev/bus/usb/ (ou /proc/bus/usb/), ce qui ne fonctionne que sous Linux.
uhubctl prend aussi en charge l'USB 3.x (voir le commit git et son suivi). Cependant, il semble que personne ne se soit soucié de ce qui se passe quand un vrai périphérique USB est connecté au hub : mes tentatives pour réinitialiser un périphérique SuperSpeed ont donné des résultats bizarres. Lorsque l'alimentation était rétablie, le périphérique restait dans l'état Polling et n'était pas énuméré. De plus, pendant que l'alimentation était coupée, « lsusb -v » restait bloqué. Quelque chose clochait là.
Voici les commandes pour couper puis rétablir l'alimentation sur un port SuperSpeed. Plus de détails sur le SuperSpeed plus bas.
# ./uhubctl -f -e -l 2 -p 4 -a 0 # ./uhubctl -f -e -l 2 -p 4 -a 1
- -f signifie qu'on utilise le hub même si celui-ci annonce qu'il ne prend pas en charge le contrôle individuel de la tension pour chaque port.
- -e signifie « exact position », c'est-à-dire qu'il faut compter un port SuperSpeed comme deux hubs.
- -l 2 désigne le second hub (le premier hub était le hub USB 2.0 de mon ordinateur)
- -p 4 désigne le port numéro quatre.
- -a 0 coupe l'alimentation (action n°0)
- -a 1 rétablit l'alimentation (action n°1)
Explications sur PORT_POWER
Lorsqu'une commande arrive de l'hôte pour mettre PORT_POWER à zéro, le port entre sans condition dans l'état Powered-Off. C'est vrai même si le hub continue d'alimenter le périphérique en 5 V. Il y a d'autres raisons pour lesquelles PORT_POWER peut passer à zéro, en particulier une surintensité sur le port (le périphérique consomme trop de courant).
La seule façon de remettre PORT_POWER à '1' est de passer par une commande de l'hôte. Cela met le port dans l'état Disconnected. Les ports USB sur lesquels rien n'est branché se trouvent normalement dans cet état. Lorsqu'un périphérique est détecté sur le port, l'état passe à Disabled après un court délai. Si le périphérique est déjà connecté et que PORT_POWER passe à '1', cela se produit immédiatement.
À partir de cet état, le seul chemin vers l'activation du périphérique consiste à réinitialiser le port (avec PORT_RESET, voir plus bas). Seul l'hôte peut le faire. Par conséquent, tout ce que le hub peut faire, c'est signaler à l'hôte qu'un périphérique est connecté et demande de l'attention.
C'est là qu'intervient l'un des bits d'état du port : PORT_CONNECTION. Ce bit doit être à zéro lorsque le port est dans l'état Powered-Off ou Disconnected. PORT_CONNECTION passe à '1' lorsque le port transite de l'état Disconnected à l'état Disabled. Un changement de PORT_CONNECTION génère un événement de hub, de sorte que le pilote est notifié (c'est-à-dire qu'un appel à port_event() dans le hub.c du noyau est effectué avec USB_PORT_FEAT_C_CONNECTION activé). Le pilote répond en réinitialisant le port (en mettant le bit PORT_RESET du port à '1') et en énumérant le périphérique connecté.
Tout cela concerne l'USB 2.0. La figure 11-10 de la spécification USB 2.0 montre comment un port de hub change d'état.
Contrôler directement PORT_RESET et PORT_ENABLE
hubpower permet aussi de modifier directement deux autres attributs : PORT_RESET et PORT_ENABLE. En fait, j'ai ajouté cette possibilité à mon propre fork de cet outil. Notez toutefois que tout ce que l'on peut faire ainsi, c'est faire échouer volontairement le périphérique USB (et donc amener l'ordinateur à réinitialiser le périphérique pour corriger le problème). Maintenant, plus de détails :
PORT_RESET doit être mis à '1' par l'hôte pour déclencher un reset de port, conformément à la section 11.5.1.5 de la spécification USB 2.0. Le hub remet PORT_RESET à '0' après avoir terminé le reset. Le hub ne lance jamais un reset de port de sa propre initiative, et l'hôte n'a pas le droit d'écrire '0' dans cet attribut (section 11.24.2.7.1.5).
Le hub ignore PORT_RESET si le port est dans l'état Powered-Off ou Disconnected.
En ce qui concerne PORT_ENABLE : lorsque cet attribut passe à '0', le port passe dans l'état Disabled. Cela peut se produire à la suite d'une demande de l'hôte, mais aussi de la déconnexion du périphérique USB, du fait que le port est hors tension, ou d'une erreur pendant le processus de reset.
PORT_ENABLE ne peut repasser à '1' qu'à la suite d'une demande de reset de port envoyée par l'hôte (section 11.24.2.7.1.2 de la spécification USB 2.0).
L'hôte n'a donc pas le droit de mettre PORT_RESET à '0' ni de mettre PORT_ENABLE à '1'. Toute tentative en ce sens aboutira à une réponse d'erreur de la part du hub (la commande ioctl() correspondante renverra un code d'erreur).
hubpower peut demander directement un reset en mettant le PORT_RESET du port à '1'. Cela réinitialisera le périphérique USB, et le périphérique oubliera son adresse de bus par la même occasion. Le périphérique ne sera donc plus accessible.
Cette commande est envoyée directement au hub ; le pilote du hub dans le noyau Linux ne saura donc pas que cela s'est produit. En fait, l'ordinateur ne remarquera pas que quelque chose a changé tant qu'il n'essaiera pas d'accéder au périphérique USB. La suite dépendra du pilote du périphérique. Le périphérique sera traité comme s'il avait une erreur matérielle. Des mesures de correction seront donc prises. Le plus probable est que cela inclura un reset.
Utiliser hubpower pour provoquer directement un reset donnera donc probablement le résultat souhaité, mais avec beaucoup d'agitation inutile. PORT_POWER fait cela plus élégamment. Le seul avantage possible d'un PORT_RESET direct est que le pilote pourrait se dire : « hé, ce périphérique a vraiment un problème, faisons quelque chose de radical pour le réparer ». Et cela pourrait aider.
Quant à la manipulation de PORT_ENABLE, la même chose se produira : le périphérique disparaîtra soudainement. Même dans ce cas, l'ordinateur ne saura pas immédiatement que quelque chose s'est passé : selon la section 11.24.2.7.2.2 de la spécification USB 2.0, une notification de changement sur PORT_ENABLE (c'est-à-dire C_PORT_ENABLE) n'est déclenchée que si le port devient désactivé en raison d'une erreur sur la liaison. La spécification dit explicitement que cette notification n'est pas émise pour toute autre raison.
Donc, mettre PORT_ENABLE à zéro aura à peu près le même effet que modifier directement PORT_RESET. Avec un inconvénient : selon la section 10.14.2.6.1 de la spécification USB 3.0, PORT_ENABLE « n'est pas pris en charge par les hubs SuperSpeed ».
SuperSpeed (USB 3.x)
Tout d'abord : si vous lisez cette page parce que vous avez un problème avec un périphérique USB 3.x, demandez-vous si vous avez vraiment besoin du débit offert par l'USB 3.x. Si la réponse est non, essayez de connecter le périphérique à l'ordinateur via un hub USB qui ne prend pas en charge l'USB 3.x (ou avec un court câble USB 2.0). Cela pourrait suffire à résoudre le problème.
Le SuperSpeed USB (ce qui signifie la même chose que l'USB 3.x) coexiste en parallèle avec l'USB 2.0. Chaque périphérique SuperSpeed est en réalité constitué de deux périphériques : un périphérique distinct pour le SuperSpeed et un autre périphérique distinct pour l'USB 2.0. Chacune de ces deux versions USB utilise des fils séparés du câble USB. Elles sont mutuellement indépendantes, électroniquement et conceptuellement.
Les spécifications USB exigent que chaque périphérique SuperSpeed soit composé de ces deux périphériques, même si, concrètement, cela n'est pas indispensable. En d'autres termes, un périphérique SuperSpeed qui ne prend pas en charge l'USB 2.0 fonctionne correctement lorsqu'il est connecté à un port SuperSpeed.
Lorsqu'un périphérique SuperSpeed est connecté à un port SuperSpeed, la première tentative se fait par l'interface SuperSpeed. Si elle échoue, une tentative de connexion en USB 2.0 est faite. En pratique (et conformément à la spécification), un périphérique USB ne se connecte jamais par les deux versions en même temps. C'est toutefois possible, et le périphérique USB se comporterait alors comme deux périphériques distincts.
Un hub SuperSpeed est constitué de deux hubs en parallèle : un pour l'USB 2.0 et un pour le SuperSpeed. Lorsqu'on connecte un hub USB SuperSpeed externe à un ordinateur, deux hubs sont ajoutés au système. Ils apparaissent comme deux périphériques distincts. Un périphérique USB ordinaire n'a pas le droit d'utiliser les deux versions en parallèle, mais un hub, lui, doit le faire.
Si un hub SuperSpeed est connecté à un port USB 2.0, il se comporte comme un hub USB 2.0 ordinaire.
D'une manière générale, chacun de ces deux hubs fonctionne indépendamment de l'autre. Chaque hub a ses propres ports, et chacun de ces ports fonctionne indépendamment. En particulier, si l'on modifie les paramètres d'un port sur l'un de ces hubs en parallèle, cela n'a aucun effet sur les ports de l'autre hub.
Autre conclusion : si « lsusb -t » montre qu'un périphérique est connecté à l'ordinateur par l'intermédiaire du hub racine SuperSpeed, c'est qu'il fonctionne comme un périphérique SuperSpeed. Autrement dit, son débit est de 5 Gbit/s ou plus. De même, si le périphérique est connecté par le hub racine USB 2.0, son débit est de 480 Mbit/s ou moins.
SuperSpeed et PORT_POWER
Rappelons que ce que fait réellement hubpower, c'est modifier l'attribut PORT_POWER du port.
Mais un hub SuperSpeed est constitué de deux hubs en parallèle. Chacun de ces deux hubs possède son propre attribut PORT_POWER, indépendant, pour chaque port. Alors, quand le hub doit-il couper la tension VBUS ? Chaque interrupteur d'alimentation physique dépend de deux attributs PORT_POWER, un pour chacun des hubs en parallèle.
La table 10-2 de la spécification USB 3.0 donne la table de vérité indiquant si le hub doit activer ou non l'alimentation. On peut la résumer ainsi : si le hub se comporte uniquement comme un hub USB 2.0 (par exemple s'il est connecté à un ordinateur qui ne prend pas en charge le SuperSpeed), il suit le PORT_POWER de sa partie USB 2.0. S'il est connecté en tant que hub SuperSpeed (ou si les deux hubs en parallèle sont connectés), VBUS n'est coupée que si les deux PORT_POWER sont à zéro.
Si cela paraissait compliqué, c'était en réalité la partie facile : la partie SuperSpeed du hub possède une machine à états (state machine) interne différente. C'est assez logique, car l'établissement de la liaison (link training) se fait différemment. Mais cette machine à états a trois états différents (au lieu d'un seul, comme l'USB 2.0) qui peuvent s'appliquer lorsque PORT_POWER est à zéro :
- DSPORT.Powered-off, ce qui signifie que le port est complètement inactif
- DSPORT.Powered-off-detect, ce qui signifie que le port essaie de détecter un partenaire de liaison SuperSpeed
- DSPORT.Powered-off-reset, ce qui signifie que le port effectue un warm reset sur le partenaire de liaison (plus de détails sur le warm reset ci-dessous).
Le but de ces deux derniers états est de garantir que si un périphérique SuperSpeed disposant de sa propre alimentation est connecté au port, la connexion ne retombe pas en USB 2.0. Cela se produirait parce que le périphérique n'aurait aucun moyen de savoir qu'il est connecté à un port SuperSpeed.
Qu'avons-nous appris de tout cela ? Principalement que réinitialiser un périphérique en modifiant PORT_POWER sur un hub SuperSpeed n'est pas aussi simple qu'avec un hub USB 2.0.
SuperSpeed : warm reset et hot reset
L'USB 2.0 dispose d'une seule méthode simple pour réinitialiser le périphérique : les deux fils sont mis à la terre par le hub (SE0) pendant 10 ms. Les périphériques SuperSpeed, en revanche, ont deux façons de réinitialiser un périphérique : le warm reset et le hot reset. Il ne faut pas les confondre avec le PowerOn Reset et l'Inband Reset, qui sont des termes utilisés pour définir la raison du reset. Inband Reset signifie que le reset se produit à la suite d'une demande de l'hôte. Cela peut aboutir à un warm reset ou à un hot reset, selon le type de la demande (plus de détails juste en dessous).
Il est important de distinguer le warm reset du hot reset : en particulier, un warm reset implique l'arrêt du flux de données et sa remise en route depuis le début. Un hot reset est envoyé sur le flux de données lui-même et maintient ce flux actif. Le hot reset est donc beaucoup plus rapide, mais si le problème du flux de données peut être corrigé en le redémarrant, c'est un warm reset qu'il faut. Un exemple d'un tel problème est la présence d'erreurs binaires sur la couche physique. Couper le flux binaire (bitstream) et repartir de zéro peut corriger le problème, peut-être parce que cela permet de corriger un réglage sous-optimal de l'égaliseur (equalizer).
Rappelons que usbreset effectue un ioctl() avec USBDEVFS_RESET. Entre toutes les autres choses qui se passent, cela met PORT_RESET à zéro, quelle que soit la version USB du périphérique (en tout cas à partir du noyau Linux v5.16).
Selon la section 7.4.2 de la spécification USB 3.0, une requête PORT_RESET donne lieu à un Hot Reset. Cela signifie que si le flux de données est actif, il n'est pas interrompu, mais que la commande de reset est envoyée sur ce flux. Cette spécification définit aussi BH_PORT_RESET (feature numéro 28), qui force un warm reset (sauf si le port est désactivé). Ce reset est plus fondamental : il interrompt le flux de données et relance la procédure permettant de le rétablir (au moyen de la signalisation LFPS). La différence importante est que si le flux de données doit être relancé, BH_PORT_RESET le fera, mais PORT_RESET ne le fera pas.
La connexion d'un nouveau périphérique SuperSpeed implique un Warm Reset. Il est donc dommage qu'il n'existe apparemment aucun outil capable de faire convenablement le coup de PORT_POWER sur un port SuperSpeed. Comme mentionné plus haut, uhubctl peut techniquement le faire, mais le périphérique se retrouve dans un état douteux.
Notes en vrac
Voici des bribes d'informations qui peuvent être utiles, mais qui n'ont pas vraiment de contexte attitré.
- Les codes des commandes comme PORT_POWER sont listés comme « Hub class feature selectors » dans la table 11-17 de la spécification USB 2.0 et dans la table 10-8 de la spécification USB 3.0.
- La fonction du noyau qui active PORT_RESET est hub_port_reset(). Mais pour réellement réinitialiser un port, il y a usb_reset_device(), qui prépare aussi le pilote concerné au reset, puis effectue un appel à usb_reset_and_verify_device(). Toutes ces fonctions sont définies dans drivers/usb/core/hub.c, et seule usb_reset_device() est exportée.
- Dans drivers/usb/core/devio.c, usb_reset_device() est appelée par proc_resetdevice(), qui peut être déclenchée par un ioctl() USBDEVFS_RESET sur le fichier de périphérique concerné. C'est ce que fait usbreset (voir plus haut).
- Cela équivaut à la fonction op_reset_device() de libusb dans linux_usbfs.c (elle utilise IOCTL_USBFS_RESET à la place, mais c'est égal à USBDEVFS_RESET : les deux valent 20, comparez avec include/uapi/linux/usbdevice_fs.h du noyau). La fonction de libusb libère aussi les interfaces qu'elle avait réclamées. proc_resetdevice(), quant à elle, refusera poliment de procéder au reset si l'une des interfaces du périphérique a été réclamée.
- Dans hub.c, port_event() est appelée par hub_event(). C'est la première qui détecte les changements dans les bits du port. hub_event() est un élément de file de travail (work item), lancé par kick_hub_wq() (en particulier par hub_irq(), qui « se déclenche lors des changements d'état des ports et de divers défauts » en réponse aux notifications de changement d'état qui arrivent sur l'endpoint IN du hub, désigné à cet effet).