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

Aus C und Assembler mit Raspberry
KKeine Bearbeitungszusammenfassung
KKeine Bearbeitungszusammenfassung
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:
<syntaxhighlight lang="asm">
.section .text
.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:
  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
  ldr x0, [sp], #16
  ldp x1, x2, [sp], #16
  ldp x3, x4, [sp], #16
  ldp x5, x6, [sp], #16
  ldp x7, x8, [sp], #16
  ldp x9, x10, [sp], #16
  ldp x11, x12, [sp], #16
  ldp x13, x14, [sp], #16
  ldp x15, x16, [sp], #16
  ldp x17, x18, [sp], #16
  ldp x19, x20, [sp], #16
  ldp x21, x22, [sp], #16
  ldp x23, x24, [sp], #16
  ldp x25, x26, [sp], #16
  ldp x27, x28, [sp], #16
  ldp x29, x30, [sp], #16
  eret
</syntaxhighlight>
Dies speichern wir als vecotor.s in unser Projekt ab.
Wir verwenden den GIC-400 Kontroller.
Als nächstes müssen wir noch Interrupts Initialisieren. Dazu erstellen wir eine C-Funktion und legen diese in interrupt.c ab:
<syntaxhighlight lang="C">
void Interrupt_Initialize (void)
{
  write32(0,GICD_CTLR);
  // Enable interrupt 30 (falls < 32, dann ISENABLER0)
  write32 ((1 << TIMER_IRQ_ID),GICD_ISENABLER0);
  write32 (0xA0A0A0A0,GICD_IPRIORITYR7); // Priority
  write32 (0x01010101,GICD_ITARGETSR7); // CPU0
  write32(1,GICD_CTLR);
  write32(0xff,GICC_PMR);
  write32(1,GICC_CTLR);
}
</syntaxhighlight>
Timer erstellen
<syntaxhighlight lang="C">
void InitCoreTimer(void)
{
  unsigned long freq;
  asm volatile("mrs %0, cntfrq_el0" : "=r"(freq));
  asm volatile ("msr cntp_tval_el0, %0" :: "r"(freq));  // 1 Sekunde
  asm volatile ("msr cntp_ctl_el0, %0" :: "r"(1));      // Timer aktivieren
  write32(2,TIMER_CNTRL0);
}
</syntaxhighlight>
* asm volatile("mrs %0, cntfrq_el0" : "=r"(freq)):
** cntfrq_el0: Dieses Systemregister enthält die Frequenz (in Hz) des Generators für das Systemzählregister (Counter Frequency Register). Hier wird die Frequenz in die Variable freq geladen (z. B. ist bei einem typischen ARMv8-Prozessor die Frequenz oft 1 MHz, abhängig von der Systemkonfiguration).
* asm volatile ("msr cntp_tval_el0, %0" :: "r"(freq)):
** cntp_tval_el0: Dieses Register legt fest, nach wie vielen Zählschritten der cntp-Timer (permanent Timer) einen Interrupt auslösen soll.
** Der Wert von freq wird in dieses Register geschrieben, was bedeutet: Der Timer löst einen Interrupt nach 1 Sekunde aus (da freq die Taktfrequenz in Hz angibt und tval die Anzahl der Takte bis zur Auslösung enthält).
* asm volatile ("msr cntp_ctl_el0, %0" :: "r"(1)):
** cntp_ctl_el0: Das Control-Register für den cntp-Timer.
*** Mit 1 wird der Timer aktiviert (Bitfeld):
**** Bit 0: Aktiviert den Timer (1 = an, 0 = aus).
**** Bit 1: (Optional) Kann konfiguriert werden, um den Timer zu maskieren (nicht im Code verwendet).
**** Bit 2: Konfiguriert, ob Timer abläuft oder permanent fortgesetzt wird.
** Mit diesem Befehl startet der Timer und zählt von cntp_tval_el0 (freq, d. h. 1 Sekunde) rückwärts.
* write32(2,TIMER_CNTRL0):
** Hier wird ein Wert 2 (binär: 10) in das Steuerregister des Timers (TIMER_CNTRL0) geschrieben. Das ist ein typischer Schritt zum Aktivieren eines lokalen Timers im SoC-Interrupt-Controller (z. B. ARM GIC oder Broadcom Interrupt Controller auf Raspberry Pi):
*** Bit 0–1: Kann Timer-Interrupts aktivieren/deaktivieren.
** Es stellt sicher, dass der Timer-Interrupt an den Prozessor weitergeleitet wird.
Im Anschluss müssen wir noch Interrupts erlauben. Dazu verwenden wir das DAIF-Register:
<syntaxhighlight lang="asm">
.globl irq_enable
irq_enable:
  msr daifclr, #0xf
  ret
