💻 C y ensamblador con Raspberry Pi
🌐 Este artículo solo está disponible en alemán por el momento.

USB Teil 1 (RPI4, C)

1. Das zentrale Kernel-Objekt

Der erste wichtige Schritt ist die Erstellung eines zentralen Datenhalters, der den globalen Zustand unseres Systems verwaltet. In der main.c definieren wir eine Struktur CKernel. Zum jetzigen Zeitpunkt enthält sie lediglich das xHCI-Gerät, wird aber im Laufe des Tutors erweitert.

typedef struct CKernel
{
    CXHCIDevice m_USBHCI;
    // Später kommen hier z.B. Maus, Tastatur und Storage-Pointer hinzu
} CKernel;

static CKernel Kernel; // Globale Instanz

Die Struktur `CXHCIDevice` repräsentiert den logischen USB-Controller in unserem System:

struct CXHCIDevice {
    boolean m_bInterruptConnected;
    void *m_pScratchpadBuffers;
    u64 *m_pScratchpadBufferArray;
    boolean m_bShutdown;
};

Bedeutung der Felder:

  • m_bInterruptConnected: Wird auf TRUE gesetzt, sobald der xHCI-Controller erfolgreich einen IRQ beim Interrupt-Controller (GIC) angemeldet hat.
  • m_pScratchpadBuffers: Zeiger auf den physisch zusammenhängenden (coherenten) Speicherbereich, den der xHCI-Controller für interne Operationen (z.B. Context-Switches) zwingend benötigt.
  • m_pScratchpadBufferArray: Zeiger auf ein Array von Pointern. Dieses Array teilt dem Controller beim Start die physikalischen Adressen der einzelnen Scratchpad-Puffer mit.
  • m_bShutdown: Ein Flag, das signalisiert, ob der Treiber gerade heruntergefahren wird. Dies ist wichtig, um neue Interrupts zu ignorieren und Ressourcen sauber freizugeben.

2. Die angepasste main()-Funktion

Unsere bekannte main()-Funktion wird erweitert. Die PCIe-spezifischen Aufrufe von zuvor werden nun in die USB-Initialisierung integriert, da sie hier logisch hingehören.

#include "usb.h"
#include "timer.h"
#include "kernel.h"
#include "memory.h"
#include "screen.h"
#include "printf.h"
#include "interrupt.h"
#include "sync.h"
#include "config.h"

static CKernel Kernel;

void main(void)
{
    // 1. MMU + Heap (MUSS zuerst erfolgen)
    Memory_Initialize(TRUE);

    // 2. Bildschirm und Terminal initialisieren
    Init_Screen();
    InitTerminal();

    // 3. Banner anzeigen
    SetFrontColor(satyr);
    printf("*****************************************************************************************\n");
    printf("***                                                                                   ***\n");
    printf("***                                (c) by Satyria                                     ***\n");
    printf("***                                                                                   ***\n");
    printf("*****************************************************************************************\n\n");

    // 4. USB-Subsystem vorbereiten (Nur Datenstrukturen, noch keine Hardware!)
    SetFrontColor(white);
    printf("[1/4] USB-Subsystem vorbereiten (usb_Create) ... ");
    usb_Create(&Kernel.m_USBHCI, TRUE);
    SetFrontColor(green);
    printf("OK\n");

    // 5. Interrupts und Timer initialisieren
    SetFrontColor(white);
    printf("[2/4] Interrupts + Timer starten ............... ");
    Interrupt_Initialize();
    Timer_Initialize();
    SetFrontColor(green);
    printf("OK\n");

    // 6. IRQ27 für den Rotor-Timer konfigurieren (Unser "Herzschlag")
    SetFrontColor(white);
    printf("[3/4] Rotor-Timer konfigurieren ................ ");
    SetIRQ27Target();
    write32(1 << 27, GICD_ISENABLER0);
    DataSyncBarrier();
    Interrupt_ConnectIRQ(27, RotorTimerHandler, NULL);
    InitRotorTimer();
    SetFrontColor(green);
    printf("OK\n");

    // 7. Endlosschleife (Kernel-Loop)
    SetFrontColor(white);
    printf("[4/4] System läuft. Warten auf Ereignisse ...... \n\n");
    
    while(1)
    {
        // Hier läuft später die Ereignisverarbeitung (Events, USB-Polling etc.)
    }
}

Tipp zur Debug-Ausgabe: Die printf-Anweisungen dienen hier der visuellen Kontrolle im Tutor. Im fertigen Treiber-Code werden diese schrittweise entfernt oder durch ein echtes Logging-System ersetzt, um die Bildschirmausgabe nicht mit unwichtigen Initialisierungs-Details zu überfluten.


3. Die usb_Create() Funktion

Die Funktion usb_Create() in der usb.c ist unser Startpunkt. Wichtig zu verstehen: Hier wird noch keine Hardware angefasst! Wir legen lediglich die "Baupläne" (Datenstrukturen) im RAM an und bereiten den Speicher vor.

#include "usb.h"
#include "pcie.h"
#include "memory.h"
#include "sync.h"

