- Fasst den Ablauf zusammen, den man beim ersten Bau eines USB-Geräts braucht – von physischer Verdrahtung über PCB, USB-2.0-Geschwindigkeiten, Protokollschichten bis hin zu einer STM32-Praxisübung
- Die Grundverbindung von USB 2.0 besteht aus vier Leitungen: +5V, GND, D+, D-;
D+ und D- übertragen als differentielles Paar gemeinsam 1 Bit
- Auf dem PCB sind bei differentiellen Paaren Längenabgleich, nahe Führung und Impedanz wichtig, aber ein 12-Mbit/s-Full-Speed-Prototyp kann relativ tolerant funktionieren
- Die Übung konfiguriert auf dem NUCLEO-F103RB
PA12 als USB_DP und PA11 als USB_DM, baut ein USB-Serial-Gerät, das als virtueller CDC-COM-Port erkannt wird, und schaltet per Eingabe von 1 eine LED ein
- Der Ansatz mit STM32CubeIDE bringt UI-basierte Codegenerierung und Boilerplate-Kopplung als Belastung mit sich; eine Linux-basierte USB-Geräteimplementierung kann standardisiertere APIs und eine stärker getrennte Struktur bieten
Umfang beim ersten Implementieren von USB
- Ziel ist es, ein USB-Gerät, das beim Einstecken in einen Computer erkannt wird, in der einfachsten Form komplett umzusetzen
- Mit Gerät ist hier ein Peripheriegerät gemeint, das die Funktionen eines Computers erweitert; der Computer wird Host genannt
- Der Umfang reicht von der physischen Verbindung zwischen USB-Gerät und Host bis zu einer einfachen Anwendung, die auf dem Host mit dem USB-Gerät interagiert
- Es ist weniger eine autoritative Erläuterung der Spezifikation, sondern eher ein einfachstes E2E-Projekt zum Bau eines USB-Geräts plus Index zu Referenzmaterial
Grundkonzepte von USB
- USB ist ein Industriestandard, der Datenaustausch und Stromversorgung zwischen verschiedenen elektronischen Geräten ermöglicht
- USB ist ein serieller Bus, bei dem Bits nicht parallel, sondern nacheinander über den Bus übertragen werden
- Moderne Busse sind überwiegend seriell; USB lässt sich als Verfahren verstehen, bei dem Bits seriell zwischen Host und Device ausgetauscht werden
- USB umfasst nicht nur eine physische Spezifikation, um zwei Geräte zu verbinden und Bits auszutauschen, sondern auch ein Kommunikationsprotokoll für Datenübertragung und Stromversorgung
- Der Schwerpunkt der Erklärung liegt auf USB 2.0
USB-Verdrahtung und differentielles Paar
- Eine übliche USB-2.0-Verbindung lässt sich mit mindestens vier Leitungen beschreiben
+5 V: Leitung, über die der Host das Device mit Strom versorgt
D-, D+: zwei Leitungen, die gemeinsam als differentielles Paar arbeiten und 1 Bit übertragen
GND: Masse
- Manche Verbindungen können zusätzliche Pins wie
ID haben, die Übung behandelt aber nur die vier oben genannten Leitungen
-
Hinweis zu USB-C
- Auch USB-C verwendet intern differentielle Paare, unterscheidet sich aber von der allgemeinen USB-2.0-Erklärung unter anderem dadurch, dass es auch bei umgedrehter Steckrichtung funktionieren muss
- Ein USB-C-Stecker legt weder Geschwindigkeit noch USB-Version fest
- Auch ein USB-C-Gerät kann USB 2.0 sein oder ein moderneres USB-3.0-Gerät
- In der weiteren Übung wird USB-C nicht weiter behandelt
-
Wie ein differentielles Paar Rauschen reduziert
- Eine einzelne Leitung stellt den Bitwert dar, indem ihre Spannung mit
GND verglichen wird; bei langen kabelgebundenen Verbindungen kann der Einfluss von Rauschen jedoch größer werden
- Ein differentielles Paar verwendet zwei Leitungen: Eine Leitung führt
V, die andere -V
- Die Empfangsseite betrachtet die Spannungsdifferenz zwischen den beiden Leitungen
- Vereinfacht man, dass auf beide Leitungen dasselbe Rauschen
Vn addiert wird, ergibt sich (V + Vn) - (-V + Vn) = 2V, sodass sich das Rauschen aufhebt
- Dieses Modell ist eine stark vereinfachte Erklärung; für ein tieferes Verständnis differentieller Paare kann man das Altium-YouTube-Video von Zach Peterson heranziehen
- Video 15
Umgang mit USB auf dem PCB
- Wer keine eigene Hardware baut und ein Entwicklungsboard verwendet, kann den PCB-Abschnitt überspringen; die Grundprinzipien beim Platzieren eines USB-Steckers auf einem PCB sind aber nützlich
- Library-Bauteile für USB-Stecker enthalten die zuvor genannten Pins, und das differentielle Paar muss zu den entsprechenden benachbarten Pins des Mikrocontrollers oder SoC geroutet werden
- Die Grundprinzipien für das Routing differentieller Paare sind drei Punkte
- Die Länge der Leiterbahn von
D+ zum Plus-Pin des Chips und die Länge der anderen Leiterbahn müssen angeglichen werden
- Die beiden Leiterbahnen werden sehr nah beieinander geführt
- Die zum Signal passende Impedanz wird berücksichtigt
- Da beide Leitungen durch nahezu dieselbe Umgebung geführt werden, kann man im zuvor genannten einfachen Modell annehmen, dass das Rauschen auf beiden Leitungen gleich ist
- Die Impedanzberechnung kann man erledigen, indem man Zielimpedanz, Abstand zur Ground Plane, Abstand zwischen den differentiellen Leitungen usw. in den Rechner des Herstellers eingibt und daraus die notwendige Leiterbahnbreite ermittelt
- Videos von Zach Peterson zu differentiellen Paaren und USB-Routing sind als Referenzmaterial enthalten
Geschwindigkeiten von USB 2.0
- USB 2.0 bedeutet nicht, dass die Geschwindigkeit auf einen einzigen Wert festgelegt ist
- USB 2.0 kann zum Beispiel zwei Geschwindigkeitsstufen haben
- Full Speed: 12 Mbit/s
- High Speed: 480 Mbit/s
- Device und Host müssen beim Verbinden entscheiden, welche Geschwindigkeit verwendet wird
- Um High Speed auf einem PCB stabil zu erreichen, ist Impedanzanpassung wichtig
- Für einen einfachen Prototyp kann 12 Mbit/s Full Speed ausreichend sein; wenn die Leiterbahnen vom USB-Stecker zum Chip kurz sind, kann man bei Leiterbahnbreite und Ähnlichem relativ tolerant sein
Protokoll- und Softwareschichten
- Bei USB muss man nicht nur die Hardware, sondern auch die Softwareschichten auf Host- und Device-Seite berücksichtigen
- Ein etwa 45-minütiges Video, das USB aus Linux-Perspektive erklärt, ist als empfohlene Referenz enthalten
- Dieses Video behandelt USB-Frames, Endpoints, Configuration und die Art, wie ein einzelnes Device mehrere USB-Funktionen ausführen kann
- Wichtig ist die Perspektive, USB als Netzwerk von Geräten zu betrachten
- Da es unrealistisch ist, dass ein Host-Betriebssystem für jedes USB-Gerät einen eigenen Treiber individuell implementiert, erkennen Betriebssysteme mehrere USB Device Classes
- Mass Storage Device
- Serial Device
- und viele weitere Klassen
- Das Übungsgerät verhält sich aus Sicht des Hosts als Serial-Port-Gerät
Ein USB-Serial-Port-Gerät mit STM32 bauen
- Das Übungsgerät ist ein einfaches USB-Gerät, das eine LED einschaltet, wenn es vom Host eine Anfrage erhält
- Der Host erkennt dieses Gerät als Serial Port Device
- Die Implementierungsweise lässt sich grob in zwei Varianten unterteilen
- Einen Mikrocontroller mit USB-Unterstützung verwenden
- Ein SoC verwenden, auf dem Linux laufen kann, sodass der Kernel einen Großteil der Arbeit übernimmt
- Aus Gründen der Einfachheit nutzt die Übung den Mikrocontroller-Ansatz
-
NUCLEO-F103RB-Board
- Als Entwicklungsboard wird das NUCLEO-F103RB verwendet
- In den USA kann man es bei Anbietern wie Digikey für etwas mehr als 10 Dollar kaufen
- Dieses Board besteht aus zwei verbundenen Bereichen
- Der kleinere Bereich ist der Programmer-Teil, der größere Bereich ist der eigentliche Main-MCU-Bereich, der programmiert wird
- Die standardmäßige USB-Verbindung des Boards ist nicht mit der Main MCU verbunden, sondern mit der MCU auf der Programmer-Seite
- Dieser Programmer nutzt das ST-LINK-Protokoll, um USB-Nachrichten mit dem Computer auszutauschen, und programmiert anschließend die Main MCU
- Wenn man ein eigenes STM32-basiertes PCB baut, könnte man nur einen einzigen Chip auf dem Board haben, ihn über USB programmieren und danach denselben USB-Port für die Gerätelogik verwenden
- Als Referenzmaterial zu STM32-basierten PCBs und USB-Programmierung sind Videos von Phil’s Lab enthalten
- Video 19
- Video 20
Verdrahtung des tatsächlichen USB-Ports
- Da die standardmäßige USB-Verbindung des Nucleo-Boards nicht mit der Main MCU verbunden ist, muss der USB-Port der Main MCU direkt mit dem Host verbunden werden
- Nach dem Aufspielen des Programms wird der ST-LINK-Programmer vom Computer getrennt, und die Main MCU wird über den tatsächlichen USB-Port mit Strom versorgt
- Für das Schreiben der Software wird STM32CubeIDE verwendet, für das Aufspielen auf das Board STM32CubeProgrammer
-
Benötigte Pins und Einstellungen
- Für den Bau eines USB-2.0-Geräts werden vier Pins benötigt
- 5-V-Stromversorgung vom Host
GND
D+
D-
- Damit beim NUCLEO-F103RB die Stromversorgung über den USB-Port der Main MCU erfolgt, muss der Jumper
JP5 umgesteckt werden, sodass das Board mit external 5 V versorgt wird
- In STM32CubeIDE werden die Pins wie folgt konfiguriert
PA12 → USB_DP
PA11 → USB_DM
- Der Chip auf diesem Board benötigt für die USB-Verbindung einen externen 1,5-kΩ-Pull-up-Widerstand
- Der Widerstand zieht
PA12 auf 3,3 V hoch
- Diese Schaltung wird auf einem Breadboard aufgebaut
- Um ein MacBook Pro mit einzelnen Pins zu verbinden, wurde ein USB-Breakout-Kabel von Amazon verwendet; der Terminal Block wurde entfernt, um die USB-Pins freizulegen, und diese wurden per Jumper Wire mit dem Nucleo-Board und dem Breadboard verbunden
- Auch ohne strikten Längenabgleich des differentiellen Paars und ohne kontrollierte Impedanz war in dieser Übung eine Verbindung mit 12 Mbit/s möglich
USB-CDC-Software schreiben
- Wenn man in CubeIDE die USB-Pins konfiguriert, erscheint eine Meldung zu den Clock-Einstellungen; man kann eine Option wählen, bei der CubeIDE das automatisch behandelt
- Unter
Middleware and Software Packs in Pinout & Configuration wird USB_DEVICE konfiguriert
- Der für die Übung wichtige Modus ist folgender
Communication Device Class (Virtual Port Com)
- Mit dieser Einstellung verhält sich das Nucleo-Board aus Host-Sicht als CDC Serial Port-Gerät
- Der Host kann anhand dieser Klasseninformation einen passenden Treiber für die Kommunikation mit dem Custom Device einrichten
- CubeIDE generiert C-Code; in
main.c ist der folgende Initialisierungsaufruf enthalten
MX_USB_DEVICE_Init();
- Um die LED einzuschalten, wird in der Routine
CDC_Receive_FS der folgende Code ergänzt
/* USER CODE BEGIN 6 */
if (Buf[0] == '1') {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, 1);
}
- Dieser HAL-Aufruf schaltet die Onboard-LED ein, die mit Pin 5 von Port A verbunden ist
- Das Schritt-für-Schritt-Referenzvideo ist folgendes
Flashen und Ausführen
- Nach dem Build der ELF-Datei wird der Code mit CubeProgrammer auf das Board geladen
- Danach wird der Programmer getrennt und das Board wie oben beschrieben über external 5V verdrahtet
- Sobald das Board eingeschaltet ist, sollte es im Device Manager des Betriebssystems als Eintrag ähnlich einem COM Port oder Serial Port erscheinen
- Wenn man im Middleware-Menü
USB_DEVICE von CubeIDE den Wert Device Descriptor ändert, kann man den Custom Device Name festlegen, der im Device Manager des Betriebssystems angezeigt wird
- Unter Mac OS erscheint das neue Gerät unterhalb des
/dev-Dateisystems
- Beispielpfad:
/dev/tty.usbmodem497A0F6739561
- Unter Linux kann es mit einem Namen wie
/dev/ttyUSB0 erscheinen
- Ein Beispiel für die Verbindung mit dem Serial Device per Minicom ist folgendes
minicom --device /dev/tty.usbmodem497A0F6739561
- Gibt man nach dem Verbinden auf der Tastatur
1 ein, leuchtet die grüne LED des Nucleo-Boards auf
Softwareunterschiede zwischen STM32-Ansatz und Linux-Ansatz
- Das Ergebnis der Übung ist ein USB Serial Port Device, das von Mainstream-Betriebssystemen erkannt wird
- Der auf STM32CubeIDE basierende Ansatz hat aus Sicht des Software Engineering einige Unannehmlichkeiten
- Man muss über UI-Menüs viel Boilerplate erzeugen
- Es ist kein Ansatz, bei dem man im Code eine flexibel parametrisierte Library wie
InitUsbDevice(UsbClass.CDC) verwendet
- Der generierte Code ist stark mit Benutzercode gekoppelt, was Code Reviews erschweren kann
- Es ist nicht klar, wie Boilerplate aktualisiert werden soll, wenn eine neue Version erscheint
- Die aktuelle Konfiguration ist stark an das STM32-Ökosystem gebunden
- Die Art, wie Linux als USB Device agiert, wird als saubererer Ansatz bewertet
- Linux-APIs sind robuster und standardisierter
- Sie können auf der Interaktion mit Pseudo-Files und System Calls basieren
- User Space und Kernel Space sind getrennt
- Linux kann als HAL-Schicht betrachtet werden
- Wenn man jedoch ein leichtes, günstiges und einfach herstellbares USB-Gerät braucht, kann ein Linux-SoC zu schwergewichtig sein und ist in vielen Anwendungsfällen überdimensioniert
- Daraus ergibt sich das Fazit, dass für die Bare-Metal-Entwicklung von USB-Geräten ein portableres und weniger meinungsstarkes Framework wünschenswert wäre
1 Kommentare
Kommentare auf Hacker News
Als USB-Einstieg ist der Artikel sehr gut, aber er ist stark auf ST-Mikrocontroller ausgerichtet; verglichen mit aktuellen Ökosystemen wie ESP32, bei denen sich USB-Geräte einfach anbinden lassen, wirken die Schritte und die Toolchain deutlich umfangreicher.
Außerdem sind differenzielle Paare aus Sicht von jemandem, der selbst mehrere USB-Boards gebaut hat, nichts, worüber sich Anfänger allzu große Sorgen machen müssen; wichtig werden sie vor allem bei High-Speed-Arbeiten.
Da die bei Arduino/ESP häufig verwendeten USB-Controller-ICs viele Details übernehmen, wirkt es übertrieben, jemanden beim ersten Gerät auch noch Berechnungen machen zu lassen.
Für Anfänger war es ein schnelles und unterhaltsames Projekt, und der lästigste Teil war das Fräsen des Holzgehäuses.
Tatsächliche Berechnungen habe ich kaum je gemacht, und die von mir verwendete ECAD-Software bietet Werkzeuge für Differential-Pair-Routing und Impedanz-Signalanalyse.
Wenn man die Leiterbahnen sehr kurz hält, ist das in der Regel aber kein großes Problem.
Normalerweise habe ich nur Mikrocontroller mit integriertem USB verwendet.
Vor langer Zeit habe ich USB-Compliance-Tests gemacht, und einer der Punkte, bei denen ich häufig Probleme gesehen habe, war der Einschaltstrom-Test.
Im Grunde ging es darum, dass auf der 5-V-Seite zu viele Bypass-Kondensatoren saßen; im Artikel habe ich diesen Punkt wohl nicht gesehen.
Man konzentriert sich leicht auf High-Speed-Digitaldesign, aber bei der Compliance sind es manchmal die weniger spektakulär wirkenden Dinge, die einen ausbremsen.
Ich weiß nicht, wie es in den aktuellen Standards läuft, aber den Test scheint es weiterhin zu geben, und der Artikel selbst war gut.
https://compliance.usb.org/index.asp?UpdateFile=Electrical#:...
Man kann mit ein paar Transistoren sicher einen Strombegrenzer bauen, aber es dürfte bessere integrierte Lösungen mit Funktionen wie Temperaturschutz geben.
Um den Artikel zu USB-C zu ergänzen: Die CC-Pins müssen mit den passenden Widerständen verbunden werden.
Andernfalls wird es sehr wahrscheinlich nicht funktionieren.
Und Differential-Routing und Impedanz sind bei USB 2.0 nicht so eine große Sache.
Man sollte die Leiterbahnlängen ähnlich halten, sie vernünftig direkt verbinden und sie möglichst beieinander führen.
Man muss sich nicht übermäßig um Feintuning von Länge und Leiterbahnbreite, Impedanzkontrolle oder RF-Best-Practices kümmern; einfach die Netze verbinden.
Viele MCUs brauchen außerdem Serienwiderstände zwischen den USB-PHY-Pins und dem Stecker.
Wahrscheinlich muss man kein Board neu machen, nur weil man 20-mil- statt 24-mil-Leiterbahnen hätte verwenden sollen; aber auch wenn das Layout eines USB-2.0-Differentialpaares eher wenig einschüchternd ist, ist es eine gute Übung, es ordentlich zu machen.
Wenn Bauteile, die sich schwer löten lassen, etwa ARM-Prozessoren, Sorgen bereiten, muss man nicht unbedingt so große Bauteile verwenden.
STM32 ist gut, wenn man Leistung braucht, aber für kleine Aufgaben kann ein kleinerer Controller besser passen.
VUSB ist zum Beispiel eine Library, die USB auf kleinen Atmel-Mikrocontrollern per Bit-Banging umsetzt: https://www.obdev.at/products/vusb/index.html
Es gibt auch den Schaltplan eines Beispielboards, das ich verwendet habe, um Studierenden Linux-Kernelmodul-Programmierung beizubringen: https://gitlab.cs.fau.de/i4/passt/passtboard-v2
Die Firmware gibt es unter http://www.poempelfox.de/ds1820tousb/ und https://gitlab.cs.fau.de/i4/passt/ds1820tousb
Wenn man Arduino-artige Programmierung bevorzugt, gibt es außerdem viele Boards, die sich mit ein paar Zeilen Include-Library wie ein USB-Gerät nutzen lassen, zum Beispiel https://www.az-delivery.de/en/products/digispark-board.
Ein Beispiel ist das NUCLEO-F429ZI: https://www.st.com/en/evaluation-tools/nucleo-f429zi.html
Es ist dem vom Autor verwendeten NUCLEO-F103RB sehr ähnlich, hat aber neben dem oberen USB-Anschluss für den integrierten Programmer/Debugger unten zusätzlich einen USB-Anschluss, der direkt mit dem Mikrocontroller verbunden ist.
Wenn man sich etwa bei der Auswahl des ESD-Schutzes orientieren möchte, kann man auch den Schaltplan des Boards herunterladen.
Das ist ein TSSOP-20 mit Hardware-USB, und der Einzelstückpreis liegt bei etwa 0,81 US-Dollar.
Allerdings kann die Software-Seite etwas schwieriger sein.
https://www.wch-ic.com/products/CH32V203.html?
Ich habe auf einem MCU schon Bare-Metal-USB-Code geschrieben, und verglichen mit einfachen digitalen Protokollen wie SPI oder I2C war das ziemlich schockierend.
Die physikalische Schicht und die Data-Link-Schicht sind zwar nicht viel komplexer als bei CAN, aber direkt danach stößt man auf die Wand aus Deskriptoren, Endpoints und Treiberkonfiguration.
USB wurde von Anfang an als Plug-and-Play-Ökosystem für PCs entworfen, und das merkt man deutlich.
Wenn möglich, ist es definitiv der bessere Weg, die vom Hersteller bereitgestellte Software so weit wie möglich zu nutzen.
Ein vage erinnerter Tipp: Für hohen Durchsatz Bulk-Transfers verwenden und isochrone Transfers am besten gar nicht erst ansehen.
USB ist ein Master/Slave-Protokoll; wenn der maximale Durchsatz nicht erreicht wird, liegt die Ursache meist auf der Host-Seite, also beim PC.
Wenn die LGPL-Lizenz zu den Anforderungen passt, ist libusb ziemlich einfach zu verwenden.
Wenn man keinen Hersteller-Treiber nutzt, ist ein Hardware-USB-Protokollanalysator wirklich nützlich, und USB in a NutShell ist eine brauchbare Web-Referenz zum Verständnis des Protokolls: https://www.beyondlogic.org/usbnutshell/usb1.shtml
Da wir gerade bei USB auf STM32 sind: Weiß jemand, wie man in einem Frame Empfang von mehr als 64 Byte unterstützt?
Derzeit verarbeite ich in Software wiederholt 64-Byte-Frames, aber ich weiß, dass größere Werte möglich sind.
Ich glaube, es waren bis zu 1 MB.
Das Problem ist, dass diese Einstellungen im Reference Manual nicht als normale Register, sondern als eine Art Pseudo-Register beschrieben sind.
Ich frage mich, ob es einen einfachen Workaround gibt.
Bezogen auf ein USB-Peripheral ohne OTG.
Ich weiß nicht, welchen STM32 du meinst, aber der stm32g4 unterstützt nur Full-Speed.
Ich nutze hauptsächlich ESP32, aber es gibt einen günstigen Hack, der für viele Projekte passt.
Wenn man sehr schnell und einfach einen Custom-Controller braucht, kann man auch in Betracht ziehen, die Controller-Platine einer ausrangierten USB-Tastatur auszubauen und zu verwenden.
Statt zu löten, kann man die Drähte mit leitfähigem Kleber an den Kontakten befestigen und sie mit einer Heißklebepistole stabil fixieren.
Ich habe so einen günstigen, aber ziemlich robusten 1-Button-Game-Controller auf Basis eines Arcade-Buttons gebaut, der Spacebar-Eingaben sendet; Debouncing und Ähnliches wird alles erledigt, und Code ist nicht nötig.
Ich frage mich, ob es ein Entwicklungsboard mit USB-3-Unterstützung gibt.
Ich möchte einen USB-C-Monitor-Sink prototypisieren, finde aber nur schwer ein Board, das leistungsfähig genug ist, um DisplayPort über USB zu empfangen.
Vorausgesetzt, man kann bereits DisplayPort-Signale empfangen oder an einen externen Monitor routen.
Man implementiert ein USB-Billboard-Device und signalisiert den richtigen Alternate Mode.
Für den Betrieb selbst ist das optional, aber soweit ich mich erinnere laut Spezifikation erforderlich.
Dann liegt am USB-C-Stecker ein DisplayPort-Signal an, und man verbindet die passenden AUX-Leitungen mit dem DisplayPort-Connector.
Der Preis wirkt auch vernünftig.
Als ich vor zwei Jahren etwas Ähnliches gesucht habe, war es um eine Größenordnung teurer.
Ich habe einen Raspberry Pi am PC angeschlossen und erstelle mit https://github.com/xairy/raw-gadget virtuelle USB-Geräte.
Im Moment nutze ich das, um eine MTP-Kamera zu emulieren und proprietäre Software auszutricksen.
Ich habe mit dem Raspberry Pi Zero und dem Composite USB des Linux-Kernels ein paar USB-Geräte prototypisiert.
Zumindest Massenspeicher und serielle Geräte ließen sich recht einfach zum Laufen bringen.
Man braucht so etwas wie ein Shell-Skript, das auf dem Raspberry Pi das Composite-Kernelmodul initialisiert, und Boilerplate-Code findet man in der Kernel-Dokumentation.
pikvm ist ebenfalls ein interessantes Projekt.
Wenn man es an einen PC anschließt, kann die USB-Verbindung nicht nur wie Tastatur und Maus funktionieren, sondern auch als USB-Laufwerk dienen, von dem das System booten kann.
Eine ziemlich interessante Methode für Installationsarbeiten.