- Présentation structurée de tout le parcours nécessaire pour créer un premier appareil USB, du câblage physique au PCB, aux débits de l’USB 2.0, aux couches de protocole et à une mise en pratique sur STM32
- La connexion de base de l’USB 2.0 repose sur quatre fils : +5V, GND, D+, D- ;
D+ et D- forment une paire différentielle qui transporte ensemble 1 bit
- Sur un PCB, l’égalisation des longueurs de la paire différentielle, son placement rapproché et l’impédance sont importants, mais un prototype full speed à 12 Mbit/s peut fonctionner avec une certaine tolérance
- La mise en pratique sur NUCLEO-F103RB consiste à configurer
PA12 en USB_DP et PA11 en USB_DM, à créer un périphérique USB série reconnu comme port COM virtuel CDC, puis à allumer une LED quand on envoie 1
- L’approche STM32CubeIDE impose une génération de code pilotée par interface graphique et fortement liée au boilerplate, tandis qu’une implémentation de périphérique USB sous Linux peut offrir des API plus standardisées et une architecture plus découplée
Portée d’une première implémentation USB
- L’objectif est de réaliser jusqu’au bout, dans sa forme la plus simple, un périphérique USB reconnu lorsqu’on le branche à un ordinateur
- Ici, le périphérique désigne un accessoire qui étend les fonctions de l’ordinateur, et l’ordinateur est appelé host
- Le périmètre couvre à la fois la connexion physique entre le périphérique USB et le host, et une application simple côté host pour interagir avec le périphérique
- Il s’agit moins d’un commentaire faisant autorité sur la spécification que d’un projet E2E très simple de fabrication d’un périphérique USB, accompagné d’un index de ressources
Concepts de base de l’USB
- L’USB est une norme industrielle qui permet l’échange de données et l’alimentation électrique entre plusieurs appareils électroniques
- L’USB est un bus série, donc les bits sont transmis un par un sur le bus, et non en parallèle
- Les bus modernes sont principalement série ; on peut comprendre l’USB comme un mécanisme d’échange série de bits entre un host et un device
- L’USB ne se limite pas à une spécification physique pour relier deux appareils et échanger des bits, il inclut aussi un protocole de communication pour le transfert de données et d’énergie
- L’explication se concentre sur l’USB 2.0
Câblage USB et paire différentielle
- Une connexion USB 2.0 classique se décrit au minimum avec quatre fils
+5 V : ligne par laquelle le host alimente le device
D-, D+ : deux lignes qui fonctionnent ensemble comme une paire différentielle pour transmettre 1 bit
GND : masse
- Certaines connexions peuvent avoir des broches supplémentaires comme
ID, mais la mise en pratique ne traite que ces quatre fils
-
Remarque sur l’USB-C
- L’USB-C utilise lui aussi des paires différentielles en interne, mais diffère d’une explication USB 2.0 classique notamment parce qu’il doit fonctionner quel que soit le sens d’insertion
- Le fait d’utiliser un connecteur USB-C ne détermine ni le débit ni la version USB
- Un appareil USB-C peut très bien être en USB 2.0, ou être un appareil plus moderne en USB 3.0
- La suite de la mise en pratique n’aborde pas davantage l’USB-C
-
Comment la paire différentielle réduit le bruit
- Une ligne unique représente un bit en comparant sa tension à
GND, mais sur une liaison filaire longue, l’effet du bruit peut devenir important
- Une paire différentielle utilise deux lignes : l’une transporte
V, l’autre -V
- Le récepteur observe la différence de tension entre les deux lignes
- Si l’on simplifie en supposant que le même bruit
Vn s’ajoute aux deux lignes, alors (V + Vn) - (-V + Vn) = 2V, ce qui signifie que le bruit s’annule
- Ce modèle est très simplifié ; pour une compréhension plus approfondie des paires différentielles, on peut consulter la vidéo YouTube d’Altium de Zach Peterson
- Video 15
Manipuler l’USB sur un PCB
- Si l’on utilise une carte de développement sans concevoir directement le matériel, on peut sauter la section PCB, mais les principes de base restent utiles lorsqu’on place un connecteur USB sur un PCB
- Les composants de bibliothèque de connecteurs USB comportent les broches mentionnées plus haut, et la paire différentielle doit être routée vers les broches adjacentes correspondantes du microcontrôleur ou du SoC
- Il y a trois principes de base pour le routage d’une paire différentielle
- Faire correspondre la longueur de la piste allant de
D+ à la broche plus du composant avec celle de l’autre piste
- Placer les deux pistes très près l’une de l’autre
- Tenir compte de l’impédance adaptée au signal
- Comme les deux lignes traversent pratiquement le même environnement, on peut supposer dans le modèle simplifié précédent qu’elles subissent le même bruit
- Le calcul d’impédance peut se faire à l’aide du calculateur du fabricant, en entrant l’impédance cible, la distance au plan de masse, l’écartement de la paire différentielle, etc., afin d’obtenir la largeur de piste nécessaire
- Des vidéos de Zach Peterson sur les paires différentielles et le routage USB sont proposées en ressources
Débits de l’USB 2.0
- USB 2.0 ne signifie pas qu’il n’existe qu’un seul débit fixe
- L’USB 2.0 peut par exemple proposer deux niveaux de vitesse
- full speed : 12 Mbit/s
- high speed : 480 Mbit/s
- Le device et le host doivent décider, lors de la connexion, quelle vitesse utiliser
- Pour atteindre de manière fiable le high speed sur un PCB, l’adaptation d’impédance est importante
- Pour un prototype de base, le full speed à 12 Mbit/s peut suffire et, si les pistes entre le connecteur USB et la puce sont courtes, on peut être relativement tolérant sur la largeur des pistes et d’autres paramètres
Protocole et couches logicielles
- L’USB ne concerne pas seulement le matériel ; il faut aussi prendre en compte les couches logicielles côté host et côté device
- Une vidéo d’environ 45 minutes expliquant l’USB du point de vue Linux est proposée en ressource
- Cette vidéo traite des trames USB, des endpoints, des configurations, et de la manière dont un même device peut remplir plusieurs fonctions USB
- Il est important de voir l’USB comme un réseau d’appareils
- Il serait irréaliste que le système d’exploitation du host implémente séparément un driver pour chaque périphérique USB ; le système reconnaît donc plusieurs classes de périphériques USB
- périphérique de stockage de masse
- périphérique série
- et de nombreuses autres classes
- Le périphérique de la mise en pratique fonctionne, du point de vue du host, comme un port série
Créer un périphérique USB serial port avec STM32
- Le périphérique de démonstration est un simple périphérique USB qui allume une LED lorsqu’il reçoit une requête du host
- Le host reconnaît ce périphérique comme un serial port device
- Il existe globalement deux approches d’implémentation
- utiliser un microcontrôleur avec prise en charge USB
- utiliser un SoC capable d’exécuter Linux pour laisser le noyau prendre en charge une grande partie du travail
- Pour des raisons de simplicité, la mise en pratique utilise l’approche microcontrôleur
-
Carte NUCLEO-F103RB
- La carte de développement utilisée est la NUCLEO-F103RB
- Aux États-Unis, on peut l’acheter un peu au-dessus de 10 dollars chez des distributeurs comme Digikey
- Cette carte est composée de deux parties attachées l’une à l’autre
- La petite partie correspond à la zone du programmer, la grande à celle du MCU principal réellement programmé
- La connexion USB par défaut de la carte ne va pas vers le MCU principal, mais vers le MCU du programmer
- Ce programmer utilise le protocole ST-LINK pour échanger des messages USB avec l’ordinateur, puis programmer le MCU principal
- Si l’on fabriquait directement un PCB à base de STM32, on pourrait n’avoir qu’une seule puce sur la carte, la programmer via USB, puis réutiliser ce même port USB pour la logique du périphérique
- Des vidéos de Phil’s Lab sur les PCB STM32 et la programmation par USB sont proposées en référence
- Video 19
- Video 20
Câblage du véritable port USB
- Comme la connexion USB par défaut de la carte Nucleo n’est pas reliée au MCU principal, il faut connecter directement le port USB du MCU principal au host
- Après avoir chargé le programme, on débranche le programmer ST-LINK de l’ordinateur, puis on alimente le MCU principal via le vrai port USB
- STM32CubeIDE est utilisé pour écrire le logiciel, et STM32CubeProgrammer pour le flasher sur la carte
-
Broches et configuration nécessaires
- Quatre connexions sont nécessaires pour créer un périphérique USB 2.0
- l’alimentation 5V venant du host
GND
D+
D-
- Sur la NUCLEO-F103RB, pour alimenter le MCU principal depuis son port USB, il faut modifier le cavalier
JP5 afin que la carte accepte une alimentation external 5 V
- Dans STM32CubeIDE, il faut configurer les broches ainsi
PA12 → USB_DP
PA11 → USB_DM
- La puce de cette carte nécessite une résistance de pull-up de 1.5 kΩ externe pour la connexion USB
- La résistance applique un pull-up de
PA12 vers 3.3V
- Le montage est réalisé sur breadboard
- Pour relier un MacBook Pro aux broches individuelles, un câble breakout USB acheté sur Amazon a été utilisé ; son terminal block a été retiré pour exposer les broches USB, puis des jumper wires ont servi à relier le tout à la carte Nucleo et à la breadboard
- Même sans respecter strictement l’égalisation des longueurs ni le contrôle d’impédance de la paire différentielle, il a été possible d’établir dans cette démonstration une liaison à 12 Mbit/s
Écriture du logiciel USB CDC
- Quand on configure les broches USB dans CubeIDE, une alerte liée à la configuration de l’horloge apparaît, et il est possible de choisir l’option où CubeIDE s’en charge automatiquement
- Dans
Pinout & Configuration, il faut configurer USB_DEVICE sous Middleware and Software Packs
- Le mode important dans cette mise en pratique est le suivant
Communication Device Class (Virtual Port Com)
- Avec ce réglage, la carte Nucleo se comporte, du point de vue du host, comme un périphérique CDC serial port
- Le host peut, à partir de cette information de classe, choisir le driver approprié pour communiquer avec le périphérique custom
- CubeIDE génère le code C, et
main.c contient notamment l’appel d’initialisation suivant
MX_USB_DEVICE_Init();
- Pour allumer la LED, on ajoute le code suivant à la routine
CDC_Receive_FS
/* USER CODE BEGIN 6 */
if (Buf[0] == '1') {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, 1);
}
- Cet appel HAL allume la LED embarquée connectée à la broche 5 du port A
- La vidéo de référence pas à pas est la suivante
Flash et exécution
- Une fois le fichier ELF compilé, on envoie le code sur la carte avec CubeProgrammer
- On débranche ensuite le programmer, puis on câble la carte en utilisant l’alimentation external 5V comme décrit plus haut
- Lorsque la carte démarre, elle doit apparaître dans le gestionnaire de périphériques du système d’exploitation sous une entrée de type port COM ou Serial port
- Dans le menu middleware
USB_DEVICE de CubeIDE, on peut modifier la valeur de Device Descriptor pour définir le nom du périphérique custom affiché dans le gestionnaire de périphériques du système
- Sous Mac OS, le nouveau périphérique apparaît dans le système de fichiers
/dev
- Exemple de chemin :
/dev/tty.usbmodem497A0F6739561
- Sous Linux, il peut apparaître sous un nom comme
/dev/ttyUSB0
- Exemple de connexion au périphérique série avec Minicom
minicom --device /dev/tty.usbmodem497A0F6739561
- Une fois connecté, si l’on tape
1 au clavier, la LED verte de la carte Nucleo s’allume
Différences logicielles entre l’approche STM32 et l’approche Linux
- Le résultat de la mise en pratique est un périphérique USB serial port reconnu par les systèmes d’exploitation grand public
- L’approche fondée sur STM32CubeIDE présente plusieurs inconvénients du point de vue de l’ingénierie logicielle
- il faut cliquer dans des menus d’interface pour générer beaucoup de boilerplate
- on n’utilise pas une bibliothèque paramétrable souplement en code comme
InitUsbDevice(UsbClass.CDC)
- le code généré peut être fortement couplé au code utilisateur, ce qui complique la revue de code
- il n’est pas clair comment mettre à jour le boilerplate lorsqu’une nouvelle version sort
- la configuration actuelle reste fortement liée à l’écosystème STM32
- La façon dont Linux se comporte comme un périphérique USB est jugée plus propre
- les API Linux sont plus robustes et standardisées
- l’interaction peut s’appuyer sur des pseudo-fichiers et des appels système
- l’espace utilisateur et l’espace noyau sont séparés
- on peut voir Linux comme une couche HAL
- En revanche, si l’on a besoin d’un périphérique USB léger, peu coûteux et facile à produire, un SoC Linux peut être trop lourd et excessif pour de nombreux cas d’usage
- Cela conduit à la conclusion qu’un framework plus portable et moins opinionated serait souhaitable pour le développement de périphériques USB bare metal
1 commentaires
Commentaires sur Hacker News
C’est un très bon article d’introduction à l’USB, mais il est très orienté vers l’utilisation des microcontrôleurs ST, et comparé aux approches récentes de l’écosystème ESP32, où l’on peut connecter et utiliser des périphériques USB plus facilement, il semble y avoir bien plus d’étapes et une chaîne d’outils beaucoup plus lourde
Par ailleurs, du point de vue de quelqu’un qui a déjà fabriqué plusieurs cartes USB, les paires différentielles ne sont pas vraiment un sujet dont les débutants doivent trop s’inquiéter, cela devient surtout important pour le haut débit
Comme les circuits intégrés de contrôleur USB couramment utilisés avec Arduino/ESP gèrent déjà beaucoup de détails, faire faire en plus les calculs à quelqu’un qui construit son premier périphérique paraît un peu excessif
C’était un projet rapide et amusant pour un débutant, et la partie la plus pénible a été de tailler le boîtier en bois
En pratique, je n’ai presque jamais fait les calculs moi-même, et le logiciel ECAD que j’utilise fournit des outils de routage de paires différentielles et d’analyse de signal d’impédance
Malgré tout, si l’on garde les pistes très courtes, cela ne pose généralement pas de gros problème
En général, je n’ai utilisé que des microcontrôleurs avec USB intégré
J’ai effectué des tests de conformité USB il y a longtemps, et l’un des problèmes que je voyais souvent concernait le test de courant d’appel
En gros, c’était quand il y avait trop de condensateurs de découplage sur le 5V, et j’ai l’impression que l’article ne traite pas ce point
Il est facile de se concentrer sur la conception numérique haut débit, mais en conformité, ce sont parfois les aspects les moins glamour qui posent problème
Je ne sais pas comment cela fonctionne dans les normes les plus récentes, mais il semble que ce test existe toujours, et l’article lui-même était bon
https://compliance.usb.org/index.asp?UpdateFile=Electrical#:...
On peut sûrement fabriquer un limiteur de courant avec quelques transistors, mais j’imagine qu’il existe de meilleures solutions intégrées avec des fonctions comme la protection thermique
Pour compléter l’article à propos de l’USB-C, il faut connecter les broches CC aux résistances appropriées
Sinon, il y a de fortes chances que cela ne fonctionne pas
Et le routage différentiel ainsi que l’impédance ne sont pas un si gros sujet que ça en USB 2.0
Il suffit d’avoir des pistes de longueur similaire, de les router directement de manière raisonnable et, si possible, de les garder proches l’une de l’autre
Pas besoin de se préoccuper excessivement de l’ajustement fin des longueurs et largeurs de piste, du contrôle d’impédance ou des bonnes pratiques RF : il suffit de connecter le net
Beaucoup de MCU exigent aussi des résistances en série entre les broches USB PHY et le connecteur
Il n’y aura probablement pas à refaire toute la carte parce qu’il fallait utiliser des pistes de 20mil au lieu de 24mil, mais même si la mise en page des paires différentielles USB 2.0 reste assez tolérante, c’est un bon exercice d’essayer de la faire correctement
Si des composants difficiles à souder comme les processeurs ARM vous inquiètent, il n’est pas nécessaire d’utiliser des composants aussi gros
Les STM32 sont bien quand on a besoin de performances, mais pour de petites tâches, un contrôleur plus modeste peut être préférable
Par exemple, VUSB est une bibliothèque de bit-banging USB sur de petits microcontrôleurs Atmel : https://www.obdev.at/products/vusb/index.html
Il existe aussi le schéma d’une carte d’exemple utilisée pour enseigner à des étudiants la programmation de modules du noyau Linux : https://gitlab.cs.fau.de/i4/passt/passtboard-v2
Le firmware est ici : http://www.poempelfox.de/ds1820tousb/ et https://gitlab.cs.fau.de/i4/passt/ds1820tousb
Si vous préférez la programmation façon Arduino, il existe aussi de nombreuses cartes qui peuvent se comporter comme un périphérique USB avec seulement quelques lignes de bibliothèques incluses, par exemple https://www.az-delivery.de/en/products/digispark-board
Par exemple, la NUCLEO-F429ZI : https://www.st.com/en/evaluation-tools/nucleo-f429zi.html
Elle est très proche de la NUCLEO-F103RB utilisée par l’auteur, mais en plus du connecteur USB supérieur pour le programmateur/débogueur intégré, elle possède en bas un connecteur USB relié directement au microcontrôleur
On peut aussi télécharger le schéma de la carte si l’on veut voir des choix comme la protection ESD
C’est un TSSOP-20 avec USB matériel intégré, pour un prix unitaire d’environ 0,81 $
En revanche, le logiciel peut être un peu plus délicat
https://www.wch-ic.com/products/CH32V203.html?
J’ai déjà essayé d’écrire du code USB bare metal sur un MCU, et comparé à un protocole numérique simple comme SPI ou I2C, c’était assez déroutant
La couche physique et la couche liaison de données ne sont pas tellement plus complexes que CAN, mais juste après, on se retrouve face au mur des descripteurs, des endpoints et de la configuration des pilotes
L’USB a été conçu dès le départ comme un écosystème plug-and-play pour PC, et cela se voit clairement
Si possible, il vaut clairement mieux utiliser au maximum les logiciels fournis par le fabricant
De mémoire, pour un débit élevé, il vaut mieux utiliser des transferts bulk et éviter complètement les transferts isochrones
L’USB est un protocole maître/esclave, donc si vous n’obtenez pas le débit maximal, la cause vient souvent de l’hôte, c’est-à-dire le PC
Si la licence LGPL correspond à vos contraintes, libusb est plutôt facile à utiliser
Si vous n’utilisez pas de pilote constructeur, un analyseur matériel du protocole USB est vraiment très utile, et USB in a NutShell est une bonne ressource web pour comprendre le protocole : https://www.beyondlogic.org/usbnutshell/usb1.shtml
Puisqu’on parle d’USB sur STM32, je voudrais demander si quelqu’un sait comment prendre en charge la réception de plus de 64 octets dans une trame
Pour l’instant, je traite simplement des trames de 64 octets en boucle dans le logiciel, mais je sais qu’il est possible de faire plus grand
Il me semble que cela allait jusqu’à 1 Mo au maximum
Le problème, c’est que dans le Reference Manual, ces réglages apparaissent comme une sorte de pseudo-registres plutôt que comme des registres ordinaires
Je me demande s’il existe une solution de contournement simple
Il s’agit du périphérique USB, pas de l’OTG
Je ne sais pas de quel STM32 il s’agit, mais le stm32g4 ne prend en charge que le full-speed
J’utilise surtout l’ESP32, mais il existe une méthode de bidouille bon marché adaptée à plusieurs projets
Si vous avez besoin d’un contrôleur personnalisé très rapide et facile à réaliser, cela peut valoir le coup de récupérer la carte de contrôle d’un clavier USB mis au rebut
Au lieu de souder, on peut fixer des fils aux contacts avec de la colle conductrice, puis les maintenir solidement avec un pistolet à colle
J’ai déjà fabriqué ainsi un contrôleur de jeu à un seul bouton, basé sur un bouton d’arcade qui envoie l’entrée de la barre d’espace ; c’était peu coûteux mais assez robuste, tout le debounce, etc., est déjà géré, et aucun code n’est nécessaire
Je me demande s’il existe des cartes de développement avec prise en charge de l’USB 3
J’essaie de prototyper un sink pour moniteur USB-C, mais j’ai du mal à trouver une carte assez performante pour recevoir du DisplayPort via USB
À condition bien sûr de pouvoir déjà recevoir le signal DisplayPort ou de le router vers un moniteur externe
Il suffit d’implémenter un périphérique USB Billboard et de signaler le bon mode alternatif
Pour le fonctionnement lui-même, c’est optionnel, mais de mémoire la spécification l’exige
Ensuite, le signal DisplayPort sortira sur la prise USB-C, et il suffira de relier les bonnes lignes AUX au connecteur DisplayPort
Le prix semble aussi raisonnable
Quand j’ai cherché quelque chose de similaire il y a deux ans, c’était un ordre de grandeur plus cher
J’ai un Raspberry Pi branché sur un PC et j’utilise https://github.com/xairy/raw-gadget pour créer un périphérique USB virtuel
En ce moment, je m’en sers pour émuler un appareil photo MTP afin de tromper un logiciel propriétaire
J’ai déjà prototypé plusieurs périphériques USB avec un Raspberry Pi Zero et l’USB composite du noyau Linux
Au minimum, il a été assez facile de faire fonctionner le stockage de masse et le port série
Il faut une sorte de script shell pour initialiser le module composite du noyau sur le Raspberry Pi, et on peut trouver le code passe-partout dans la documentation du noyau
pikvm est aussi un projet intéressant
Une fois connecté à un PC, l’USB ne fonctionne pas seulement comme un clavier et une souris, mais peut aussi devenir un lecteur USB utilisable au démarrage du système
C’est une approche assez intéressante pour les installations de logiciels