/* ============================================================================
 * 1. STATISCHE SYSTEMZUSTÄNDE & GLOBALE SINGLETONS
 * ============================================================================ */
static boolean s_XHCIDeviceShutdown = FALSE;
static CUSBHostController s_hostController = {0};
static CXHCISharedMemAllocator s_xhciSharedAllocator = {0};

/* ============================================================================
 * 2. INITIALISIERUNG & CORE MEMORY CONTROLLER
 * ============================================================================ */
void usb_Create(CXHCIDevice *pXHCIDevice, boolean bPlugAndPlay)
{
    // 1. Gerätestruktur zurücksetzen
    pXHCIDevice->m_bInterruptConnected = FALSE;
    pXHCIDevice->m_pScratchpadBuffers = 0;
    pXHCIDevice->m_pScratchpadBufferArray = 0;
    s_XHCIDeviceShutdown = FALSE;

    // 2. Host-Controller-Status vorbereiten
    s_hostController.m_bFirstUpdateCall = TRUE;
    s_hostController.m_bInitialized = TRUE; // Logisch vorbereitet, nicht hardware-seitig!
    s_hostController.m_SpinLock.nTargetLevel = IRQ_LEVEL;
    s_hostController.s_bPlugAndPlay = bPlugAndPlay;

    // 3. PCIe-Brücke vorbereiten (Strukturen anlegen)
    PCIeHostBridge_Create();

    // 4. Coherent-Memory-Pool für den xHCI-Controller reservieren
    s_xhciSharedAllocator.m_nMemStart = Memory_GetCoherentPage(COHERENT_SLOT_XHCI_START);
    s_xhciSharedAllocator.m_nMemEnd = Memory_GetCoherentPage((COHERENT_SLOT_XHCI_END) + PAGE_SIZE - 1);
    s_xhciSharedAllocator.m_pFreeList = 0;
}

Erklärung der `CUSBHostController` Struktur:

Diese Struktur verwaltet den Zustand des gesamten USB-Host-Controllers (nicht nur eines einzelnen Geräts).

struct CUSBHostController {
    boolean s_bPlugAndPlay;
    boolean m_bFirstUpdateCall;
    boolean m_bInitialized;
    TPortStatusEvent m_HubEvents[256];
    unsigned m_nNumUsedHubEvents;
    CSpinLock m_SpinLock;
};

Bedeutung der Felder:

  • s_bPlugAndPlay: Steuert, ob das System Hot-Plugging (An- und Abstecken von Geräten während des Betriebs) aktiv verarbeiten soll.
  • m_bFirstUpdateCall: Ein Hilfs-Flag, das sicherstellt, dass bestimmte Aufräum- oder Setup-Routinen nur beim allerersten Durchlauf der Hauptschleife ausgeführt werden.
  • m_bInitialized: Zeigt an, dass die logische Struktur des Controllers bereit ist. (Achtung: Dies bedeutet nicht, dass die Hardware bereits programmiert ist – das folgt später).
  • m_HubEvents[256]: Ein Puffer (Ringbuffer-ähnlich), der Statusänderungen an den USB-Ports (z.B. "Gerät an Port 1 eingesteckt") zwischenspeichert, bis der Hauptthread sie verarbeiten kann.
  • m_nNumUsedHubEvents: Ein Zähler, der angibt, wie viele Ereignisse im m_HubEvents-Array aktuell auf Verarbeitung warten.
  • m_SpinLock: Ein Spinlock, der den gleichzeitigen Zugriff auf die Controller-Daten durch den Hauptthread und den Interrupt-Handler (IRQ) synchronisiert, um Race Conditions zu verhindern.

Erklärung des CXHCISharedMemAllocator:

Der xHCI-Controller verwendet DMA (Direct Memory Access). Das bedeutet, der USB-Controller schreibt und liest direkt in den RAM, ohne die CPU zu bemühen. Damit CPU und USB-Controller immer denselben Datenstand sehen, darf dieser Speicherbereich nicht im CPU-Cache liegen (oder muss explizit gemanagt werden). Wir nennen dies "Coherent Memory".

typedef struct CXHCISharedMemAllocator {
    uintptr m_nMemStart;
    uintptr m_nMemEnd;
    TXHCIBlockHeader *m_pFreeList;
} CXHCISharedMemAllocator;

Bedeutung der Felder:

  • m_nMemStart / m_nMemEnd: Definieren die untere und obere Grenze des reservierten, cache-coherenten Speicherbereichs.
  • m_pFreeList: Der Kopfzeiger einer verketteten Liste. Da wir diesen Speicherbereich nicht an das allgemeine malloc des Systems übergeben, verwalten wir ihn selbst. Dieser Zeiger zeigt auf den ersten freien Block in unserem USB-spezifischen "Mini-Heap".

4. Zusammenfassung dieses Schritts

Mit usb_Create() haben wir das Fundament gelegt. Wir haben die Strukturen im RAM angelegt und den Speicher für die spätere DMA-Kommunikation reserviert. Im nächsten Teil werden wir diese leeren Strukturen mit Leben füllen, indem wir die PCIe-Brücke und den xHCI-Controller hardwareseitig initialisieren.

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