💻 C und Assembler mit Raspberry Pi

USB Teil 2: Der xHCI-Host-Controller – Das Herz des USB-Systems (RPI4, C)

1. Die 10 Schritte der Initialisierung

Im vorherigen Kapitel haben wir die Grundlagen erstellt, damit wir den USB-Kontroller Initialisieren können. Die ersten Strukturen sind erzeugt. Die Funktion usb_Initialize() führt genau 10 Schritte aus, um den Controller betriebsbereit zu machen. Schauen wir uns jeden einzelnen an:

📋 Übersicht der 10 Schritte

================================================================
   xHCI HOST CONTROLLER INITIALISIERUNG (10 Schritte)
================================================================
  [1 ] PCIe Host Bridge aktivieren (xHCI liegt am PCIe-Bus)
  [2 ] xHCI-Gerät als PCIe-Device konfigurieren
  [3 ] Slot Manager + DCBAA (Telefonbuch der USB-Geräte)
  [4 ] Event Manager + ERST (Briefkasten für Controller-Events)
  [5 ] Command Manager + CRCR (Befehls-Ring der CPU)
  [6 ] Scratchpad-Buffer (temporärer Arbeitsspeicher)
  [7 ] Root Hub (die physikalischen USB-Ports)
  [8 ] IRQ-Handler verbinden
  [9 ] Controller starten (RUN/STOP-Bit)
  [10] Ports initialisieren + Geräte erkennen
================================================================

Den zehnten Schritt werden wir erst im nächsten Kapitel beschreiben, da bereits dieser Teil der Schritte 1 bis 9 enorm viel Code ist.


Schritt 1: PCIe Host Bridge aktivieren

PCIeHostBridge_Initialize();

Der xHCI-Controller ist ein PCIe-Gerät. Bevor wir ihn ansprechen können, muss die PCIe Host Bridge initialisiert werden. Diese Bridge ist das "Tor" zwischen dem ARM-SoC und dem PCIe-Bus.

Was passiert hier?

  • Reset-Sequenz für den PCIe-Controller
  • Konfiguration der Outbound Memory Windows (CPU ↔ PCIe Adress-Mapping)
  • Einrichtung der MSI-Interrupts
  • Aktivierung der PCIe-Verbindung

🔍 Register-Tiefblick: In pcie.c siehst du, wie wir über das Register PCIE_MISC_MISC_CTRL den SCB-Zugriff aktivieren und über PCIE_RGR1_SW_INIT_1 den Reset durchführen.


Schritt 2: xHCI-Device aktivieren

PCIeHostBridge_EnableDevice(XHCI_PCI_CLASS_CODE, XHCI_PCIE_SLOT, XHCI_PCIE_FUNC);

Jetzt aktivieren wir das xHCI-Device selbst. Es sitzt an Slot 0, Funktion 0 des PCIe-Bus 1.

Gleichzeitig lesen wir die Hardware-Capabilities aus:

Register Bedeutung Typischer Wert
HCSPARAMS1 Max. Slots, Max. Ports z.B. 32 Slots, 5 Ports
HCSPARAMS2 Scratchpad-Buffer, IST z.B. 31 Scratchpad-Buffer

💡 Wichtig: Diese Werte sind hardwareabhängig! Wir lesen sie aus, statt sie fest zu codieren. So funktioniert der Code auch auf anderen xHCI-Controllern.


Schritt 3: Slot Manager + DCBAA

s_slotManager.m_pDCBAA = (u64 *)CXHCIDevice_AllocateSharedMem(
    (XHCI_CONFIG_MAX_SLOTS + 1) * sizeof(u64), 64, XHCI_PAGE_SIZE);

write32((u32)(dma & 0xFFFFFFFF), XHCIMMIOSpace_OpBase + XHCI_REG_OP_DCBAAP);
write32((u32)(dma >> 32), XHCIMMIOSpace_OpBase + XHCI_REG_OP_DCBAAP + 4);

DCBAA = Device Context Base Address Array

Das ist das Telefonbuch des xHCI-Controllers. Für jeden USB-Slot (jedes angeschlossene Gerät) steht hier die Adresse des Device-Contexts – also der kompletten Zustandsbeschreibung des Geräts.

