Interrupts (PI4): Unterschied zwischen den Versionen
KKeine Bearbeitungszusammenfassung |
|||
| Zeile 1: | Zeile 1: | ||
== Interrupts auf dem Raspberry Pi 4 (Bare Metal) == | == Interrupts und Exceptions auf dem Raspberry Pi 4 (Bare Metal) == | ||
Interrupts sind | 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 | 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. | |||
== Was 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). | |||
Der | |||
<syntaxhighlight lang="asm"> | <syntaxhighlight lang="asm"> | ||
. | // In boot.S | ||
_start: | _start: | ||
mrs x1, mpidr_el1 // | mrs x1, mpidr_el1 // Core-ID lesen | ||
and x1, x1, #3 // | and x1, x1, #3 // Nur die unteren 2 Bits (0-3) behalten | ||
cbz x1, core0 // Wenn | cbz x1, core0 // Wenn Core 0, weiter zum Setup | ||
core_sleep: | core_sleep: | ||
wfe // Warte auf | wfe // Warte auf ein Event (Sleep) | ||
b core_sleep // | b core_sleep // Endlosschleife für Core 1-3 | ||
core0: | core0: | ||
// | // Ab hier läuft nur noch Core 0 | ||
</syntaxhighlight> | </syntaxhighlight> | ||
== 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. | |||
<syntaxhighlight lang="asm"> | <syntaxhighlight lang="asm"> | ||
mrs x0, CurrentEL | // 1. Prüfen, ob wir schon in EL1 sind (CurrentEL = 8 für EL1) | ||
cmp x0, # | mrs x0, CurrentEL | ||
beq switch_to_el1 | 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 | |||
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 | |||
mov x0, # | |||
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 | eret // Exception Return: Wechselt nach EL1 zu 'el1_return' | ||
el1_return: | el1_return: | ||
switch_to_el1: | switch_to_el1: | ||
// Wir sind jetzt in EL1! | |||
ldr x0, =VectorTable | ldr x0, =VectorTable | ||
msr vbar_el1, x0 | msr vbar_el1, x0 // Vektortabelle für EL1 aktivieren | ||
// BSS-Segment | // 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 98: | ||
bss_clean_done: | bss_clean_done: | ||
b main | b main // Ab ins C-Hauptprogramm! | ||
</syntaxhighlight> | </syntaxhighlight> | ||
== Die | == Die Exception Vector Table (VBT) == | ||
Die | 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. | |||
<syntaxhighlight lang="asm"> | <syntaxhighlight lang="asm"> | ||
.align 11 | // In vector.S | ||
.align 11 // 2^11 = 2048 Byte Ausrichtung für die ganze Tabelle | |||
.globl VectorTable | .globl VectorTable | ||
VectorTable: | VectorTable: | ||
// | // 1. Aktuelles EL, Stackpointer ist SP_EL0 (User-Mode, nutzen wir nicht) | ||
.align 7 | .align 7 | ||
b | b SynchError1 | ||
.align 7 | .align 7 | ||
b | b GenericIRQHandler | ||
.align 7 | .align 7 | ||
b | b FastInterruptHandler | ||
.align 7 | .align 7 | ||
b | b SErrorStub1 | ||
// | // 2. Aktuelles EL, Stackpointer ist SP_ELx (Kernel-Mode, EL1h) -> DAS NUTZEN WIR! | ||
.align 7 | .align 7 | ||
b | b SynchError2 // Synchronous Exception (z.B. svc #0, Memory Abort) | ||
.align 7 | .align 7 | ||
b | b GenericIRQHandler // Normaler Hardware-Interrupt (IRQ) | ||
.align 7 | .align 7 | ||
b | b FastInterruptHandler // FIQ | ||
.align 7 | .align 7 | ||
b | b SErrorStub2 // System Error | ||
// EL0 64-Bit ( | // 3. Niedrigeres EL (EL0), 64-Bit (Auch hier leiten wir auf unsere Handler um) | ||
.align 7 | .align 7 | ||
b | b SynchError1 | ||
.align 7 | .align 7 | ||
b | b GenericIRQHandler | ||
. | // ... (weitere Einträge siehe vector.S) | ||
</syntaxhighlight> | |||
🛠️ '''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: | |||
<syntaxhighlight lang="C"> | |||
// 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.) ... | |||
} | |||
</syntaxhighlight> | |||
**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. | |||
## 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.** | |||
// 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 x29, x30, [sp], #16 | |||
ldp x29, x30, [sp], #16 | |||
msr elr_el1, x29 | msr elr_el1, x29 | ||
msr spsr_el1, x30 | msr spsr_el1, x30 | ||
ldp x29, x30, [sp], #16 | ldp x29, x30, [sp], #16 | ||
// 8. Zurück zum unterbrochenen Code | |||
eret | |||
</syntaxhighlight> | </syntaxhighlight> | ||
## 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. | |||
# | |||
## 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! | ||
* | |||
* | |||
* | |||
* | |||
--- | |||
### Warum diese Überarbeitung besser ist: | |||
1. **Der `svc #0` Test ist jetzt ein klares, abgehaktes "Meilenstein"-Kapitel.** Das gibt dem Leser ein direktes Erfolgserlebnis. | |||
2. **Die Rolle von `SynchError` wird als Debugging-Werkzeug hervorgehoben**, genau wie von dir gewünscht. Das nimmt dem "Fehler" den Schrecken und macht ihn zum Freund des Entwicklers. | |||
3. **Der Assembly-Code im Handler** wurde an deinen tatsächlichen `vector.S` Code angepasst (inklusive Erwähnung der NEON-Register `q0-q31`, die in deinem Code vorkommen und für Bare-Metal C mit Floats/Stack-Alignment absolut kritisch sind). | |||
4. Die Struktur folgt einer logischen "Vom Einfachen zum Komplexen"-Didaktik. | |||
Passt dieser Text so für dein Tutorial, oder möchtest du an einer bestimmten Stelle noch tiefer ins Detail gehen (z. B. genau erklären, was `eret` im Hintergrund macht)? | |||
----- | ----- | ||
Version vom 21. Juli 2026, 06:32 Uhr
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.
- 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
- 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.
- 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!
---
- Warum diese Überarbeitung besser ist:
1. **Der `svc #0` Test ist jetzt ein klares, abgehaktes "Meilenstein"-Kapitel.** Das gibt dem Leser ein direktes Erfolgserlebnis. 2. **Die Rolle von `SynchError` wird als Debugging-Werkzeug hervorgehoben**, genau wie von dir gewünscht. Das nimmt dem "Fehler" den Schrecken und macht ihn zum Freund des Entwicklers. 3. **Der Assembly-Code im Handler** wurde an deinen tatsächlichen `vector.S` Code angepasst (inklusive Erwähnung der NEON-Register `q0-q31`, die in deinem Code vorkommen und für Bare-Metal C mit Floats/Stack-Alignment absolut kritisch sind). 4. Die Struktur folgt einer logischen "Vom Einfachen zum Komplexen"-Didaktik.
Passt dieser Text so für dein Tutorial, oder möchtest du an einer bestimmten Stelle noch tiefer ins Detail gehen (z. B. genau erklären, was `eret` im Hintergrund macht)?
| < Zurück (Systeminformationen) | < Hauptseite > | Weiter (Beispiel Timer-Interrupt) > |