</syntaxhighlight>
Nun haben wir unserer ersten Interrupt programmiert. In der Vectorbeschreibung springen wir in das Unterprogramm "irq_dispatch", wenn ein Interrupt ausgelöst wird.
Die Funktion "irq_dispatch" ist der zentrale Interrupt-Dispatcher, der beim Auftreten eines Interrupts aufgerufen wird. Hier wird analysiert, welcher Interrupt ausgelöst wurde, und entsprechend reagiert.
<syntaxhighlight lang="C">
void irq_dispatch(void)
{
  u32 irq = read32(GICC_IAR);
  if (irq == TIMER_IRQ_ID)
  {
    timer_irq_handler();
  }
  else
  {
    printf("Other IRQ: %d\n",irq);
  }
  write32(irq,GICC_EOIR);
}
</syntaxhighlight>
* GICC_IAR: Das Interrupt Acknowledge Register (Interrupt-Anerkennungsregister) des Generic Interrupt Controller (GIC) gibt die ID des auslösenden Interrupts aus.
Jeder Interrupt hat eine eindeutige ID (Interrupt Request Identifier, IRQ-ID).
Der Wert des Registers wird hier in die lokale Variable irq geladen. Dadurch weiß die Funktion, welcher Interrupt ausgelöst wurde.
* TIMER_IRQ_ID: Die ID des Timer-Interrupts (eine vordefinierte Konstante, für den Wert der IRQ-ID des Timers).
Diese Zeile vergleicht die aus dem GIC gelesene IRQ-ID (irq) mit TIMER_IRQ_ID.
Wenn der Timer-Interrupt ausgelöst wurde (z. B. durch den cntp-Timer aus dem vorherigen Code), wird die Interrupt-Service-Routine (ISR) timer_irq_handler aufgerufen.
Sollte die IRQ-ID nicht mit TIMER_IRQ_ID übereinstimmen, deutet das darauf hin, dass ein anderer Interrupt ausgelöst wurde. Dies wird abgefangen und mittels printf protokolliert.
- Beispiel: Angenommen, ein Interrupt wird durch ein GPIO-Signal ausgelöst, dann könnte als Ergebnis z. B. "Other IRQ: 42" ausgegeben werden.
* GICC_EOIR: Das End of Interrupt Register (Ende-des-Interrupt-Register) des GIC meldet, dass die Behandlung für den aktuellen Interrupt abgeschlossen ist.
Der Wert (irq) wird in dieses Register geschrieben, um den aktuellen Interrupt aus der aktiven Liste des Controllers zu entfernen.
Der GIC gibt damit den Prozessor oder das System für neue Interrupts frei.
Ohne diesen Schritt würde der GIC den Interrupt als "noch nicht abgeschlossen" betrachten, was zur Blockade weiterer Interrupts führen könnte.
Nun können wir uns ein Programm erstellen, welches darauf regiert, wenn der Timer nach 1 Sekunde einen Interrupt auslöst. Dazu erstellen wir ein Programm, welches einen Rotor auf dem oberen rechten Rand unserer Bildschirmes zeichnet.
<syntaxhighlight lang="C">
u32 Rotor = 0;
void timer_irq_handler(void)
{
  if (Rotor == 0)
  {
    DrawChar('/', SCREEN_X-8, 0);
    Rotor++;
  }
  else if (Rotor == 1)
  {
    DrawChar('-', SCREEN_X-8, 0);
    Rotor++;
  }
  else if (Rotor == 2)
  {
    DrawChar('\\', SCREEN_X-8, 0);
    Rotor++;
  }
  else if (Rotor == 3)
  {
    DrawChar('|', SCREEN_X-8, 0);
    Rotor=0;
  }
  // Reset Timer
  unsigned long freq;
  asm volatile("mrs %0, cntfrq_el0" : "=r"(freq));
  asm volatile("msr cntp_tval_el0, %0" :: "r"(freq));
}
</syntaxhighlight>
Das Zeichnen des Rotors übernimmt unsere DrawChar-Funktion und ist selbsterklärend. Wenn der Rotor gezeichnet wurde, müssen wird den Timer wieder zurücksetzen, so dass er nach 1 Sekunde wieder ausgelöst wird. Dazu verwenden wir die entsprechenden Register, die wir bereits unter "InitCoreTimer" verwendet haben.
[[Interrupt Teil 2 (PI4)]]
[https://www.satyria.de/arm/sources/RPI4/C/10.zip Source]
== Beispiel: Timer-Interrupt auf dem Raspberry Pi 4 ==
== Beispiel: Timer-Interrupt auf dem Raspberry Pi 4 ==


Zeile 246: Zeile 20:
Der folgende Code gehört in die Datei '''vector.s'''.
Der folgende Code gehört in die Datei '''vector.s'''.


<source lang="armasm">
<syntaxhighlight lang="asm">
.section .text
.section .text


Zeile 296: Zeile 70:
     wfe              // CPU wartet auf Ereignis (spart Energie)
     wfe              // CPU wartet auf Ereignis (spart Energie)
     b hang          // Zurück zur Schleife
     b hang          // Zurück zur Schleife
</source>
</syntaxhighlight>


> '''Hinweis''': Alle nicht behandelten Ausnahmen führen in die Schleife <code>hang</code>. Nur der '''IRQ''' springt zum Handler <code>IRQStub</code>.
> '''Hinweis''': Alle nicht behandelten Ausnahmen führen in die Schleife <code>hang</code>. Nur der '''IRQ''' springt zum Handler <code>IRQStub</code>.
Zeile 306: Zeile 80:
Auch dieser Teil gehört in '''vector.s'''.
Auch dieser Teil gehört in '''vector.s'''.


<source lang="armasm">
<syntaxhighlight lang="asm">
.globl IRQStub
.globl IRQStub
IRQStub:
IRQStub:
Zeile 349: Zeile 123:


     eret          // Zurück zum unterbrochenen Code
     eret          // Zurück zum unterbrochenen Code
</source>
</syntaxhighlight>


> '''Wichtig''':
'''Wichtig''':
> * Der Handler sichert alle Register, da er nicht weiß, welcher Code unterbrochen wurde.
* Der Handler sichert alle Register, da er nicht weiß, welcher Code unterbrochen wurde.
> * Nach dem Aufruf von <code>irq_dispatch</code> werden alle Register wiederhergestellt.
* Nach dem Aufruf von <code>irq_dispatch</code> werden alle Register wiederhergestellt.
> * <code>eret</code> kehrt zum ursprünglichen Code zurück.
* <code>eret</code> kehrt zum ursprünglichen Code zurück.


== Interrupt-Controller initialisieren (GIC-400) ==
== Interrupt-Controller initialisieren (GIC-400) ==
Zeile 362: Zeile 136:
Erstelle die Datei '''interrupt.c''' und füge folgende Funktion hinzu:
Erstelle die Datei '''interrupt.c''' und füge folgende Funktion hinzu:


<source lang="c">
<syntaxhighlight lang="C">
void Interrupt_Initialize(void)
void Interrupt_Initialize(void)
{
{
Zeile 386: Zeile 160:
     write32(1, GICC_CTLR);
     write32(1, GICC_CTLR);
}
}
</source>
</syntaxhighlight>


=== Erklärung der GIC-Register ===
=== Erklärung der GIC-Register ===
Zeile 419: Zeile 193:
Füge folgende Funktion in deine C-Datei ein:
Füge folgende Funktion in deine C-Datei ein:


<source lang="c">
<syntaxhighlight lang="C">
void InitCoreTimer(void)
void InitCoreTimer(void)
{
{
Zeile 436: Zeile 210:
     write32(2, TIMER_CNTRL0);
     write32(2, TIMER_CNTRL0);
}
}
</source>
</syntaxhighlight>


=== Erklärung der Timer-Register ===
=== Erklärung der Timer-Register ===
Zeile 461: Zeile 235:
Zum Schluss müssen wir globale Interrupts im Prozessor aktivieren. Dazu verwenden wir das <code>DAIF</code>-Register.
Zum Schluss müssen wir globale Interrupts im Prozessor aktivieren. Dazu verwenden wir das <code>DAIF</code>-Register.


Füge in eine Assembly-Datei (z. B. <code>irq.s</code>) folgenden Code ein:
Füge in eine Assembly-Datei (z. B. <code>utils.s</code>) folgenden Code ein:


<source lang="armasm">
<source lang="armasm">
Zeile 562: Zeile 336:
Mit diesem Wissen kannst du nun auch andere Interrupts (z. B. von GPIO, UART oder dem Network-Controller) einrichten.
Mit diesem Wissen kannst du nun auch andere Interrupts (z. B. von GPIO, UART oder dem Network-Controller) einrichten.


Im nächsten Kapitel werden wir einen '''Periodischen Timer''' oder einen '''Hardware-Interrupt von einem Taster''' hinzufügen.
[https://www.satyria.de/arm/sources/RPI4/C/10.zip Source]

Version vom 31. Juli 2025, 06:21 Uhr

Beispiel: Timer-Interrupt auf dem Raspberry Pi 4

Nachdem wir im letzten Kapitel die Grundlagen zu Interrupts kennengelernt haben, setzen wir nun das Gelernte in die Praxis um. In diesem Kapitel richten wir einen Timer-Interrupt ein, der alle eine Sekunde auslöst.

Wir verwenden den Systemtimer des ARM-Prozessors (Generic Timer) zusammen mit dem GIC-400 Interrupt-Controller (Generic Interrupt Controller). Der Interrupt führt eine einfache Aktion aus: Ein kleiner "Rotor" (Ladeanimation) dreht sich im oberen rechten Bildschirmbereich.

Dieses Beispiel zeigt dir Schritt für Schritt, wie du:

  • einen Timer konfigurierst,
  • Interrupts über den GIC aktivierst,
  • einen Interrupt-Handler schreibst und
  • den Timer nach jeder Auslösung zurücksetzt.

Wir verzichten zunächst auf Gleitkomma-Unterstützung, um den Code einfach und übersichtlich zu halten.

Vektortabelle anpassen

Zuerst erweitern wir unsere Vektortabelle, um den Timer-Interrupt korrekt zu verarbeiten. Da wir uns zunächst nur auf IRQs konzentrieren, leiten wir alle anderen Ausnahmen in eine Dauerschleife um.

Der folgende Code gehört in die Datei vector.s.

.section .text

.align 11
.globl VectorTable
VectorTable:
    // EL1 mit SP_EL0 (Benutzermodus)
    .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

    // EL1 mit SP_EL1 (Kernelmodus)
    .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

    // EL0 64-Bit (nicht erlaubt)
    .align 7
    b hang
    .align 7
    b hang
    .align 7
    b hang
    .align 7
    b hang

    // EL0 32-Bit (nicht erlaubt)
    .align 7
    b hang
    .align 7
    b hang
    .align 7
    b hang
    .align 7
    b hang

// Einfache Dauerschleife für nicht behandelte Ausnahmen
hang:
    wfe              // CPU wartet auf Ereignis (spart Energie)
    b hang           // Zurück zur Schleife

> Hinweis: Alle nicht behandelten Ausnahmen führen in die Schleife hang. Nur der IRQ springt zum Handler IRQStub.

IRQ-Handler: IRQStub

Wenn ein Interrupt ausgelöst wird, springt der Prozessor zum Label IRQStub. Dort sichern wir zunächst alle Register auf dem Stack, rufen eine C-Funktion auf und stellen danach alles wieder her.

Auch dieser Teil gehört in vector.s.

.globl IRQStub
IRQStub:
    // Kontext sichern: Alle Register auf den Stack
    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]!

    // Aufruf der C-Funktion: irq_dispatch
    bl irq_dispatch

    // Kontext wiederherstellen
    ldr x0,       [sp], #16
    ldp x1, x2,   [sp], #16
    ldp x3, x4,   [sp], #16
    ldp x5, x6,   [sp], #16
    ldp x7, x8,   [sp], #16
    ldp x9, x10,  [sp], #16
    ldp x11, x12, [sp], #16
    ldp x13, x14, [sp], #16
    ldp x15, x16, [sp], #16
    ldp x17, x18, [sp], #16
    ldp x19, x20, [sp], #16
    ldp x21, x22, [sp], #16
    ldp x23, x24, [sp], #16
    ldp x25, x26, [sp], #16
    ldp x27, x28, [sp], #16
    ldp x29, x30, [sp], #16

    eret           // Zurück zum unterbrochenen Code

Wichtig:

  • Der Handler sichert alle Register, da er nicht weiß, welcher Code unterbrochen wurde.
  • Nach dem Aufruf von irq_dispatch werden alle Register wiederhergestellt.
  • eret kehrt zum ursprünglichen Code zurück.

Interrupt-Controller initialisieren (GIC-400)

Wir verwenden den Generic Interrupt Controller (GIC-400), der im Raspberry Pi 4 integriert ist. Um den Timer-Interrupt zu nutzen, müssen wir den GIC konfigurieren.

Erstelle die Datei interrupt.c und füge folgende Funktion hinzu:

void Interrupt_Initialize(void)
{
    // Deaktiviere den Distributor vor der Konfiguration
    write32(0, GICD_CTLR);

    // Aktiviere den Timer-Interrupt (IRQ-ID = TIMER_IRQ_ID)
    write32((1 << TIMER_IRQ_ID), GICD_ISENABLER0);

    // Setze Priorität (alle auf mittel)
    write32(0xA0A0A0A0, GICD_IPRIORITYR7);

    // Ziel-CPU festlegen (CPU0)
    write32(0x01010101, GICD_ITARGETSR7);

    // Distributor wieder aktivieren
    write32(1, GICD_CTLR);

    // Prioritätsmaske setzen (akzeptiere alle Prioritäten)
    write32(0xFF, GICC_PMR);

    // CPU-Schnittstelle aktivieren
    write32(1, GICC_CTLR);
}

Erklärung der GIC-Register

Register Bedeutung
GICD_CTLR Steuert den Distributor (0 = aus, 1 = an).
GICD_ISENABLER0 Aktiviert Interrupts mit ID 0–31. Hier wird Bit TIMER_IRQ_ID gesetzt.
GICD_IPRIORITYR7 Setzt die Priorität für Interrupts 28–31. 0xA0 = mittlere Priorität.
GICD_ITARGETSR7 Legt fest, welcher CPU-Kern den Interrupt erhält. 0x01 = CPU0.
GICC_PMR Prioritätsmaske: Nur Interrupts mit höherer Priorität als 0xFF werden zugelassen.
GICC_CTLR Aktiviert die CPU-Schnittstelle des GIC.

Systemtimer konfigurieren

Der ARMv8-Prozessor verfügt über einen eingebauten System Counter und einen Core Timer (cntp). Wir nutzen diesen, um alle 1 Sekunde einen Interrupt auszulösen.

Füge folgende Funktion in deine C-Datei ein:

void InitCoreTimer(void)
{
    unsigned long freq;

    // Lese die System-Taktfrequenz (z. B. 1 MHz)
    asm volatile("mrs %0, cntfrq_el0" : "=r"(freq));

    // Setze den Timer auf 1 Sekunde (freq Zyklen)
    asm volatile("msr cntp_tval_el0, %0" :: "r"(freq));

    // Aktiviere den Timer (Bit 0 = enable)
    asm volatile("msr cntp_ctl_el0, %0" :: "r"(1));

    // Zusätzlich: Aktiviere den lokalen Timer im SoC (BCM2711)
    write32(2, TIMER_CNTRL0);
}

Erklärung der Timer-Register

Register Bedeutung
cntfrq_el0 Enthält die Frequenz des Systemzählers in Hz (z. B. 1.000.000).
cntp_tval_el0 Legt fest, nach wie vielen Takten der Timer auslöst. freq = 1 Sekunde.
cntp_ctl_el0 Steuerregister: Bit 0 aktiviert den Timer.
TIMER_CNTRL0 SoC-internes Steuerregister (Broadcom), aktiviert den Timer-Interrupt.

Globale Interrupts aktivieren

Zum Schluss müssen wir globale Interrupts im Prozessor aktivieren. Dazu verwenden wir das DAIF-Register.

Füge in eine Assembly-Datei (z. B. utils.s) folgenden Code ein:

.globl irq_enable
irq_enable:
    msr daifclr, #0xf    // Lösche alle DAIF-Flags (D, A, I, F)
    ret

> Hinweis: daifclr, #0xf entsperrt alle Interrupts (IRQ, FIQ, SError, Debug).

Der Interrupt-Dispatcher

Die Funktion irq_dispatch ist der zentrale Punkt, an dem alle IRQs landen. Sie prüft, welcher Interrupt ausgelöst wurde, und ruft den passenden Handler auf.

void irq_dispatch(void)
{
    u32 irq = read32(GICC_IAR);  // Lese die IRQ-ID

    if (irq == TIMER_IRQ_ID)
    {
        timer_irq_handler();     // Behandele Timer-Interrupt
    }
    else
    {
        printf("Unbekannter IRQ: %d\n", irq);
    }

    // Melde "Ende des Interrupts" an den GIC
    write32(irq, GICC_EOIR);
}

Wichtige GIC-Register

Register Bedeutung
GICC_IAR Interrupt Acknowledge Register: Gibt die ID des auslösenden Interrupts zurück.
GICC_EOIR End of Interrupt Register: Signalisiert, dass der Interrupt behandelt ist.

> Wichtig: Ohne GICC_EOIR bleibt der Interrupt aktiv und kann nicht erneut ausgelöst werden!

Timer-Interrupt-Handler

Der eigentliche Handler führt die gewünschte Aktion aus. In unserem Fall aktualisieren wir einen kleinen Rotor im oberen rechten Bildschirmbereich.

u32 Rotor = 0;

void timer_irq_handler(void)
{
    // Zeichne den nächsten Rotor-Zustand
    if (Rotor == 0)
    {
        DrawChar('/', SCREEN_X - 8, 0);
        Rotor++;
    }
    else if (Rotor == 1)
    {
        DrawChar('-', SCREEN_X - 8, 0);
        Rotor++;
    }
    else if (Rotor == 2)
    {
        DrawChar('\\', SCREEN_X - 8, 0);
        Rotor++;
    }
    else if (Rotor == 3)
    {
        DrawChar('|', SCREEN_X - 8, 0);
        Rotor = 0;
    }

    // Timer für nächste Sekunde zurücksetzen
    unsigned long freq;
    asm volatile("mrs %0, cntfrq_el0" : "=r"(freq));
    asm volatile("msr cntp_tval_el0, %0" :: "r"(freq));
}

> Hinweis: Der Timer wird nach jeder Auslösung neu gestartet, um den 1-Sekunden-Takt beizubehalten.

Zusammenfassung

Du hast jetzt einen vollständigen Timer-Interrupt auf dem Raspberry Pi 4 im Bare-Metal-Modus implementiert!

  • Die Vektortabelle leitet den IRQ an IRQStub weiter.
  • IRQStub sichert den Kontext und ruft irq_dispatch in C auf.
  • Der GIC wird konfiguriert, um den Timer-Interrupt zu empfangen.
  • Der Systemtimer wird auf 1 Sekunde eingestellt.
  • Der Handler aktualisiert eine Anzeige und setzt den Timer zurück.

Mit diesem Wissen kannst du nun auch andere Interrupts (z. B. von GPIO, UART oder dem Network-Controller) einrichten.

Source