Beispiel Timer-Interrupt (PI4)

Aus C und Assembler mit Raspberry
Version vom 21. Juli 2026, 14:38 Uhr von Satyria (Diskussion | Beiträge)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
  1. 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. 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".

---

    1. 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!

---

    1. 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.

---

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

---

    1. 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
   }

} ```

---

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