Auf früheren Raspberry Pi Modellen war der USB-Controller direkt über den internen Systembus angebunden. Beim Raspberry Pi 4 ist das anders: Der Broadcom-Hauptprozessor (BCM2711) besitzt eine eigene PCI Express (PCIe) Root Bridge.
Der bordeigene USB 3.0 Host-Controller (VL805 von VIA) hängt als eigenständiges PCIe-Endgerät an dieser Bridge. Bevor wir also auch nur ein einziges Paket per USB senden können, müssen wir zuerst den PCIe-Bus aufwecken, konfigurieren und eine Brücke bauen!
Hinweis zur Datei pcie.c
Unsere pcie.c stammt im Kern direkt aus dem offiziellen Linux-Kernel-Treiber (drivers/pci/controller/pcie-brcmstb.c). Sie ist recht umfangreich, weil sie tief in die Hardware-Register der Broadcom STB-Architektur eingreift. Für das Verständnis müssen wir aber nur drei Kernaufgaben verstehen!
Die 3 Kernaufgaben von pcie.c
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ARM64 CPU (RAM) β
ββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
β (1. Outbound Window: 0x600000000 -> 0xF8000000)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Broadcom PCIe Root Complex β
ββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
β (2. Inbound BAR2: DMA Host-RAM)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β VL805 USB 3.0 Host-Controller β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1. Die Hardware-Reset-Sequenz (pcie_probe())
Beim Systemstart befindet sich der PCIe-Controller in einem undefinierten Zustand. In pcie_probe() führen wir eine präzise zeitgesteuerte Reset-Sequenz durch:
PERST# (PCIe Reset): Das physische Reset-Signal an das PCIe-Endgerät wird kurz gezogen und wieder freigegeben, um den VL805-USB-Chip neu zu starten.
Phy & Clock: Der SerDes-Phy-Laye und die Taktversorgung (CLKREQ) werden stabilisiert.
2. Die Adress-Übersetzung (Outbound & Inbound Windows)
Das ist das Herzstück der PCIe-Kommunikation! CPU und PCIe-Geräte sprechen oft in unterschiedlichen Adresssprachen. Wir müssen zwei Brücken bauen:
A. Outbound Memory Window (CPU liest/schreibt USB-Register)
Wenn unsere CPU Steuerbefehle an den USB-Controller schickt, schreibt sie ab der virtuellen Adresse 0x600000000 (6 GB Grenze).
Das Outbound Window sorgt dafür, dass die PCIe-Bridge diesen Zugriff abfängt und auf die physische PCIe-Adresse 0xF8000000 übersetzt.
Funktion im Code: pcie_set_outbound_win(...)
B. Inbound Window / BAR2 (USB liest/schreibt Host-RAM per DMA)
Wenn ein USB-Stick Daten liest oder schreibt, tut das der USB-Controller selbstständig per DMA.
Das Inbound Window (BAR2) öffnet dem USB-Chip ein 4 GB großes Fenster direkt in unseren Hauptspeicher (RAM).
Funktion im Code: In pcie_probe() über PCIE_MISC_RC_BAR2_CONFIG_LO
3. Das Aktivieren des Endgeräts (PCIeHostBridge_EnableDevice())
Nachdem die Brücke steht, müssen wir das PCIe-Gerät im Bus aufwecken und registrieren:
- Es setzt die Class-Code-Register (damit der Chip weiß, dass er als PCI-to-PCI Bridge agiert).
- Es schaltet im PCI Command Register die Bits Memory Access, Bus Mastering (für DMA!) sowie Paritätsprüfungen ein.
write16(PCI_COMMAND_MEMORY | PCI_COMMAND_MASTER | PCI_COMMAND_PARITY | PCI_COMMAND_SERR,
conf + PCI_COMMAND);
Worauf muss man bei der Arbeit mit pcie.c achten? (Wichtige Regeln)
Wenn man an Treibern wie pcie.c arbeitet, gibt es paar typische Fallen, über die man stolpern kann:
Strikte 32-Bit-Zugriffe einhalten:
Auch auf einem 64-Bit ARM64-System sind die meisten PCI-Konfigurationsregister physisch nur 32 Bit breit. Eine 64-Bit-Adresse wird daher immer in zwei aufeinanderfolgenden 32-Bit-Schreibbefehlen (write32) für LO und HI übertragen!
Timings & Delays sind überlebenswichtig:
Zwischen dem Anlegen und Löschen von Reset-Signalen muss die CPU per Timer_SimpleusDelay() pausieren. Schreibt man Register zu schnell hintereinander, antwortet der PCIe-Bus nicht rechtzeitig und das System friert ein.
Ende mit Bus Mastering:
Ohne das Bit PCI_COMMAND_MASTER im PCI Command Register darf das PCIe-Gerät keine Daten im RAM ablegen (DMA schlägt fehl!).
Zusammenfassung für unsere main.c
Für den Aufrufer in unserem Kernel schrumpft der gigantische Treiber auf zwei simple Zeilen zusammen:
void main(void)
{
// ------------------------------------------------------------------------
// Hardware & Display Initialization
// ------------------------------------------------------------------------
LED_off();
Memory_Initialize(TRUE); // Initialize memory and enable MMU;
Init_Screen();
InitTerminal();
// Print Welcome Banner
SetFrontColor(satyr);
printf("*****************************************************************************************\n");
printf("*** ***\n");
printf("*** (c) by Satyria ***\n");
printf("*** ***\n");
printf("*****************************************************************************************\n");
SetFrontColor(white);
printf("\n=== PCIe wird initialisiert ===\n\n");
SetFrontColor(yellow);
// 1. PCIe-Struktur erstellen
PCIeHostBridge_Create();
// 2. Hardware-Reset, Outbound/Inbound Windows & Brücke initialisieren
PCIeHostBridge_Initialize();
SetFrontColor(white);
printf("PCIe-Host-Bridge erfolgreich gestartet!\n");
Interrupt_Initialize();
InitRotorTimer();
// Trigger LED error pattern (2 blinks) as a physical proof-of-life signal
LED_Error(2);
}
Sobald dieser Schritt durchgelaufen ist, ist die PCIe-Brücke betriebsbereit und wir können uns im nächsten Kapitel direkt auf den USB xHCI-Controller stürzen!