Interrupts (PI4): Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
KKeine Bearbeitungszusammenfassung
KKeine Bearbeitungszusammenfassung
 
(36 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
Interrupts sind Signale, die den normalen Ablauf eines Prozessors unterbrechen, um spezielle Routineabläufe auszuführen. Sie spielen eine wichtige Rolle bei der Steuerung und Kommunikation in Echtzeitsystemen und bei der Handhabung von Hardware-Ereignissen.
<span style="background:#FF0000; color:#FFFFFF;"> Wichtig! Durch die Entwicklung zur USB-Unterstützung wurde 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. </span>


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


Ein Interrupt ist ein Signal, das den CPU dazu veranlasst, den aktuell ausgeführten Befehlssatz zu unterbrechen und eine Interrupt Service Routine (ISR) oder Interrupt-Handler auszuführen.
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.


== Auslöser für Interrupts ==
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.


Interrupts können durch verschiedene Ereignisse ausgelöst werden:
== Exceptions vs. Interrupts: Was ist der Unterschied? ==


=== Externe Hardware-Ereignisse ===
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.


* Tastatureingaben: Wenn eine Taste gedrückt wird.
''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.
* Mausbewegungen: Wenn die Maus bewegt wird oder eine Taste gedrückt wird.
* Timer: Timer können so konfiguriert werden, dass sie periodische Interrupts auslösen.


=== Interne Hardware-Ereignisse ===
== Die Spielregeln: Exception Levels (EL) und DAIF ==


* Peripheriegeräte: Ereignisse von Geräten wie Netzwerkadaptern, Festplatten, usw.
Der ARM Cortex-A72 kennt verschiedene Privilegienstufen, die '''Exception Levels (EL)''':
* Signale von Sensoren: Daten von Temperatursensoren, Beschleunigungssensoren, usw.
* '''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)


=== Betriebssystem-Ereignisse ===
'''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.


* Systemaufrufe: Bestimmte Anfragen von Software an das Betriebssystem.
Zudem kontrolliert das '''DAIF'''-Register, welche Interrupts gerade erlaubt sind:
* Fehlermeldungen: Wenn Fehler wie Division durch Null oder Speicherzugriffsfehler auftreten.
* '''D''' (Debug): Debug-Exceptions sperren
* '''A''' (SError): Asynchrone Systemfehler sperren
* '''I''' (IRQ): Normale Interrupts sperren
* '''F''' (FIQ): Schnelle Interrupts (Fast IRQs) sperren


== Typen von Interrupts ==
Ist ein Bit auf `1` gesetzt, ist der Interrupt '''gesperrt'''. Um IRQs zu erlauben, müssen wir das I-Bit auf `0` setzen (clear).


Interrupts können auch kategorisiert werden in:
== Vorbereitung: Nur ein Kern darf laufen ==


* Maskierbare Interrupts: Diese können vom CPU vorübergehend ignoriert oder maskiert werden.
Der 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).
* Nicht maskierbare Interrupts (NMI): Diese können nicht ignoriert werden und haben höchste Priorität.


----
<syntaxhighlight lang="asm">
Ja, der Raspberry Pi 4 muss sich in einem bestimmten Betriebsmodus befinden, um Interrupts effizient zu handhaben. Besonders wichtig sind die Interrupt-Freigabe (globale Interrupts) und der Modus des Prozessors.
// In boot.S
 
Betriebsmodi und Interrupts
 
Der ARM-Cortex-A72-Prozessor, der im Raspberry Pi 4 verwendet wird, unterstützt verschiedene Ebenen und Modi, die sich auf das Verhalten von Interrupts auswirken können:
 
Interrupt Enable Stufe (EL1 und niedrigere Ebenen):
 
Der Prozessor muss sich in einem Modus befinden, in dem Interrupts erlaubt sind (EL1 oder niedriger). Dies bedeutet, dass die globalen Interrupts nicht deaktiviert sein dürfen. Dies wird durch das Setzen bestimmter Bits im Program Status Register (PSTATE) kontrolliert.
 
DAIF-Flags (erlauben Interrupts):
 
D: Debug-Exceptions
A: SError-Interrupts
I: IRQ-Interrupts
F: FIQ-Interrupts Diese Flags können im DAIF-Register eingestellt werden. Um IRQ-Interrupts zu ermöglich, sollte das I-Flag gelöscht werden.
Wichtige Befehle und Register
 
Hier ein Überblick über die relevanten Register und Befehle:
 
Programmstatusregister (PSTATE): Kontrolliert u.a. die globalen Interrupts.
Beispiel (ARM-Assembly) zur Aktivierung von Interrupts
 
Folgender Code zeigt, wie man die IRQ-Interrupts explizit zulässt:
 
.global enable_interrupts
 
enable_interrupts:
    mrs x0, DAIF          // DAIF Register lesen
    bic x0, x0, #(1<<7)    // IRQ-Freigabe (Setze I-Flag auf 0)
    msr DAIF, x0          // DAIF Register zurückschreiben
    ret
 
Setup und Initialisierung
 
Stack Pointer setzen: Stelle sicher, dass der Stack Pointer korrekt initialisiert ist, bevor Interrupts aktiviert werden.
 
Setzen des Vektortabellenpointers: Leitet den Prozessor zu der richtigen Vektortabelle, welche die Adressen der ISR enthält.
 
.global _start
_start:
_start:
     // Setup Stack Pointer und andere Initialisierungen hier
     mrs x1, mpidr_el1    // Core-ID lesen
     //...
    and x1, x1, #3      // Nur die unteren 2 Bits (0-3) behalten
     cbz x1, core0        // Wenn Core 0, weiter zum Setup


     // Set Vector Base Address Register (VBAR)
core_sleep:
     LDR  x0, =vector_table
     wfe                  // Warte auf ein Event (Sleep)
    MSR  VBAR_EL1, x0      // VBAR auf die Vektortabelle setzen
     b core_sleep        // Endlosschleife für Core 1-3


     // Interrupts aktivieren
core0:
    BL enable_interrupts
     // Ab hier läuft nur noch Core 0
</syntaxhighlight>


    // Hauptprogrammschleife
