Der USB-Stack implementiert alle USB-Host-Operationen direkt via Linux usbdevfs-ioctls — kein libusb, keine externen Abhängigkeiten. Alle Transfers (Control, Bulk, Interrupt, Isochronous) sind vollständig in Lyx implementiert.
Import-Muster: Jede Sub-Unit wird einzeln importiert, je nach benötigtem Funktionsumfang:
import std.hardware.usb_discovery;
import std.hardware.usb_control;
import std.hardware.usb_bulk;
→ std.hardware · Standard Library
Der Stack ist in 13 Sub-Units gegliedert, die aufeinander aufbauen:
| Schicht | Sub-Unit | Beschreibung |
|---|---|---|
| High-Level / Sicherheit | usb_endpoint_types | Typsichere Endpoint-Wrapper mit Richtungsvalidierung (Bulk OUT/IN, Interrupt IN) |
| High-Level / Sicherheit | usb_endpoint_bind | Endpoint-Registry: Adressen einmalig binden, danach nur per Schlüssel nutzen |
| Async-Infrastruktur | usb_urb_pool | URB-Pool (4 Slots) für überlappende Interrupt-Transfers |
| Async-Infrastruktur | usb_ifc_mgr | Idempotenter Interface-Claim/Release-Manager |
| Transfer-Schicht | usb_interrupt | Asynchrone Interrupt-Transfers: URB allokieren, einreichen, warten |
| Transfer-Schicht | usb_iso | Isochronous Transfers (Audio/Video): ISO-URB mit Paket-Deskriptoren |
| Transfer-Schicht | usb_bulk | Synchrone Bulk-Transfers (OUT/IN) |
| Transfer-Schicht | usb_control | Control-Transfers + Interface-Claim/Release-IOCTLs |
| Erkennung | usb_discovery | Geräteerkennung via /dev/bus/usb/: VID/PID-Scan |
| Descriptor-Parsing | usb_parse | Configuration-Descriptor-Strom → Config/Interface/Endpoint-Baum |
| Typen & Accessoren | usb_types | Descriptor-Konstanten, Lyx-Structs, Accessor-Funktionen |
| Hilfstypen | usb_util | Dirent64-Parser, String-Utilities, Pfad-Builder |
| Grundschicht | usb_syscalls | open/read/write/ioctl/close/lseek/getdents64/poll-Wrapper |
Rohe Linux-Syscall-Wrapper. Alle anderen Sub-Units bauen darauf auf.
Konstanten:
| Konstante | Wert | Bedeutung |
|---|---|---|
USB_O_RDONLY / USB_O_RDWR | 0 / 2 | open()-Flags |
USB_O_DIRECTORY | 65536 | Verzeichnis öffnen |
USB_SEEK_SET/CUR/END | 0 / 1 / 2 | lseek()-Whence |
USB_DT_DIR / USB_DT_REG | 4 / 8 | dirent-Typ |
USB_POLLIN | 1 | poll()-Event: Daten verfügbar |
Funktionen:
| Funktion | Parameter | Beschreibung |
|---|---|---|
usb_open(path, flags) | pchar, int64 | Datei oder Verzeichnis öffnen |
usb_read(fd, buf, len) | int64 × 3 | Bytes lesen |
usb_write(fd, buf, len) | int64 × 3 | Bytes schreiben |
usb_ioctl(fd, request, arg) | int64 × 3 | ioctl-Aufruf (usbdevfs) |
usb_close(fd) | int64 | fd schließen |
usb_lseek(fd, offset, whence) | int64 × 3 | Leseposition setzen |
usb_getdents64(fd, buf, count) | int64 × 3 | Verzeichniseinträge lesen |
usb_poll(fds_ptr, nfds, timeout_ms) | int64 × 3 | Auf fd-Ereignisse warten |
usb_pollfd_set(ptr, fd, events) | int64 × 3 | pollfd-Struct befüllen |
usb_pollfd_revents(ptr) | int64 | revents aus pollfd lesen |
Hilfstypen für Verzeichnis-Parsing und Pfad-Operationen. Wird intern von usb_discovery genutzt.
| Funktion | Beschreibung |
|---|---|
Dirent64RecLen(entry) | Gesamtlänge des Dirent64-Eintrags (für Iteration) |
Dirent64Type(entry) | Dateityp (USB_DT_REG / USB_DT_DIR) |
Dirent64Name(entry) | Zeiger auf null-terminierten Dateinamen |
Dirent64Ino(entry) | Inode-Nummer |
UsbIsNumeric(ptr) | Gibt 1 wenn String nur Dezimalziffern enthält |
UsbParseNum(ptr) | Dekodiert Dezimal-String → int64 |
UsbBuildPath(base, segment, suffix, buf, buf_size) | Pfadsegmente zusammensetzen |
UsbStrLen(ptr) | Länge eines null-terminierten Strings (int64-Zeiger) |
Alle USB-Descriptor-Typen, Struct-Definitionen und Accessor-Funktionen.
Descriptor-Typ-Konstanten:
| Konstante | Wert | Bedeutung |
|---|---|---|
USB_DT_DEVICE | 1 | Device Descriptor |
USB_DT_CONFIG | 2 | Configuration Descriptor |
USB_DT_STRING | 3 | String Descriptor |
USB_DT_INTERFACE | 4 | Interface Descriptor |
USB_DT_ENDPOINT | 5 | Endpoint Descriptor |
Transfer-Typ und Richtung:
| Konstante | Wert |
|---|---|
USB_TRANS_CONTROL / ISO / BULK / INTERRUPT | 0 / 1 / 2 / 3 |
USB_EP_DIR_OUT / USB_EP_DIR_IN | 0 / 1 |
Struct-Größen:
| Konstante | Größe |
|---|---|
USB_SIZEOF_DEVICE_DESC | 18 Bytes (USB 2.0 §9.6.1) |
USB_SIZEOF_CONFIG_DESC | 9 Bytes |
USB_SIZEOF_INTERFACE_DESC | 9 Bytes |
USB_SIZEOF_ENDPOINT_DESC | 7 Bytes |
USB_SIZEOF_ENDPOINT | 48 Bytes (Lyx-Struct, 6 × int64) |
USB_SIZEOF_INTERFACE | 56 Bytes (7 × int64) |
USB_SIZEOF_DEVICE | 72 Bytes (9 × int64) |
Lyx-Typen:
type UsbEndpoint = struct {
address: int64; direction: int64; number: int64;
transfer_type: int64; max_packet: int64; interval: int64;
};
type UsbInterface = struct {
interface_number: int64; alternate_setting: int64;
class_code: int64; subclass_code: int64; protocol_code: int64;
num_endpoints: int64; endpoints: int64; // Zeiger auf alloc'd Array
};
type UsbDevice = struct {
vendor_id: int64; product_id: int64; device_class: int64;
usb_version: int64; bus_num: int64; dev_num: int64;
handle: int64; // fd — bleibt offen
num_configs: int64; configs: int64;
};
Accessor-Funktionen (Auswahl):
| Funktion | Beschreibung |
|---|---|
UsbDevDesc_idVendor(buf) | Vendor-ID aus Raw-Buffer |
UsbDevDesc_idProduct(buf) | Product-ID |
UsbDevDesc_bNumConfigurations(buf) | Anzahl Konfigurationen |
UsbEpDesc_bEndpointAddress(buf) | Endpoint-Adresse (Bit 7 = Richtung) |
UsbEpDesc_bmAttributes(buf) | Transfer-Typ (Bits 1-0) |
UsbEpDirection(epAddr) | USB_EP_DIR_OUT oder USB_EP_DIR_IN |
UsbEpNumber(epAddr) | Endpoint-Nummer (Bits 0-3) |
UsbEndpointAt(base, i) | Zeiger auf UsbEndpoint[i] im Array |
UsbInterfaceAt(base, i) | Zeiger auf UsbInterface[i] |
UsbEndpointFromDesc(ep, epDescBuf) | Befüllt UsbEndpoint-Struct aus Raw-Descriptor |
Liest den vollständigen Descriptor-Strom von einem geöffneten USB-fd und baut daraus einen vollständigen Config/Interface/Endpoint-Baum.
| Funktion | Beschreibung |
|---|---|
UsbAllocDevice() | Allokiert und nullt UsbDevice-Struct (72 Bytes) |
UsbParseConfiguration(dev_fd, devicePtr) | Parst Descriptor-Strom → Config/Iface/EP-Baum. Rückgabe: 1=Erfolg, 0=Fehler |
Hinweis: UsbParseConfiguration setzt den Dateizeiger via lseek auf 0 zurück (Kernel-fd ist seekbar). Class-spezifische Descriptoren (HID-Report etc.) werden stillschweigend übersprungen.
Scannt /dev/bus/usb/ nach einem Gerät mit gegebener VID/PID. Öffnet das gefundene Gerät und parst automatisch seine Konfiguration.
| Konstante | Bedeutung |
|---|---|
USB_FIND_OK (0) | Gerät gefunden |
USB_FIND_NOTFOUND (-1) | Nicht gefunden |
| Funktion | Beschreibung |
|---|---|
UsbFindDevice(vendor, product, devicePtr) | Sucht in /dev/bus/usb/ |
UsbFindDevicePath(busRoot, vendor, product, devicePtr) | Sucht mit konfigurierbarem Bus-Root |
Verwendung:
import std.hardware.usb_discovery;
import std.hardware.usb_parse;
var dev: int64 := UsbAllocDevice();
if (UsbFindDevice(0x046D, 0xC52B, dev) == USB_FIND_OK) {
// dev.handle ist geöffnet, Konfiguration geparst
}
Control-Transfers und Interface-Claim/Release via usbdevfs-ioctls.
IOCTL-Codes:
| Konstante | Wert | Bedeutung |
|---|---|---|
USBDEVFS_CONTROL | 0xC0185500 | Control-Transfer |
USBDEVFS_CLAIMINTERFACE | 0x4004550F | Interface beanspruchen |
USBDEVFS_RELEASEINTERFACE | 0x40045510 | Interface freigeben |
Request-Typen (USB 2.0 §9.4):
| Konstante | Bedeutung |
|---|---|
USB_DIR_IN / USB_DIR_OUT | 0x80 / 0x00 — Transfer-Richtung |
USB_REQ_GET_DESCRIPTOR | 6 |
USB_REQ_SET_CONFIGURATION | 9 |
USB_TIMEOUT_DEFAULT / USB_TIMEOUT_SHORT | 5000 / 1000 ms |
Funktionen:
| Funktion | Beschreibung |
|---|---|
UsbCtrlSetRequest(buf, reqType, req, wValue, wIndex) | Request-Header (Bytes 0–7) befüllen |
UsbCtrlSetTransfer(buf, data, length, timeout) | Länge/Timeout/Datenzeiger (Bytes 6–23) |
UsbControlTransferRaw(dev_fd, ctrlBuf) | Fertigen ctrlBuf via ioctl senden |
UsbGetDeviceDescriptor(dev_fd, descBuf) | Device Descriptor lesen (18 Bytes) |
UsbClaimInterface(dev_fd, interface_num) | Interface für exklusiven Zugriff beanspruchen |
UsbReleaseInterface(dev_fd, interface_num) | Interface freigeben |
Synchrone Bulk-Transfers via USBDEVFS_BULK (0x40185502).
| Funktion | Beschreibung |
|---|---|
UsbBulkWrite(dev_fd, endpoint, data, length, timeout) | Daten an OUT-Endpoint senden (Bit 7 = 0) |
UsbBulkRead(dev_fd, endpoint, buffer, length, timeout) | Daten von IN-Endpoint empfangen (Bit 7 = 1) |
UsbBulkTransfer(dev_fd, ep, data, length, timeout) | Niedrig-Level (OUT und IN) |
UsbBuildBulkTransfer(buf, ep, data, length, timeout) | usbdevfs_bulktransfer-Struct befüllen |
Rückgabe: Übertragene Bytes (≥ 0) oder negativer Fehlercode (z.B. -ETIMEDOUT, -ENODEV).
Asynchrone Interrupt-Transfers via URBs (Kernel-interne Puffer). Ablauf: allokieren → befüllen → einreichen → warten → auswerten → erneut einreichen.
IOCTL-Codes:
| Konstante | Wert | Bedeutung |
|---|---|---|
USBDEVFS_SUBMITURB | 0x4038550A | URB einreichen |
USBDEVFS_DISCARDURB | 0x0000550B | URB abbrechen |
USBDEVFS_REAPURB | 0x4008550C | Blockierend warten |
USBDEVFS_REAPURBNDELAY | 0x4008550D | Nicht-blockierend abholen |
URB-Struct (56 Bytes, Feld-Offsets als Konstanten URB_OFF_*):
| Offset | Feld | Größe |
|---|---|---|
| +0 | type (USBDEVFS_URB_TYPE_*) | uint8 |
| +1 | endpoint (Bit 7: Richtung) | uint8 |
| +4 | status (0 = OK, vom Kernel gesetzt) | int32 |
| +16 | buffer | void * (8 Bytes) |
| +24 / +28 | buffer_length / actual_length | int32 |
| +44 | signr (uint32, 0 = kein Signal) | uint32 |
| +48 | usercontext | void * (8 Bytes) |
Funktionen:
| Funktion | Beschreibung |
|---|---|
UsbAllocUrb() | Allokiert + nullt URB-Struct (Heap, 56 Bytes) |
UsbFreeUrb(urbPtr) | Gibt URB-Heap frei |
UsbInitInterruptRead(urbPtr, endpoint, buffer, length, context) | Interrupt-IN-URB befüllen |
UsbSubmitUrb(dev_fd, urbPtr) | URB beim Kernel einreichen (kehrt sofort zurück) |
UsbSubmitInterruptRead(dev_fd, endpoint, buffer, length, context) | Allokiert + befüllt + reicht ein |
UsbWaitForUrb(dev_fd, timeout_ms) | poll + REAPURBNDELAY — gibt URB-Zeiger zurück |
UsbDiscardUrb(dev_fd, urbPtr) | Laufenden URB abbrechen |
UsbUrbStatus(urbPtr) | Fehlercode nach Abschluss (0 = OK) |
UsbUrbActualLength(urbPtr) | Tatsächlich empfangene Bytes |
UsbUrbContext(urbPtr) | Usercontext-Zeiger (wie beim Submit übergeben) |
Verwendung (einfacher HID-Empfang):
import std.hardware.usb_interrupt;
var buf: int64 := alloc(64);
var urb: int64 := UsbSubmitInterruptRead(dev_fd, 0x81, buf, 64, 0);
var done: int64 := UsbWaitForUrb(dev_fd, 1000);
if (done != 0 && UsbUrbStatus(done) == 0) {
var n: int64 := UsbUrbActualLength(done);
// buf enthält n Bytes HID-Daten
UsbFreeUrb(urb);
}
Typsichere Endpoint-Wrapper: UsbBulkOutEndpoint hat nur Write-Operationen, UsbBulkInEndpoint nur Read. Falsche Richtung erzeugt einen Sentinel-fd (dev_fd=-1) → alle Ops geben -1.
Typen: UsbBulkOutEndpoint, UsbBulkInEndpoint, UsbInterruptInEndpoint
| Funktion | Beschreibung |
|---|---|
UsbMakeBulkOut(dev_fd, ep, timeout) | Erstellt OUT-Wrapper (ep muss Bit 7 = 0 haben) |
UsbMakeBulkIn(dev_fd, ep, timeout) | Erstellt IN-Wrapper (ep muss Bit 7 = 1 haben) |
UsbMakeInterruptIn(dev_fd, ep, bufSize, timeout) | Interrupt-IN-Wrapper |
UsbBulkOutWrite(ep, data, length) | Sendet via OUT-Endpoint |
UsbBulkInRead(ep, buffer, length) | Empfängt vom IN-Endpoint |
UsbInterruptInSubmit(ep, buffer, context) | URB einreichen |
UsbInterruptInWait(ep, timeout_ms) | Auf URB-Abschluss warten |
UsbBulkOutValid(ep) / UsbBulkInValid(ep) | 1 wenn Endpoint gültig (kein Sentinel) |
Endpoint-Registry: Adressen einmalig unter einem Schlüssel registrieren, danach nur noch per Schlüssel nutzen — keine Adresse beim Aufruf nötig.
| Funktion | Beschreibung |
|---|---|
UsbEpRegistryAlloc() | Neue Registry anlegen (max. 8 Slots) |
UsbEpRegistryFree(reg) | Registry freigeben |
UsbDefOutEndpoint(reg, key, fd, ep, timeout) | OUT-Endpoint unter Schlüssel registrieren |
UsbDefInEndpoint(reg, key, fd, ep, timeout) | IN-Endpoint registrieren |
UsbEpWrite(reg, key, data, len) | Senden über registrierten OUT-Endpoint |
UsbEpRead(reg, key, buf, len) | Empfangen vom registrierten IN-Endpoint |
UsbEpAddr(reg, key) | EP-Adresse eines Slots lesen |
UsbEpIsOut(reg, key) | 1 = OUT, 0 = IN, -1 = nicht gefunden |
import std.hardware.usb_endpoint_bind;
con EP_DATA_OUT: int64 := 1;
con EP_DATA_IN: int64 := 2;
var reg: int64 := UsbEpRegistryAlloc();
UsbDefOutEndpoint(reg, EP_DATA_OUT, dev_fd, 0x01, 1000);
UsbDefInEndpoint(reg, EP_DATA_IN, dev_fd, 0x81, 1000);
UsbEpWrite(reg, EP_DATA_OUT, dataPtr, 64);
UsbEpRead(reg, EP_DATA_IN, recvBuf, 64);
Isochronous Transfers für Audio/Video. ISO-URBs enthalten N angehängte Paket-Deskriptoren (usbdevfs_iso_packet_desc, je 12 Bytes) direkt hinter dem Basis-URB (Offset 56).
| Funktion | Beschreibung |
|---|---|
UsbAllocIsoUrb(num_packets) | ISO-URB allokieren (56 + n × 12 Bytes) |
UsbFreeIsoUrb(urbPtr, num_packets) | Freigeben (Größe muss stimmen) |
UsbInitIsoRead(urbPtr, endpoint, buffer, packet_size, num_packets) | ISO-URB befüllen |
UsbSubmitIsoUrb(dev_fd, urbPtr) | ISO-URB einreichen |
UsbSubmitIsoRead(dev_fd, ep, buf, pkt_size, num_pkts) | Allokiert + init + submit |
UsbReapIsoUrb(dev_fd) | Blockierend warten (USBDEVFS_REAPURB) |
UsbReapIsoUrbNd(dev_fd) | Nicht-blockierend (USBDEVFS_REAPURBNDELAY) |
UsbIsoPacketActualLen(urbPtr, i) | Tatsächliche Bytes von Paket i |
UsbIsoPacketStatus(urbPtr, i) | Status von Paket i (0 = OK) |
UsbIsoErrorCount(urbPtr) | Gesamt-Fehleranzahl aller Pakete |
Idempotenter Claim/Release-Zustandsautomat: doppeltes Claim ist No-op, Release ohne vorheriges Claim ebenfalls.
| Funktion | Beschreibung |
|---|---|
UsbIfcMgrAlloc(dev_fd, ifc_num) | Neuen Manager anlegen (24 Bytes, unclaimed) |
UsbIfcMgrFree(mgrPtr) | Manager freigeben |
UsbIfcAcquire(mgrPtr) | Interface beanspruchen (1 = Erfolg, 0 = Fehler) |
UsbIfcRelease(mgrPtr) | Interface freigeben (No-op wenn nicht geclaimt) |
UsbIfcIsClaimed(mgrPtr) | Aktueller Claim-Status (1 oder 0) |
Pool mit 4 vorbelegten URBs für überlappende Interrupt-Transfers. Während ein URB verarbeitet wird, warten die anderen bereits im Kernel. UsbUrbPoolPoll gibt den Entry-Index zurück — kein Callback-Mechanismus nötig.
Wichtig: Bei UsbUrbPoolGetStatus(urbp, idx) == -ENODEV (Gerät abgesteckt) nicht erneut einreichen.
| Funktion | Beschreibung |
|---|---|
UsbUrbPoolAlloc(dev_fd, endpoint, bufSize) | Pool anlegen (4 Slots, max. 64 Bytes/Slot) |
UsbUrbPoolFree(urbp) | Pool freigeben |
UsbUrbPoolInit(urbp) | Alle 4 URBs einreichen, Rückgabe: Anzahl eingereicht |
UsbUrbPoolPoll(urbp, timeout_ms) | Warten — gibt Entry-Index (0..3) oder -1 zurück |
UsbUrbPoolResubmit(urbp, idx) | Eintrag erneut einreichen |
UsbUrbPoolGetBuffer(urbp, idx) | Zeiger auf Datenpuffer von Slot idx |
UsbUrbPoolGetActualLen(urbp, idx) | Empfangene Bytes nach Poll |
UsbUrbPoolGetStatus(urbp, idx) | URB-Status (0=OK, -ENODEV=Disconnect) |
import std.hardware.usb_urb_pool;
var pool: int64 := UsbUrbPoolAlloc(dev_fd, 0x81, 64);
UsbUrbPoolInit(pool);
while (true) {
var idx: int64 := UsbUrbPoolPoll(pool, 2000);
if (idx < 0) { break; }
if (UsbUrbPoolGetStatus(pool, idx) == 0) {
var data: int64 := UsbUrbPoolGetBuffer(pool, idx);
var len: int64 := UsbUrbPoolGetActualLen(pool, idx);
// data enthält len Bytes
UsbUrbPoolResubmit(pool, idx);
}
}
UsbUrbPoolFree(pool);
import std.hardware.usb_parse;
import std.hardware.usb_discovery;
import std.hardware.usb_control;
import std.hardware.usb_bulk;
fn main(): int64 {
var dev: int64 := UsbAllocDevice();
if (UsbFindDevice(0x1234, 0x5678, dev) != USB_FIND_OK) {
return -1;
}
var fd: int64 := peek64(dev + 48); // dev.handle
if (UsbClaimInterface(fd, 0) < 0) { return -2; }
var txBuf: int64 := alloc(64);
poke8(txBuf, 0x01);
var sent: int64 := UsbBulkWrite(fd, 0x01, txBuf, 64, 1000);
var rxBuf: int64 := alloc(64);
var recv: int64 := UsbBulkRead(fd, 0x81, rxBuf, 64, 1000);
UsbReleaseInterface(fd, 0);
usb_close(fd);
free(txBuf, 64); free(rxBuf, 64);
return 0;
}
Letzte Aktualisierung: 2026-06-07