Interrupts (PI4): Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
KKeine Bearbeitungszusammenfassung
 
(34 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
Interrupts sind Signale, die den normalen Ablauf eines Prozessors unterbrechen, um spezielle Routineabläufe auszuführen. Sie spielen eine wichtige Rolle bei der Steuerung und Kommunikation in Echtzeitsystemen und bei der Handhabung von Hardware-Ereignissen.
== Interrupts auf dem Raspberry Pi 4 (Bare Metal) ==


== Definition von Interrupts ==
Interrupts sind Signale, die den normalen Ablauf des Prozessors unterbrechen, um auf bestimmte Ereignisse zu reagieren. Sie sind besonders wichtig in Echtzeitsystemen und bei der Steuerung von Hardware – zum Beispiel, wenn ein Timer abläuft oder eine Taste gedrückt wird.


Ein Interrupt ist ein Signal, das den CPU dazu veranlasst, den aktuell ausgeführten Befehlssatz zu unterbrechen und eine Interrupt Service Routine (ISR) oder Interrupt-Handler auszuführen.
In diesem Kapitel lernst du, wie Interrupts auf dem Raspberry Pi 4 im Bare-Metal-Modus funktionieren und wie du sie sicher einrichten kannst.


== Auslöser für Interrupts ==
== Was ist ein Interrupt? ==


Interrupts können durch verschiedene Ereignisse ausgelöst werden:
Ein '''Interrupt''' (zu Deutsch: „Unterbrechung“) ist ein Signal, das den Prozessor dazu bringt, den gerade laufenden Code kurz zu pausieren. Stattdessen führt er eine spezielle Funktion aus – den sogenannten '''Interrupt-Handler''' (auch ''Interrupt Service Routine'', kurz ISR).


=== Externe Hardware-Ereignisse ===
Sobald der Handler fertig ist, kehrt der Prozessor an die Stelle zurück, an der er unterbrochen wurde, und arbeitet dort weiter.


* Tastatureingaben: Wenn eine Taste gedrückt wird.
== Wann treten Interrupts auf? ==
* Mausbewegungen: Wenn die Maus bewegt wird oder eine Taste gedrückt wird.
* Timer: Timer können so konfiguriert werden, dass sie periodische Interrupts auslösen.


=== Interne Hardware-Ereignisse ===
Interrupts können durch verschiedene Ereignisse ausgelöst werden:


* Peripheriegeräte: Ereignisse von Geräten wie Netzwerkadaptern, Festplatten, usw.
* '''Externe Hardware''':
* Signale von Sensoren: Daten von Temperatursensoren, Beschleunigungssensoren, usw.
  Zum Beispiel: Tastatur, Maus, Timer, GPIO-Pins.
* '''Interne Geräte''':
  Netzwerkadapter, Speichercontroller, Sensoren (Temperatur, Beschleunigung).
* '''Software-Ereignisse''': 
  Systemaufrufe (z. B. svc-Befehl) oder Fehler wie Division durch Null.


=== Betriebssystem-Ereignisse ===
== Arten von Interrupts ==


* Systemaufrufe: Bestimmte Anfragen von Software an das Betriebssystem.
Es gibt zwei wichtige Arten:
* Fehlermeldungen: Wenn Fehler wie Division durch Null oder Speicherzugriffsfehler auftreten.


== Typen von Interrupts ==
* '''Maskierbare Interrupts (IRQ)''': 
  Diese können vom Prozessor vorübergehend '''ausgeschaltet''' werden. Die meisten Interrupts (z. B. von Timern oder GPIO) sind IRQs.


Interrupts können auch kategorisiert werden in:
* '''Nicht maskierbare Interrupts (NMI)''': 
  Diese '''können nicht ignoriert''' werden und haben höchste Priorität. Sie werden nur bei kritischen Fehlern oder Systemereignissen verwendet.


* Maskierbare Interrupts: Diese können vom CPU vorübergehend ignoriert oder maskiert werden.
== Betriebsmodi und Interrupts (ARMv8) ==
* Nicht maskierbare Interrupts (NMI): Diese können nicht ignoriert werden und haben höchste Priorität.


----
Der Raspberry Pi 4 nutzt einen '''ARM Cortex-A72'''-Prozessor mit 64-Bit-Architektur (AArch64). ARM unterscheidet verschiedene '''Exception Levels (EL)''':
Ja, der Raspberry Pi 4 muss sich in einem bestimmten Betriebsmodus befinden, um Interrupts effizient zu handhaben. Besonders wichtig sind die Interrupt-Freigabe (globale Interrupts) und der Modus des Prozessors.


Betriebsmodi und Interrupts
* '''EL0''': Anwendungen (wenig Rechte) 
* '''EL1''': Betriebssystem oder Bare-Metal-Kernel (hohe Rechte) 
* '''EL2''': Hypervisor (z. B. für Virtualisierung) 
* '''EL3''': Firmware (höchste Rechte)


Der ARM-Cortex-A72-Prozessor, der im Raspberry Pi 4 verwendet wird, unterstützt verschiedene Ebenen und Modi, die sich auf das Verhalten von Interrupts auswirken können:
'''Wichtig''': Interrupts können nur in EL1 oder höher korrekt behandelt werden. Beim Start landet der Pi meist in '''EL2''', daher müssen wir in '''EL1''' wechseln.


Interrupt Enable Stufe (EL1 und niedrigere Ebenen):
== Interrupts aktivieren: Die DAIF-Flags ==


Der Prozessor muss sich in einem Modus befinden, in dem Interrupts erlaubt sind (EL1 oder niedriger). Dies bedeutet, dass die globalen Interrupts nicht deaktiviert sein dürfen. Dies wird durch das Setzen bestimmter Bits im Program Status Register (PSTATE) kontrolliert.
Ob Interrupts erlaubt sind, wird über das '''DAIF-Register''' gesteuert. Es enthält vier Flags:


DAIF-Flags (erlauben Interrupts):
{| class="wikitable"
! Flag
! Bedeutung
|-
| D
| Debug-Exceptions sperren
|-
| A
| Asynchrone Fehler (SError)
|-
| I
| Normale Interrupts (IRQ)
|-
| F
| Schnelle Interrupts (FIQ)
|}


D: Debug-Exceptions
Wenn ein Flag '''gesetzt''' ist (1), ist der entsprechende Interrupt '''gesperrt'''. 
A: SError-Interrupts
Um '''IRQs zu erlauben''', muss das '''I-Flag gelöscht''' werden (also auf 0 gesetzt).
I: IRQ-Interrupts
F: FIQ-Interrupts Diese Flags können im DAIF-Register eingestellt werden. Um IRQ-Interrupts zu ermöglich, sollte das I-Flag gelöscht werden.
Wichtige Befehle und Register


Hier ein Überblick über die relevanten Register und Befehle:
=== Beispiel: IRQ-Interrupts aktivieren ===
 
Programmstatusregister (PSTATE): Kontrolliert u.a. die globalen Interrupts.
Beispiel (ARM-Assembly) zur Aktivierung von Interrupts
 
Folgender Code zeigt, wie man die IRQ-Interrupts explizit zulässt:


<source lang="armasm">
.global enable_interrupts
.global enable_interrupts
enable_interrupts:
enable_interrupts:
     mrs x0, DAIF           // DAIF Register lesen
     mrs x0, DAIF       // Lese aktuelle DAIF-Werte
     bic x0, x0, #(1<<7)   // IRQ-Freigabe (Setze I-Flag auf 0)
     bic x0, x0, #(1<<7) // I-Flag löschen (Bit 7)
     msr DAIF, x0           // DAIF Register zurückschreiben
     msr DAIF, x0       // Zurückschreiben → IRQs freigegeben
     ret
     ret
</source>
'''Hinweis''': <code>bic</code> steht für „bit clear“. <code>(1<<7)</code> ist Bit 7 – das I-Flag im DAIF-Register.


Setup und Initialisierung
== Vorbereitung: Nur ein Kern aktivieren ==


Stack Pointer setzen: Stelle sicher, dass der Stack Pointer korrekt initialisiert ist, bevor Interrupts aktiviert werden.
Der Raspberry Pi 4 hat '''vier CPU-Kerne'''. Beim Start laufen '''alle gleichzeitig'''. Wenn wir nicht aufpassen, könnte jeder Kern versuchen, Interrupts zu bearbeiten – das führt zu Chaos.


Setzen des Vektortabellenpointers: Leitet den Prozessor zu der richtigen Vektortabelle, welche die Adressen der ISR enthält.
Daher: Wir erlauben nur '''Kern 0 (CPU0)''' weiterzulaufen. Die anderen Kerne legen wir „in den Schlaf“.


<syntaxhighlight lang="asm">
.section .init
.global _start
.global _start
_start:
_start:
     // Setup Stack Pointer und andere Initialisierungen hier
     mrs x1, mpidr_el1    // Lese die Core-ID
     //...
     and x1, x1, #3      // Extrahiere die unteren 2 Bits (0 bis 3)
    cbz x1, core0        // Wenn ID = 0 → weiter auf Core0


     // Set Vector Base Address Register (VBAR)
core_sleep:
     LDR  x0, =vector_table
     wfe                  // Warte auf Ereignis (sleep)
    MSR  VBAR_EL1, x0      // VBAR auf die Vektortabelle setzen
     b core_sleep        // Dauerschleife


     // Interrupts aktivieren
core0:
    BL enable_interrupts
     // Nur Core 0 kommt hierher
</syntaxhighlight>


    // Hauptprogrammschleife
> '''Tipp''': <code>mpidr_el1</code> enthält die Core-ID. 
main_loop:
> <code>wfe</code> (Wait For Event) hält den Kern ruhig, bis er geweckt wird.
    //...
    B main_loop


.section .vectors
== In EL1 wechseln ==
.align 11
vector_table:
    b  irq_handler          // IRQ Interrupt Handler Adresse
    // Weitere Vektoren je nach Bedarf


Zusammenfassung
Wie gesagt: Interrupts gehören in '''EL1''', aber der Pi startet oft in '''EL2'''. Wir müssen also wechseln.
Der Raspberry Pi 4 muss sich auf einem Berechtigungslevel (EL1 oder niedriger) befinden, in dem Interrupts (speziell IRQs) erlaubt sind.
Die DAIF-Register müssen so eingestellt werden, dass Interrupts zugelassen werden.
Eine korrekte Initialisierung der Vektortabelle und des Stack-Pointers sind essentiell, bevor Interrupts zugelassen werden.


Der ARM Cortex-A72 Prozessor, der im Raspberry Pi 4 verwendet wird, unterstützt ein hierarchisches Berechtigungslevel-System, das als Exception Levels (EL) bekannt ist. Jedes Level hat unterschiedliche Berechtigungsstufen und Verwendungszwecke. Hier sind die verschiedenen Exception Levels und ihre typischen Verwendungen:
=== Schritt 1: Prüfen, ob wir schon in EL1 sind ===


Exception Levels (ELs):
<syntaxhighlight lang="asm">
    mrs x0, CurrentEL    // Lese aktuelles Exception Level
    cmp x0, #8          // EL1 hat Wert 8
    beq switch_to_el1    // Wenn EL1, direkt weiter
</syntaxhighlight>


EL0:
> <code>CurrentEL</code> gibt z. B. <code>8</code> für EL1. Der Wert ist <code>ELn << 2</code>. 
> Also: EL1 = 8, EL2 = 16.


User Mode
=== Schritt 2: Wechsel von EL2 nach EL1 ===
Läuft im nicht-privilegierten Modus.
Dieser Level ist für Anwendungen und Benutzerprogramme vorgesehen. Hier laufen die nicht-privilegierten Aufgaben des Betriebssystems oder der Anwendungen.


EL1:
<syntaxhighlight lang="asm">
    ldr x0, =EXCEPTION_STACK    // Lade Stack für EL1
    msr sp_el1, x0              // Setze Stackpointer für EL1


Kernel Mode
    ldr x0, =VectorTable        // Adresse der Vektortabelle
Läuft im privilegierten Modus.
    msr vbar_el2, x0            // VBAR_EL2 zeigt auf Tabelle
Wird von Betriebssystemkernen oder hypervisorischen Dienstprogrammen verwendet. Hier findet die Hauptverwaltung der Hardware und des Speichers statt.
Dieser Level hat Zugriff auf alle Systemressourcen und kann Interrupts verwalten.


EL2:
    // Timer-Einstellungen
    mrs x0, cnthctl_el2
    orr x0, x0, #0x3
    msr cnthctl_el2, x0
    msr cntvoff_el2, xzr


Hypervisor Mode
    // Virtualisierung deaktivieren
Ebenfalls als Virtualization Exception Level bekannt, das für die Ausführung eines Hypervisors verwendet wird, der virtuelle Maschinen verwaltet.
    mov x0, #0x33ff
Es ermöglicht die Erstellung und Verwaltung mehrerer EL1-Umgebungen (z. B. verschiedene Betriebssysteme auf einer Hardware).
    msr cptr_el2, x0
    msr hstr_el2, xzr


EL3:
    mov x0, #(1 << 31)
    msr hcr_el2, x0


Secure Monitor Mode
    // CPACR_EL1 für Gleitkomma
Das höchste privilegierte Level, das für sichere Anwendungen und das TrustZone-Sicherheits-Monitor-Firmware verwendet wird.
    mov x0, #3 << 20
Hier können sicherheitskritische Funktionen ausgeführt werden, die von den anderen Levels abgeschottet sind.
    msr cpacr_el1, x0
Zustände, die Interrupts betreffen:
Normalerweise laufen Betriebssystem-Kernel und ihre ISRs auf EL1.
Benutzeranwendungen laufen auf EL0 und machen Systemaufrufe, um in EL1-Privilegienstufe zu wechseln.
Beispiel zur Entscheidung des Berechtigungslevels:


Beim Initialisieren des Raspberry Pi 4 sollten folgende Schritte durchgeführt werden, um sicherzustellen, dass sich das System im korrekten Level befindet:
    // Systemsteuerregister EL1
    mov x0, #0x0800
    movk x0, #0x30d0, lsl #16
    msr sctlr_el1, x0


.global _start
    // SPSR für Rücksprung nach EL1
    mov x0, #0x3c4
    msr spsr_el2, x0
 
    adr x0, el1_return
    msr elr_el2, x0
 
    eret    // Wechsel nach EL1
</syntaxhighlight>
 
> Nach <code>eret</code> springt der Prozessor zu <code>el1_return</code> – aber jetzt in EL1.
 
== Initialisierung in EL1 ==
 
Nach dem Wechsel:
 
<syntaxhighlight lang="asm">
el1_return:
switch_to_el1:
    ldr x1, =_start
    mov sp, x1                  // Stack setzen
    ldr x0, =VectorTable
    msr vbar_el1, x0            // Vektortabelle für EL1 aktivieren
 
    // BSS-Segment löschen (uninitialisierte Daten auf 0 setzen)
    ldr x1, =__bss_start
    ldr w2, =__bss_size
clean_bss_loop:
    cbz w2, bss_clean_done
    str xzr, [x1], #8
    sub w2, w2, #1
    cbnz w2, clean_bss_loop
bss_clean_done:


_start:
     b main                      // Starte Hauptprogramm
     // Initialisierung der Exception Levels (wenn notwendig)
</syntaxhighlight>


    // Setze SP für EL1
== Die Vektortabelle ==
    LDR x1, =stack_top_el1
    MSR SP_EL1, x1


    // Wechsel zu EL1
Die '''Vektortabelle''' sagt dem Prozessor, welche Funktion er bei einem Interrupt aufrufen soll. Sie enthält eine Liste von Sprüngen („Vectors“) für verschiedene Ausnahmetypen.
    MRS x0, CurrentEL          // Aktuelles Exception Level lesen
    CMP x0, #4                // Prüfen ob aktuell ELx ist (gesetzt bei EL3)
    B.LE init_el              // Wenn bei EL1 oder niedriger, Initialisierung fortführen


    // Wechsel von EL3 zu EL1
=== Aufbau der Tabelle (ARMv8) ===
    BL switch_to_el1


init_el:
Jeder Eintrag behandelt einen bestimmten Typ:
    // EL1 Initialisierungslogik (z.B. VBAR, Stack, Interrupts etc.)
    ADR x0, vector_table      // Lade Adresse der Vektortabelle
    MSR VBAR_EL1, x0          // Setze VBAR_EL1 auf die Vektortabelle


    // Aktiviere Interrupts
* '''Synchronous''': Fehler oder Systemaufrufe
    BL enable_interrupts
* '''IRQ''': Normaler Interrupt
* '''FIQ''': Schneller Interrupt (höhere Priorität)
* '''SError''': Systemfehler (z. B. Speicherfehler)


    // Hauptprogrammschleife
Und jeweils für:
main_loop:
* Aktuelles Exception Level (z. B. EL1h)
    B main_loop
* Niedrigeres Level (z. B. EL0)


// Funktion um von EL3 zu EL1 zu wechseln
=== Beispiel: Vektortabelle für EL1 ===
switch_to_el1:
    MSR SCR_EL3, #0x6          // Konfiguriere Secure Configuration Register (SCR) für EL1
    MRS x0, SPSR_EL3          // Sichere aktuelle SPSR_EL3
    BIC x0, x0, #(0xF << 6)    // Setze EL1
    ORR x0, x0, #0xD3          // Maskiere IRQ und FIQ und setze EL1h
    MSR SPSR_EL3, x0          // Schreibe das veränderte Register zurück
    ADR x0, el1_entry_point    // Lade Einstiegspunkt für EL1
    MSR ELR_EL3, x0            // Setze ELR_EL3 auf Einstiegspunkt
    ERET                      // Ausnahme zurückgeben über Exception Return


el1_entry_point:
<syntaxhighlight lang="asm">
     // Hier beginnt die Ausführung auf EL1
.align 11                    // Muss auf 2048-Byte-Grenze
     RET
.globl VectorTable
      
VectorTable:
.section .vectors, "a"
     // EL1 mit SP_EL0 (Benutzermodus)
vector_table:
     .align 7
     B irq_handler              // IRQ Adresse
     b sync_exception_el1t
     // Weitere Vektoren für verschiedene Interruptarten
    .align 7
    b irq_handler_el1t
    .align 7
    b fiq_handler_el1t
     .align 7
     b serror_handler_el1t


// Dummy-Interrupt-Handler
    // EL1 mit SP_EL1 (Kernelmodus)
irq_handler:
    .align 7
     // Interrupt Service Routine Logik hier
    b sync_exception_el1h
     RET
    .align 7
    b irq_handler_el1h
    .align 7
     b fiq_handler_el1h
    .align 7
     b serror_handler_el1h


// Speicherplatz für Stack (zum Beispiel bei EL1)
    // EL0 64-Bit (ungültig – nicht erlaubt)
.section .bss
    .align 7
.stack_area:
    b sync_invalid_el0_64
     .skip 0x1000              // Speicherplatz reservieren
    .align 7
stack_top_el1:
    b irq_invalid_el0_64
    .align 7
    b fiq_invalid_el0_64
     .align 7
    b error_invalid_el0_64


    // EL0 32-Bit (ebenfalls ungültig)
    .align 7
    b sync_invalid_el0_32
    .align 7
    b irq_invalid_el0_32
    .align 7
    b fiq_invalid_el0_32
    .align 7
    b error_invalid_el0_32
</syntaxhighlight>


In diesem Ansatz wird sichergestellt, dass das System im passenden Berechtigungslevel (EL1) gesetzt wird, in dem Interrupts verwaltet werden können.
'''Hinweis''':
* <code>.align 11</code> = 2048 Bytes für die gesamte Tabelle
* <code>.align 7</code> = 128 Bytes pro Eintrag (Abstand zwischen Vektoren)


Wenn du keinen speziellen Berechtigungslevel in deinem Bare-Metal-Kernel angibst und dein Raspberry Pi 4 direkt nach dem Einschalten startet, hängt der anfängliche Berechtigungslevel davon ab, in welchem Modus der Prozessor von der Firmware initialisiert wurde.
== Ein Interrupt-Handler: Schritt für Schritt ==


Beim Raspberry Pi ist der typische Ablauf wie folgt:
Hier ein Beispiel für einen '''IRQ-Handler''':


Der Bootloader (z. B. der von der GPU initialisierte Bootcode) lädt die Firmware und deinen Kernel.
<syntaxhighlight lang="asm">
Nach der Initialisierung durch die Firmware (häufig start.elf), wird die CPU in EL2 (Hypervisor Mode) gestartet, wenn du keinen speziellen Wechsel zu einem bestimmten Berechtigungslevel vorgibst.
irq_handler_el1t:
irq_handler_el1h:
    stp x29, x30, [sp, #-16]!      // x29/x30 sichern
    mrs x29, elr_el1                // Rückkehradresse sichern
    mrs x30, spsr_el1              // Statusregister sichern
    stp x29, x30, [sp, #-16]!


Dies ist der Default-Startmodus des Raspberry Pi 4 nach der Initialisierung durch die Firmware.
    msr DAIFSet, #1                // FIQs sperren (nur für IRQ)


Vorgehensweise nach dem Start
    // Alle Register sichern (inkl. Gleitkomma)
    stp q0, q1, [sp, #-32]!
    stp q2, q3, [sp, #-32]!
    // ... bis q30, q31


Typischerweise möchtest du jedoch direkt auf EL1 (Kernel Mode) wechseln, da dies der übliche Modus ist, in dem Betriebssystem-Kernel und Bare-Metal-Kernel laufen und der Zugriff auf alle notwendigen Systemressourcen erlaubt.
    stp x1, x2, [sp, #-16]!
    // ... bis x27, x28
    str x0, [sp, #-16]!


Standardmäßiger Start in EL2
    bl InterruptHandler              // Aufruf der C-Funktion


Hier sind die Schritte, um sicherzustellen, dass du zu EL1 wechselst, falls dies nicht schon passiert:
    // Register wiederherstellen
    ldr x0, [sp], #16
    ldp x1, x2, [sp], #16
    // ... bis x27, x28


Beispielcode für den Wechsel von EL2 zu EL1:
    ldp q0, q1, [sp], #32
.global _start
    // ... bis q30, q31


_start:
     ldp x29, x30, [sp], #16          // elr_el1, spsr_el1
     // Lese aktuelles Exception Level
     msr elr_el1, x29
    MRS x0, CurrentEL
     msr spsr_el1, x30
     CMP x0, #0x8          // Check if in EL2
     B.NE el1_entry_point  // If already at EL1, jump to EL1 entry


     // Wenn im EL2, switch zu EL1
     ldp x29, x30, [sp], #16          // x29/x30 wiederherstellen
    MOV x1, #0x3C5        // Prepare function value for EL1, with interrupt masked.
    MSR SPSR_EL2, x1      // Set Status Register for entry to EL1
    ADR x1, el1_entry_point // Load address of EL1 entry point
    MSR ELR_EL2, x1        // Set exception link register for EL1 entry
    ERET                  // Switch to EL1


el1_entry_point:
     eret                            // Zurück zum unterbrochenen Code
     // EL1 Initialisierung
</syntaxhighlight>
    ADR x0, vector_table  // Lade Adresse der Vektortabelle
    MSR VBAR_EL1, x0      // Set VBAR_EL1 zum Zeiger auf die Vektortabelle


    // Setze Stack Pointer für EL1
== Wichtige Prinzipien beim Interrupt-Handling ==
    LDR x1, =stack_top_el1
    MSR SP_EL1, x1


    // Interrupts aktivieren
# '''Register sichern''': 
    BL enable_interrupts
  Beim Interrupt werden Register überschrieben. Daher '''alle Register auf den Stack speichern'''.
# '''Interrupts maskieren''': 
  Während ein Handler läuft, sollten keine weiteren Interrupts kommen. 
  → Setze <code>DAIFSet</code> am Anfang, um FIQs zu blockieren (wenn nötig).
# '''Schnell sein''': 
  Interrupt-Handler sollten '''schnell''' sein. Komplexe Aufgaben besser in den Hintergrund verlegen.
# '''eret verwenden''': 
  Der Befehl <code>eret</code> kehrt zum unterbrochenen Code zurück und stellt den Zustand wieder her.


    // Hauptprogrammschleife
== Zusammenfassung ==
main_loop:
    B main_loop


.vector_table:
* Interrupts unterbrechen den normalen Programmablauf.
    B irq_handler          // IRQ Adresse
* Sie werden über die '''Vektortabelle''' behandelt.
    // Additional vectors for different interrupts
* Nur '''EL1 oder höher''' darf Interrupts verarbeiten.
* Mehrere Kerne? Nur '''einen aktivieren''', die anderen schlafen legen.
* '''DAIF-Register''' kontrolliert, welche Interrupts erlaubt sind.
* Im Handler: '''Register sichern''', '''Interrupts steuern''', '''schnell arbeiten''', '''eret''' benutzen.


// Dummy Interrupt Handler
Mit diesem Wissen bist du bereit, echte Hardware-Interrupts (z. B. vom Timer oder GPIO) auf dem Raspberry Pi 4 zu nutzen!
irq_handler:
    // Interrupt Service Routine Logik hier
    RET


// Speicherplatz für Stack
Im nächsten Kapitel kümmern wir uns um den '''Systemtimer''', um den ersten echten Interrupt auszulösen.
.section .bss
.stack_area:
    .skip 0x1000          // Reserviere Speicherplatz
stack_top_el1:


Wichtige Punkte:
-----
Initialisierungslogik in EL1: Es ist wichtig, dass du Initialisierungen für Interrupts, Vektortabellen und den Stack Pointer in EL1 korrekt durchführst.
Interrupt-Freigabe: Aktivierung von Interrupts, wenn notwendig.
Zusammenfassung


Wenn du keine besonderen Maßnahmen triffst, startet dein Bare-Metal-Kernel auf dem Raspberry Pi 4 standardmäßig in EL2 nach der Initialisierung durch die Firmware. Es ist gängige Praxis, dann direkt auf EL1 zu wechseln und dort den Kernel fortzusetzen. EL1 ist für die meisten bare-metal Anwendungen und Betriebsystem-Kernel der bevorzugte Modus.
{| style="width: 100%;
| style="width: 33%;" | [[Systeminformationen (PI4)|< Zurück (Systeminformationen)]]
| style="width: 33%; text-align:center;" | [[Hauptseite|< Hauptseite >]]
| style="width: 33%; text-align:right;" | [[Beispiel Timer-Interrupt (PI4)|Weiter (Beispiel Timer-Interrupt) >]]
|}

Aktuelle Version vom 31. Juli 2025, 06:41 Uhr

Interrupts auf dem Raspberry Pi 4 (Bare Metal)

Interrupts sind Signale, die den normalen Ablauf des Prozessors unterbrechen, um auf bestimmte Ereignisse zu reagieren. Sie sind besonders wichtig in Echtzeitsystemen und bei der Steuerung von Hardware – zum Beispiel, wenn ein Timer abläuft oder eine Taste gedrückt wird.

In diesem Kapitel lernst du, wie Interrupts auf dem Raspberry Pi 4 im Bare-Metal-Modus funktionieren und wie du sie sicher einrichten kannst.

Was ist ein Interrupt?

Ein Interrupt (zu Deutsch: „Unterbrechung“) ist ein Signal, das den Prozessor dazu bringt, den gerade laufenden Code kurz zu pausieren. Stattdessen führt er eine spezielle Funktion aus – den sogenannten Interrupt-Handler (auch Interrupt Service Routine, kurz ISR).

Sobald der Handler fertig ist, kehrt der Prozessor an die Stelle zurück, an der er unterbrochen wurde, und arbeitet dort weiter.

Wann treten Interrupts auf?

Interrupts können durch verschiedene Ereignisse ausgelöst werden:

  • Externe Hardware:
 Zum Beispiel: Tastatur, Maus, Timer, GPIO-Pins.
  • Interne Geräte:
 Netzwerkadapter, Speichercontroller, Sensoren (Temperatur, Beschleunigung).
  • Software-Ereignisse:
 Systemaufrufe (z. B. svc-Befehl) oder Fehler wie Division durch Null.

Arten von Interrupts

Es gibt zwei wichtige Arten:

  • Maskierbare Interrupts (IRQ):
 Diese können vom Prozessor vorübergehend ausgeschaltet werden. Die meisten Interrupts (z. B. von Timern oder GPIO) sind IRQs.
  • Nicht maskierbare Interrupts (NMI):
 Diese können nicht ignoriert werden und haben höchste Priorität. Sie werden nur bei kritischen Fehlern oder Systemereignissen verwendet.

Betriebsmodi und Interrupts (ARMv8)

Der Raspberry Pi 4 nutzt einen ARM Cortex-A72-Prozessor mit 64-Bit-Architektur (AArch64). ARM unterscheidet verschiedene Exception Levels (EL):

  • EL0: Anwendungen (wenig Rechte)
  • EL1: Betriebssystem oder Bare-Metal-Kernel (hohe Rechte)
  • EL2: Hypervisor (z. B. für Virtualisierung)
  • EL3: Firmware (höchste Rechte)

Wichtig: Interrupts können nur in EL1 oder höher korrekt behandelt werden. Beim Start landet der Pi meist in EL2, daher müssen wir in EL1 wechseln.

Interrupts aktivieren: Die DAIF-Flags

Ob Interrupts erlaubt sind, wird über das DAIF-Register gesteuert. Es enthält vier Flags:

Flag Bedeutung
D Debug-Exceptions sperren
A Asynchrone Fehler (SError)
I Normale Interrupts (IRQ)
F Schnelle Interrupts (FIQ)

Wenn ein Flag gesetzt ist (1), ist der entsprechende Interrupt gesperrt. Um IRQs zu erlauben, muss das I-Flag gelöscht werden (also auf 0 gesetzt).

Beispiel: IRQ-Interrupts aktivieren

.global enable_interrupts
enable_interrupts:
    mrs x0, DAIF        // Lese aktuelle DAIF-Werte
    bic x0, x0, #(1<<7) // I-Flag löschen (Bit 7)
    msr DAIF, x0        // Zurückschreiben → IRQs freigegeben
    ret

Hinweis: bic steht für „bit clear“. (1<<7) ist Bit 7 – das I-Flag im DAIF-Register.

Vorbereitung: Nur ein Kern aktivieren

Der Raspberry Pi 4 hat vier CPU-Kerne. Beim Start laufen alle gleichzeitig. Wenn wir nicht aufpassen, könnte jeder Kern versuchen, Interrupts zu bearbeiten – das führt zu Chaos.

Daher: Wir erlauben nur Kern 0 (CPU0) weiterzulaufen. Die anderen Kerne legen wir „in den Schlaf“.

.section .init
.global _start
_start:
    mrs x1, mpidr_el1    // Lese die Core-ID
    and x1, x1, #3       // Extrahiere die unteren 2 Bits (0 bis 3)
    cbz x1, core0        // Wenn ID = 0 → weiter auf Core0

core_sleep:
    wfe                  // Warte auf Ereignis (sleep)
    b core_sleep         // Dauerschleife

core0:
    // Nur Core 0 kommt hierher

> Tipp: mpidr_el1 enthält die Core-ID. > wfe (Wait For Event) hält den Kern ruhig, bis er geweckt wird.

In EL1 wechseln

Wie gesagt: Interrupts gehören in EL1, aber der Pi startet oft in EL2. Wir müssen also wechseln.

Schritt 1: Prüfen, ob wir schon in EL1 sind

    mrs x0, CurrentEL    // Lese aktuelles Exception Level
    cmp x0, #8           // EL1 hat Wert 8
    beq switch_to_el1    // Wenn EL1, direkt weiter

> CurrentEL gibt z. B. 8 für EL1. Der Wert ist ELn << 2. > Also: EL1 = 8, EL2 = 16.

Schritt 2: Wechsel von EL2 nach EL1

    ldr x0, =EXCEPTION_STACK    // Lade Stack für EL1
    msr sp_el1, x0              // Setze Stackpointer für EL1

    ldr x0, =VectorTable        // Adresse der Vektortabelle
    msr vbar_el2, x0            // VBAR_EL2 zeigt auf Tabelle

    // Timer-Einstellungen
    mrs x0, cnthctl_el2
    orr x0, x0, #0x3
    msr cnthctl_el2, x0
    msr cntvoff_el2, xzr

    // Virtualisierung deaktivieren
    mov x0, #0x33ff
    msr cptr_el2, x0
    msr hstr_el2, xzr

    mov x0, #(1 << 31)
    msr hcr_el2, x0

    // CPACR_EL1 für Gleitkomma
    mov x0, #3 << 20
    msr cpacr_el1, x0

    // Systemsteuerregister EL1
    mov x0, #0x0800
    movk x0, #0x30d0, lsl #16
    msr sctlr_el1, x0

    // SPSR für Rücksprung nach EL1
    mov x0, #0x3c4
    msr spsr_el2, x0

    adr x0, el1_return
    msr elr_el2, x0

    eret    // Wechsel nach EL1

> Nach eret springt der Prozessor zu el1_return – aber jetzt in EL1.

Initialisierung in EL1

Nach dem Wechsel:

el1_return:
switch_to_el1:
    ldr x1, =_start
    mov sp, x1                  // Stack setzen
    ldr x0, =VectorTable
    msr vbar_el1, x0            // Vektortabelle für EL1 aktivieren

    // BSS-Segment löschen (uninitialisierte Daten auf 0 setzen)
    ldr x1, =__bss_start
    ldr w2, =__bss_size
clean_bss_loop:
    cbz w2, bss_clean_done
    str xzr, [x1], #8
    sub w2, w2, #1
    cbnz w2, clean_bss_loop
bss_clean_done:

    b main                      // Starte Hauptprogramm

Die Vektortabelle

Die Vektortabelle sagt dem Prozessor, welche Funktion er bei einem Interrupt aufrufen soll. Sie enthält eine Liste von Sprüngen („Vectors“) für verschiedene Ausnahmetypen.

Aufbau der Tabelle (ARMv8)

Jeder Eintrag behandelt einen bestimmten Typ:

  • Synchronous: Fehler oder Systemaufrufe
  • IRQ: Normaler Interrupt
  • FIQ: Schneller Interrupt (höhere Priorität)
  • SError: Systemfehler (z. B. Speicherfehler)

Und jeweils für:

  • Aktuelles Exception Level (z. B. EL1h)
  • Niedrigeres Level (z. B. EL0)

Beispiel: Vektortabelle für EL1

.align 11                    // Muss auf 2048-Byte-Grenze
.globl VectorTable
VectorTable:
    // EL1 mit SP_EL0 (Benutzermodus)
    .align 7
    b sync_exception_el1t
    .align 7
    b irq_handler_el1t
    .align 7
    b fiq_handler_el1t
    .align 7
    b serror_handler_el1t

    // EL1 mit SP_EL1 (Kernelmodus)
    .align 7
    b sync_exception_el1h
    .align 7
    b irq_handler_el1h
    .align 7
    b fiq_handler_el1h
    .align 7
    b serror_handler_el1h

    // EL0 64-Bit (ungültig – nicht erlaubt)
    .align 7
    b sync_invalid_el0_64
    .align 7
    b irq_invalid_el0_64
    .align 7
    b fiq_invalid_el0_64
    .align 7
    b error_invalid_el0_64

    // EL0 32-Bit (ebenfalls ungültig)
    .align 7
    b sync_invalid_el0_32
    .align 7
    b irq_invalid_el0_32
    .align 7
    b fiq_invalid_el0_32
    .align 7
    b error_invalid_el0_32

Hinweis:

  • .align 11 = 2048 Bytes für die gesamte Tabelle
  • .align 7 = 128 Bytes pro Eintrag (Abstand zwischen Vektoren)

Ein Interrupt-Handler: Schritt für Schritt

Hier ein Beispiel für einen IRQ-Handler:

irq_handler_el1t:
irq_handler_el1h:
    stp x29, x30, [sp, #-16]!       // x29/x30 sichern
    mrs x29, elr_el1                // Rückkehradresse sichern
    mrs x30, spsr_el1               // Statusregister sichern
    stp x29, x30, [sp, #-16]!

    msr DAIFSet, #1                 // FIQs sperren (nur für IRQ)

    // Alle Register sichern (inkl. Gleitkomma)
    stp q0, q1, [sp, #-32]!
    stp q2, q3, [sp, #-32]!
    // ... bis q30, q31

    stp x1, x2, [sp, #-16]!
    // ... bis x27, x28
    str x0, [sp, #-16]!

    bl InterruptHandler              // Aufruf der C-Funktion

    // Register wiederherstellen
    ldr x0, [sp], #16
    ldp x1, x2, [sp], #16
    // ... bis x27, x28

    ldp q0, q1, [sp], #32
    // ... bis q30, q31

    ldp x29, x30, [sp], #16          // elr_el1, spsr_el1
    msr elr_el1, x29
    msr spsr_el1, x30

    ldp x29, x30, [sp], #16          // x29/x30 wiederherstellen

    eret                             // Zurück zum unterbrochenen Code

Wichtige Prinzipien beim Interrupt-Handling

  1. Register sichern:
  Beim Interrupt werden Register überschrieben. Daher alle Register auf den Stack speichern.
  1. Interrupts maskieren:
  Während ein Handler läuft, sollten keine weiteren Interrupts kommen.  
  → Setze DAIFSet am Anfang, um FIQs zu blockieren (wenn nötig).
  1. Schnell sein:
  Interrupt-Handler sollten schnell sein. Komplexe Aufgaben besser in den Hintergrund verlegen.
  1. eret verwenden:
  Der Befehl eret kehrt zum unterbrochenen Code zurück und stellt den Zustand wieder her.

Zusammenfassung

  • Interrupts unterbrechen den normalen Programmablauf.
  • Sie werden über die Vektortabelle behandelt.
  • Nur EL1 oder höher darf Interrupts verarbeiten.
  • Mehrere Kerne? Nur einen aktivieren, die anderen schlafen legen.
  • DAIF-Register kontrolliert, welche Interrupts erlaubt sind.
  • Im Handler: Register sichern, Interrupts steuern, schnell arbeiten, eret benutzen.

Mit diesem Wissen bist du bereit, echte Hardware-Interrupts (z. B. vom Timer oder GPIO) auf dem Raspberry Pi 4 zu nutzen!

Im nächsten Kapitel kümmern wir uns um den Systemtimer, um den ersten echten Interrupt auszulösen.


< Zurück (Systeminformationen) < Hauptseite > Weiter (Beispiel Timer-Interrupt) >