💻 C und Assembler mit Raspberry Pi

Interrupts (PI4)

Wichtig! Durch die Entwicklung mit USB-Unterstützung wurden einige Funktionen, die wir bisher genutzt haben, überarbeitet. Diese "neuen" Funktionen sind ab hier eingebaut. Darunter zählt zum Beispiel die printf-Funktion, die hier komplett überarbeitet ist. Zusätzlich wurden einige Screen-Funktionen überarbeitet, die ab hier NEON verwenden können.

Welche Änderungen durchgefürt wurden, kanns du im Kapitel Erweiterungen und Verbesserungen für RPI 4 in C nachlesen.

Interrupts und Exceptions auf dem Raspberry Pi 4 (Bare Metal)

Interrupts (Unterbrechungen) und Exceptions (Ausnahmen) sind das Nervensystem jedes Betriebssystems. Sie sorgen dafür, dass der Prozessor nicht blind Code abarbeitet, sondern auf Ereignisse reagieren kann – sei es, dass ein Timer abläuft, eine Taste gedrückt wird oder ein schwerwiegender Programmierfehler auftritt.

In diesem Kapitel lernst du, wie die Ausnahmebehandlung im ARMv8 (AArch64) funktioniert, wie wir die Vektortabelle einrichten und wie wir mit einem einfachen "Blindtest" sicherstellen, dass unser System bereit für echte Hardware-Interrupts ist.

Exceptions vs. Interrupts

Was ist der Unterschied?

