Interrupts (PI4): Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
KKeine Bearbeitungszusammenfassung
 
(4 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
== Interrupts auf dem Raspberry Pi 4 (Bare Metal) ==
<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>


Interrupts sind Signale, die den normalen Ablauf des Prozessors unterbrechen, um auf bestimmte Ereignisse zu reagieren. Sie sind besonders wichtig in Echtzeitsystemen und bei der Steuerung von Hardware – zum Beispiel, wenn ein Timer abläuft oder eine Taste gedrückt wird.
== Interrupts und Exceptions auf dem Raspberry Pi 4 (Bare Metal) ==


In diesem Kapitel lernst du, wie Interrupts auf dem Raspberry Pi 4 im Bare-Metal-Modus funktionieren und wie du sie sicher einrichten kannst.
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.


== Was ist ein Interrupt? ==
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.


Ein '''Interrupt''' (zu Deutsch: „Unterbrechung“) ist ein Signal, das den Prozessor dazu bringt, den gerade laufenden Code kurz zu pausieren. Stattdessen führt er eine spezielle Funktion aus – den sogenannten '''Interrupt-Handler''' (auch ''Interrupt Service Routine'', kurz ISR).
== Exceptions vs. Interrupts: Was ist der Unterschied? ==


Sobald der Handler fertig ist, kehrt der Prozessor an die Stelle zurück, an der er unterbrochen wurde, und arbeitet dort weiter.
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.


== Wann treten Interrupts auf? ==
''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.


Interrupts können durch verschiedene Ereignisse ausgelöst werden:
== Die Spielregeln: Exception Levels (EL) und DAIF ==


* '''Externe Hardware''':
Der ARM Cortex-A72 kennt verschiedene Privilegienstufen, die '''Exception Levels (EL)''':
  Zum Beispiel: Tastatur, Maus, Timer, GPIO-Pins.
* '''EL0:''' User-Mode (Anwendungen, wenig Rechte)
* '''Interne Geräte'''
* '''EL1:''' Kernel-Mode (Unser Bare-Metal-Code, volle Hardware-Kontrolle)
  Netzwerkadapter, Speichercontroller, Sensoren (Temperatur, Beschleunigung).
* '''EL2:''' Hypervisor (Virtualisierung, wird vom Bootloader kurz genutzt)
* '''Software-Ereignisse'''
* '''EL3:''' Secure Monitor (Highest Privilege, z. B. ARM Trusted Firmware)
  Systemaufrufe (z. B. svc-Befehl) oder Fehler wie Division durch Null.


== Arten von Interrupts ==
'''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.


Es gibt zwei wichtige Arten:
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


* '''Maskierbare Interrupts (IRQ)''': 
Ist ein Bit auf `1` gesetzt, ist der Interrupt '''gesperrt'''. Um IRQs zu erlauben, müssen wir das I-Bit auf `0` setzen (clear).
  Diese können vom Prozessor vorübergehend '''ausgeschaltet''' werden. Die meisten Interrupts (z. B. von Timern oder GPIO) sind IRQs.


* '''Nicht maskierbare Interrupts (NMI)''':
== Vorbereitung: Nur ein Kern darf laufen ==
  Diese '''können nicht ignoriert''' werden und haben höchste Priorität. Sie werden nur bei kritischen Fehlern oder Systemereignissen verwendet.


== Betriebsmodi und Interrupts (ARMv8) ==
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).
 
Der Raspberry Pi 4 nutzt einen '''ARM Cortex-A72'''-Prozessor mit 64-Bit-Architektur (AArch64). ARM unterscheidet verschiedene '''Exception Levels (EL)''':
 
* '''EL0''': Anwendungen (wenig Rechte) 
* '''EL1''': Betriebssystem oder Bare-Metal-Kernel (hohe Rechte) 
* '''EL2''': Hypervisor (z. B. für Virtualisierung) 
* '''EL3''': Firmware (höchste Rechte)
 
'''Wichtig''': Interrupts können nur in EL1 oder höher korrekt behandelt werden. Beim Start landet der Pi meist in '''EL2''', daher müssen wir in '''EL1''' wechseln.
 
== Interrupts aktivieren: Die DAIF-Flags ==
 
Ob Interrupts erlaubt sind, wird über das '''DAIF-Register''' gesteuert. Es enthält vier Flags:
 
{| class="wikitable"
! Flag
! Bedeutung
|-
| D
| Debug-Exceptions sperren
|-
| A
| Asynchrone Fehler (SError)
|-
| I
| Normale Interrupts (IRQ)
|-
| F
| Schnelle Interrupts (FIQ)
|}
 
Wenn ein Flag '''gesetzt''' ist (1), ist der entsprechende Interrupt '''gesperrt'''. 
Um '''IRQs zu erlauben''', muss das '''I-Flag gelöscht''' werden (also auf 0 gesetzt).
 
=== Beispiel: IRQ-Interrupts aktivieren ===
 
<source lang="armasm">
.global enable_interrupts
enable_interrupts:
    mrs x0, DAIF        // Lese aktuelle DAIF-Werte
    bic x0, x0, #(1<<7) // I-Flag löschen (Bit 7)
    msr DAIF, x0        // Zurückschreiben → IRQs freigegeben
    ret
</source>
 
'''Hinweis''': <code>bic</code> steht für „bit clear“. <code>(1<<7)</code> ist Bit 7 – das I-Flag im DAIF-Register.
 
== Vorbereitung: Nur ein Kern aktivieren ==
 
Der Raspberry Pi 4 hat '''vier CPU-Kerne'''. Beim Start laufen '''alle gleichzeitig'''. Wenn wir nicht aufpassen, könnte jeder Kern versuchen, Interrupts zu bearbeiten das führt zu Chaos.
 
Daher: Wir erlauben nur '''Kern 0 (CPU0)''' weiterzulaufen. Die anderen Kerne legen wir „in den Schlaf“.


<syntaxhighlight lang="asm">
<syntaxhighlight lang="asm">
.section .init
// In boot.S
.global _start
_start:
_start:
     mrs x1, mpidr_el1    // Lese die Core-ID
     mrs x1, mpidr_el1    // Core-ID lesen
     and x1, x1, #3      // Extrahiere die unteren 2 Bits (0 bis 3)
     and x1, x1, #3      // Nur die unteren 2 Bits (0-3) behalten
     cbz x1, core0        // Wenn ID = 0 weiter auf Core0
     cbz x1, core0        // Wenn Core 0, weiter zum Setup


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


core0:
core0:
     // Nur Core 0 kommt hierher
     // Ab hier läuft nur noch Core 0
</syntaxhighlight>
 
> '''Tipp''': <code>mpidr_el1</code> enthält die Core-ID. 
> <code>wfe</code> (Wait For Event) hält den Kern ruhig, bis er geweckt wird.
 
== In EL1 wechseln ==
 
Wie gesagt: Interrupts gehören in '''EL1''', aber der Pi startet oft in '''EL2'''. Wir müssen also wechseln.
 
=== Schritt 1: Prüfen, ob wir schon in EL1 sind ===
 
<syntaxhighlight lang="asm">
    mrs x0, CurrentEL    // Lese aktuelles Exception Level
    cmp x0, #8          // EL1 hat Wert 8
    beq switch_to_el1    // Wenn EL1, direkt weiter
</syntaxhighlight>
</syntaxhighlight>


> <code>CurrentEL</code> gibt z. B. <code>8</code> für EL1. Der Wert ist <code>ELn << 2</code>. 
== Der Wechsel von EL2 nach EL1 ==
> Also: EL1 = 8, EL2 = 16.


=== Schritt 2: 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.


<syntaxhighlight lang="asm">
<syntaxhighlight lang="asm">
     ldr x0, =EXCEPTION_STACK    // Lade Stack für EL1
     // 1. Prüfen, ob wir schon in EL1 sind (CurrentEL = 8 für EL1)
     msr sp_el1, x0             // Setze Stackpointer für EL1
     mrs x0, CurrentEL
    cmp x0, #8
    beq switch_to_el1


     ldr x0, =VectorTable        // Adresse der Vektortabelle
    // 2. EL1 Stackpointer setzen
     msr vbar_el2, x0           // VBAR_EL2 zeigt auf Tabelle
     ldr x0, =EXCEPTION_STACK
     msr sp_el1, x0


     // Timer-Einstellungen
     // 3. Vektortabelle für EL2 setzen (für den Fall, dass während des Wechsels was passiert)
    mrs x0, cnthctl_el2
     ldr x0, =VectorTable
    orr x0, x0, #0x3
     msr vbar_el2, x0
    msr cnthctl_el2, x0
    msr cntvoff_el2, xzr
 
    // Virtualisierung deaktivieren
    mov x0, #0x33ff
    msr cptr_el2, x0
    msr hstr_el2, xzr
 
    mov x0, #(1 << 31)
     msr hcr_el2, x0
 
    // CPACR_EL1 für Gleitkomma
    mov x0, #3 << 20
     msr cpacr_el1, x0


     // Systemsteuerregister EL1
     // ... (Hier folgen Timer- und Virtualisierungs-Einstellungen, siehe boot.S) ...
    mov x0, #0x0800
    movk x0, #0x30d0, lsl #16
    msr sctlr_el1, x0


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


     eret   // Wechsel nach EL1
     eret                       // Exception Return: Wechselt nach EL1 zu 'el1_return'
</syntaxhighlight>
 
> Nach <code>eret</code> springt der Prozessor zu <code>el1_return</code> – aber jetzt in EL1.
 
== Initialisierung in EL1 ==
 
Nach dem Wechsel:


<syntaxhighlight lang="asm">
el1_return:
el1_return:
switch_to_el1:
switch_to_el1:
     ldr x1, =_start
     // Wir sind jetzt in EL1!
    mov sp, x1                  // Stack setzen
     ldr x0, =VectorTable
     ldr x0, =VectorTable
     msr vbar_el1, x0           // Vektortabelle für EL1 aktivieren
     msr vbar_el1, x0           // Vektortabelle für EL1 aktivieren


     // BSS-Segment löschen (uninitialisierte Daten auf 0 setzen)
     // BSS-Segment (uninitialisierte Variablen) mit Nullen füllen
     ldr x1, =__bss_start
     ldr x1, =__bss_start
     ldr w2, =__bss_size
     ldr w2, =__bss_size
Zeile 186: Zeile 100:
bss_clean_done:
bss_clean_done:


     b main                     // Starte Hauptprogramm
     b main                     // Ab ins C-Hauptprogramm!
</syntaxhighlight>
</syntaxhighlight>


== Die Vektortabelle ==
== Die Exception Vector Table (VBT) ==
 
Die '''Vektortabelle''' sagt dem Prozessor, welche Funktion er bei einem Interrupt aufrufen soll. Sie enthält eine Liste von Sprüngen („Vectors“) für verschiedene Ausnahmetypen.


=== Aufbau der Tabelle (ARMv8) ===
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.


Jeder Eintrag behandelt einen bestimmten Typ:
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.
 
* '''Synchronous''': Fehler oder Systemaufrufe
* '''IRQ''': Normaler Interrupt
* '''FIQ''': Schneller Interrupt (höhere Priorität)
* '''SError''': Systemfehler (z. B. Speicherfehler)
 
Und jeweils für:
* Aktuelles Exception Level (z. B. EL1h)
* Niedrigeres Level (z. B. EL0)
 
=== Beispiel: Vektortabelle für EL1 ===


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


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


     // EL0 64-Bit (ungültig – nicht erlaubt)
     // 3. Niedrigeres EL (EL0), 64-Bit (Auch hier leiten wir auf unsere Handler um)
     .align 7
     .align 7
     b sync_invalid_el0_64
     b SynchError1
     .align 7
     .align 7
     b irq_invalid_el0_64
     b GenericIRQHandler
     .align 7
     // ... (weitere Einträge siehe vector.S)
    b fiq_invalid_el0_64
</syntaxhighlight>
    .align 7
 
    b error_invalid_el0_64
🛠️ '''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.


    // EL0 32-Bit (ebenfalls ungültig)
Füge diesen Test vorübergehend in deine `main.c` ein, *bevor* du `printf` oder andere Initialisierungen machst:
    .align 7
    b sync_invalid_el0_32
    .align 7
    b irq_invalid_el0_32
    .align 7
    b fiq_invalid_el0_32
    .align 7
    b error_invalid_el0_32
</syntaxhighlight>


'''Hinweis''':
<syntaxhighlight lang="C">
* <code>.align 11</code> = 2048 Bytes für die gesamte Tabelle
// In main.c
* <code>.align 7</code> = 128 Bytes pro Eintrag (Abstand zwischen Vektoren)
#include "printf.h"
#include "sync.h" // Für Stop()


== Ein Interrupt-Handler: Schritt für Schritt ==
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();
    // ----------------------------------


Hier ein Beispiel für einen '''IRQ-Handler''':
    // ... Rest deiner Initialisierung (Interrupt_Initialize, Timer, etc.) ...
}
</syntaxhighlight>


