24 points par GN⁺ 2026-04-10 | 1 commentaires | Partager sur WhatsApp
  • 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 un PID (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)
    • 18d1 est le VID de Google, et 4ee0 le PID du bootloader Nexus/Pixel
  • La commande lsusb -t permet de vérifier la classe et l’état du pilote
    • Class=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 combinaison VID:PID donné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.sys est 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
  • 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, bDeviceClass et d’autres informations sur le périphérique
  • La commande lsusb -v permet d’inspecter en détail tous les descripteurs (périphérique, configuration, interface, endpoint, etc.)
    • Exemple : l’interface Android Fastboot possède des endpoints Bulk IN(0x81) et Bulk OUT(0x02)

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
  • 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ées
    • OUT : 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 vaut 0, 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"
  • 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
      
    • OKAY indique un état de succès, 0.4 est la version de Fastboot

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

 
GN⁺ 2026-04-10
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

    • La période d’attente chez Guitar Center ne sert pas seulement à vérifier si l’objet a été volé. Ils ont aussi l’obligation légale, comme un mont-de-piété (pawn shop), de ne pas le vendre pendant un certain délai, afin de laisser à l’ancien propriétaire le temps de le récupérer
    • J’utilise le même matériel et j’ai empaqueté ce pilote dans l’AUR. Le blob binaire ne fonctionnait pas, mais pour servir de simple routeur MIDI, c’est suffisant
  • 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

    • En Rust, je recommande nusb
  • 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é

    • On peut atténuer ça, car l’override du pilote ne demande d’ajuster qu’une seule couche
  • 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

    • Les périphériques standardisés utilisent généralement USB/CDC/ECM ou RNDIS, donc ils sont reconnus automatiquement. L’accès en espace utilisateur est surtout utile pour les périphériques non standard. Sous Windows, on peut le faire de manière portable avec libusb sans signature de pilote
    • Sous Linux, il faut créer un périphérique tun/tap pour communiquer entre l’espace utilisateur et le noyau, ou alors faire tourner d’autres sous-systèmes eux aussi en espace utilisateur
  • 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 une ligature de police de programmation. Si vous copiez le code, vous verrez en réalité ->. C’est la syntaxe moderne de trailing return type en C++
    • Certains développeurs préfèrent les polices avec ligatures. Elles fusionnent deux caractères en un seul glyphe
    • Si vous configurez une touche Compose, vous pouvez saisir “→” avec n’importe quel clavier
    • Au final, c’est juste "->". La police le rend simplement sous forme de flèche
  • Je 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 ?

    • Les périphériques USB n’accèdent pas directement à la mémoire de l’hôte comme en PCIe ou FireWire. C’est le contrôleur XHCI qui effectue le DMA, et la plupart des contrôleurs de périphériques prennent aussi en charge le DMA entre leur propre RAM et l’USB
    • Tous les transferts sont pilotés par l’hôte. Même quand on a l’impression que le périphérique envoie d’abord des données, c’est en réalité l’hôte qui les demande. Un DMA direct serait un gros risque de sécurité
  • 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

    • Les descripteurs me semblaient mystérieux aussi, puis j’ai fini par comprendre qu’il s’agissait simplement de structures binaires fixes. Tant que vous respectez les champs et endpoints définis par chaque classe USB, le périphérique est reconnu
    • L’USB est correct, mais sur le plan électrique, l’USB 1/2 n’est pas un vrai signal différentiel
    • Il n’existe presque pas de tutoriels, mais pour un standard de grande entreprise, c’est plutôt raisonnable. Le vrai problème, c’est qu’il y a trop d’options, donc il faut lire beaucoup de spécifications
  • 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