🎓 Analogie: Stell dir vor, der xHCI-Controller ist ein Hotel-Rezeptionist. Das DCBAA ist das Gästebuch: "Zimmer 1 = Maus, Zimmer 2 = Tastatur, ..."

Ohne DCBAA: Der Controller weiß nicht, wo die Daten der angeschlossenen Geräte liegen!


Schritt 4: Event Manager + ERST

CXHCIRing_Create(&s_eventManager.m_EventRing, XHCIRingTypeEvent, XHCI_CONFIG_EVENT_RING_SIZE);
s_eventManager.m_pERST = (TXHCIERSTEntry *)CXHCIDevice_AllocateSharedMem(...);

ERST = Event Ring Segment Table

Der xHCI-Controller arbeitet asynchron. Wenn er einen Befehl abgearbeitet hat, legt er ein Event in den Event-Ring. Die CPU muss diese Events regelmäßig abholen.

🎓 Analogie: Der Event-Ring ist der Briefkasten des Controllers. Er wirft seine Antworten dort ein, und die CPU muss regelmäßig nachschauen, ob Post da ist.

Ohne ERST: Der Controller weiß nicht, wohin er seine Events schreiben soll!


Schritt 5: Command Manager + CRCR

CXHCIRing_Create(&s_commandManager.m_CmdRing, XHCIRingTypeCommand, XHCI_CONFIG_CMD_RING_SIZE);
write32(low, XHCIMMIOSpace_OpBase + XHCI_REG_OP_CRCR);

CRCR = Command Ring Control Register

Der Command-Ring ist der Briefkasten der CPU an den Controller. Wir schreiben Befehle (TRBs = Transfer Request Blocks) hinein und "läuten" an der Doorbell. Der Controller holt die Befehle ab und arbeitet sie ab.

Typische Befehle:

  • ENABLE_SLOT – Einen neuen Slot für ein Gerät reservieren
  • ADDRESS_DEVICE – Dem Gerät eine USB-Adresse zuweisen
  • CONFIGURE_ENDPOINT – Endpunkte des Geräts konfigurieren

🎓 Analogie: Der Command-Ring ist die Wunschliste an den Controller. "Bitte aktiviere Slot 1", "Bitte konfiguriere Endpoint 2", ...

Ohne CRCR: Die CPU kann keine Befehle an den Controller senden!


Schritt 6: Scratchpad-Buffer

pXHCIDevice->m_pScratchpadBuffers = CXHCIDevice_AllocateSharedMem(
    XHCI_PAGE_SIZE * nMaxScratchpadBufs, XHCI_PAGE_SIZE, XHCI_PAGE_SIZE);

Der Scratchpad-Buffer ist der temporäre Notizblock des Controllers. Manche xHCI-Controller brauchen Arbeitsspeicher für interne Berechnungen. Die Anzahl steht in HCSPARAMS2.

💡 Interessant: Die Adresse des Scratchpad-Arrays wird in DCBAA[0] geschrieben – also im "Telefonbuch" an der Stelle, wo sonst Geräte stehen. Das ist kein Zufall: Slot 0 ist für den Controller selbst reserviert!

Ohne Scratchpad: Manche Controller verweigern den Start komplett!


Schritt 7: Root Hub vorbereiten

s_xhciRootHub.m_nPorts = nMaxPorts;
for (unsigned nPort = 0; nPort < s_xhciRootHub.m_nPorts; nPort++) {
    s_xhciRootHub.m_pRootPort[nPort].m_nPortIndex = nPort;
    ...
}

Der Root Hub ist der physikalische USB-Hub des Controllers. Er hat typischerweise 5 Ports – das sind die USB-A-Buchsen am Raspberry Pi 4.

Wir initialisieren hier nur die Port-Strukturen. Die eigentliche Konfiguration passiert in Schritt 10.


Schritt 8: IRQ verbinden

Interrupt_ConnectIRQ(ARM_IRQ_XHCI, CXHCIDevice_InterruptStub, pXHCIDevice);

Der xHCI-Controller löst Interrupts aus, wenn:

  • Ein Befehl abgearbeitet wurde
  • Ein USB-Gerät eingesteckt wurde
  • Ein Transfer abgeschlossen ist