== Der Wechsel von EL2 nach EL1 ==
main_loop:
    //...
    B main_loop


.section .vectors
Da wir ein Kernel schreiben, gehört unser Platz in EL1. Der folgende Code konfiguriert EL1, setzt den Stackpointer und bereitet den Rücksprung vor.
.align 11
vector_table:
    b  irq_handler          // IRQ Interrupt Handler Adresse
    // Weitere Vektoren je nach Bedarf


Zusammenfassung
<syntaxhighlight lang="asm">
Der Raspberry Pi 4 muss sich auf einem Berechtigungslevel (EL1 oder niedriger) befinden, in dem Interrupts (speziell IRQs) erlaubt sind.
    // 1. Prüfen, ob wir schon in EL1 sind (CurrentEL = 8 für EL1)
Die DAIF-Register müssen so eingestellt werden, dass Interrupts zugelassen werden.
    mrs x0, CurrentEL
Eine korrekte Initialisierung der Vektortabelle und des Stack-Pointers sind essentiell, bevor Interrupts zugelassen werden.
    cmp x0, #8
    beq switch_to_el1


Der ARM Cortex-A72 Prozessor, der im Raspberry Pi 4 verwendet wird, unterstützt ein hierarchisches Berechtigungslevel-System, das als Exception Levels (EL) bekannt ist. Jedes Level hat unterschiedliche Berechtigungsstufen und Verwendungszwecke. Hier sind die verschiedenen Exception Levels und ihre typischen Verwendungen:
    // 2. EL1 Stackpointer setzen
    ldr x0, =EXCEPTION_STACK
    msr sp_el1, x0


Exception Levels (ELs):
    // 3. Vektortabelle für EL2 setzen (für den Fall, dass während des Wechsels was passiert)
    ldr x0, =VectorTable
    msr vbar_el2, x0


EL0:
    // ... (Hier folgen Timer- und Virtualisierungs-Einstellungen, siehe boot.S) ...


User Mode
    // 4. Rücksprung-Adresse und Status für EL1 vorbereiten
Läuft im nicht-privilegierten Modus.
    mov x0, #0x3c4            // SPSR_EL2: EL1h, Interrupts (DAIF) sind hier noch maskiert
Dieser Level ist für Anwendungen und Benutzerprogramme vorgesehen. Hier laufen die nicht-privilegierten Aufgaben des Betriebssystems oder der Anwendungen.
    msr spsr_el2, x0
    adr x0, el1_return        // Adresse, zu der wir nach dem Wechsel springen
    msr elr_el2, x0


EL1:
    eret                      // Exception Return: Wechselt nach EL1 zu 'el1_return'


Kernel Mode
el1_return:
Läuft im privilegierten Modus.
switch_to_el1:
Wird von Betriebssystemkernen oder hypervisorischen Dienstprogrammen verwendet. Hier findet die Hauptverwaltung der Hardware und des Speichers statt.
    // Wir sind jetzt in EL1!
Dieser Level hat Zugriff auf alle Systemressourcen und kann Interrupts verwalten.
    ldr x0, =VectorTable
    msr vbar_el1, x0          // Vektortabelle für EL1 aktivieren


EL2:
    // BSS-Segment (uninitialisierte Variablen) mit Nullen füllen
    ldr x1, =__bss_start
    ldr w2, =__bss_size
clean_bss_loop:
    cbz w2, bss_clean_done
    str xzr, [x1], #8
    sub w2, w2, #1
    cbnz w2, clean_bss_loop
bss_clean_done:


Hypervisor Mode
    b main                    // Ab ins C-Hauptprogramm!
Ebenfalls als Virtualization Exception Level bekannt, das für die Ausführung eines Hypervisors verwendet wird, der virtuelle Maschinen verwaltet.
</syntaxhighlight>
Es ermöglicht die Erstellung und Verwaltung mehrerer EL1-Umgebungen (z. B. verschiedene Betriebssysteme auf einer Hardware).


EL3:
== Die Exception Vector Table (VBT) ==


Secure Monitor Mode
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.  
Das höchste privilegierte Level, das für sichere Anwendungen und das TrustZone-Sicherheits-Monitor-Firmware verwendet wird.
Hier können sicherheitskritische Funktionen ausgeführt werden, die von den anderen Levels abgeschottet sind.
Zustände, die Interrupts betreffen:
Normalerweise laufen Betriebssystem-Kernel und ihre ISRs auf EL1.
Benutzeranwendungen laufen auf EL0 und machen Systemaufrufe, um in EL1-Privilegienstufe zu wechseln.
Beispiel zur Entscheidung des Berechtigungslevels:


Beim Initialisieren des Raspberry Pi 4 sollten folgende Schritte durchgeführt werden, um sicherzustellen, dass sich das System im korrekten Level befindet:
Die Tabelle muss exakt '''2048 Byte (2 KB)''' groß sein (''.align 11''). Sie ist in 16 Einträge unterteilt (4 Ausnahmetypen × 4 Ausführungsstatus). Jeder Eintrag muss exakt '''128 Byte''' (''.align 7'') voneinander entfernt sein.


.global _start
<syntaxhighlight lang="asm">
// In vector.S
.align 11                      // 2^11 = 2048 Byte Ausrichtung für die ganze Tabelle
.globl VectorTable
VectorTable:
    // 1. Aktuelles EL, Stackpointer ist SP_EL0 (User-Mode, nutzen wir nicht)
    .align 7
    b SynchError1
    .align 7
    b GenericIRQHandler
    .align 7
    b FastInterruptHandler
    .align 7
    b SErrorStub1


_start:
    // 2. Aktuelles EL, Stackpointer ist SP_ELx (Kernel-Mode, EL1h) -> DAS NUTZEN WIR!
     // Initialisierung der Exception Levels (wenn notwendig)
    .align 7
     b SynchError2              // Synchronous Exception (z.B. svc #0, Memory Abort)
    .align 7
    b GenericIRQHandler        // Normaler Hardware-Interrupt (IRQ)
    .align 7
    b FastInterruptHandler    // FIQ
    .align 7
    b SErrorStub2              // System Error


     // Setze SP für EL1
     // 3. Niedrigeres EL (EL0), 64-Bit (Auch hier leiten wir auf unsere Handler um)
     LDR x1, =stack_top_el1
    .align 7
     MSR SP_EL1, x1
    b SynchError1
    .align 7
     b GenericIRQHandler
     // ... (weitere Einträge siehe vector.S)