<syntaxhighlight lang="asm">
**Erwartetes Ergebnis:**
irq_handler_el1t:
Auf dem Bildschirm sollte erscheinen:
irq_handler_el1h:
`Starte Exception-Blindtest...`
    stp x29, x30, [sp, #-16]!      // x29/x30 sichern
`Error: Exception 'SynchError2' ausgelöst`
    mrs x29, elr_el1                // Rückkehradresse sichern
Und das Programm bleibt stehen (dank `Stop()` im `ExceptionHandler`).
    mrs x30, spsr_el1              // Statusregister sichern
    stp x29, x30, [sp, #-16]!


    msr DAIFSet, #1                // FIQs sperren (nur für IRQ)
**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.


    // Alle Register sichern (inkl. Gleitkomma)
## 7. Der IRQ-Handler: Den Zustand retten und wiederherstellen
    stp q0, q1, [sp, #-32]!
    stp q2, q3, [sp, #-32]!
    // ... bis q30, q31


    stp x1, x2, [sp, #-16]!
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.
    // ... bis x27, x28
    str x0, [sp, #-16]!


    bl InterruptHandler              // Aufruf der C-Funktion
Die goldene Regel lautet: **Rette alles, was du veränderst, und stelle es exakt so wieder her.**


     // Register wiederherstellen
<syntaxhighlight lang="asm">
// 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
     ldr x0, [sp], #16
     ldp x1, x2, [sp], #16
     ldp x1, x2, [sp], #16
     // ... bis x27, x28
     // ... (bis x27, x28)
 
      
     ldp q0, q1, [sp], #32
     ldp x29, x30, [sp], #16
    // ... bis q30, q31
 
     ldp x29, x30, [sp], #16         // elr_el1, spsr_el1
     msr elr_el1, x29
     msr elr_el1, x29
     msr spsr_el1, x30
     msr spsr_el1, x30
 
   
     ldp x29, x30, [sp], #16         // x29/x30 wiederherstellen
     ldp x29, x30, [sp], #16
 
   
     eret                            // Zurück zum unterbrochenen Code
     // 8. Zurück zum unterbrochenen Code
    eret
</syntaxhighlight>
</syntaxhighlight>


== Wichtige Prinzipien beim Interrupt-Handling ==
## 8. Zusammenfassung
 
# '''Register sichern''': 
  Beim Interrupt werden Register überschrieben. Daher '''alle Register auf den Stack speichern'''.
# '''Interrupts maskieren''': 
  Während ein Handler läuft, sollten keine weiteren Interrupts kommen. 
  → Setze <code>DAIFSet</code> am Anfang, um FIQs zu blockieren (wenn nötig).
# '''Schnell sein''': 
  Interrupt-Handler sollten '''schnell''' sein. Komplexe Aufgaben besser in den Hintergrund verlegen.
# '''eret verwenden''': 
  Der Befehl <code>eret</code> kehrt zum unterbrochenen Code zurück und stellt den Zustand wieder her.
 
== Zusammenfassung ==
 
* Interrupts unterbrechen den normalen Programmablauf.
* Sie werden über die '''Vektortabelle''' behandelt.
* Nur '''EL1 oder höher''' darf Interrupts verarbeiten.
* Mehrere Kerne? Nur '''einen aktivieren''', die anderen schlafen legen.
* '''DAIF-Register''' kontrolliert, welche Interrupts erlaubt sind.
* Im Handler: '''Register sichern''', '''Interrupts steuern''', '''schnell arbeiten''', '''eret''' benutzen.


Mit diesem Wissen bist du bereit, echte Hardware-Interrupts (z. B. vom Timer oder GPIO) auf dem Raspberry Pi 4 zu nutzen!
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.


Im nächsten Kapitel kümmern wir uns um den '''Systemtimer''', um den ersten echten Interrupt auszulösen.
## 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!


{| style="width: 100%;
{| style="width: 100%;

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