Unser Handler CXHCIDevice_InterruptHandler wird dann aufgerufen und holt die Events aus dem Event-Ring.

⚠️ Wichtig: Der IRQ-Handler muss extrem schnell sein! Keine printf()-Ausgaben im IRQ-Kontext – das würde zu Timeouts führen.


Schritt 9: Controller starten

write32(read32(XHCIMMIOSpace_OpBase + XHCI_REG_OP_USBCMD) | XHCI_REG_OP_USBCMD_INTE, ...);
write32(read32(XHCIMMIOSpace_OpBase + XHCI_REG_OP_USBCMD) | XHCI_REG_OP_USBCMD_RUN_STOP, ...);

Jetzt wird der Controller gestartet:

  • INTE = Interrupt Enable – Interrupts erlauben
  • RUN/STOP = Controller aus dem HALT-Zustand holen

Kontrolle: Wir lesen das USBSTS-Register. Wenn das Bit HCH (Host Controller Halted) nicht gesetzt ist, läuft der Controller!


Schritt 10: Ports initialisieren (Ausblick auf das nächste Kapitel)

// Dieser Schritt wird im nächsten Tutorial-Teil behandelt!
for (unsigned nPort = 0; nPort < s_xhciRootHub.m_nPorts; nPort++) {
    CXHCIRootPort_Initialize(&s_xhciRootHub.m_pRootPort[nPort]);
    CXHCIRootPort_Configure(&s_xhciRootHub.m_pRootPort[nPort]);
}

Was in Schritt 10 passiert:

  1. Port-Reset durchführen (USB 2.0) bzw. auf U0-Zustand warten (USB 3.0)
  2. Port-Speed auslesen (Low/Full/High/SuperSpeed)
  3. Enable Slot Command an den Controller senden
  4. Address Device Command – dem Gerät eine USB-Adresse zuweisen
  5. Device Descriptor auslesen (Vendor ID, Product ID, ...)
  6. Configuration Descriptor auslesen (Interfaces, Endpoints)
  7. Treiber binden (Maus, Tastatur, Mass Storage)

Das ist der spannendste Teil – hier "erwachen" die USB-Geräte zum Leben! Aber er ist so umfangreich, dass wir ihm ein eigenes Kapitel widmen.


4. Der Shared-Memory-Allocator

Eine Besonderheit des xHCI-Codes ist der Shared-Memory-Allocator (CXHCISharedMemAllocator). Warum brauchen wir den?

Das Problem: Der xHCI-Controller greift über DMA (Direct Memory Access) auf den Speicher zu. Er sieht die physikalischen Adressen, nicht die virtuellen. Und er hat strenge Anforderungen:

  • Alignment: Manche Strukturen müssen an 64-Byte-Grenzen liegen
  • Boundary: Start und Ende einer Allokation dürfen keine 4KB-Grenze überschreiten

Die Lösung: Ein spezieller Allocator im Coherent Memory (nicht cachebar), der diese Anforderungen automatisch erfüllt.

🎓 Analogie: Stell dir vor, du musst Pakete verschicken, die genau in einen Briefkasten passen. Der Allocator ist dein "Packing-Service", der sicherstellt, dass jedes Paket die richtige Größe hat.


5. Zusammenfassung

In diesem Kapitel haben wir:

  • ✅ Die PCIe Host Bridge aktiviert
  • ✅ Den xHCI-Controller als PCIe-Device konfiguriert
  • ✅ Das DCBAA (Telefonbuch der Geräte) angelegt
  • ✅ Den Event-Ring (Briefkasten des Controllers) eingerichtet
  • ✅ Den Command-Ring (Wunschliste der CPU) erstellt
  • ✅ Den Scratchpad-Buffer alloziert
  • ✅ Den Root Hub vorbereitet
  • ✅ Den IRQ-Handler verbunden
  • ✅ Den Controller gestartet

Was als Nächstes kommt:

Im nächsten Kapitel werden wir die USB-Ports initialisieren und die ersten Geräte erkennen. Du wirst sehen, wie eine Maus oder Tastatur "zum Leben erwacht"!


📚 Weiterführende Literatur

Source: https://www.satyria.de/arm/sources/RPI4/C/USB2_Init2.zip