Obwohl die Begriffe oft synonym verwendet werden, gibt es einen feinen, aber wichtigen Unterschied:

  • Exceptions (Synchron): Treten direkt durch die Ausführung eines Befehls auf.
    Beispiele: Ein ungültiger Speicherzugriff, eine Division durch Null oder ein bewusster Systemaufruf (wie svc #0). Der Prozessor weiß genau, welcher Befehl die Ausnahme ausgelöst hat.
  • Interrupts (Asynchron): Treten unabhängig vom aktuellen Befehl auf, ausgelöst durch externe Hardware.
    Beispiele: Der System-Timer, eine USB-Maus oder ein GPIO-Pin.

📣Tipp für die OS-Entwicklung: Synchronous Exceptions (wie unsere SynchError-Handler) sind deine besten Freunde beim Debuggen! Wenn dein Code versehentlich in einen SynchError-Handler springt, weißt du sofort: Hier ist ein schwerer Fehler (z. B. Sprung an eine ungültige Adresse oder falscher Befehl). Wir werden das gleich gezielt ausnutzen.

Exception Levels (EL)

Der ARM Cortex-A72 kennt verschiedene Privilegienstufen, die Exception Levels (EL):

  • EL0: User-Mode (Anwendungen, wenig Rechte)
  • EL1: Kernel-Mode (Unser Bare-Metal-Code, volle Hardware-Kontrolle)
  • EL2: Hypervisor (Virtualisierung, wird vom Bootloader kurz genutzt)
  • EL3: Secure Monitor (Highest Privilege, z. B. ARM Trusted Firmware)

Wichtig: Um Interrupts korrekt zu verarbeiten, müssen wir uns in EL1 befinden. Da der Raspberry Pi 4 oft in EL2 startet, ist unser erster Schritt der Wechsel nach EL1.

boot.S: Nur ein Kern darf laufen

Der Raspberry Pi 4 hat 4 Kerne. Beim Start führen alle denselben Code aus. Würden alle versuchen, Interrupts zu bearbeiten oder den Stack zu nutzen, würde das System sofort abstürzen. Wir lassen nur Core 0 arbeiten und schicken die anderen in einen energiesparenden Schlafmodus (wfe = Wait For Event).

.section .init                // Ensure the linker places this at the beginning of the kernel image
.global _start                // Entry point for execution

_start:
    // ------------------------------------------------------------------------
    // 1. Multicore handling: Identify current core
    // ------------------------------------------------------------------------
    mrs     x1, mpidr_el1     // Read Multiprocessor Affinity Register
    and     x1, x1, #3        // Mask the lower 2 bits to obtain Core-ID (0-3)
    cbz     x1, core0         // Core 0 proceeds to system setup

core_sleep:
    // Core 1-3 enter low-power state
    wfe                       // Wait For Event (sleep loop)
    b       core_sleep        // Infinite loop for secondary cores

core0

Im Register mpidr_el1 werden Informationen zu den jeweiligen CPUs abgelegt. Hier stehen außer der eigentlichen CPU-ID viele Informationen, wie das Arbeiten in Clustern (mehrere CPU-Kerne, hier 4 Stück) oder für riesige Server-CPUs mit vielen Sockeln.

Da das Ergebnis dieser Anfrage mehr informationen enthält, als nur die ID, müssen wir mit and #3 alle anderen Bits aus der Antwort löschen. Die restlichen Bits benötigen wir nicht. Mit cbz vergleichen wir das Ergebnis mit Null, was dem Core0 entspricht und springen hier weiter, um das System weiter zu initialisieren. Die restlichen Cores lassen wir mit wfe schlafen. Sollten diese unerwartet aufgeweckt werden, befinden sie sich in der Dauerschleife und setzen wfe wiederholt ein.

Der Wechsel von EL2 nach EL1

Da wir einen Kernel schreiben, benötigen wir die volle Kontrolle über das System. Wie zuvor geschrieben, ist dies der Exception Level 1 (EL1). In der Regel startet der Raspberry komment aus der Firmware-Initialisierung in EL2

    // ------------------------------------------------------------------------
    // 2. Exception Level Check (EL2 vs EL1)
    // ------------------------------------------------------------------------
    mrs     x0, CurrentEL     // Read current Exception Level
    cmp     x0, #4            // Check if already running in EL1 (CurrentEL[3:2] == 0b01)
    beq     switch_to_el1     // Skip EL2 setup if booted directly into EL1

Das Register CurrentEL enthält das Exception Level in den Bits [3:2]. Ein Wert von 0b01 an Bitposition [3:2] entspricht hexadezimal 0x04 (bzw. dezimal 4). Mit cmp x0, #4 prüfen wir somit direkt, ob der Bit-Wert für EL1 an den Bits [3:2] anliegt.

    // ------------------------------------------------------------------------
    // 3. Initialisation for EL2 (Drop execution level to EL1)
    // ------------------------------------------------------------------------
    ldr     x0, =MEM_KERNEL_STACK
    msr     sp_el1, x0        // Set target EL1 stack pointer

Damit die CPU später im EL1 korrekt läuft, müssen wir hier noch Informationen weitergeben. Zur Zeit sind wir noch in EL2. Zunächst definieren wir den Stack, den wir mit sp_el1 definieren.

    ldr     x0, =vectors
    msr     vbar_el2, x0      // Set Vector Base Address Register for EL2

In der kurzen Phase, in der wir noch in EL2 sind, können Exceptions entstehen. Wir nutzen hier bereits unsere Vector-Tabelle, damit das System nicht abstürzt. Im Kernel-Modus verwenden wir später die gleiche Tabelle. Diese beschreibe ich später hier im Kapitel.

    // Enable generic timer access for EL1
    mrs     x0, cnthctl_el2
    orr     x0, x0, #0x3
    msr     cnthctl_el2, x0
    msr     cntvoff_el2, xzr   // Zero virtual timer offset

Interessant ist dieser Abschnitt! Damit wir auf EL1 Zugriff auf den Timer bekommen, müssen wir bereits hier im EL2-Modus dieses Recht an EL1 erteilen. Dazu müssen wir die Bits [1:0] jeweils auf 1 setzen. Mit cntvoff_el2 wird ein Offset des Timers erstellt. Damit wir aber die richtige CPU-Time haben, setzen wir den offset auf Null.

    // Pass core architecture identity to EL1
    mrs     x0, midr_el1
    mrs     x1, mpidr_el1
    msr     vpidr_el2, x0
    msr     vmpidr_el2, x1

Damit das System nach dem Wechsel zu EL1 die gleichen Informationen wie in EL2 hat, werden die entsprechenden Konfigurationsregister nach EL1 gespiegelt.

    // Disable coprocessor traps in EL2
    mov     x0, #0x33ff
    msr     cptr_el2, x0      // Disable CP10/CP11 traps
    msr     hstr_el2, xzr     // Disable hypervisor system traps

CPTR_EL2 (Architectural Feature Trap Register, EL2) ist ein Steuerregister, das festlegt, ob Zugriffe auf bestimmte Hardware-Features (wie Floating Point, Advanced SIMD/NEON, Timer oder Trace-Register) durch den Kernel (EL1) abgefangen und an den Hypervisor (EL2) gemeldet werden sollen.

Laut ARMv8-Handbuch müssen die RES1-Bits auf 1 gesetzt werden, ansonsten könnte es zu unvorhergesehenem Verhalten führen. Alle anderen Bits werden mit NULL definiert und damit deaktiviert. Der Wert 0x33ff ist aus dem Linux-Kernel übernommen.

HSTR_EL2 (Hypervisor System Trap Register) ist ein weiteres Register, das (hauptsächlich aus Kompatibilitätsgründen zur 32-Bit-Welt AArch32) steuert, ob Zugriffe auf bestimmte Systemregister via MRS/MSR-Befehlen in EL2 abgefangen werden. Auch hier deaktivieren wir es und setzen alles auf Null. Damit haben wir vollen Zugriff darauf.

    // Enable SIMD / FPU access in EL1 (Coprocessor Access Control)
    mov     x0, #3 << 20      // Set bits 20-21 (FPEN) to enable Full Access
    msr     cpacr_el1, x0

Das Register CPACR_EL1 (Architectural Feature Access Control Register) steuert auf EL1-Ebene, ob der Prozessor den Zugriff auf bestimmte Hardware-Features (wie die FPU oder Advanced SIMD/NEON) erlaubt oder ob er eine Exception (einen Trap) auslösen soll, wenn der Code versucht, diese zu nutzen.

Nach dem Reset oder Bootvorgang sind die FPU- und SIMD-Hardwareeinheiten der ARM-CPU aus Stromspargründen standardmäßig deaktiviert.

Der Befehl setzt die Bits 20 und 21 (genannt FPEN – Floating-Point Enable) im Register auf den binären Wert 0b11 (was der 3 entspricht). Laut ARM-Architektur-Spezifikation haben diese Bits folgende Bedeutung:

  • 0b00: Jede Nutzung von FPU/SIMD löst einen Trap (Exception) aus.
  • 0b01: Nur EL0 (User-Space) darf FPU/SIMD nutzen, EL1 (Kernel) nicht.
  • 0b10: Jede Nutzung löst einen Trap aus.
  • 0b11: Full Access. Es gibt keinerlei Abfangmechanismen mehr. FPU- und SIMD-Befehle werden normal von der Hardware ausgeführt.
    // Set Execution State for EL1: 64-bit AArch64 execution
    mov     x0, #(1 << 31)    // Bit 31 (RW): 1 = EL1 is AArch64
    msr     hcr_el2, x0

Das Register HCR_EL2 (Hypervisor Configuration Register) ist das Hauptkonfigurationsregister für den Hypervisor (EL2). Es steuert, wie sich der Prozessor verhält, wenn Code in den niedrigeren Ebenen (EL1 und EL0) ausgeführt wird. Da der Raspberry Pi 4 Virtualisierung unterstützt, muss EL2 dem System mitteilen, wie es die darunterliegende Ebene behandeln soll.
Das Bit 31 in diesem Register ist das sogenannte RW-Bit (Register Width). Es entscheidet über die "Bitness" (den Execution State) der nächstniedrigeren Exception Level (in diesem Fall EL1), wenn der Prozessor per eret (Exception Return) dorthin wechselt:

  • RW = 1: Die nächstniedrigere Ebene (EL1) läuft im 64-Bit-Modus (AArch64).
  • RW = 0: Die nächstniedrigere Ebene (EL1) läuft im 32-Bit-Modus (AArch32).
    // Set System Control Register defaults for EL1
    mov     x0, #0x0800
    movk    x0, #0x30d0, lsl #16
    msr     sctlr_el1, x0

Das Register SCTLR_EL1 (System Control Register, Exception Level 1) ist das zentrale Steuerregister für den Prozessor. Es regelt grundlegende Hardware-Features wie:

  • MMU (Memory Management Unit): Übersetzung von virtuellen in physische Adressen.
  • Caches: Aktivierung der L1/L2 Caches für Daten (D-Cache) und Instruktionen (I-Cache).
  • Alignment Checks: Überprüfung, ob Speicherzugriffe auf korrekten Adressgrenzen (z.B. 4-Byte oder 8-Byte) erfolgen.

Der ARMv8-Befehlssatz erlaubt es nicht, eine große 32-Bit-Zahl mit einem einzigen Befehl direkt in ein 64-Bit-Register (x0) zu laden. Daher wird der Wert in zwei Schritten zusammengesetzt:

  1. mov x0, #0x0800: Lädt die unteren 16 Bits (0x0800) in das Register x0.
  2. movk x0, #0x30d0, lsl #16: Der Befehl movk (Move Keep) lädt die oberen 16 Bits (0x30d0), verschiebt sie um 16 Stellen nach links (lsl #16) und behält die unteren 16 Bits bei.
  3. Ergebnis: Das Register x0 enthält am Ende den 32-Bit-Wert 0x30d00800.

Dieser spezifische Wert ist der Standard-Reset-Wert bzw. der empfohlene "sichere" Startzustand für ARMv8.0-Prozessoren (wie den Cortex-A72 im Raspberry Pi 4). Er setzt exakt die sogenannten RES1-Bits (Reserved, must be 1) auf 1, wie es das ARM-Handbuch vorschreibt.

Die wichtigsten Einstellungen, die dieser Wert implizit vornimmt, sind:

  • Bit 0 (M - MMU Enable) = 0: Die MMU ist ausgeschaltet.
  • Bit 2 (C - Data Cache Enable) = 0: Der Daten-Cache ist ausgeschaltet.
  • Bit 12 (I - Instruction Cache Enable) = 0: Der Instruktions-Cache ist ausgeschaltet.

Jetzt könnte ein Einwand kommen, da man sicherlich schon gehört hat, dass MMU recht praktisch ist. Zu dieser Zeit, während der Raspberry bootet, haben wir noch keine Page-Tables erzeugt. Wenn wir zu dieser Zeit die MMU einschalten würden, würde der Raspberry sofort abstürzen. MMU wird später in der Speicherverwaltung eingeschaltet.

    // Prepare Saved Program Status Register (SPSR_EL2) for return to EL1h
    mov     x0, #0x3c5        // Mask interrupts (D, A, I, F) and set EL1h mode
    msr     spsr_el2, x0

Das SPSR_EL2 (Saved Program Status Register) ist ein Schatten-Register. Wenn du später den Befehl eret ausführst, kopiert der Prozessor den Inhalt dieses Registers in den aktuellen Prozessorzustand (PSTATE). Du konfigurierst hier also im Voraus, mit welchen Rechten, welchem Stack und welchen Interrupt-Sperren dein Kernel in EL1 starten soll.

Der Wert 0x3c5 (Binär: 0011 1100 0101) setzt sich aus zwei logischen Bereichen zusammen, die den Prozessorzustand (PSTATE) für den Moment definieren, in dem der eret-Befehl (Exception Return) ausgeführt wird:

  • Bits 9:6 (DAIF-Maske) = 1111 (Hex 0x3C0)
    Diese vier Bits steuern die Interrupt-Maskierung. Eine 1 bedeutet, dass der entsprechende Interrupt-Typ blockiert (maskiert) wird:
D (Debug Exception): Maskiert
A (Asynchronous SError Abort): Maskiert (Hardware-Fehler)
I (IRQ - Normaler Interrupt): Maskiert
F (FIQ - Fast Interrupt): Maskiert

Warum? Das ist zwingend erforderlich für den Bootvorgang. Zu diesem Zeitpunkt hast du den Interrupt-Controller (GIC) des Raspberry Pi 4 noch gar nicht initialisiert und die Interrupt-Vektortabelle (VBAR_EL1) noch nicht final aktiviert. Würde genau jetzt z.B. ein Timer-Interrupt feuern, wüsste die CPU nicht, wohin sie springen soll, und das System würde abstürzen. Der Kernel startet also in einer "stillen" Umgebung.

  • Bits 4:0 (Execution State & Modus) = 00101

Diese Bits definieren, wie die CPU nach dem Sprung in EL1 arbeiten soll:
Bit 4 = 0: Die CPU bleibt im AArch64 (64-Bit) Modus.
Bits 3:0 = 0101: Dies definiert den Modus als EL1h (Exception Level 1, handler mode).

    // Set ELR_EL2 to target address for Exception Return
    adr     x0, el1_return
    msr     elr_el2, x0

    eret                      // Perform Exception Return (drops to EL1 at el1_return)

Der Befehl adr (Address) ist ein PC-relativer Ladebefehl. Er berechnet die aktuelle Speicheradresse des Labels el1_return (bezogen auf den aktuellen Program Counter) und lädt diese Adresse in das Register x0. Da wir uns noch im flachen physischen Speicher befinden (die MMU ist aus), ist dies die exakte physische RAM-Adresse, an der dein EL1-Code beginnt.

Das Register ELR_EL2 (Exception Link Register) dient der CPU als "Notizblock" für Rücksprünge. Normalerweise wird hier automatisch die Adresse gespeichert, an der ein Programm unterbrochen wird, wenn es per Exception (z.B. Systemaufruf oder Interrupt) in eine höhere Ebene (hier EL2) wechselt.

Da wir EL2 hier aber nicht durch eine Unterbrechung erreicht haben, sondern nur als Durchgangsstation nutzen, um das System zu konfigurieren, fälschen wir diesen Mechanismus: Wir schreiben manuell die Adresse von el1_return in dieses Register.

Der Befehl eret ist der einzige legitime Weg in der ARM-Architektur, um von einer höheren Exception Level in eine niedrigere zu wechseln.

Wenn die CPU eret ausführt, passieren intern drei Dinge gleichzeitig und atomar:

  • PSTATE-Update: Die CPU nimmt den Inhalt von SPSR_EL2 (welches du im vorherigen Schritt auf 0x3c5 gesetzt hast) und kopiert ihn in das Prozessor-Statusregister (PSTATE).
    Effekt: Die CPU ist nun im EL1h-Modus, läuft im 64-Bit-Modus und hat die Interrupts (DAIF) maskiert.
  • PC-Update: Die CPU nimmt den Inhalt von ELR_EL2 (die Adresse von el1_return) und schreibt ihn in den Program Counter (PC).
  • Level-Wechsel: Die CPU senkt hardwareseitig das Exception Level von EL2 auf EL1.
el1_return:
switch_to_el1:
    // ------------------------------------------------------------------------
    // 4. EL1 Runtime Configuration
    // ------------------------------------------------------------------------
    // Set Core 0 Kernel Stack Pointer (Top of Stack, grows downward)
    ldr     x1, =MEM_KERNEL_STACK
    mov     sp, x1

    // Enable Advanced SIMD / FPU (Enables execution of NEON / Float code generated by C compiler)
    mov     x0, #3 << 20      // FPEN bits [21:20] = 0b11 (No trapping of FP/SIMD instructions)
    msr     cpacr_el1, x0

Wir sind nun im Kernel-Modus (EL1). Da wir in erster Linie mit C programmieren, benötigen wir unbedingt einen Kernel-Stack. Den haben wir in der config.h definiert und setzen den sp auf diesen Wert.

Mit msr cpacr_el1, x0 erlauben wir die Nutzung von FPU und SIMD / NEON. Warum setzen wir cpacr_el1 hier noch einmal, obwohl es im EL2-Block stand? Ganz einfach: Wenn der Raspberry Pi 4 durch entsprechende Firmware-Einstellungen direkt in EL1 bootet, wird der gesamte EL2-Setup-Block oben per beq switch_to_el1 übersprungen! Ohne das erneute Setzen an dieser Stelle würde die FPU beim direkten EL1-Start deaktiviert bleiben und der C-Code bei der ersten Fließkomma- oder NEON-Optimierung sofort abstürzen.

    // Register Vector Table Base Address for EL1 Exceptions
    ldr     x0, =vectors
    msr     vbar_el1, x0

    // ------------------------------------------------------------------------
    // 5. Zero-initialize BSS Section (.bss)
    // ------------------------------------------------------------------------
bss_clean:
    ldr     x1, =__bss_start  // Load start boundary of .bss section
    ldr     x2, =__bss_end    // Load end boundary of .bss section

clean_bss_loop: 
    cmp     x1, x2            // Compare current address with end address
    bge     bss_clean_done    // Exit loop once end boundary is reached
    str     xzr, [x1], #8     // Store 64-bit zero and post-increment pointer by 8 bytes
    b       clean_bss_loop

bss_clean_done:

Das Register VBAR_EL1 (Vector Base Address Register (EL1)) sagt dem Prozessor, wo im Arbeitsspeicher er die sogenannte Vektortabelle findet.

Warum ist das nötig? Wenn im laufenden Betrieb ein Fehler auftritt (z. B. eine "Undefined Instruction", ein "Data Abort" durch ungültigen Speicherzugriff oder ein Hardware-Interrupt vom Timer), wechselt die CPU sofort in einen Exception-Modus. Sie schaut dann in das VBAR_EL1-Register und springt an die dort hinterlegte Basis-Adresse, um den passenden Fehler- oder Interrupt-Handler auszuführen.

=vectors: Dies ist ein Label, das wir später in der vector.S definieren. Es markiert den Start eines Speicherbereichs, der exakt nach den strengen ARM-Vorgaben aufgebaut ist (mit jeweils 16 Sprungmarken für verschiedene Exception-Typen). Ohne diesen Befehl wüsste der Raspberry Pi bei einem Fehler nicht, wohin er springen soll, und würde sich kommentarlos aufhängen.

BSS-Segment: In C gibt es globale oder statische Variablen, die bei der Deklaration keinen expliziten Startwert bekommen, z. B.:

int frame_buffer[1920 * 1080]; // Uninitialisiert

Nach dem C-Standard müssen solche Variablen beim Start des Programms automatisch den Wert 0 haben. Dieser Speicherbereich wird vom Compiler in das sogenannte .bss-Segment (Block Started by Symbol) gepackt.

Da wir nicht wissen, was in diesem Bereich im Speicher abgelegt ist, müssen wir diesen Bereich mit dem Wert 0 füllen. Dies machen wir mit dieser Schleife. Ab hier ist unser System bereit und wir können unseren Kernel in C schreiben. Zusätzlich gibt es noch folgenden Code, der optional ist, aber sehr hilfreich ist:

    // ------------------------------------------------------------------------
    // 6. Optional Debug Loop & Jump to Kernel Main
    // ------------------------------------------------------------------------
    ldr     x0, =0            // Set x0=1 via GDB or debugger to pause boot
debug_loop:
    nop                       // No Operation
    cbnz    x0, debug_loop    // Stay in loop if x0 != 0

Wenn hier der Wert von x0 auf 1 gesetzt wird, hängt der Raspberry in einer Dauerschleife. Dies ist recht nützlich, wenn der Debugger beim Start des Raspberrys noch nicht bereit ist. Sobald der Debugger läuft, können wir zunächst Breakpoints setzen und dann einfach x0 über den Debugger auf 0 setzen. Das Programm läuft dann bis zum Breakpoint, und man kann den Code besser überprüfen.

Der Abschluss:

    // Transfer control to C entry point
    b       main              // Jump to main() function

Hier springen wir in unsere main()-Funktion in C.

Die Exception Vector Table (VBT)

Die VBT ist das "Telefonbuch" des Prozessors. Bei einer Ausnahme schaut die CPU in dieses Register (vbar_el1) und springt zu der dort hinterlegten Adresse.

Die Tabelle muss exakt 2048 Byte (2 KB) groß sein (.align 11).

Sie besteht aus 16 Slots (4 Gruppen x 4 Exception-Typen). Jeder Slot ist exakt 128 Bytes (.align 7) groß. Das reicht genau aus, um ein paar Befehle direkt dort auszuführen. 

Die 4 Gruppen (Die Zeilen der Tabelle)

Die Gruppe wird von der Hardware automatisch basierend auf dem aktuellen Zustand der CPU ausgewählt, wenn eine Exception auftritt.

Gruppe 1: Current EL with SP0

Bedeutung: Eine Exception tritt in EL1 auf, während die CPU das Stack-Pointer-Register SP_EL0 verwendet (PSTATE.M = EL1t).
Wir haben in unserer boot.S mit 0x3C5 auf EL1h geschaltet. Damit nutzt unser Kernel das Register SP_EL1. Diese Gruppe wird im normalen Kernel-Betrieb nicht angesprungen. Sie würde nur greifen, wenn wir explizit auf SP_0 wechseln.

Gruppe 2: Current EL with SPx

Bedeutung: Eine Exception tritt in EL1 auf, während die CPU das eigene Stack-Pointer-Register SP_EL1 verwendet (PSTATE.M = EL1h).

Das ist die wichtigste Gruppe in unserem Kernel!

  • Wenn im laufenden C-Code (z. B. bei einer Division durch Null, einem Speicherfehler) eine Synchronous Exception auftritt, wird der erste Slot angewählt.
  • Wenn ein Hardware-Timer oder ein Peripherie-Gerät einen Interrupt auslöst, landet die CPU im zweiten Slot.

Gruppe 3: Lower EL using AArch64

Bedeutung: Eine Exception tritt auf, während der Code in einer niedrigeren Exception Level (EL0) im 64-Bit-Modus läuft, und löst einen Wechsel nach EL1 aus.

Sobald wir unser System, wie ein Betriebssystem nutzen und dort "fremde" Programme erlauben, wird es hier extrem wichtig, dass wir in unserem Kernel weiterhin die Kontrolle behalten. Fremde Programme lassen wir im EL0-Modus laufen. Wenn ein User-Programm einen Systemaufruf macht (svc #0), springt die CPU aus EL0 direkt in diesen Bereich der EL1-Vektortabelle.

Gruppe 4: Lower EL using AArch32

Bedeutung: Eine Exception tritt auf, während der Code in EL0 im alten 32-Bit-Modus ausgeführt wird.

Da wir ein reines 64-Bit-System (AArch64) bauen und keine alten 32-Bit-Anwendungen in EL0 unterstützen möchten, wird diese Gruppe in der Praxis nie angesprungen. Sie muss aber laut ARM64-Spezifikation als Platzhalter vorhanden sein, damit die Offsets der Vektortabelle stimmen.

Die 4 Exception-Typen (Die Spalten der Tabelle)

Innerhalb jeder der 4 Gruppen gibt es genau 4 Ereignistypen:

1. Synchronous (Synchrone Exception)

Was ist das? Fehler oder Ereignisse, die direkt durch den aktuell ausgeführten Befehl ausgelöst werden. Sie sind "synchron", weil sie genau an dieser Stelle im Programmcode reproduzierbar wieder auftreten, wenn man denselben Befehl erneut ausführt.

Beispiele:

  • Ausführung von svc #0 (Supervisor Call – den wir hier absichtlich programmieren werden).
  • Data Abort: Zugriff auf eine ungültige/nicht gemappte Speicheradresse.
  • Instruction Abort: Versuch, Code von einer ungültigen Speicheradresse auszuführen.
  • Alignment Fault: Unausgerichteter Speicherzugriff (falls Alignment Checks im SCTLR_EL1 aktiv sind).
  • Undefined Instruction: Die CPU stößt auf einen Befehl, den sie nicht kennt oder der unzulässig ist (z. B. FPU-Befehl bei deaktivierter FPU).

2. IRQ (Interrupt Request – Normaler Hardware-Interrupt)

Was ist das? Ein asynchroner Hardware-Interrupt von außen. Er hat nichts mit dem aktuell ausgedruckten Befehl zu tun, sondern wird von der Peripherie ausgelöst.

Beispiele:

  • Der System-Timer meldet, dass die Zeit abgelaufen ist.
  • Die Mini-UART / UART erhält ein neues Zeichen über die serielle Schnittstelle.
  • Die USB-Hardware (xHCI) meldet ein Datenpaket.

3. FIQ (Fast Interrupt Request)

Was ist das? Ein spezieller Hardware-Interrupt mit höherer Priorität als normale IRQs.

Bedeutung bei ARM64 / Pi 4: In modernen ARM64-Systemen mit einem GIC (Generic Interrupt Controller) wird FIQ selten für "schnelle Interrupts" genutzt, sondern oft für Secure World Interrupts (TrustZone) reserviert oder vom Hypervisor abgefangen. Bei einem normalen Bare-Metal-Kernel leitet man meist alle Hardware-Interrupts über IRQ.

4. SError (System Error / Asynchroner Abort)

Was ist das? Ein asynchroner Speicher- oder Hardware-Fehler.

Beispiel:

Der Prozessor schickt einen Schreibbefehl an den System-Bus (z. B. an die RAM- oder Peripherie-Hardware). Der CPU-Kern wartet aber nicht auf die Antwort, sondern rechnet weiter. Wenn das RAM / der Bus Nanosekunden später zurückmeldet: "Diese Adresse existiert nicht!", lässt sich das keinem einzelnen Befehl mehr zuordnen. Die CPU löst dann einen SError aus.

Mit diesen Informationen können wir nun unsere Vector-Tabelle erstellen.

Umsetzung für unser Projekt

In diesem Teil unsereres Tutorials testen wird zunächst “nur” eine Synchrone Exception, die wir absichtlich mit dem Supervisor Call (svc #0) auslösen, um zu sehen, ob das System so arbeitet, wie wir es möchten. Dazu erstellen wir zunächst ein kleines Macro in der Datei vector.S:

.section ".text"

// ----------------------------------------------------------------------------
// Macro: Entry Slot Handler
// ----------------------------------------------------------------------------
// Each vector entry MUST be 128-byte aligned (.align 7).
// ----------------------------------------------------------------------------
.macro vector_entry type
    .align 7
    mov     x0, #\type
    b       exception_common_handler
.endm

Dem Macro geben wir den Namen vector_entry, welchem wir einen Parameter “type” übergeben. An der Stelle, in dem dieses Macro eingesetzt wird, wird zunächst das wichtige .align 7 geschrieben. In x0 übergeben wir den Wert aus type und springen dann nach exception_common_handler.

Umgesetzt in die Vector-Tabelle sieht es dann so aus:

.align 11
.globl vectors
vectors:
    // Current EL with SP0
    vector_entry 0   // Synchronous
    vector_entry 1   // IRQ
    vector_entry 2   // FIQ
    vector_entry 3   // SError

    // Current EL with SPx
    vector_entry 4   // Synchronous
    vector_entry 5   // IRQ
    vector_entry 6   // FIQ
    vector_entry 7   // SError

    // Lower EL using AArch64
    vector_entry 8   // Synchronous
    vector_entry 9   // IRQ
    vector_entry 10  // FIQ
    vector_entry 11  // SError

    // Lower EL using AArch32
    vector_entry 12  // Synchronous
    vector_entry 13  // IRQ
    vector_entry 14  // FIQ
    vector_entry 15  // SError

Hiermit haben wir unsere Tabelle vollständig umgesetzt.

Wie im Macro beschrieben wurde, springen wir, egal welche Exception zur Zeit entsteht, nach exception_common_handler. Hier müssen wir nun noch etwas wichtiges beachten. Da eine Exception jederzeit unser laufende Programm unterbrechen kann und wir in der Regel auch wieder zurückkommen, müssen wir alle Register, die zum Zeitpunkt des Auftrettens der Exception sichern:

exception_common_handler:
    sub     sp, sp, #768

    stp     x0,  x1,  [sp, #16 * 0]
    stp     x2,  x3,  [sp, #16 * 1]
    stp     x4,  x5,  [sp, #16 * 2]
    stp     x6,  x7,  [sp, #16 * 3]
    stp     x8,  x9,  [sp, #16 * 4]
    stp     x10, x11, [sp, #16 * 5]
    stp     x12, x13, [sp, #16 * 6]
    stp     x14, x15, [sp, #16 * 7]
    stp     x16, x17, [sp, #16 * 8]
    stp     x18, x19, [sp, #16 * 9]
    stp     x20, x21, [sp, #16 * 10]
    stp     x22, x23, [sp, #16 * 11]
    stp     x24, x25, [sp, #16 * 12]
    stp     x26, x27, [sp, #16 * 13]
    stp     x28, x29, [sp, #16 * 14]
    str     x30,      [sp, #16 * 15]

    stp     q0,  q1,  [sp, #256 + 32 * 0]
    stp     q2,  q3,  [sp, #256 + 32 * 1]
    stp     q4,  q5,  [sp, #256 + 32 * 2]
    stp     q6,  q7,  [sp, #256 + 32 * 3]
    stp     q8,  q9,  [sp, #256 + 32 * 4]
    stp     q10, q11, [sp, #256 + 32 * 5]
    stp     q12, q13, [sp, #256 + 32 * 6]
    stp     q14, q15, [sp, #256 + 32 * 7]
    stp     q16, q17, [sp, #256 + 32 * 8]
    stp     q18, q19, [sp, #256 + 32 * 9]
    stp     q20, q21, [sp, #256 + 32 * 10]
    stp     q22, q23, [sp, #256 + 32 * 11]
    stp     q24, q25, [sp, #256 + 32 * 12]
    stp     q26, q27, [sp, #256 + 32 * 13]
    stp     q28, q29, [sp, #256 + 32 * 14]
    stp     q30, q31, [sp, #256 + 32 * 15]

Damit sind die Werte alle auf dem Stack gesichert. Erst jetzt beschreiben wir, was der Prozessor durchführen soll, wenn die Exception ausgelöst wurde. Für unseren Test gehen wir wie folgt vor:

    // Register x0 already contains the exception type ID (loaded in vector_entry)
    mrs     x1, esr_el1       // Argument 2: Exception Syndrome Register
    mrs     x2, elr_el1       // Argument 3: Exception Link Register (Return Address)
    bl      show_invalid_entry_message

x0 ist bereits aus der Vektor-Tabelle gesetzt und gibt uns den Typ der Exception an. In x1 speichern wir das “Exception Syndrome Register” und in x2 das “Exception Link Register” ab. Anschließend lassen wir uns diese Informationen mit der Funktion “show_invalid_entry_message” anzeigen. Wir erstellen diese später.

Wenn wir wieder aus der Funktion zurückkommen, setzen wir das System wieder auf den Ursprungszustand und laden die Register aus dem Stack zurück:

    ldp     q0,  q1,  [sp, #256 + 32 * 0]
    ldp     q2,  q3,  [sp, #256 + 32 * 1]
    ldp     q4,  q5,  [sp, #256 + 32 * 2]
    ldp     q6,  q7,  [sp, #256 + 32 * 3]
    ldp     q8,  q9,  [sp, #256 + 32 * 4]
    ldp     q10, q11, [sp, #256 + 32 * 5]
    ldp     q12, q13, [sp, #256 + 32 * 6]
    ldp     q14, q15, [sp, #256 + 32 * 7]
    ldp     q16, q17, [sp, #256 + 32 * 8]
    ldp     q18, q19, [sp, #256 + 32 * 9]
    ldp     q20, q21, [sp, #256 + 32 * 10]
    ldp     q22, q23, [sp, #256 + 32 * 11]
    ldp     q24, q25, [sp, #256 + 32 * 12]
    ldp     q26, q27, [sp, #256 + 32 * 13]
    ldp     q28, q29, [sp, #256 + 32 * 14]
    ldp     q30, q31, [sp, #256 + 32 * 15]

    ldp     x0,  x1,  [sp, #16 * 0]
    ldp     x2,  x3,  [sp, #16 * 1]
    ldp     x4,  x5,  [sp, #16 * 2]
    ldp     x6,  x7,  [sp, #16 * 3]
    ldp     x8,  x9,  [sp, #16 * 4]
    ldp     x10, x11, [sp, #16 * 5]
    ldp     x12, x13, [sp, #16 * 6]
    ldp     x14, x15, [sp, #16 * 7]
    ldp     x16, x17, [sp, #16 * 8]
    ldp     x18, x19, [sp, #16 * 9]
    ldp     x20, x21, [sp, #16 * 10]
    ldp     x22, x23, [sp, #16 * 11]
    ldp     x24, x25, [sp, #16 * 12]
    ldp     x26, x27, [sp, #16 * 13]
    ldp     x28, x29, [sp, #16 * 14]
    ldr     x30,      [sp, #16 * 15]

    add     sp, sp, #768
    eret

Mit eret gehen wir wieder zu unserem eigentlichen Programm zurück.

🛠️ Debugging-Hinweis: Die SynchError-Handler sind extrem wertvoll. Wenn dein Programm unerwartet in SynchError2 landet, weißt du: Ein Befehl in EL1 war ungültig oder hat einen Fehler verursacht.

Auswertung der Exception (exception.c)

Wie zuvor beschrieben, wurde in der Vektor-Tabelle die Funktion show_invalid_entry_message verwendet, um Informationen zu der ausgelösten Exception anzuzeigen. Diese Funktion beschreiben wir in der exception.c

#include "printf.h"
#include "types.h"

void show_invalid_entry_message(u32 type, u64 esr, u64 elr)
{
    printf("\n");
    printf("=====================================================\n");
    printf("         [ EXCEPTION HANDLER ]                       \n");
    printf("=====================================================\n");
    printf("  Exception Type: 0x%X\n", type);
    printf("  ESR (Exception Syndrome Reg): 0x%016llX\n", esr);
    printf("  ELR (Exception Link Reg):     0x%016llX\n", elr);

    // Extract Exception Class (EC) from bits [31:26]
    u32 ec = (esr >> 26) & 0x3F;

    if (ec == 0x15) // 0x15 = System call (SVC instruction execution in AArch64)
    {
        printf("  Cause: Supervisor Call (SVC) intercepted successfully!\n");
        
        // IMPORTANT: Advance ELR_EL1 by 4 bytes to skip the 'svc #0' instruction!
        // Prevents execution from returning to the same faulting instruction.
        asm volatile ("mrs x0, elr_el1 \n"
                      "add x0, x0, #4  \n"
                      "msr elr_el1, x0 \n"
                      ::: "x0");
    }
    else
    {
        printf("  Cause: Unhandled Exception (EC Class: 0x%X)\n", ec);
        printf("  System Halted to prevent crash loop.\n");
        printf("=====================================================\n\n");
        
        // Halt execution for fatal/unhandled exceptions (Data Abort, Faults, etc.)
        while (1)
        {
            asm volatile ("wfe");
        }
    }

    printf("=====================================================\n\n");
}

In unserem Test werden wir absichtlich eine Exception mit einem Supervisor Call auslösen. Dies Information können wir aus dem esr_el1 erfahren, welchen wir als Parameter 2 übergeben haben. Dazu extrahieren wir den Wert. Wenn es “0x15” ist, wissen wir, dass es ein SystemCall war. Da wir es hier erwarten, setzen wir die Adresse im elr_el1-Register um 4 Bytes nach oben, damit wir diese Ausnahme nicht nochmal bekommen und unser System ohne Probleme weiterlaufen kann.

Sollte es eine andere Exception geben, lassen wir uns die “EC Class” anzeigen und lassen die CPU mit wfe schlafen. Also unser System würde hier dann stehen bleiben.

Exceptions erlauben (Optional)

Da wir bereits in unsere boot.S bereits das vbar_el1 -Register auf die Vektor-Tabelle gesetzt haben, ist diese Routine technisch redundant. Allerdings möchte mancher Entwickler dies nicht der Boot-Initialisierung überlassen, sondern ihren eigenen Weg gehen. Ich habe diese Funktion im Code belassen, da es nicht schadet, es doppelt zu setzen.

void Exception_Initialize(void)
{
    asm volatile ("msr vbar_el1, %0" : : "r" (vectors));
    asm volatile ("isb" ::: "memory");
}

Die Exception_Initialize-Funktion macht hier nichts anderes, als es die boot.S bereits macht. Sie setzt den Zeiger der Vektor-Tabelle in das vbar_el1-Register. Zusätzlich führt es ein isb (Instruction Synchronization Barrier) aus, was nicht schadet.

Hier ist es dem Entwickler überlassen, ob er die Funktion verwendet, oder auch nicht. Möglich wäre es auch, dies aus dem Boot-Prozess herauszunehmen und nur diese Funktion zu verwenden. Es schadet aber auch nicht, es doppelt durchzuführen.

Der Blindtest: svc #0 (Supervisor Call)

Bevor wir uns mit komplexer Hardware (wie dem GIC-Interrupt-Controller) herumschlagen, müssen wir sicherstellen, dass unser Exception-Mechanismus grundlegend funktioniert.

Der Befehl svc #0 (Supervisor Call) löst absichtlich eine synchrone Exception aus. Da wir uns in EL1 befinden, sollte die CPU in den Eintrag SynchError2 unserer Vektortabelle springen.

Hier unsere angepasste main-Funktion:

void main(void)
{
    LED_off();
    Init_Screen();
    InitTerminal();

    SetFrontColor(satyr);
    printf("*****************************************************************************************\n");
    printf("***                                                                                   ***\n");
    printf("***                                (c) by Satyria                                     ***\n");
    printf("***                                                                                   ***\n");
    printf("*****************************************************************************************\n");
    SetFrontColor(white);

    printf("1. Initializing Exception Vector Table (VBAR_EL1)...\n");
    Exception_Initialize();
    printf("   -> VBAR_EL1 successfully set.\n\n");

    printf("2. TRIGGERING SYNCHRONOUS EXCEPTION (Instruction: svc #0)...\n");

    asm volatile ("svc #0");

    printf("3. SUCCESS: Returned safely from Exception Handler!\n");
    printf("   The system continues to execute stably.\n");

    LED_Error(2);
}

Was macht nun dieser Code:

  • Zunächst Initialisieren wir unser System
  • Erzeugen einen Screen
  • Erstellen unser Terminal
  • Zeigen mit printf ein paar Zeilen an
  • Initialisieren die Exceptions (Optional)
  • Lösen dann die Exception mit svc #0 aus
    -> Es wird über die Exception die Funktion show_invalid_entry_message() aufgerufen
  • Zeige es an, dass wir zurück sind
  • LED_Error(2) zeigt, dass wir noch leben.

Den Sourcecode dafür gibt es hier: https://www.satyria.de/arm/sources/RPI4/C/Inter1.zip

Verwendung der Exceptions zum Debuggen

Nun wissen wir, dass unsere Vektor-Tabelle funktioniert. Nun nutzen wir es, um unvorhergesehene Exceptions abzufangen und lassen uns wichtige Systeminformationen anzeigen. Dies ist sehr hilfreich, wenn wir unser System weiterentwickeln.

Zunächst erweitern wir unsere vector.S die exception_common_handler()-Funktion:

    …    
    stp     q28, q29, [sp, #256 + 32 * 14]
    stp     q30, q31, [sp, #256 + 32 * 15]

    sub     sp, sp, #64                // Platz für Systemregister schaffen

    mrs     x1, elr_el1
    mrs     x2, far_el1
    stp     x1, x2,   [sp, #0]         // Offset 0 (elr), Offset 8 (far)

    mrs     x1, esr_el1
    mrs     x2, spsr_el1
    stp     x1, x2,   [sp, #16]        // Offset 16 (esr), Offset 24 (spsr)

    add     x1, sp, #832               // Ursprünglichen SP berechnen (768 + 64)
    str     x1,       [sp, #32]        // Offset 32 (sp)

    add     x1, sp, #64                // Zeiger auf x0-x30
    str     x1,       [sp, #40]        // Offset 40 (gp_regs pointer)

    // Aufruf: show_invalid_entry_message(code, context)
    // x0 enthält bereits den Vector-Code (0-15)
    mov     x1, sp                     // Zeiger auf unsere System-Info Struktur
    bl      show_invalid_entry_message

restore_registers:
    // --- D. Restore NEON/SIMD Floating Point Registers ---
    ldp     q0,  q1,  [sp, #256 + 32 * 0]
    ldp     q2,  q3,  [sp, #256 + 32 * 1]
    …

Hiermit legen wir den Inhalt der System-Register elr_el1, far_el1, esr_el1, spsr_el1 und den Stack (sp) auf den Stack ab. Wir übergeben x0 und x1, welche einen Zeiger auf den Stack enthält der Funktion show_invalid_entry_message().

Nun erstellen wir eine neue exception.c, die diese Informationen auswertet und anzeigt:

// ============================================================================
// exception.c - C-level Exception Handler & Diagnostics
// ============================================================================

#include "printf.h"
#include "types.h"
#include "screen.h"
#include "sync.h"

// Passt exakt zum Diagnosestack aus vector.S bei tödlichen Exceptions
typedef struct PACKED {
    u64 elr;            // Offset 0
    u64 far;            // Offset 8
    u64 esr;            // Offset 16
    u64 spsr;           // Offset 24
    u64 sp;             // Offset 32: Ursprünglicher Stack Pointer
    u64 *gp_regs;       // Offset 40: Zeiger auf x0-x30 auf dem Hauptstack
} ExceptionContext;

static const char *vector_names[16] = {
    "Current EL with SP0 - Synchronous",
    "Current EL with SP0 - IRQ",
    "Current EL with SP0 - FIQ",
    "Current EL with SP0 - SError",
    "Current EL with SPx - Synchronous",
    "Current EL with SPx - IRQ",
    "Current EL with SPx - FIQ",
    "Current EL with SPx - SError",
    "Lower EL (AArch64) - Synchronous",
    "Lower EL (AArch64) - IRQ",
    "Lower EL (AArch64) - FIQ",
    "Lower EL (AArch64) - SError",
    "Lower EL (AArch32) - Synchronous",
    "Lower EL (AArch32) - IRQ",
    "Lower EL (AArch32) - FIQ",
    "Lower EL (AArch32) - SError"
};

static const char* decode_esr_ec(u32 esr)
{
    u32 ec = (esr >> 26) & 0x3F;
    switch (ec)
    {
        case 0x00: return "Unknown reason";
        case 0x01: return "Trapped WFI/WFE instruction";
        case 0x0E: return "Illegal Execution state";
        case 0x15: return "SVC instruction execution (AArch64)";
        case 0x18: return "Trapped MSR/MRS/System instruction";
        case 0x20: return "Instruction Abort (Lower EL)";
        case 0x21: return "Instruction Abort (Current EL)";
        case 0x22: return "PC alignment fault";
        case 0x24: return "Data Abort (Lower EL)";
        case 0x25: return "Data Abort (Current EL)";
        case 0x26: return "SP alignment fault";
        case 0x2C: return "Trapped Floating Point exception";
        case 0x2F: return "SError interrupt";
        case 0x30: return "Breakpoint exception (Lower EL)";
        case 0x31: return "Breakpoint exception (Current EL)";
        case 0x34: return "Watchpoint exception (Lower EL)";
        case 0x35: return "Watchpoint exception (Current EL)";
        default:   return "Reserved / Unclassified Fault";
    }
}

void show_invalid_entry_message(u32 code, ExceptionContext *context)
{
    const char *Exmsg = (code < 16) ? vector_names[code] : "Unknown Vector";

    SetFrontColor(red);
    printf("\n==================================================\n");
    printf("!!! FATAL SYSTEM EXCEPTION OCCURRED !!!\n");
    printf("Vector Triggered: %s (Internal Code: %d)\n", Exmsg, code);
    printf("==================================================\n");

    SetFrontColor(yellow);
    if (context != NULL)
    {
        printf("Faulting Instruction (ELR): 0x%016llx\n", context->elr);
        printf("Faulting Memory Addr (FAR): 0x%016llx\n", context->far);
        printf("Syndrome Register    (ESR): 0x%08llx\n", context->esr);
        printf("  -> Decoded EC:           %s\n", decode_esr_ec((u32)context->esr));
        printf("  -> Specific ISS Code:    0x%llx\n", context->esr & 0x1FFFFFFULL);
        printf("Saved Processor State(SPSR): 0x%08llx\n", context->spsr);
        printf("Stack Pointer         (SP): 0x%016llx\n", context->sp);
        printf("--------------------------------------------------\n");

        printf("Post-Mortem Register Dump:\n");
        if (context->gp_regs != NULL)
        {
            for (int i = 0; i < 30; i += 2)
            {
                printf("x%02d: 0x%016llx   x%02d: 0x%016llx\n", 
                       i, context->gp_regs[i], 
                       i + 1, context->gp_regs[i + 1]);
            }
            printf("x30: 0x%016llx (Link Register / Return Address)\n", context->gp_regs[30]);
        }
    }
    printf("==================================================\n");

    SetFrontColor(white);
    
    // Anhalten des Systems
    Stop();
}

Alle Exceptions, die nicht geplant sind, werden hier nun mit wichtigen Informationen im Ist-Zustand angezeigt. Auch unser Blindtest, den wir zuvor programmiert haben. Im Gegensatz zu der vorhergehenden Version gehen wir nicht mehr zurück, sondern erzeugen mit dem Macro “Stop()” eine Endlosschleife und bleiben hier hängen.

In der späteren Entwicklung hat mir diese Anzeige viele Gründe genannt, warum das System abgestürzt ist.

Den Sourcecode gibt es hier: https://www.satyria.de/arm/sources/RPI4/C/Inter1.2.zip 

Fazit & Ausblick: Das Fundament steht!

Herzlichen Glückwunsch! Du hast einen wichtigen Schritt in der Bare-Metal-Entwicklung auf dem Raspberry Pi 4 gemeistert:

  • Boot-Sequenz (boot.S): Wir haben die CPU-Kerne sortiert, sind sicher von EL2 auf EL1 gewechselt, haben die FPU aktiviert und den BSS-Speicherbereich genullt.
  • Vektortabelle (vector.S): Wir haben eine ARM64-konforme Ausnahmetabelle im Speicher registriert.
  • Erster System-Call (main.c): Mit dem Befehl svc #0 haben wir bewusst eine synchrone Exception ausgelöst. Unser C-Handler hat sie abgefangen, verarbeitet, das Rücksprungregister um 4 Bytes erhöht und die Kontrolle sicher an main() zurückgegeben.
  • Das blinkende LED-Signal (LED_Error(2)) am Ende zeigt uns unmissverständlich: Die CPU läuft stabil im Kernel-Modus weiter!

Wie geht es als Nächstes weiter?

In diesem Kapitel haben wir uns ausschließlich auf synchrone Exceptions (vom Prozessor selbst erzeugte Ereignisse) konzentriert.

Im nächsten Kapitel widmen wir uns den asynchronen Hardware-Interrupts (IRQs):

  • Wir werden den GIC-400 (Generic Interrupt Controller) des Raspberry Pi 4 konfigurieren.
  • Wir schalten die Interrupts in der CPU frei (msr DAIFClr, #2).
  • Wir lassen das System mit einem Hardware-Timer in regelmäßigen Abständen automatisch per IRQ reagieren!