USB-Treiber ohne Kopfschmerzen: Von der Theorie zum Code mit libusb
Das Schreiben von Treibern für USB-Geräte schreckt Entwickler oft durch die Komplexität des Low-Level-Codes ab. Mit dem richtigen Toolkit wie libusb wird der Prozess jedoch so einfach wie Netzwerkprogrammierung mit Sockets. In diesem Artikel gehen wir Schritt für Schritt durch die Erstellung eines Treibers für ein USB-Gerät und verwenden ein Android-Gerät im Bootloader-Modus als Beispiel.
USB-Grundlagen: Was ein Entwickler wissen muss
Die USB-Spezifikation definiert einen Standardmechanismus zur Identifizierung von Geräten über VID (Vendor ID) und PID (Product ID). Diese Identifikatoren werden dem Hersteller zugewiesen (VID durch USB-IF) und dem spezifischen Produkt (PID durch den Hersteller selbst). Wenn ein Gerät angeschlossen wird, fordert der Host Deskriptoren an – binäre Strukturen in der Firmware, die Infos zur Geräteklasse, Fähigkeiten und zum erforderlichen Treiber enthalten. Wichtig: Die meisten Aufgaben lassen sich im Benutzerraum über Standardtreiber wie Winusb.sys (Windows) oder usbfs (Linux) erledigen, ohne Kernel-Code.
Wichtige Terminologie:
- Endpoint: Logischer Kanal für Datenübertragung (Control, Bulk, Interrupt, Isochronous).
- Device Descriptor: Wurzelstruktur mit VID/PID und grundlegenden Parametern.
- Device Class: Standardisierte Funktionalität (HID, Mass Storage), aber viele Geräte nutzen Vendor Specific Class.
Gerät vorbereiten: Auswahl und Einrichtung
Wir verwenden ein Android-Gerät im Bootloader (fastboot)-Modus als Beispiel. Gründe für diese Wahl:
- Erreichbarkeit: Die meisten Smartphones unterstützen den Wechsel zu fastboot.
- Einfaches Protokoll: Die fastboot-Dokumentation ist offen und minimalistisch.
- Keine vorinstallierten Treiber: Das OS übernimmt die Interaktion nicht.
Der Wechsel in den Bootloader-Modus erfordert meist das Halten einer Tastenkombination beim Einschalten (z. B. Leiser + Power). Um das Gerät unter Linux zu identifizieren, verwenden Sie lsusb:
$ lsusb
Bus 008 Device 014: ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot)
Hier ist 18d1 Googles VID, 4ee0 der PID für fastboot. Unter Windows liefert USB Device Tree Viewer ähnliche Infos. Wichtig: Wenn das OS einen Treiber lädt (z. B. mit einem ⚠️ im Geräte-Manager angezeigt wird), müssen Sie ihn mit Winusb.sys ersetzen, indem Sie Zadig verwenden.
Gerät auflisten: Von der manuellen Analyse zur programmgesteuerten Umsetzung
Der Enumerationsprozess startet automatisch bei der Verbindung. Das OS analysiert VID/PID und Geräteklasse, um einen Treiber auszuwählen. Im Benutzerraum setzen wir das mit libusb um. Hier ist Code für einen Verbindungshandler:
#include <print>
#include <libusb-1.0/libusb.h>
auto hotplug_callback(
libusb_context *ctx,
libusb_device *device,
libusb_hotplug_event event,
void *user_data
) -> int {
std::println("Device plugged in!");
return 0;
}
auto main() -> int {
libusb_context *context = nullptr;
libusb_init(&context);
libusb_hotplug_callback_handle handle;
libusb_hotplug_register_callback(
context,
LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED,
LIBUSB_HOTPLUG_ENUMERATE,
0x18d1, 0x4ee0,
LIBUSB_HOTPLUG_MATCH_ANY,
hotplug_callback, nullptr,
&handle
);
while (true) {
if (libusb_handle_events(context) < 0) break;
}
libusb_hotplug_deregister_callback(context, handle);
libusb_exit(context);
}
Dieser Code initialisiert libusb, registriert einen Callback für Geräte mit VID=0x18d1 und PID=0x4ee0 und wartet auf die Verbindung. Bei Erfolg druckt er "Device plugged in!". Unter Windows müssen Sie ggf. den Kernel-Treiber ablösen:
libusb_detach_kernel_driver(handle, 0);
Kommunikation mit dem Gerät über den Control Endpoint
Für grundlegende Interaktion nutzen Sie den Control Endpoint (ID 0x00) – den Standardkanal für Serviceanfragen. Hier die Umsetzung einer GET_STATUS-Anfrage:
libusb_device_handle *handle = nullptr;
libusb_open(device, &handle);
std::vector<std::uint8_t> data(0xFF);
const auto result = libusb_control_transfer(
handle,
LIBUSB_ENDPOINT_IN | LIBUSB_RECIPIENT_DEVICE | LIBUSB_REQUEST_TYPE_STANDARD,
LIBUSB_REQUEST_GET_STATUS,
0x00, 0x00,
data.data(), data.size(),
1000
);
if (result >= 0) print_bytes(std::span(data).subspan(0, result));
libusb_close(handle);
Eine erfolgreiche Antwort (z. B. 01 00) wird nach der Spezifikation dekodiert: Erstes Byte – Stromquelle (1 = batterieversorgt), zweites – Remote Wakeup-Unterstützung (0 = nicht unterstützt). Der Control Endpoint ermöglicht auch andere Standardanfragen (GET_DESCRIPTOR, SET_CONFIGURATION), die für das Abrufen von Deskriptoren essenziell sind.
Deskriptoren anfordern: Der Schlüssel zum Verständnis des Geräts
Deskriptoren bilden die Grundlage der USB-Geräteinteraktion. Fordern Sie den Device Descriptor über GET_DESCRIPTOR an:
const auto result = libusb_control_transfer(
handle,
LIBUSB_ENDPOINT_IN | LIBUSB_RECIPIENT_DEVICE | LIBUSB_REQUEST_TYPE_STANDARD,
LIBUSB_REQUEST_GET_DESCRIPTOR,
(LIBUSB_DT_DEVICE << 8), // Tip deskriptora
0x00,
data.data(), data.size(),
1000
);
Die empfangenen Daten entsprechen der libusb_device_descriptor-Struktur mit Feldern wie:
bcdUSB: USB-SpezifikationsversionbMaxPacketSize0: Maximale Paketgröße für Control EndpointidVendor/idProduct: VID und PIDbNumConfigurations: Anzahl der Konfigurationen
Die Analyse von Deskriptoren offenbart unterstützte Schnittstellen und Endpoints. Für ein Konfigurationsdeskriptor ändern Sie den Request-Typ zu LIBUSB_DT_CONFIG und geben den Config-Index in wIndex an.
Wichtige Punkte
- Betrieb im Benutzerraum: libusb statt Kernel-Code verwenden – einfacher zu debuggen und sicherer.
- VID/PID als primäre Identifikatoren: Treiberbindung basiert darauf; Geräteklasse ist oft unergiebig (Vendor Specific Class).
- Control Endpoint als Basistool: Damit Deskriptoren abrufen und das Gerät steuern, bevor andere Endpoints eingerichtet werden.
- Deskriptoren als Quelle der Wahrheit: Ihre Struktur ist in der USB-Spezifikation definiert; Dekodierung liefert das volle Bild der Gerätefähigkeiten.
— Editorial Team
Noch keine Kommentare.