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

Aus C und Assembler mit Raspberry
Die Seite wurde neu angelegt: „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" und "Synchronous Exception". Andere Ausnahmen werden in eine Dauerschleife versetzt: <syntaxhighlight lang="asm"> .align 11 .globl VectorTable VectorTable: // Vektoren für EL1…“
 
KKeine Bearbeitungszusammenfassung
 
(19 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" und "Synchronous Exception". Andere Ausnahmen werden in eine Dauerschleife versetzt:
# Kapitel: Der erste echte Hardware-Interrupt – Der System-Timer (Rotor)
<syntaxhighlight lang="asm">
 
.align 11
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.
.globl VectorTable
 
VectorTable:
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.
// Vektoren für EL1t (Current Exception Level SP_el0)
 
     .align 7
Dieses Kapitel zeigt dir, wie die drei Komponenten zusammenspielen:
     b exception     // Synchronous Exception
1. Der **ARM Virtual Timer** (erzeugt das Signal).
     .align 7
2. Der **GIC-400** (Generic Interrupt Controller, verteilt das Signal an die CPU).
     b IRQStub        // IRQ - Normal Interrupt
3. Unser **C-Handler** (verarbeitet das Signal und zeichnet den Rotor).
     .align 7
 
     b hang        // FIQ - Fast Interrupt
---
    .align 7
 
    b hang     // SError - System Error
## 1. Der Generic Interrupt Controller (GIC-400)
// Vektoren für EL1h (Current Exception Level SP_el1)
 
     .align 7
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.
     b exception     // Synchronous Exception
 
     .align 7
In deiner `interrupt.c` gibt es bereits die Funktion `Interrupt_Initialize()`. Sie macht Folgendes:
     b IRQStub        // IRQ - Normal Interrupt
1. Sie schaltet den GIC-Distributor kurz ab.
    .align 7
2. Sie setzt alle Interrupts auf "Level-Triggered" und weist sie standardmäßig CPU 0 zu.
     b hang        // FIQ - Fast Interrupt
3. Sie aktiviert die CPU-Schnittstelle (`GICC_CTLR`) und erlaubt Interrupts.
     .align 7
 
     b hang     // SError - System Error
> 💡 **Merke:** Der GIC muss *einmalig* zu Beginn initialisiert werden. Danach können wir gezielt einzelne Interrupt-Quellen "freischalten".
// Vektoren für EL0 64-bit Modus
 
     .align 7
---
     b hang     // Synchronous EL0 (64-bit)
 
     .align 7
## 2. Den Virtual Timer aktivieren
     b hang      // IRQ EL0 (64-bit)
 
     .align 7
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.
     b hang      // FIQ EL0 (64-bit)
 
     .align 7
In `timer.c` findest du zwei entscheidende Funktionen:
     b hang    // Error EL0 (64-bit)
 
// Vektoren für EL0 32-bit Modus
```c
     .align 7
void EnableVirtualTimer(void)
     b hang     // Synchronous EL0 (32-bit)
{
     .align 7
    u64 cntkctl;
     b hang      // IRQ EL0 (32-bit)
    asm volatile("mrs %0, cntkctl_el1" : "=r"(cntkctl));
     .align 7
   
     b hang      // FIQ EL0 (32-bit)
    // Erlaube EL1 den Zugriff auf den Physical Count und den Virtual Timer
     .align 7
    cntkctl |= (1 << 0);  // EL1PCTEN
     b hang    // Error EL0 (32-bit)
    cntkctl |= (1 << 1);  // EL1PCEN
</syntaxhighlight>
    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)) >