πŸ’» C und Assembler mit Raspberry Pi

PCI Express (PCIe) (PI4,C)

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!