Beispiel Timer-Interrupt (PI4)
- Kapitel: Der erste echte Hardware-Interrupt – Der System-Timer (Rotor)
Nachdem wir im letzten Kapitel die Vektortabelle (`vector.S`) aufgebaut und den Blindtest mit `svc #0` erfolgreich bestanden haben, wissen wir: Unser Prozessor kann auf Ausnahmen reagieren.
Jetzt machen wir den nächsten großen Schritt: Wir verbinden eine **echte Hardware** mit unserem Code. Wir richten den ARM System Timer so ein, dass er alle 250 Millisekunden einen Interrupt auslöst. Als visuelle Bestätigung zeichnen wir einen kleinen "Rotor" (`/`, `-`, `\`, `|`) in die obere rechte Ecke des Bildschirms.
Dieses Kapitel zeigt dir, wie die drei Komponenten zusammenspielen: 1. Der **ARM Virtual Timer** (erzeugt das Signal). 2. Der **GIC-400** (Generic Interrupt Controller, verteilt das Signal an die CPU). 3. Unser **C-Handler** (verarbeitet das Signal und zeichnet den Rotor).
---
- 1. Der Generic Interrupt Controller (GIC-400)
Der ARM-Prozessor selbst weiß nicht, welches Peripheral (Timer, USB, UART) einen Interrupt auslöst. Dafür gibt es den GIC. Er sitzt zwischen der Hardware und der CPU.
In deiner `interrupt.c` gibt es bereits die Funktion `Interrupt_Initialize()`. Sie macht Folgendes: 1. Sie schaltet den GIC-Distributor kurz ab. 2. Sie setzt alle Interrupts auf "Level-Triggered" und weist sie standardmäßig CPU 0 zu. 3. Sie aktiviert die CPU-Schnittstelle (`GICC_CTLR`) und erlaubt Interrupts.
> 💡 **Merke:** Der GIC muss *einmalig* zu Beginn initialisiert werden. Danach können wir gezielt einzelne Interrupt-Quellen "freischalten".
---
- 2. Den Virtual Timer aktivieren
ARMv8-Prozessoren haben zwei Timer pro Kern: den *Physical Timer* (`cntp`) und den *Virtual Timer* (`cntv`). Für Betriebssysteme (EL1) wird fast immer der **Virtual Timer** verwendet, da das Hypervisor (EL2) ihn transparent verwalten kann, ohne dass unser Kernel-Code davon etwas merkt.
In `timer.c` findest du zwei entscheidende Funktionen:
```c void EnableVirtualTimer(void) {
u64 cntkctl;
asm volatile("mrs %0, cntkctl_el1" : "=r"(cntkctl));
// Erlaube EL1 den Zugriff auf den Physical Count und den Virtual Timer
cntkctl |= (1 << 0); // EL1PCTEN
cntkctl |= (1 << 1); // EL1PCEN
cntkctl |= (1 << 2); // EL1VTEN
asm volatile("msr cntkctl_el1, %0" :: "r"(cntkctl));
} ``` Diese Funktion schaltet quasi den "Hahn" auf, damit unser Code den Timer überhaupt benutzen darf.
Als Nächstes konfigurieren wir die Auslösezeit:
```c void InitRotorTimer(void) {
// 1. Frequenz des Timers auslesen (beim Pi 4 meist 54.000.000 Hz)
u64 freq;
asm volatile("mrs %0, cntfrq_el0" : "=r"(freq));
// 2. Ziel: Alle 250 ms (freq / 4 Takte)
u64 ticks_per_250ms = freq / 4;
// 3. Timer-Wert setzen (Countdown beginnt)
asm volatile("msr cntv_tval_el0, %0" :: "r"(ticks_per_250ms));
// 4. Timer aktivieren: Bit 0 = Enable, Bit 1 = Interrupt Mask (0 = erlaubt)
asm volatile("msr cntv_ctl_el0, %0" :: "r"(1));
} ``` > ⚠️ **Wichtig:** Der ARM Timer ist ein *Countdown*-Timer. Wenn er 0 erreicht, löst er den Interrupt aus und **stoppt**. Wir müssen ihn im Handler also immer wieder neu laden!
---
- 3. Die Interrupt-Verbindung herstellen (IRQ 27)
Der Virtual Timer löst standardmäßig den Interrupt mit der ID **27** (`ARM_IRQLOCAL0_CNTPNS`) aus. Wir müssen dem GIC nun zwei Dinge sagen: 1. "Schicke IRQ 27 an CPU 0." 2. "Wenn IRQ 27 kommt, rufe diese C-Funktion auf."
Das erledigen wir mit diesen beiden Funktionen:
```c void SetIRQ27Target(void) {
// Lese das Target-Register für IRQs 24-31 u32 reg = read32(GICD_ITARGETSR0 + 4*(27/4)); // Lösche die Bits für IRQ 27 und setze sie auf CPU 0 (Wert 1) reg &= ~(0xFF << 24); reg |= (1 << 24); // Zurückschreiben und sicherstellen, dass die Änderung wirksam wird write32(reg, GICD_ITARGETSR0 + 4*(27/4)); DataSyncBarrier();
} ```
Und das Verbinden der Funktion: ```c // Registriere RotorTimerHandler für IRQ 27 Interrupt_ConnectIRQ(27, RotorTimerHandler, NULL);
// Schalte IRQ 27 im GIC Distributor scharf write32(1 << 27, GICD_ISENABLER0); DataSyncBarrier(); ```
- Hinweis:* `Interrupt_ConnectIRQ` speichert den Zeiger auf `RotorTimerHandler` in einem Array (`s_Interrupt_apIRQHandler`). Wenn der Interrupt kommt, schaut dein zentraler `InterruptHandler` in `interrupt.c` genau dort nach und ruft deine Funktion auf.
---
- 4. Der Interrupt-Handler: `RotorTimerHandler`
Hier passiert die Magie. Diese Funktion wird asynchron aufgerufen, egal was die `main()`-Schleife gerade tut.
```c void RotorTimerHandler(void* pParam) {
// 1. Die Rotor-Animation zeichnen
static int rotor = 0;
char ch;
switch (rotor % 4) {
case 0: ch = '/'; break;
case 1: ch = '-'; break;
case 2: ch = '\\'; break; // Beachte das Escape-Zeichen \
case 3: ch = '|'; break;
}
// Zeichne an Position X-8, Y=0 (oben rechts)
DrawChar(ch, SCREEN_X - 8, 0);
rotor++;
// 2. WICHTIG: Timer für die nächste Runde neu laden!
u64 freq;
asm volatile("mrs %0, cntfrq_el0" : "=r"(freq));
asm volatile("msr cntv_tval_el0, %0" :: "r"(freq / 4));
// Hinweis: Das Senden des "End of Interrupt" (EOIR) an den GIC
// wird bereits automatisch von unserem zentralen InterruptHandler
// in interrupt.c erledigt, bevor diese Funktion aufgerufen wird!
} ```
> 🛡️ **Warum kein `GICC_EOIR` hier?** > In älteren Tutorials siehst du oft `write32(irq, GICC_EOIR);` im Handler. Unser Code ist besser: Die Assembly-Routine `GenericIRQHandler` in `vector.S` ruft `InterruptHandler` auf, und *dieser* erledigt das `EOIR` zentral, *nachdem* deine `RotorTimerHandler`-Funktion fertig ist. Das verhindert, dass du es vergisst (was zu einem sofortigen Systemabsturz durch "Interrupt Storms" führen würde).
---
- 5. Alles in `main.c` zusammenfügen
Damit der Rotor sich dreht, müssen die Initialisierungen in der richtigen Reihenfolge in deiner `main.c` (oder `Kernel_Initialize`) stehen:
```c void Kernel_Initialize(CKernel *pKernel) {
// 1. Basis-Initialisierung (Bildschirm, Terminal, printf)
Init_Screen();
InitTerminal();
SetFrontColor(satyr);
printf("System startet...\n");
SetFrontColor(white);
// 2. Interrupt-Subsystem initialisieren (GIC-400)
Interrupt_Initialize();
// 3. Timer vorbereiten
EnableVirtualTimer();
// 4. IRQ 27 an CPU 0 routen und scharf schalten
SetIRQ27Target();
write32(1 << 27, GICD_ISENABLER0);
DataSyncBarrier();
// 5. Handler registrieren
Interrupt_ConnectIRQ(27, RotorTimerHandler, NULL);
// 6. Timer starten (ab jetzt kommen alle 250ms Interrupts)
InitRotorTimer();
printf("Rotor sollte oben rechts rotieren...\n");
// 7. Hauptschleife (kann leer sein, der Interrupt läuft im Hintergrund!)
while (1)
{
// Optional: CPU in energiesparenden Zustand versetzen,
// bis der nächste Interrupt kommt
asm volatile("wfi"); // Wait For Interrupt
}
} ```
---
- Zusammenfassung & Lernkontrolle
Du hast soeben einen vollständigen, produktionsreifen Interrupt-Pfad implementiert: 1. **Hardware** (Timer) zählt herunter. 2. **GIC** fängt das Signal ab und reicht es an CPU 0 weiter. 3. **CPU** unterbricht den aktuellen Code, springt in `vector.S` -> `GenericIRQHandler`. 4. **Handler** sichert alle Register (inkl. NEON), ruft `InterruptHandler` auf. 5. **Dispatcher** findet `RotorTimerHandler` in der Tabelle und führt ihn aus. 6. **Rotor** wird gezeichnet, Timer neu geladen. 7. **Rücksprung:** Register werden wiederhergestellt, `eret` kehrt zum unterbrochenen Code zurück.
| < Zurück (Interrupt) | < Hauptseite > | Weiter (Interrupt Teil 2 (PI4)) > |