Nachdem wir im vorherigen Kapitel unsere Landkarte (die Page-Tables) sauber im Speicher aufgebaut und verifiziert haben, folgt nun der spannendste Moment: Wir schalten die MMU und die CPU-Caches physisch ein!
Bis zu diesem Punkt lief unser Kernel im sogenannten Flat-Memory-Modus mit deaktivierten Caches. Jeder Speicherzugriff – ob das Holen eines Maschinenbefehls oder das Schreiben eines Bildschirmpixels – musste mühsam über den langsamen Systembus direkt in den RAM-Chip greifen.
Mit dem Aktivieren der MMU schalten wir gleichzeitig die extrem schnellen L1- und L2-Caches der ARM64-CPU frei.
Das Ergebnis: Eine bis zu 50-fache Performance-Steigerung bei Speicheroperationen!
Die 4 Schritte zum MMU-Start
Das Aktivieren der MMU erfolgt über vier zentrale Systemregister der ARM-Architektur:
- MAIR_EL1 (Memory Attribute Indirection Register):
Definiert die Eigenschaften unserer Speicherbereiche (z. B. Cacheable für normalen RAM, Uncached für Hardware-Register und DMA). - TTBR0_EL1 (Translation Table Base Register 0):
Verrät der CPU-Hardware die physische Basisadresse unserer Master-Page-Table (Level 2). - TCR_EL1 (Translation Control Register):
Konfiguriert die Übersetzungsparameter (z. B. 64KB Granularität und die Adressraum-Breite). - SCTLR_EL1 (System Control Register):
Der Hauptschalter! Hier setzen wir das M-Bit (MMU an), das C-Bit (Data-Cache an) und das I-Bit (Instruction-Cache an).
Die C-Funktion: Memory_EnableMMU()
Wir bündeln das Konfigurieren der Register in einer sauberen C-Funktion in memory.c:
#include "memory.h"
#include "translationtable64.h"
void Memory_EnableMMU(void)
{
// ------------------------------------------------------------------------
// 1. MAIR_EL1: Speicherattribute definieren
// ------------------------------------------------------------------------
// Index 0: Normaler RAM -> 0xFF (Inner/Outer Write-Back Cacheable)
// Index 1: MMIO-Register -> 0x04 (Device-nGnRE, Uncached)
// Index 2: Coherent DMA -> 0x00 (Device-nGnRnE, Uncached)
u64 nMAIR_EL1 = (0xFFULL << (ATTRINDX_NORMAL * 8))
| (0x04ULL << (ATTRINDX_DEVICE * 8))
| (0x00ULL << (ATTRINDX_COHERENT * 8));
asm volatile ("msr mair_el1, %0" : : "r" (nMAIR_EL1));
// ------------------------------------------------------------------------
// 2. TTBR0_EL1: Zeiger auf die Master-Page-Table übergeben
// ------------------------------------------------------------------------
uintptr pTableBase = TranslationTable_GetBaseAddress(&m_pTranslationTable);
asm volatile ("msr ttbr0_el1, %0" : : "r" (pTableBase));
// ------------------------------------------------------------------------
// 3. TCR_EL1: Übersetzungsparameter setzen (64KB Granule, 64GB Space)
// ------------------------------------------------------------------------
u64 nTCR_EL1;
asm volatile ("mrs %0, tcr_el1" : "=r" (nTCR_EL1));
// Alte Konfiguration säubern
nTCR_EL1 &= ~( TCR_EL1_IPS__MASK
| TCR_EL1_A1
| TCR_EL1_TG0__MASK
| TCR_EL1_SH0__MASK
| TCR_EL1_ORGN0__MASK
| TCR_EL1_IRGN0__MASK
| TCR_EL1_EPD0
| TCR_EL1_T0SZ__MASK);
// Neue Parameter anwenden
nTCR_EL1 |= TCR_EL1_EPD1 // TTBR1 deaktivieren
| (TCR_EL1_TG0_64KB << TCR_EL1_TG0__SHIFT) // 64 KB Seiten
| (TCR_EL1_SH0_INNER << TCR_EL1_SH0__SHIFT) // Inner Shareable
| (TCR_EL1_ORGN0_WR_BACK_ALLOCATE << TCR_EL1_ORGN0__SHIFT)
| (TCR_EL1_IRGN0_WR_BACK_ALLOCATE << TCR_EL1_IRGN0__SHIFT)
| (TCR_EL1_IPS_64GB << TCR_EL1_IPS__SHIFT) // 64 GB Adressraum
| (TCR_EL1_T0SZ_64GB << TCR_EL1_T0SZ__SHIFT);
asm volatile ("msr tcr_el1, %0" : : "r" (nTCR_EL1));
// ------------------------------------------------------------------------
// 4. SCTLR_EL1: MMU & Caches aktivieren
// ------------------------------------------------------------------------
u64 nSCTLR_EL1;
asm volatile ("mrs %0, sctlr_el1" : "=r" (nSCTLR_EL1));
// WXN und Strict-Alignment-Faults löschen
nSCTLR_EL1 &= ~(SCTLR_EL1_WXN | SCTLR_EL1_A);
// MMU (M), Data-Cache (C) und Instruction-Cache (I) einschalten
nSCTLR_EL1 |= SCTLR_EL1_I | SCTLR_EL1_C | SCTLR_EL1_M;
// Schreiben & Synchronisieren
asm volatile ("msr sctlr_el1, %0" : : "r" (nSCTLR_EL1) : "memory");
asm volatile ("isb" ::: "memory"); // Instruction Pipeline leeren
}
Was passiert im Moment des Einschaltens?
Ein häufiger Befürchtung bei Betriebssystem-Einsteigern ist: „Stürzt mein Programm ab, wenn die MMU plötzlich eingeschaltet wird?“
Die Antwort lautet: Nein, solange wir Identity Mapping nutzen!
Da wir jede virtuelle Adresse exakt auf dieselbe physische Adresse gemappt haben (VA == PA), bleibt die nächste Befehlsadresse im PC-Register (Program Counter) nach der Assembler-Instruktion msr sctlr_el1 vollkommen gültig. Die CPU führt nahtlos den nächsten Befehl aus – nur ab jetzt mit eingeschaltetem Hardware-Cache und Adressschutz.
In der main.c sieht der finale Ablauf nun so aus:
void main(void)
{
// ------------------------------------------------------------------------
// Hardware & Display Initialization
// ------------------------------------------------------------------------
LED_off();
init_heap();
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=== Memory wird initialisiert ===\n\n");
SetFrontColor(yellow);
Memory_Initialize(TRUE); // Initialize memory and enable MMU;
SetFrontColor(white);
printf("\n=== MMU aktiviert ===\n\n");
Interrupt_Initialize();
InitRotorTimer();
// Trigger LED error pattern (2 blinks) as a physical proof-of-life signal
LED_Error(2);
}
Man kann hier nun viel behaupten, da man hier nicht viel sieht, aber es gibt ein sehr deutlicher Hinweis, dass nun die MMU eingeschaltet ist. Als wir den Interrupt für den Rotor programmiert haben, ist es eventuell einigen Usern aufgefallen, dass die Funktion LED_Error() sehr unregelmäßig geblinkt hat und wir eigentlich den Fehlercode nicht mehr erkennen konnten. Nachdem die MMU angeschaltet ist, funktioniert unsere LED_Error()-Funktion wieder.
Den Sourcecode bekommst du hier: https://www.satyria.de/arm/sources/RPI4/C/speicher3.zip
Fazit & Ausblick
Mit dem Aktivieren der MMU haben wir einen riesigen Meilenstein in der Kernel-Entwicklung erreicht:
- Unser Kernel läuft auf maximaler Hardware-Geschwindigkeit.
- Wir haben Speicherbereiche gegen versehentliches Ausführen von Code geschützt.
- Die Grundlage für spätere DMA-Treiber (z. B. USB xHCI oder PCIe) über die
ATTRINDX_COHERENT-Sektion steht!