💻 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ührt wurden, kannst 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 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. Mit cbz vergleichen wir das Ergebnis mit Null (Core 0) und springen weiter. Die restlichen Cores lassen wir mit wfe schlafen.

Der Wechsel von EL2 nach EL1

Da wir einen Kernel schreiben, benötigen wir die volle Kontrolle über das System im Exception Level 1 (EL1).

    // ------------------------------------------------------------------------
    // 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 entspricht dezimal 4. Mit cmp x0, #4 prüfen wir somit direkt, ob der Bit-Wert für EL1 anliegt.

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

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

    // 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

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

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

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

    // Set Execution State for EL1: 64-bit AArch64 execution
    mov     x0, #(1 << 31)    // Bit 31 (RW): 1 = EL1 is AArch64
    msr     hcr_el2, x0

    // Set System Control Register defaults for EL1
    mov     x0, #0x0800
    movk    x0, #0x30d0, lsl #16
    msr     sctlr_el1, x0

    // 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

    // 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)
el1_return:
switch_to_el1:
    // ------------------------------------------------------------------------
    // 4. EL1 Runtime Configuration
    // ------------------------------------------------------------------------
    ldr     x1, =MEM_KERNEL_STACK
    mov     sp, x1

    mov     x0, #3 << 20      // FPEN bits [21:20] = 0b11
    msr     cpacr_el1, x0

    ldr     x0, =vectors
    msr     vbar_el1, x0

bss_clean:
    ldr     x1, =__bss_start
    ldr     x2, =__bss_end

clean_bss_loop: 
    cmp     x1, x2
    bge     bss_clean_done
    str     xzr, [x1], #8
    b       clean_bss_loop

bss_clean_done:
    b       main

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.

.section ".text"

.macro vector_entry type
    .align 7
    mov     x0, #\type
    b       exception_common_handler
.endm

.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

Auswertung der Exception (exception.c)

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

void show_invalid_entry_message(u32 type, u64 esr, u64 elr)
{
    printf("\n=====================================================\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);

    u32 ec = (esr >> 26) & 0x3F;

    if (ec == 0x15)
    {
        printf("  Cause: Supervisor Call (SVC) intercepted successfully!\n");
        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");
        
        while (1)
        {
            asm volatile ("wfe");
        }
    }

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

Den vollständigen Quellcode dazu findest du hier: https://www.satyria.de/arm/sources/RPI4/C/Inter1.2.zip