Beispiel Timer-Interrupt (PI4): Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
KKeine Bearbeitungszusammenfassung
KKeine Bearbeitungszusammenfassung
 
(16 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
Nun versuchen wir, einen Timer-Interrupt zu erstellen und verwenden eine Vector-Tabelle, wie diese zuvor beschrieben wurde. Wir verzichten zunächst auf die Gleitkomma-Unterstützung.


Wir verwenden die Vectortabelle aus der vorigen Beschreibung. Unterstützen allerdings nur "IRQs". Andere Ausnahmen werden in eine Dauerschleife versetzt:
# Kapitel: Der erste echte Hardware-Interrupt – Der System-Timer (Rotor)
<syntaxhighlight lang="asm">
.align 11
.globl VectorTable
VectorTable:
// Vektoren für EL1t (Current Exception Level SP_el0)
    .align 7
    b hang    // Synchronous Exception
    .align 7
    b IRQStub        // IRQ - Normal Interrupt
    .align 7
    b hang        // FIQ - Fast Interrupt
    .align 7
    b hang    // SError - System Error
// Vektoren für EL1h (Current Exception Level SP_el1)
    .align 7
    b hang    // Synchronous Exception
    .align 7
    b IRQStub        // IRQ - Normal Interrupt
    .align 7
    b hang        // FIQ - Fast Interrupt
    .align 7
    b hang    // SError - System Error
// Vektoren für EL0 64-bit Modus
    .align 7
    b hang    // Synchronous EL0 (64-bit)
    .align 7
    b hang      // IRQ EL0 (64-bit)
    .align 7
    b hang      // FIQ EL0 (64-bit)
    .align 7
    b hang    // Error EL0 (64-bit)
// Vektoren für EL0 32-bit Modus
    .align 7
    b hang    // Synchronous EL0 (32-bit)
    .align 7
    b hang      // IRQ EL0 (32-bit)
    .align 7
    b hang      // FIQ EL0 (32-bit)
    .align 7
    b hang    // Error EL0 (32-bit)


hang:
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.  
    wfe        // spare CPU cycles
    b hang
</syntaxhighlight>
Im Anschluss müssen wir beschreiben, was passieren soll, wenn der Interrupt ausgelöst wurde:
<syntaxhighlight lang="asm">
.globl IRQStub
IRQStub:
  stp x29, x30, [sp, #-16]!
  stp x27, x28, [sp, #-16]!
  stp x25, x26, [sp, #-16]!
  stp x23, x24, [sp, #-16]!
  stp x21, x22, [sp, #-16]!
  stp x19, x20, [sp, #-16]!
  stp x17, x18, [sp, #-16]!
  stp x15, x16, [sp, #-16]!
  stp x13, x14, [sp, #-16]!
  stp x11, x12, [sp, #-16]!
  stp x9, x10, [sp, #-16]!
  stp x7, x8, [sp, #-16]!
  stp x5, x6, [sp, #-16]!
  stp x3, x4, [sp, #-16]!
  stp x1, x2, [sp, #-16]!
  str x0, [sp, #-16]!


    bl irq_dispatch  //Springe zur Auswertung
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.


  ldr x0, [sp], #16
Dieses Kapitel zeigt dir, wie die drei Komponenten zusammenspielen:
  ldp x1, x2, [sp], #16
1. Der **ARM Virtual Timer** (erzeugt das Signal).
  ldp x3, x4, [sp], #16
2. Der **GIC-400** (Generic Interrupt Controller, verteilt das Signal an die CPU).
  ldp x5, x6, [sp], #16
3. Unser **C-Handler** (verarbeitet das Signal und zeichnet den Rotor).
  ldp x7, x8, [sp], #16
 
  ldp x9, x10, [sp], #16
---
  ldp x11, x12, [sp], #16
 
  ldp x13, x14, [sp], #16
## 1. Der Generic Interrupt Controller (GIC-400)
  ldp x15, x16, [sp], #16
 
  ldp x17, x18, [sp], #16
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.
  ldp x19, x20, [sp], #16
 
  ldp x21, x22, [sp], #16
In deiner `interrupt.c` gibt es bereits die Funktion `Interrupt_Initialize()`. Sie macht Folgendes:
  ldp x23, x24, [sp], #16
1. Sie schaltet den GIC-Distributor kurz ab.
  ldp x25, x26, [sp], #16
2. Sie setzt alle Interrupts auf "Level-Triggered" und weist sie standardmäßig CPU 0 zu.
  ldp x27, x28, [sp], #16
3. Sie aktiviert die CPU-Schnittstelle (`GICC_CTLR`) und erlaubt Interrupts.
  ldp x29, x30, [sp], #16
 
  eret
> 💡 **Merke:** Der GIC muss *einmalig* zu Beginn initialisiert werden. Danach können wir gezielt einzelne Interrupt-Quellen "freischalten".
</syntaxhighlight>
 
---
 
## 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.
 
 
-----
 
{| style="width: 100%;
| style="width: 33%;" | [[Interrupts (PI4)|< Zurück (Interrupt)]]
| style="width: 33%; text-align:center;" | [[Hauptseite|< Hauptseite >]]
| style="width: 33%; text-align:right;" | [[Interrupt Teil 2 (PI4)|Weiter (Interrupt Teil 2 (PI4)) >]]
|}

Aktuelle Version vom 21. Juli 2026, 14:38 Uhr

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