</syntaxhighlight>


    // Wechsel zu EL1
🛠️ '''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. Wir werden das gleich gezielt provozieren, um zu prüfen, ob die Tabelle funktioniert.
    MRS x0, CurrentEL          // Aktuelles Exception Level lesen
    CMP x0, #4                // Prüfen ob aktuell ELx ist (gesetzt bei EL3)
    B.LE init_el              // Wenn bei EL1 oder niedriger, Initialisierung fortführen


    // Wechsel von EL3 zu EL1
== Der Blindtest: `svc #0` (Supervisor Call) ==
    BL switch_to_el1


init_el:
Bevor wir uns mit komplexer Hardware (wie dem GIC-Interrupt-Controller) herumschlagen, müssen wir sicherstellen, dass unser Exception-Mechanismus grundlegend funktioniert.  
    // EL1 Initialisierungslogik (z.B. VBAR, Stack, Interrupts etc.)
    ADR x0, vector_table      // Lade Adresse der Vektortabelle
    MSR VBAR_EL1, x0          // Setze VBAR_EL1 auf die Vektortabelle


    // Aktiviere Interrupts
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.
    BL enable_interrupts


    // Hauptprogrammschleife
Füge diesen Test vorübergehend in deine `main.c` ein, *bevor* du `printf` oder andere Initialisierungen machst:
main_loop:
    B main_loop


// Funktion um von EL3 zu EL1 zu wechseln
<syntaxhighlight lang="C">
switch_to_el1:
// In main.c
    MSR SCR_EL3, #0x6          // Konfiguriere Secure Configuration Register (SCR) für EL1
#include "printf.h"
    MRS x0, SPSR_EL3          // Sichere aktuelle SPSR_EL3
#include "sync.h" // Für Stop()
    BIC x0, x0, #(0xF << 6)    // Setze EL1
    ORR x0, x0, #0xD3          // Maskiere IRQ und FIQ und setze EL1h
    MSR SPSR_EL3, x0          // Schreibe das veränderte Register zurück
    ADR x0, el1_entry_point    // Lade Einstiegspunkt für EL1
    MSR ELR_EL3, x0            // Setze ELR_EL3 auf Einstiegspunkt
    ERET                      // Ausnahme zurückgeben über Exception Return


el1_entry_point:
int main(void)
     // Hier beginnt die Ausführung auf EL1
{
     RET
    // --- BLINDTEST FÜR EXCEPTIONS ---
    printf("Starte Exception-Blindtest...\n");
   
    // Dieser Befehl löst absichtlich eine synchrone Exception aus.
     // Die CPU sollte in vector.S -> SynchError2 -> ExceptionHandler springen.
     asm volatile("svc #0");
      
      
.section .vectors, "a"
    // Diese Zeile sollte NICHT erreicht werden!
vector_table:
    printf("FEHLER: svc #0 wurde nicht abgefangen!\n");
     B irq_handler              // IRQ Adresse
     Stop();
     // Weitere Vektoren für verschiedene Interruptarten
     // ----------------------------------


// Dummy-Interrupt-Handler
    // ... Rest deiner Initialisierung (Interrupt_Initialize, Timer, etc.) ...
irq_handler:
}
    // Interrupt Service Routine Logik hier
</syntaxhighlight>
    RET


// Speicherplatz für Stack (zum Beispiel bei EL1)
**Erwartetes Ergebnis:**
.section .bss
Auf dem Bildschirm sollte erscheinen:
.stack_area:
`Starte Exception-Blindtest...`
    .skip 0x1000              // Speicherplatz reservieren
`Error: Exception 'SynchError2' ausgelöst`
stack_top_el1:
Und das Programm bleibt stehen (dank `Stop()` im `ExceptionHandler`).


**Wenn das funktioniert, hast du den schwersten Teil bereits geschafft:** Der Wechsel nach EL1, das Setzen von `vbar_el1` und die korrekte Ausrichtung der Vektortabelle sind verifiziert! Du kannst den `svc #0`-Test danach wieder entfernen.


In diesem Ansatz wird sichergestellt, dass das System im passenden Berechtigungslevel (EL1) gesetzt wird, in dem Interrupts verwaltet werden können.
## 7. Der IRQ-Handler: Den Zustand retten und wiederherstellen


Wenn du keinen speziellen Berechtigungslevel in deinem Bare-Metal-Kernel angibst und dein Raspberry Pi 4 direkt nach dem Einschalten startet, hängt der anfängliche Berechtigungslevel davon ab, in welchem Modus der Prozessor von der Firmware initialisiert wurde.
Wenn ein echter Hardware-Interrupt (z. B. vom Timer) kommt, springt die CPU zu `GenericIRQHandler`. Da der Interrupt *jederzeit* passieren kann, weiß der Handler nicht, welche Register gerade benutzt wurden.  


Beim Raspberry Pi ist der typische Ablauf wie folgt:
Die goldene Regel lautet: **Rette alles, was du veränderst, und stelle es exakt so wieder her.**


Der Bootloader (z. B. der von der GPU initialisierte Bootcode) lädt die Firmware und deinen Kernel.
<syntaxhighlight lang="asm">
Nach der Initialisierung durch die Firmware (häufig start.elf), wird die CPU in EL2 (Hypervisor Mode) gestartet, wenn du keinen speziellen Wechsel zu einem bestimmten Berechtigungslevel vorgibst.
// In vector.S (vereinfachte Darstellung des GenericIRQHandler)
 
.globl GenericIRQHandler
Dies ist der Default-Startmodus des Raspberry Pi 4 nach der Initialisierung durch die Firmware.
GenericIRQHandler:
 
     // 1. Frame-Pointer und Link-Register retten
Vorgehensweise nach dem Start
     stp x29, x30, [sp, #-16]!
 
      
Typischerweise möchtest du jedoch direkt auf EL1 (Kernel Mode) wechseln, da dies der übliche Modus ist, in dem Betriebssystem-Kernel und Bare-Metal-Kernel laufen und der Zugriff auf alle notwendigen Systemressourcen erlaubt.
     // 2. Rücksprungadresse (elr_el1) und Status (spsr_el1) retten
 
     mrs x29, elr_el1
Standardmäßiger Start in EL2
     mrs x30, spsr_el1
 
     stp x29, x30, [sp, #-16]!
Hier sind die Schritte, um sicherzustellen, dass du zu EL1 wechselst, falls dies nicht schon passiert:
      
 
     // 3. FIQs erlauben (falls sie gesperrt waren), IRQs bleiben durch Eintritt automatisch gesperrt
Beispielcode für den Wechsel von EL2 zu EL1:
     msr DAIFClr, #1
.global _start
      
 
     // 4. ALLE General Purpose Register (x0-x28) auf den Stack sichern
_start:
    stp x27, x28, [sp, #-16]!
     // Lese aktuelles Exception Level
    // ... (weitere stp Befehle bis x0) ...
     MRS x0, CurrentEL
     str x0, [sp, #-16]!
    CMP x0, #0x8          // Check if in EL2
      
     B.NE el1_entry_point  // If already at EL1, jump to EL1 entry
     // 5. ALLE NEON/FP-Register (q0-q31) sichern (wichtig für C-Code mit Floats!)
 
     stp q30, q31, [sp, #-32]!
     // Wenn im EL2, switch zu EL1
     // ... (weitere stp Befehle bis q0) ...
    MOV x1, #0x3C5        // Prepare function value for EL1, with interrupt masked.
   
    MSR SPSR_EL2, x1      // Set Status Register for entry to EL1
    // 6. Jetzt ist der Stack sauber. Wir können sicher C-Code aufrufen!
     ADR x1, el1_entry_point // Load address of EL1 entry point
     bl InterruptHandler
     MSR ELR_EL2, x1        // Set exception link register for EL1 entry
      
    ERET                  // Switch to EL1
     // 7. Epilog: Alles in EXAKT umgekehrter Reihenfolge wiederherstellen!
 
     ldp q0, q1, [sp], #32
el1_entry_point:
     // ... (bis q30, q31)
     // EL1 Initialisierung
   
    ADR x0, vector_table  // Lade Adresse der Vektortabelle
    ldr x0, [sp], #16
    MSR VBAR_EL1, x0      // Set VBAR_EL1 zum Zeiger auf die Vektortabelle
    ldp x1, x2, [sp], #16
 
    // ... (bis x27, x28)
    // Setze Stack Pointer für EL1
   
    LDR x1, =stack_top_el1
    ldp x29, x30, [sp], #16
     MSR SP_EL1, x1
     msr elr_el1, x29
 
     msr spsr_el1, x30
     // Interrupts aktivieren
      
    BL enable_interrupts
     ldp x29, x30, [sp], #16
 
      
    // Hauptprogrammschleife
     // 8. Zurück zum unterbrochenen Code
main_loop:
     eret
    B main_loop
</syntaxhighlight>
 
.vector_table:
     B irq_handler          // IRQ Adresse
     // Additional vectors for different interrupts
 
// Dummy Interrupt Handler
irq_handler:
     // Interrupt Service Routine Logik hier
    RET
 
// Speicherplatz für Stack
.section .bss
.stack_area:
    .skip 0x1000          // Reserviere Speicherplatz
stack_top_el1:
 
Wichtige Punkte:
Initialisierungslogik in EL1: Es ist wichtig, dass du Initialisierungen für Interrupts, Vektortabellen und den Stack Pointer in EL1 korrekt durchführst.
Interrupt-Freigabe: Aktivierung von Interrupts, wenn notwendig.
Zusammenfassung
 
Wenn du keine besonderen Maßnahmen triffst, startet dein Bare-Metal-Kernel auf dem Raspberry Pi 4 standardmäßig in EL2 nach der Initialisierung durch die Firmware. Es ist gängige Praxis, dann direkt auf EL1 zu wechseln und dort den Kernel fortzusetzen. EL1 ist für die meisten bare-metal Anwendungen und Betriebsystem-Kernel der bevorzugte Modus.
 
 
Ja, Vektortabellen müssen einer bestimmten Struktur und Reihenfolge entsprechen, damit sie korrekt funktionieren. Dies ist wichtig, um sicherzustellen, dass der Prozessor bei einem Interrupt den richtigen Handler anspringt.
 
Struktur einer Vektortabelle
 
Die ARMv8-Architektur, die im Raspberry Pi 4 verwendet wird, verfügt über eine spezielle Vektortabelle für verschiedene Arten von Ausnahmen. Die Tabelle enthält Adressen für die folgenden Ausnahmen:
 
Synchronous Exception - Lower Level
IRQ (Normal Interrupt) - Lower Level
FIQ (Fast Interrupt) - Lower Level
SError (System Error) - Lower Level
Synchronous Exception - Current Level
IRQ (Normal Interrupt) - Current Level
FIQ (Fast Interrupt) - Current Level
SError (System Error) - Current Level
Beispiel einer Vektortabelle
 
Hier ist ein Beispiel, wie eine Vektortabelle für EL1 aussehen könnte:
 
.align 11
.section .vectors, "a"
vector_table:
    // Vektoren für EL1 (Current Level)
    b sync_exception_el1    // Synchronous Exception
    b irq_handler_el1        // IRQ - Normal Interrupt
    b fiq_handler_el1        // FIQ - Fast Interrupt
    b serror_handler_el1    // SError - System Error
 
    // Vektoren für EL0 (Lower Level)
    b sync_exception_el0    // Synchronous Exception
    b irq_handler_el0        // IRQ - Normal Interrupt
    b fiq_handler_el0        // FIQ - Fast Interrupt
    b serror_handler_el0    // SError - System Error
 
Konfiguration der Vektortabelle
 
Die Vektortabelle muss dem Prozessor mitgeteilt werden, indem die Basisadresse der Tabelle in das VBAR_ELx-Register geschrieben wird:
 
.global _start
 
_start:
     // Setze Stack-Pointer für EL1
    LDR x1, =stack_top_el1
    MSR SP_EL1, x1
 
    // Lade Adresse der Vektortabelle
    ADR x0, vector_table
    MSR VBAR_EL1, x0          // Setze VBAR_EL1 auf die Adresse der Vektortabelle
 
    // Initialisiere Interrupts
    BL enable_interrupts
 
    // Hauptprogrammschleife
main_loop:
    B main_loop
 
// Dummy-Handler
sync_exception_el1:
     // Handler-Code für synchrone Ausnahmen in EL1
    RET
 
irq_handler_el1:
     // Handler-Code für IRQ-Interrupts in EL1
    RET
 
fiq_handler_el1:
    // Handler-Code für FIQ-Interrupts in EL1
    RET
 
serror_handler_el1:
    // Handler-Code für Systemfehler in EL1
    RET
 
sync_exception_el0:
    // Handler-Code für synchrone Ausnahmen in EL0/EL1
     RET
 
irq_handler_el0:
    // Handler-Code für IRQ-Interrupts in EL0
    RET
 
fiq_handler_el0:
     // Handler-Code für FIQ-Interrupts in EL0
    RET
 
serror_handler_el0:
    // Handler-Code für Systemfehler in EL0
    RET
 
.section .bss
.stack_area:
    .skip 0x1000              // Speicherplatz reservieren
stack_top_el1:
 
Wichtige Punkte zur Vektortabelle
Ausrichtung: Die Vektortabelle muss auf eine bestimmte Weise ausgerichtet sein, typischerweise 2^11 Byte (2048 Byte, align 11), um den Anforderungen des ARM-Prozessors zu entsprechen.
Reihenfolge: Die Reihenfolge der Einträge ist fix und muss den Spezifikationen entsprechend sein. Jeder Vektor muss die korrekte Entsprechung der Ausnahme-Adressen haben.
Handlers: Jeder Eintrag in der Vektortabelle verweist auf einen spezifischen Handler für jede Art von Ausnahme.
Zusammenfassung
 
Eine korrekt strukturierte Vektortabelle ist essentiell für die korrekte Handhabung von Interrupts und Ausnahmen. Stelle sicher, dass alle Einträge in der richtigen Reihenfolge vorliegen und dass die Ausrichtung der Tabelle korrekt ist. Setze die Basisadresse der Tabelle im entsprechenden VBAR-Register und implementiere die entsprechenden Handler für jede Ausnahmeart.
 
Um zu verhindern, dass während der Bearbeitung einer Ausnahme (Interrupt Handling) eine weitere Ausnahme auftritt und zu unerwünschten Zuständen führt, gibt es mehrere Mechanismen, die verwendet werden können. Diese Mechanismen beinhalten das Maskieren von Interrupts, das Setzen von Flags und das Sicherstellen einer richtigen Reihenfolge und Priorität von Ausnahmebehandlungen.
 
Mechanismen zur Verhinderung von Ausnahmen während der Ausnahmebehandlung
 
Maskieren von Interrupts:
 
Beim Eintritt in einen Ausnahme-Handler kannst du alle weiteren Interrupts maskieren. Dies bedeutet, dass andere Interrupts so lange blockiert werden, bis der aktuelle Handler vollständig abgeschlossen ist.
Die ARM-Architektur stellt dafür spezielle Register (DAIF, CPSR) zur Verfügung, mit denen Interrupts individuell oder global maskiert werden können.
 
DAIF-Register:
 
Das DAIF-Register enthält Flags, um Debug-, SError-, IRQ- und FIQ-Interrupts zu maskieren.
Um alle Interrupts zu maskieren, wird das DAIF-Register mit den entsprechenden Bits gesetzt.
Beispielcode für das Maskieren von Interrupts in einem Ausnahme-Handler
.global exception_handler
 
exception_handler:
     // Maskiere alle Interrupts
     MRS x0, DAIF      // Lese DAIF Register
     ORR x0, x0, #0x3C0 // Setze die Bits zum Maskieren: D, A, I, F
    MSR DAIF, x0      // Schreibe zurück zum DAIF Register
 
    // Ausgangszustände retten
    PUSH {x0-x30, lr}
 
    // Handler Code hier
 
    // Ausgangszustände wiederherstellen
     POP {x0-x30, lr}
 
    // Entmaske alle Interrupts
    MRS x0, DAIF      // Lese DAIF Register
    BIC x0, x0, #0x3C0 // Lösche die Bits zum Entmaskieren: D, A, I, F
     MSR DAIF, x0      // Schreibe zurück zum DAIF Register
 
    RET
 
Schritt-für-Schritt Erklärung
 
Maskieren von Interrupts:
 
MRS x0, DAIF liest den aktuellen Zustand des DAIF-Registers und speichert ihn in x0.
ORR x0, x0, #0x3C0 setzt die Bits 8 (D), 9 (A), 6 (I), und 7 (F) auf 1, um Debug, SError, IRQ und FIQ Interrupts zu maskieren.
MSR DAIF, x0 schreibt den modifizierten Wert zurück in das DAIF-Register und maskiert somit alle diese Interrupts.
 
Ausführung des Handlers:
 
Speicherungen der Registersätze (x0-x30, lr), um das Rückkehren nach der Ausnahme sicherzustellen.
Ausführen der eigentlichen Ausnahmebehandlung.
 
Wiederherstellen Ausgangszustände und Entmaske Interrupts:
 
POP {x0-x30, lr} stellt die gespeicherten Register wieder her.
MRS x0, DAIF liest den aktuellen Zustand des DAIF-Registers.
BIC x0, x0, #0x3C0 löscht die Bits 8 (D), 9 (A), 6 (I), und 7 (F), um die Interruptmaskierung aufzuheben.
MSR DAIF, x0 schreibt den modifizierten Wert zurück in das DAIF-Register, um die Interrupts wieder zu erlauben.
Priority und Reihenfolge von Ausnahmen
 
Zusätzlich zum Maskieren von Interrupts können auch Priorisieren und die richtige Verwaltung der Reihenfolge helfen:
 
Priorisieren von Interrupts:
Bestimmte Interrupts können kritisch und hoch-priorisierter sein, während andere niederpriorisiert behandelt werden.
Der interrupt controller kann verwendet werden, um Prioritäten der Interrupts zu verwalten.
Zusammenfassung
 
Interrupt-Maskierung und richtige Priorisierung sind die Schlüssel, um zu verhindern, dass während des Ausnahme-Handlings weitere Ausnahmen auftreten. Das Maskieren der Interrupts kann über das DAIF-Register erfolgen und sollte vor Austritt aus dem Ausnahme-Handler aufgehoben werden. Diese Mechanismen stellen sicher, dass die Ausnahmebehandlung ohne Störungen abgeschlossen werden kann.
 
Ja, es gibt Unterschiede im Aufbau der Vektortabellen, insbesondere wenn verschiedene Betriebsmodi und Ausnahmelevel berücksichtigt werden. In der ARMv8-Architektur werden normalerweise 16 Einträge verwendet, um den verschiedenen Ausnahmebedingungen innerhalb der verschiedenen Exception Levels gerecht zu werden. Dies bezieht sich sowohl auf 64-Bit- als auch auf 32-Bit-Modi und auf die verschiedenen Zustände eines Exception Levels.
 
Erklärung der 16 Einträge
 
Die ARMv8-Architektur teilt die Exception Level Vektortabelle in vier verschiedene Ausnahmearten und zwei verschiedene Zustände jeder Ausnahmeart (z. B. EL1t und EL1h), und dies sowohl für 64-Bit- als auch für 32-Bit-Modi. Hier ist die detaillierte Aufschlüsselung:
 
Ausnahmearten und Zustände:
Synchronous: Sorgt für synchrone Ausnahmen, z.B. Software-Fehler.
IRQ: Normaler Interrupt Request.
FIQ: Schneller Interrupt Request.
SError: System Error.
 
Diese Ausnahmen haben jeweils einen Zustand für:
 
Current Exception Level with SP_el0 (_el1t)
Wenn der Stack Pointer SP_el0 verwendet wird.
Current Exception Level with SP_elx (_el1h)
Wenn der Stack Pointer von SP_elx verwendet wird (z.B. SP_el1).
 
Zusätzlich dazu müssen die Einträge sowohl für 64-Bit als auch 32-Bit-Untermodi der Ausnahmebehandlung berücksichtigt werden. Dies kommt typischerweise bei der Arbeit in der 64-Bit-Arbeitsumgebung vor. Im Allgemeinen:
 
64-Bit Mode EL0 (_el0_64)
32-Bit Mode EL0 (_el0_32)
Beispiel einer vollständigen 16-Eintrag-Vektortabelle:
.align 11
.section .vectors, "a"
 
vector_table:
 
// Einträge für EL1t (Current Exception Level SP_el0)
ventry sync_invalid_el1t          // Synchronous EL1t
ventry irq_invalid_el1t          // IRQ EL1t
ventry fiq_invalid_el1t          // FIQ EL1t
ventry error_invalid_el1t        // Error EL1t
 
// Einträge für EL1h (Current Exception Level SP_el1)
ventry sync_invalid_el1h          // Synchronous EL1h
ventry handle_el1_irq            // IRQ EL1h
ventry fiq_invalid_el1h          // FIQ EL1h
ventry error_invalid_el1h        // Error EL1h
 
// Einträge für EL0 64-bit Modus
ventry sync_invalid_el0_64        // Synchronous EL0 (64-bit)
ventry irq_invalid_el0_64        // IRQ EL0 (64-bit)
ventry fiq_invalid_el0_64        // FIQ EL0 (64-bit)
ventry error_invalid_el0_64      // Error EL0 (64-bit)
 
// Einträge für EL0 32-bit Modus
ventry sync_invalid_el0_32        // Synchronous EL0 (32-bit)
ventry irq_invalid_el0_32        // IRQ EL0 (32-bit)
ventry fiq_invalid_el0_32        // FIQ EL0 (32-bit)
ventry error_invalid_el0_32      // Error EL0 (32-bit)
 
.macro ventry label
b \label
.endm
 
Erläuterungen zu den Einträgen:
sync_invalid_*: Diese Handler werden für synchrone Ausnahmen verwendet, wie z.B. system calls oder undefinierte Instruktionen.
irq_invalid_*: Diese Handler werden für normale Interrupt-Anfragen verwendet.
fiq_invalid_*: Diese Handler werden für schnelle Interrupt-Anfragen verwendet.
error_invalid_*: Diese Handler werden für Systemfehler verwendet.
Verwendung und Initialisierung der Vektortabelle:
 
Die Registerzüge VBAR_EL1, VBAR_EL2 und VBAR_EL3 müssen entsprechend initialisiert werden, um die Adresse dieser Vektortabellenstruktur zu verwenden. Hier ist ein Beispiel für die Initialisierung auf EL1:
 
.global _start
 
_start:
    // Initialisiere Stack Pointer für EL1
     LDR x1, =stack_top_el1
     MSR SP_EL1, x1
 
     // Lade die Adresse der Vektortabelle
     ADR x0, vector_table
    MSR VBAR_EL1, x0          // Setze VBAR_EL1 zur Adresse der Vektortabelle
 
     // Initialise Interrupts
    BL enable_interrupts
 
     // Hauptprogrammschleife
main_loop:
    B main_loop
 
sync_invalid_el1t:
    // Handler für synchronous exception in EL1t
    RET
 
irq_invalid_el1t:
    // Handler für IRQ in EL1t
    RET
 
fiq_invalid_el1t:
    // Handler für FIQ in EL1t
    RET
 
error_invalid_el1t:
    // Handler für System Error in EL1t
    RET
 
sync_invalid_el1h:
    // Handler für synchronous exception in EL1h
    RET
 
handle_el1_irq:
    // Hauptinterrupt Handler für IRQ in EL1h
    RET
 
fiq_invalid_el1h:
    // Handler für FIQ in EL1h
    RET
 
error_invalid_el1h:
    // Handler für System Error in EL1h
    RET
 
sync_invalid_el0_64:
    // Handler für synchronous exception in 64-bit EL0
    RET
 
irq_invalid_el0_64:
    // Handler für IRQ in 64-bit EL0
    RET
 
fiq_invalid_el0_64:
    // Handler für FIQ in 64-bit EL0
     RET
 
error_invalid_el0_64:
    // Handler für System Error in 64-bit EL0
    RET
 
sync_invalid_el0_32:
    // Handler für synchronous exception in 32-bit EL0
    RET
 
irq_invalid_el0_32:
    // Handler für IRQ in 32-bit EL0
    RET


fiq_invalid_el0_32:
## 8. Zusammenfassung
    // Handler für FIQ in 32-bit EL0
    RET


error_invalid_el0_32:
1. **Nur ein Kern:** Wir schicken Core 1-3 in den Schlaf (`WFE`), um Chaos zu vermeiden.
    // Handler für System Error in 32-bit EL0
2. **EL1 ist Pflicht:** Wir wechseln von EL2 nach EL1, um Kernel-Rechte zu haben.
    RET
3. **Vektortabelle:** Sie muss auf 2 KB ausgerichtet sein und zeigt der CPU den Weg zu den Handlern.
4. **Blindtest:** Ein `svc #0` ist der perfekte, sichere Weg, um zu prüfen, ob Exceptions korrekt abgefangen werden, bevor man Hardware-IRQs konfiguriert.
5. **State Saving:** Der Assembly-Handler muss *jedes* Register sichern, bevor er C-Code (`InterruptHandler`) aufruft, und es danach exakt wiederherstellen.


.section .bss
## Ausblick
.stack_area:
    .skip 0x1000              // Speicherplatz reservieren
stack_top_el1:


Zusammenfassung
Nun, da das Fundament steht und wir wissen, dass unsere Exception-Tabelle funktioniert, können wir uns im nächsten Kapitel dem **Generic Interrupt Controller (GIC-400)** und dem **System Timer** widmen. Dort werden wir lernen, wie wir einen echten Hardware-Interrupt (IRQ 27) aktivieren, um unseren ersten periodischen "Rotor" auf dem Bildschirm zu animieren!


In der ARMv8-Architektur gibt es eine erweiterte Struktur für Vektortabellen mit 16 Einträgen, um verschiedene Arten und Zustände von Ausnahmen vollständig zu unterstützen. Bei der Ausnahmebehandlung können unterschiedliche Modi (64-Bit und 32-Bit) und verschiedene Betriebsarten (_el1t, _el1h, _el0_64, _el0_32) gleichzeitig berücksichtigt werden. Eine korrekte Initialisierung und Einhaltung der Reihenfolge dieser Einträge ist für die zuverlässige Ausnahmebehandlung unerlässlich.
{| style="width: 100%;
| style="width: 33%;" | [[Systeminformationen (PI4)|< Zurück (Systeminformationen)]]
| style="width: 33%; text-align:center;" | [[Hauptseite|< Hauptseite >]]
| style="width: 33%; text-align:right;" | [[Beispiel Timer-Interrupt (PI4)|Weiter (Beispiel Timer-Interrupt) >]]
|}

Aktuelle Version vom 22. Juli 2026, 11:50 Uhr

Wichtig! Durch die Entwicklung zur USB-Unterstützung wurde 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.

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.

Die Spielregeln: Exception Levels (EL) und DAIF

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.

Zudem kontrolliert das DAIF-Register, welche Interrupts gerade erlaubt sind:

  • D (Debug): Debug-Exceptions sperren
  • A (SError): Asynchrone Systemfehler sperren
  • I (IRQ): Normale Interrupts sperren
  • F (FIQ): Schnelle Interrupts (Fast IRQs) sperren

Ist ein Bit auf `1` gesetzt, ist der Interrupt gesperrt. Um IRQs zu erlauben, müssen wir das I-Bit auf `0` setzen (clear).

Vorbereitung: Nur ein Kern darf laufen

Der 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).

// In boot.S
_start:
    mrs x1, mpidr_el1    // Core-ID lesen
    and x1, x1, #3       // Nur die unteren 2 Bits (0-3) behalten
    cbz x1, core0        // Wenn Core 0, weiter zum Setup

core_sleep:
    wfe                  // Warte auf ein Event (Sleep)
    b core_sleep         // Endlosschleife für Core 1-3

core0:
    // Ab hier läuft nur noch Core 0

Der Wechsel von EL2 nach EL1

Da wir ein Kernel schreiben, gehört unser Platz in EL1. Der folgende Code konfiguriert EL1, setzt den Stackpointer und bereitet den Rücksprung vor.

    // 1. Prüfen, ob wir schon in EL1 sind (CurrentEL = 8 für EL1)
    mrs x0, CurrentEL
    cmp x0, #8
    beq switch_to_el1

    // 2. EL1 Stackpointer setzen
    ldr x0, =EXCEPTION_STACK
    msr sp_el1, x0

    // 3. Vektortabelle für EL2 setzen (für den Fall, dass während des Wechsels was passiert)
    ldr x0, =VectorTable
    msr vbar_el2, x0

    // ... (Hier folgen Timer- und Virtualisierungs-Einstellungen, siehe boot.S) ...

    // 4. Rücksprung-Adresse und Status für EL1 vorbereiten
    mov x0, #0x3c4             // SPSR_EL2: EL1h, Interrupts (DAIF) sind hier noch maskiert
    msr spsr_el2, x0
    adr x0, el1_return         // Adresse, zu der wir nach dem Wechsel springen
    msr elr_el2, x0

    eret                       // Exception Return: Wechselt nach EL1 zu 'el1_return'

el1_return:
switch_to_el1:
    // Wir sind jetzt in EL1!
    ldr x0, =VectorTable
    msr vbar_el1, x0           // Vektortabelle für EL1 aktivieren

    // BSS-Segment (uninitialisierte Variablen) mit Nullen füllen
    ldr x1, =__bss_start
    ldr w2, =__bss_size
clean_bss_loop:
    cbz w2, bss_clean_done
    str xzr, [x1], #8
    sub w2, w2, #1
    cbnz w2, clean_bss_loop
bss_clean_done:

    b main                     // Ab ins C-Hauptprogramm!

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 ist in 16 Einträge unterteilt (4 Ausnahmetypen × 4 Ausführungsstatus). Jeder Eintrag muss exakt 128 Byte (.align 7) voneinander entfernt sein.

// In vector.S
.align 11                      // 2^11 = 2048 Byte Ausrichtung für die ganze Tabelle
.globl VectorTable
VectorTable:
    // 1. Aktuelles EL, Stackpointer ist SP_EL0 (User-Mode, nutzen wir nicht)
    .align 7
    b SynchError1
    .align 7
    b GenericIRQHandler
    .align 7
    b FastInterruptHandler
    .align 7
    b SErrorStub1

    // 2. Aktuelles EL, Stackpointer ist SP_ELx (Kernel-Mode, EL1h) -> DAS NUTZEN WIR!
    .align 7
    b SynchError2              // Synchronous Exception (z.B. svc #0, Memory Abort)
    .align 7
    b GenericIRQHandler        // Normaler Hardware-Interrupt (IRQ)
    .align 7
    b FastInterruptHandler     // FIQ
    .align 7
    b SErrorStub2              // System Error

    // 3. Niedrigeres EL (EL0), 64-Bit (Auch hier leiten wir auf unsere Handler um)
    .align 7
    b SynchError1
    .align 7
    b GenericIRQHandler
    // ... (weitere Einträge siehe vector.S)

🛠️ 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. Wir werden das gleich gezielt provozieren, um zu prüfen, ob die Tabelle funktioniert.

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.

Füge diesen Test vorübergehend in deine `main.c` ein, *bevor* du `printf` oder andere Initialisierungen machst:

// In main.c
#include "printf.h"
#include "sync.h" // Für Stop()

int main(void)
{
    // --- BLINDTEST FÜR EXCEPTIONS ---
    printf("Starte Exception-Blindtest...\n");
    
    // Dieser Befehl löst absichtlich eine synchrone Exception aus.
    // Die CPU sollte in vector.S -> SynchError2 -> ExceptionHandler springen.
    asm volatile("svc #0"); 
    
    // Diese Zeile sollte NICHT erreicht werden!
    printf("FEHLER: svc #0 wurde nicht abgefangen!\n");
    Stop();
    // ----------------------------------

    // ... Rest deiner Initialisierung (Interrupt_Initialize, Timer, etc.) ...
}
    • Erwartetes Ergebnis:**

Auf dem Bildschirm sollte erscheinen: `Starte Exception-Blindtest...` `Error: Exception 'SynchError2' ausgelöst` Und das Programm bleibt stehen (dank `Stop()` im `ExceptionHandler`).

    • Wenn das funktioniert, hast du den schwersten Teil bereits geschafft:** Der Wechsel nach EL1, das Setzen von `vbar_el1` und die korrekte Ausrichtung der Vektortabelle sind verifiziert! Du kannst den `svc #0`-Test danach wieder entfernen.
    1. 7. Der IRQ-Handler: Den Zustand retten und wiederherstellen

Wenn ein echter Hardware-Interrupt (z. B. vom Timer) kommt, springt die CPU zu `GenericIRQHandler`. Da der Interrupt *jederzeit* passieren kann, weiß der Handler nicht, welche Register gerade benutzt wurden.

Die goldene Regel lautet: **Rette alles, was du veränderst, und stelle es exakt so wieder her.**

// In vector.S (vereinfachte Darstellung des GenericIRQHandler)
.globl GenericIRQHandler
GenericIRQHandler:
    // 1. Frame-Pointer und Link-Register retten
    stp x29, x30, [sp, #-16]!
    
    // 2. Rücksprungadresse (elr_el1) und Status (spsr_el1) retten
    mrs x29, elr_el1
    mrs x30, spsr_el1
    stp x29, x30, [sp, #-16]!
    
    // 3. FIQs erlauben (falls sie gesperrt waren), IRQs bleiben durch Eintritt automatisch gesperrt
    msr DAIFClr, #1
    
    // 4. ALLE General Purpose Register (x0-x28) auf den Stack sichern
    stp x27, x28, [sp, #-16]!
    // ... (weitere stp Befehle bis x0) ...
    str x0, [sp, #-16]!
    
    // 5. ALLE NEON/FP-Register (q0-q31) sichern (wichtig für C-Code mit Floats!)
    stp q30, q31, [sp, #-32]!
    // ... (weitere stp Befehle bis q0) ...
    
    // 6. Jetzt ist der Stack sauber. Wir können sicher C-Code aufrufen!
    bl InterruptHandler
    
    // 7. Epilog: Alles in EXAKT umgekehrter Reihenfolge wiederherstellen!
    ldp q0, q1, [sp], #32
    // ... (bis q30, q31)
    
    ldr x0, [sp], #16
    ldp x1, x2, [sp], #16
    // ... (bis x27, x28)
    
    ldp x29, x30, [sp], #16
    msr elr_el1, x29
    msr spsr_el1, x30
    
    ldp x29, x30, [sp], #16
    
    // 8. Zurück zum unterbrochenen Code
    eret
    1. 8. Zusammenfassung

1. **Nur ein Kern:** Wir schicken Core 1-3 in den Schlaf (`WFE`), um Chaos zu vermeiden. 2. **EL1 ist Pflicht:** Wir wechseln von EL2 nach EL1, um Kernel-Rechte zu haben. 3. **Vektortabelle:** Sie muss auf 2 KB ausgerichtet sein und zeigt der CPU den Weg zu den Handlern. 4. **Blindtest:** Ein `svc #0` ist der perfekte, sichere Weg, um zu prüfen, ob Exceptions korrekt abgefangen werden, bevor man Hardware-IRQs konfiguriert. 5. **State Saving:** Der Assembly-Handler muss *jedes* Register sichern, bevor er C-Code (`InterruptHandler`) aufruft, und es danach exakt wiederherstellen.

    1. Ausblick

Nun, da das Fundament steht und wir wissen, dass unsere Exception-Tabelle funktioniert, können wir uns im nächsten Kapitel dem **Generic Interrupt Controller (GIC-400)** und dem **System Timer** widmen. Dort werden wir lernen, wie wir einen echten Hardware-Interrupt (IRQ 27) aktivieren, um unseren ersten periodischen "Rotor" auf dem Bildschirm zu animieren!

< Zurück (Systeminformationen) < Hauptseite > Weiter (Beispiel Timer-Interrupt) >