💻 C und Assembler mit Raspberry Pi

Der Coherent-Slot-Manager (PI 4, C)

Der Coherent-Slot-Manager (DMA-Speicher ohne Cache-Fallen)

Nachdem wir unsere MMU und die CPU-Caches erfolgreich aktiviert haben, läuft unser Kernel auf Höchstgeschwindigkeit. Doch die extrem schnellen L1- und L2-Caches bergen eine riesige Gefahr, sobald externe Hardware-Controller (DMA) ins Spiel kommen.

In diesem Kapitel bauen wir einen einfachen, aber hocheffektiven Coherent-Slot-Manager, der unserem Betriebssystem garantiert ungecachte Speicherseiten für die Kommunikation mit Hardware wie USB, Mailboxen oder PCIe bereitstellt.

Das Problem: Das "Cache-Blindheits-Dilemma"

Wenn die CPU Daten verarbeitet, schreibt sie diese aus Performance-Gründen erst einmal in ihren schnellen L1/L2-Data-Cache und nicht sofort in den physischen RAM-Chip (Write-Back Caching).

Ein DMA-Controller (z. B. der USB-xHCI-Controller auf dem Pi 4 oder die GPU) liest und schreibt aber direkt in den physischen RAM-Chip – er hat keinen Zugriff auf die internen Caches der CPU!

  CPU (L1/L2 Cache)               DMA-Hardware (z.B. USB / PCIe)
         │                                   │
         ▼ (Schreibt Daten)                  ▼ (Liest RAM)
┌──────────────────┐                ┌──────────────────┐
│   CACHE-ZEILE    │  != (asynchron)│   RAM-CHIP       │
│ (neue USB-Daten) │                │ (veraltete Daten)│
└──────────────────┘                └──────────────────┘

Beim Schreiben: Die CPU baut ein Datenpaket für USB im Cache auf. Die USB-Hardware liest den RAM-Chip und sieht nur Nullen. Folge: Der USB-Stick reagiert nicht.

Beim Lesen: Der USB-Stick schreibt empfangene Daten per DMA direkt in den RAM-Chip. Die CPU liest bei ihrer nächsten Abfrage die alten Daten aus ihrem Cache. Folge: Datenkorruption.

Die Lösung: Unsere COHERENT-Speicherregion

Erinnerst du dich an unser Linker-Skript und das Page-Table-Setup?

Im Linker-Skript haben wir den Bereich __coherent_start bis __coherent_end reserviert (z. B. 4 MB).  

In der MMU haben wir diese Seiten mit dem Attribut ATTRINDX_COHERENT (Uncached / Non-Cacheable) versehen.  

Wenn die CPU auf eine Adresse in diesem Bereich zugreift, umgeht sie den Cache vollständig. Jeder Schreib- und Lesezugriff landet sofort auf dem echten RAM-Chip.  

Die Lösung im Code: Memory_GetCoherentPage()

Um diesen Speicher sauber zu verwalten, teilen wir den 4 MB großen Coherent-Bereich in feste 64-KB-Slots (Pages) auf.

In der memory.h definieren wir zunächst, welcher Treiber welchen Slot zugewiesen bekommt:

#define COHERENT_SLOT_PROP_MAILBOX    0   // Slot 0 für Mailbox-Kommunikation
#define COHERENT_SLOT_GPIO_VIRTBUF    1   // Slot 1 für GPIO Virtbuf
#define COHERENT_SLOT_TOUCHBUF        2   // Slot 2 für Touchscreen

#define COHERENT_SLOT_XHCI_START     (MEGABYTE / PAGE_SIZE)       // Slots für USB xHCI Controller
#define COHERENT_SLOT_XHCI_END       (4*MEGABYTE / PAGE_SIZE - 1)

Neben unseren USB-xHCI-Slots finden sich im Header auch Reservierungen wie COHERENT_SLOT_VCHIQ_START. VCHIQ ist die High-Speed-Schnittstelle zwischen der ARM-CPU und der VideoCore-GPU. Wenn wir später komplexe Treiber wie HDMI-Audio oder die Raspberry Pi Kamera einbauen möchten, nutzen diese Treiber genau diesen reservierten DMA-Speicherbereich, um Audiosignale und Videoframes stoßfrei zur GPU zu streamen.

In memory.c programmieren wir die Funktion, die die physische Startadresse des gewünschten Slots berechnet:

/**
 * @brief Liefert eine ungecachte (Coherent) 64KB-Speicherseite für DMA-Hardware.
 * @param nSlot Die Slot-Nummer (0, 1, 2, ...)
 * @return Startadresse der 64KB Page im Uncached RAM
 */
uintptr Memory_GetCoherentPage(unsigned nSlot)
{
    uintptr coherent_start = (uintptr)&__coherent_start;
    uintptr coherent_end   = (uintptr)&__coherent_end;

    // Berechne die Startadresse des gewünschten 64KB-Slots
    uintptr slot_addr = coherent_start + (nSlot * PAGE_SIZE);

    // Schutz: Prüfen, ob der Slot noch innerhalb des Coherent-Bereichs liegt
    if (slot_addr + PAGE_SIZE > coherent_end)
    {
        printf("FEHLER: Coherent Slot %d liegt ausserhalb des reservierten Bereichs!\n", nSlot);
        return 0;
    }

    return slot_addr;
}

 Was haben wir damit erreicht?

Typsichere Hardware-Buffer: Ein Treiber wie pcie.c oder xhci.c ruft einfach Memory_GetCoherentPage(COHERENT_SLOT_XHCI_START) auf und bekommt eine saubere, 64KB große, ungecachte Speicherseite garantiert an einer 64KB-Grenze ausgeliefert.  

Kein automatischer Cache-Flush nötig: Da alle Zugriffe auf diese Seite an den Caches vorbeigehen, sparen wir uns komplizierte Assembler-Flush-Befehle im laufenden Betrieb.

Schutz des normalen Heaps: Unser normaler Heap (malloc / palloc) bleibt zu 100 % gecacht und ultraschnell für C-Variablen und Kernel-Logik.  

Damit ist das Fundament für den PCI-Express- und USB-Treiber (xHCI) vollständig gelegt!

Den Code bekommst du hier: https://www.satyria.de/arm/sources/RPI4/C/coherent.zip