USB für Softwareentwickler: Einführung in das Schreiben von USB-Treibern im User Space
(werwolv.net)- Die Entwicklung von USB-Treibern gilt oft als Arbeit auf Kernel-Ebene, lässt sich in der Praxis jedoch auch im User Space mit einer ähnlichen Komplexität wie Socket-Programmierung umsetzen
- Mit libusb lassen sich Geräteerkennung, Control-Transfers sowie Senden und Empfangen von Daten vollständig ausführen, ohne Kernel-Code zu schreiben
- USB-Kommunikation besteht aus den vier Transferarten Control, Bulk, Interrupt, Isochronous sowie den Richtungen IN/OUT; jeder Endpunkt arbeitet dabei als unidirektionaler Kanal
- Am Beispiel des Fastboot-Protokolls von Android-Geräten wird per Code gezeigt, wie Befehle und Antworten über Bulk-Endpunkte ausgetauscht werden
- Auch im User Space lässt sich ein vollständiger USB-Treiber implementieren, und alle USB-Protokolle teilen dieselbe Grundstruktur
Einführung
- Treiber für USB-Geräte wirken oft schwierig, weil man annimmt, dafür Kernel-Code anfassen zu müssen, tatsächlich ist die Komplexität aber vergleichbar mit Anwendungen, die Sockets verwenden
- Auch Entwickler mit wenig Hardware-Erfahrung können lernen, wie man USB im User Space behandelt
- Es gibt zwar Materialien zu den Details der USB-Funktionsweise, für Einsteiger sind sie jedoch oft schwer zugänglich
- Für die Arbeit mit USB ist kein Wissen auf dem Niveau eingebetteter Systeme erforderlich; man kann ähnlich wie bei Netzwerk-Sockets an die Sache herangehen
USB-Gerät
- Als Beispiel wird ein Android-Smartphone im Bootloader-Modus verwendet
- Es ist leicht verfügbar, das Protokoll ist einfach, und da das OS keinen Standardtreiber dafür hat, eignet es sich gut für Experimente
- Der Einstieg in den Bootloader-Modus unterscheidet sich je nach Gerät, ist aber meist über eine Kombination aus Power- und Lautstärketasten möglich
Manuelle Geräteerkennung
- Enumeration ist der Prozess, bei dem der Host Geräteinformationen anfordert, damit sich das Gerät identifiziert; das geschieht beim Anschließen automatisch
- Für Standardgeräte werden Treiber anhand der USB-Klasse automatisch geladen, während herstellerspezifische Geräte über
VID(Vendor ID) undPID(Product ID) identifiziert werden - Unter Linux lassen sich Geräteinformationen mit dem Befehl
lsusbprüfen- Beispiel:
ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot) 18d1ist die VID von Google,4ee0die PID des Nexus/Pixel-Bootloaders
- Beispiel:
- Mit
lsusb -tlassen sich Klasse und Treiberstatus prüfen- Die Anzeige
Class=Vendor Specific Class,Driver=[none]bedeutet, dass das OS keinen Treiber geladen hat
- Die Anzeige
- Unter Windows lassen sich dieselben Informationen über den Geräte-Manager oder USB Device Tree Viewer einsehen
Geräteerkennung mit libusb
- Mit der Bibliothek libusb kann man im User Space mit USB-Geräten kommunizieren, ohne Kernel-Code zu schreiben
- Über
libusb_hotplug_register_callback()lässt sich ein Callback registrieren, der ausgeführt wird, wenn ein Gerät mit einer bestimmtenVID:PID-Kombination angeschlossen wird - Wenn nach dem Start des Programms ein Gerät angeschlossen wird, erscheint die Meldung
"Device plugged in!" - Unter Linux funktioniert das standardmäßig; falls nötig kann mit
libusb_detach_kernel_driver()ein Kernel-Treiber gelöst werden - Unter Windows wird der Treiber
Winusb.sysbenötigt; fehlt er, kann er mit dem Tool Zadig manuell ersetzt werden
Kommunikation mit dem Gerät
- Die erste Kommunikation mit einem USB-Gerät erfolgt über den Control-Endpunkt (Adresse 0x00)
- Mit
libusb_control_transfer()wird eine Standardanfrage (GET_STATUS) gesendet, um den Gerätestatus zu lesen- Beispielantwort:
01 00→ Das erste Byte bedeutet Self-Powered, das zweite keine Unterstützung für Remote Wakeup
- Beispielantwort:
- Danach kann mit einer GET_DESCRIPTOR-Anfrage der Gerätedeskriptor abgerufen werden
- Die zurückgegebenen Daten enthalten Geräteinformationen wie
idVendor,idProduct,bDeviceClassusw.
- Die zurückgegebenen Daten enthalten Geräteinformationen wie
- Mit
lsusb -vlassen sich alle Deskriptoren (Gerät, Konfiguration, Interface, Endpunkt usw.) im Detail prüfen- Beispiel: Das Interface
Android Fastbootbesitzt die Endpunkte Bulk IN(0x81) und Bulk OUT(0x02)
- Beispiel: Das Interface
Endpunkte
- Endpunkte sind ein Konzept ähnlich zu Netzwerk-Ports: Kanäle, über die ein Gerät Daten sendet und empfängt
- In den Deskriptoren sind Typ und Richtung jedes Endpunkts definiert
-
Control-Transferart
- Jedes Gerät hat genau einen solchen Endpunkt, und seine Adresse ist immer
0x00 - Er wird für die Initialkonfiguration und das Abfragen von Geräteinformationen verwendet
- Er gehört nicht zu einem Interface, sondern ist Teil des Geräts selbst
- Jedes Gerät hat genau einen solchen Endpunkt, und seine Adresse ist immer
-
Bulk-Transferart
- Wird für große, nicht echtzeitkritische Datenübertragungen verwendet
- Beispiele: Mass Storage, CDC-ACM (seriell), RNDIS (Ethernet)
- Die Bandbreite ist hoch, aber die Priorität niedrig
-
Interrupt-Transferart
- Wird für kleine Datenmengen mit geringer Latenz verwendet
- Tastaturen, Mäuse usw. pollen damit Tasten- und Eingabeereignisse schnell ab
- Es handelt sich nicht um echte Hardware-Interrupts; der Host fragt periodisch an
-
Isochronous-Transferart
- Wird für zeitkritische Daten mit hohem Durchsatz verwendet, etwa Audio- oder Video-Streaming
- Wenn Verzögerungen auftreten, zeigt sich die Qualitätsminderung sofort
- In libusb wird dies asynchron verarbeitet
-
IN-/OUT-Richtung
- USB ist hostzentriert aufgebaut; ein Gerät sendet keine Daten, bevor es eine Anfrage erhalten hat
IN: Richtung, in der der Host Daten empfängtOUT: Richtung, in der der Host Daten sendet- Wenn das höchstwertige Bit (MSB) der Endpunktadresse
1ist, handelt es sich um IN, bei0um OUT - Es können bis zu 127 benutzerdefinierte Endpunkte verwendet werden (
0x00ist ausschließlich für Control reserviert) - Endpunkte sind unidirektional und werden wie beim Fastboot-Interface als IN/OUT-Paar aufgebaut
Fastboot-Protokoll
- Fastboot ist ein Android-Bootloader-Kommunikationsprotokoll, bei dem Befehlsstrings gesendet und ein 4-Byte-Statuscode plus Daten empfangen werden
- Beispiel:
Host: "getvar:version"→Client: "OKAY0.4"Host: "getvar:nonexistant"→Client: "OKAY"
- Beispiel:
- Beispielcode zum Senden eines Fastboot-Befehls mit libusb
- Interface 0 wird mit
libusb_claim_interface()beansprucht - Der Befehl
"getvar:version"wird an den Endpunkt Bulk OUT(0x02) gesendet - Die Antwort wird vom Endpunkt Bulk IN(0x81) empfangen
- Beispielausgabe:
Request: getvar:version Response: OKAY0.4 OKAYist der Erfolgsstatus,0.4die Fastboot-Version
- Interface 0 wird mit
Fazit
- Es ist möglich, einen vollständigen USB-Treiber im User Space zu implementieren, ohne Kernel-Code zu schreiben
- Alle USB-Treiber folgen denselben Grundprinzipien; nur das Protokoll unterscheidet sich
- Auch komplexe Protokolle wie MTP haben dieselbe Grundstruktur und lassen sich mit einem ähnlichen Konzept wie Socket-Kommunikation angehen
1 Kommentare
Hacker-News-Kommentare
Das Timing ist wirklich perfekt. Ich werde bald ein MOTU MIDI Express XT bei meinem lokalen Guitar Center abholen
Es handelt sich um Gebrauchtware, die gesetzlich für eine gewisse Zeit zurückgehalten werden muss, daher warte ich noch. Das Problem ist, dass dieses Gerät nicht standardmäßiges MIDI-over-USB verwendet, sondern ein proprietäres Protokoll, sodass ich es auf meinen Systemen wie Linux, OpenBSD oder Haiku nicht direkt per USB nutzen kann
Im Moment ist das in Ordnung, weil ich nur einfaches Routing zwischen Synth-Modulen und Controllern brauche, aber es wäre schön, es auch auf dem PC zum Laufen zu bringen
Es gibt zwar einen vorhandenen Linux-Treiber, aber wie stabil er ist, ist unklar, und ob XT unterstützt wird, ist ebenfalls fraglich. Das Kernel-Panic-Problem wurde zwar behoben, aber es gibt noch offene Issues
Deshalb will ich selbst einen LibUSB-basierten User-Space-Treiber bauen. Wenn er MIDI-Ports bereitstellt und Routing-Tooling hinzufügt, wäre das ziemlich nützlich
Wenn man so etwas in Go ausprobieren möchte: Ich habe die Bibliothek go-usb erstellt, mit der USB-Zugriff ohne cgo möglich ist
Damit habe ich auch go-uvc für UVC-Geräte entwickelt
Ich setze gerade ebenfalls auf einem Macbook M3 ein usbip-System auf ähnliche Weise um
Allerdings gibt es unter aktuellem macOS Einschränkungen. Für USB-Geräte, die das System erkennt, kann man keinen LibUSB-basierten User-Space-Treiber bauen, außer man deaktiviert Sicherheitsfunktionen manuell
Bei diesem Ansatz übernimmt der USB-Treiber letztlich auch die Rolle von Anwendungscode. Er ist also eher Bibliothek + Programm als ein klassischer Treiber
Mich würde zum Beispiel interessieren, wie man ein USB-Ethernet-Gerät als Netzwerkadapter des Betriebssystems anbinden würde
Wenn ich diesen Artikel vor ein paar Jahren gelesen hätte, wäre das Reverse Engineering von Laptop-Funktionen viel einfacher gewesen. Besonders das Programm zur Steuerung der Tastatur-LEDs ist bis heute eines meiner Lieblingsprojekte
Das war wirklich eine nützliche Einführung. Mit Low-Level-Hardware-APIs zu arbeiten ist schwierig, aber lohnend. Dank der Abstraktionsschichten moderner Betriebssysteme ist es einfacher geworden, aber es bleibt wichtig zu verstehen, was darunter passiert
Der C++-Code sah seltsam aus. Ich habe noch nie eine Tastatur gesehen, auf der man das Pfeilsymbol direkt tippen kann
->. Das ist die Syntax für einen trailing return type in modernem C++"->". Die Schriftart rendert das lediglich als PfeilIch habe mich gefragt, ob USB-Geräte DMA unterstützen. Ob das nur über den Host möglich ist oder ob das Gerät direkt auf den Speicher zugreifen kann
Ich wollte früher einmal ein einfaches USB-Gerät bauen, aber es gab fast keine Informationen darüber, wie man Deskriptoren (descriptor) schreibt. Meist hieß es nur: „Such ein ähnliches Gerät, kopiere es und ändere ein bisschen etwas.“ Ich habe mich gefragt, ob USB wirklich ein so großartiger Standard ist
Wenn mich jemand bitten würde: „Schreib den USB-Gerätetreiber selbst“, würde ich das Gerät zurückgeben und zuerst prüfen, ob es sich nicht einfach über einen virtuellen COM-Port lösen lässt