USB pour les développeurs logiciels : introduction à l’écriture de pilotes USB en espace utilisateur
(werwolv.net)- Le développement de pilotes USB est souvent perçu comme un travail au niveau noyau, mais il peut en réalité être réalisé en espace utilisateur avec une complexité comparable à la programmation socket
- Avec libusb, il est possible d’effectuer l’énumération des périphériques, les transferts de contrôle, ainsi que l’envoi et la réception de données sans écrire de code noyau
- La communication USB repose sur quatre types de transfert — Control, Bulk, Interrupt, Isochronous — ainsi que sur les directions IN/OUT, chaque endpoint fonctionnant comme un canal unidirectionnel
- En prenant comme exemple le protocole Fastboot des appareils Android, l’article montre en code comment échanger des commandes et des réponses via des endpoints Bulk
- Il est possible d’implémenter un pilote USB complet en espace utilisateur, et tous les protocoles USB partagent la même structure de base
Introduction
- Les pilotes pour périphériques USB paraissent difficiles parce qu’on pense qu’ils nécessitent de manipuler du code noyau, alors qu’en pratique leur complexité est comparable à celle d’une application utilisant des sockets
- Même un développeur ayant peu d’expérience matérielle peut apprendre à manipuler l’USB en espace utilisateur
- Il existe des ressources qui détaillent le fonctionnement interne de l’USB, mais elles restent difficiles d’accès pour les débutants
- L’usage de l’USB ne demande pas de connaissances de niveau systèmes embarqués et peut être abordé comme les sockets réseau
Périphérique USB
- L’exemple utilisé est un smartphone Android en mode bootloader
- Il est facile à trouver, le protocole est simple, et l’absence de pilote par défaut dans l’OS en fait un bon support d’expérimentation
- L’entrée en mode bootloader varie selon les appareils, mais se fait généralement via une combinaison du bouton d’alimentation et des boutons de volume
Énumération manuelle du périphérique
- L’énumération (Enumeration) est le processus par lequel l’hôte interroge le périphérique pour l’identifier, et elle est exécutée automatiquement lors de la connexion
- Les périphériques standard chargent automatiquement un pilote en fonction de leur classe USB, tandis que les périphériques spécifiques au constructeur utilisent un
VID(Vendor ID) et unPID(Product ID) - Sous Linux, on peut consulter les informations du périphérique avec la commande
lsusb- Exemple :
ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot) 18d1est le VID de Google, et4ee0le PID du bootloader Nexus/Pixel
- Exemple :
- La commande
lsusb -tpermet de vérifier la classe et l’état du piloteClass=Vendor Specific Class,Driver=[none]indiquent que l’OS n’a chargé aucun pilote
- Sous Windows, on peut obtenir les mêmes informations via le Gestionnaire de périphériques ou USB Device Tree Viewer
Énumération du périphérique avec libusb
- La bibliothèque libusb permet de communiquer avec des périphériques USB en espace utilisateur sans écrire de code noyau
libusb_hotplug_register_callback()permet de configurer l’exécution d’un callback lorsqu’un périphérique correspondant à une combinaisonVID:PIDdonnée est connecté- Après le lancement du programme, le message
"Device plugged in!"s’affiche lorsque le périphérique est branché - Sous Linux, cela fonctionne par défaut et, si nécessaire,
libusb_detach_kernel_driver()permet de détacher le pilote noyau - Sous Windows, le pilote
Winusb.sysest nécessaire ; s’il n’est pas présent, l’outil Zadig peut être utilisé pour le remplacer manuellement
Communication avec le périphérique
- La première communication avec un périphérique USB s’effectue via l’endpoint Control (adresse 0x00)
libusb_control_transfer()permet d’envoyer une requête standard (GET_STATUS) pour lire l’état du périphérique- Exemple de réponse :
01 00→ le premier octet indique Self-Powered, le second l’absence de prise en charge de Remote Wakeup
- Exemple de réponse :
- On peut ensuite récupérer le descripteur du périphérique avec une requête GET_DESCRIPTOR
- Les données retournées contiennent notamment
idVendor,idProduct,bDeviceClasset d’autres informations sur le périphérique
- Les données retournées contiennent notamment
- La commande
lsusb -vpermet d’inspecter en détail tous les descripteurs (périphérique, configuration, interface, endpoint, etc.)- Exemple : l’interface
Android Fastbootpossède des endpoints Bulk IN(0x81) et Bulk OUT(0x02)
- Exemple : l’interface
Endpoints
- Les endpoints sont un concept proche des ports réseau : ce sont les canaux par lesquels le périphérique envoie et reçoit des données
- Le type et la direction de chaque endpoint sont définis dans les descripteurs
-
Type de transfert Control
- Tous les périphériques en possèdent un, toujours à l’adresse
0x00 - Il sert à la configuration initiale et aux demandes d’informations sur le périphérique
- Il n’appartient pas à une interface et fait partie du périphérique lui-même
- Tous les périphériques en possèdent un, toujours à l’adresse
-
Type de transfert Bulk
- Utilisé pour les transferts volumineux de données non temps réel
- Exemples : Mass Storage, CDC-ACM (série), RNDIS (Ethernet)
- La bande passante est élevée mais la priorité est faible
-
Type de transfert Interrupt
- Utilisé pour les petits transferts à faible latence
- Utilisé notamment par les claviers et les souris pour sonder rapidement les entrées utilisateur
- Il ne s’agit pas d’une véritable interruption matérielle : c’est l’hôte qui effectue les requêtes périodiquement
-
Type de transfert Isochronous
- Utilisé pour les données volumineuses sensibles au temps (audio, streaming vidéo)
- En cas de latence, la dégradation de qualité devient immédiatement visible
- Dans libusb, il est traité de manière asynchrone
-
Directions IN / OUT
- L’USB repose sur une architecture centrée sur l’hôte, et le périphérique ne peut pas transmettre de données tant qu’il n’a pas reçu de requête
IN: direction dans laquelle l’hôte reçoit les donnéesOUT: direction dans laquelle l’hôte envoie les données- Si le bit de poids fort (MSB) de l’adresse de l’endpoint vaut
1, il s’agit de IN ; s’il vaut0, il s’agit de OUT - Jusqu’à 127 endpoints personnalisés peuvent être utilisés (
0x00étant réservé au Control) - Les endpoints sont unidirectionnels et sont souvent organisés en paires IN/OUT, comme dans l’interface Fastboot
Protocole Fastboot
- Fastboot est un protocole de communication avec le bootloader Android : on envoie une chaîne de commande et on reçoit un code d’état de 4 octets ainsi que des données
- Exemples :
Host: "getvar:version"→Client: "OKAY0.4"Host: "getvar:nonexistant"→Client: "OKAY"
- Exemples :
- Exemple de code utilisant libusb pour envoyer une commande Fastboot
- L’interface 0 est réservée via
libusb_claim_interface() - La commande
"getvar:version"est envoyée sur l’endpoint Bulk OUT(0x02) - La réponse est reçue via l’endpoint Bulk IN(0x81)
- Exemple de sortie :
Request: getvar:version Response: OKAY0.4 OKAYindique un état de succès,0.4est la version de Fastboot
- L’interface 0 est réservée via
Conclusion
- Il est possible d’implémenter un pilote USB complet en espace utilisateur sans écrire de code noyau
- Tous les pilotes USB reposent sur les mêmes principes fondamentaux ; seul le protocole change
- Même les protocoles plus complexes (comme MTP) conservent la même structure de base et peuvent être abordés avec des concepts similaires à la communication par sockets
1 commentaires
Commentaires Hacker News
Le timing était vraiment parfait. Je dois bientôt récupérer un MOTU MIDI Express XT dans un Guitar Center près de chez moi
Comme c’est du matériel d’occasion, il doit être retenu un certain temps pour des raisons légales, donc j’attends. Le problème, c’est que cet appareil n’utilise pas le MIDI-over-USB standard, mais un protocole propriétaire, donc je ne peux pas l’utiliser directement en USB sur mes systèmes comme Linux, OpenBSD ou Haiku
Pour l’instant, j’ai seulement besoin d’un routage simple entre modules de synthé et contrôleurs, donc ce n’est pas grave, mais ce serait bien de le faire fonctionner aussi côté PC
Il existe déjà un pilote Linux, mais sa stabilité est incertaine et la prise en charge du XT n’est pas très claire. Le problème de kernel panic a été résolu, mais il reste encore des issues
Du coup, je pense écrire moi-même un pilote en espace utilisateur basé sur LibUSB. S’il expose les ports MIDI et ajoute des outils de routage, ça pourrait être vraiment utile
Si vous voulez faire ce genre de chose en Go, j’ai créé la bibliothèque go-usb, qui permet d’accéder à l’USB sans cgo
J’ai aussi développé avec ça go-uvc pour gérer les appareils UVC
Je suis moi aussi en train d’implémenter le système usbip sur un Macbook M3 avec une approche similaire
Il y a toutefois des limitations sur les versions récentes de macOS. Pour les périphériques USB reconnus par le système, on ne peut pas construire de pilote en espace utilisateur basé sur libusb sans désactiver manuellement certaines fonctions de sécurité
Cette approche revient finalement à faire jouer au pilote USB le rôle de code applicatif aussi. Autrement dit, c’est plus proche d’une bibliothèque + d’un programme que d’un pilote
Par exemple, je me demande comment on ferait pour connecter un périphérique USB-Ethernet comme adaptateur réseau de l’OS
Si j’avais lu cet article il y a quelques années, ça m’aurait beaucoup simplifié la vie quand je faisais de la rétro-ingénierie sur des fonctions de portable. En particulier, mon programme de contrôle des LED du clavier reste encore aujourd’hui l’un de mes projets préférés
C’était vraiment une introduction très utile. Travailler avec des API matérielles bas niveau est difficile, mais gratifiant. Les couches d’abstraction des OS modernes ont rendu ça plus accessible, mais il reste important de comprendre ce qu’il y a en dessous
Le code C++ avait l’air étrange. Je n’ai jamais vu de clavier permettant de saisir directement le caractère flèche
->. C’est la syntaxe moderne de trailing return type en C++"->". La police le rend simplement sous forme de flècheJe me demandais si les périphériques USB prennent en charge le DMA. Est-ce que tout passe forcément par l’hôte, ou bien le périphérique peut-il accéder directement à la mémoire ?
J’avais essayé autrefois de fabriquer un petit périphérique USB, mais il y avait très peu d’informations sur la manière d’écrire les descripteurs (descriptors). En général, on me disait surtout de trouver un périphérique similaire, puis de copier et modifier
Je me demandais si l’USB était vraiment un si bon standard
Si on me demandait « écris toi-même le pilote de ce périphérique USB », je commencerais par rendre l’appareil et vérifier s’il n’est pas possible de le gérer via un port COM virtuel