24 Punkte von GN⁺ 20 일 전 | 1 Kommentare | Auf WhatsApp teilen
  • 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) und PID (Product ID) identifiziert werden
  • Unter Linux lassen sich Geräteinformationen mit dem Befehl lsusb prüfen
    • Beispiel: ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot)
    • 18d1 ist die VID von Google, 4ee0 die PID des Nexus/Pixel-Bootloaders
  • Mit lsusb -t lassen sich Klasse und Treiberstatus prüfen
    • Die Anzeige Class=Vendor Specific Class, Driver=[none] bedeutet, dass das OS keinen Treiber geladen hat
  • 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 bestimmten VID: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.sys benö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
  • Danach kann mit einer GET_DESCRIPTOR-Anfrage der Gerätedeskriptor abgerufen werden
    • Die zurückgegebenen Daten enthalten Geräteinformationen wie idVendor, idProduct, bDeviceClass usw.
  • Mit lsusb -v lassen sich alle Deskriptoren (Gerät, Konfiguration, Interface, Endpunkt usw.) im Detail prüfen
    • Beispiel: Das Interface Android Fastboot besitzt die Endpunkte Bulk IN(0x81) und Bulk OUT(0x02)

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
  • 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ängt
    • OUT: Richtung, in der der Host Daten sendet
    • Wenn das höchstwertige Bit (MSB) der Endpunktadresse 1 ist, handelt es sich um IN, bei 0 um OUT
    • Es können bis zu 127 benutzerdefinierte Endpunkte verwendet werden (0x00 ist 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"
  • 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
      
    • OKAY ist der Erfolgsstatus, 0.4 die Fastboot-Version

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

 
GN⁺ 20 일 전
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

    • Die Wartezeit bei Guitar Center dient nicht nur dazu zu prüfen, ob die Ware gestohlen ist. Rechtlich gelten ähnliche Vorgaben wie bei einem Pfandhaus (pawn shop): Der Verkauf ist für eine bestimmte Zeit verboten, damit der ursprüngliche Besitzer das Gerät zurückholen kann
    • Ich benutze dasselbe Gerät und habe den Treiber für AUR paketiert. Der Binary Blob funktionierte nicht, aber als einfacher MIDI-Router reicht es aus
  • 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

    • Für Rust empfehle ich nusb
  • 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

    • Das lässt sich etwas abmildern, weil man beim Driver-Override nur eine Schicht anpassen muss
  • 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

    • Standardisierte Geräte verwenden normalerweise USB/CDC/ECM oder RNDIS und werden automatisch erkannt. Der Zugriff aus dem User Space ist eher für nicht standardisierte Geräte nützlich. Unter Windows kann man das ohne Treibersignatur mit libusb portabel umsetzen
    • Unter Linux würde man ein tun/tap-Gerät anlegen, um zwischen User Space und Kernel zu kommunizieren, oder andere Subsysteme ebenfalls im User Space betreiben
  • 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 eine Ligatur einer Programmierschriftart. Wenn man es kopiert, erscheint tatsächlich ->. Das ist die Syntax für einen trailing return type in modernem C++
    • Manche Entwickler bevorzugen Schriftarten mit Ligaturen. Dabei werden zwei Zeichen zu einem Glyphen zusammengezogen
    • Wenn man eine Compose-Taste einrichtet, kann man mit jeder Tastatur „→“ eingeben
    • Im Grunde ist es einfach nur "->". Die Schriftart rendert das lediglich als Pfeil
  • Ich 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

    • USB-Geräte greifen nicht direkt auf den Host-Speicher zu wie bei PCIe oder FireWire. Stattdessen führt der XHCI-Controller DMA aus, und die meisten Gerätekontroller unterstützen DMA zwischen ihrem eigenen RAM und USB
    • Alle Übertragungen werden vom Host initiiert. Selbst wenn es so aussieht, als würde das Gerät zuerst Daten senden, fordert in Wirklichkeit der Host sie an. Direktes DMA wäre ein großes Sicherheitsrisiko
  • 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

    • Für mich waren Deskriptoren auch lange rätselhaft, aber irgendwann wurde mir klar, dass es letztlich feste binäre Strukturen sind. Wenn die Felder und Endpunkte passen, die die jeweilige USB-Klasse vorgibt, wird das Gerät erkannt
    • USB ist schon okay, aber elektrisch betrachtet sind USB 1/2 keine echten differentiellen Signale
    • Tutorial-Material gibt es kaum, aber für einen Standard aus einem Großkonzern ist es ziemlich vernünftig. Allerdings gibt es zu viele Optionen, sodass man eine Menge Spezifikationen lesen muss
  • 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