|
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 (wiesvc #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.
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