Interrupts (PI4): Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
Zeile 535: Zeile 535:
</source>
</source>


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



Version vom 30. Juli 2025, 18:31 Uhr

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.

Definition von Interrupts

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.

Auslöser für Interrupts

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

Externe Hardware-Ereignisse

  • Tastatureingaben, wie das Drücken einer Taste..
  • 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

  • Peripheriegeräte: Ereignisse von Geräten wie Netzwerkadaptern, Festplatten, usw.
  • Signale von Sensoren: Daten von Temperatursensoren, Beschleunigungssensoren, usw.

Betriebssystem-Ereignisse

  • Systemaufrufe: Bestimmte Anfragen von Software an das Betriebssystem.
  • Fehlermeldungen: Wenn Fehler wie Division durch Null oder Speicherzugriffsfehler auftreten.

Typen von Interrupts

Interrupts können in verschiedene Kategorien eingeteilt werden::

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

Betriebsmodi und Interrupts

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:

Interrupt Enable Stufe (EL1 und niedrigere Ebenen)

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.

DAIF-Flags (erlauben Interrupts)

D Debug-Exceptions
A SError-Interrupts
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:

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:

.global enable_interrupts

enable_interrupts:
    mrs x0, DAIF           // DAIF Register lesen
    bic x0, x0, #(1<<7)    // IRQ-Freigabe (Setze I-Flag auf 0)
    msr DAIF, x0           // DAIF Register zurückschreiben
    ret

Interrupts mit dem Raspberry 4 im 64-Bit-Modus

Wie bereits zuvor beschrieben, muss der Raspberry Pi 4 in einen bestimmten Level sein.

Wenn kein spezieller Berechtigungslevel im Bare-Metal-Kernel angegeben wurde und der 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.

Beim Raspberry Pi ist der typische Ablauf wie folgt:

Der Bootloader (z. B. der von der GPU initialisierte Bootcode) lädt die Firmware und deinen Kernel. Nach der Initialisierung durch die Firmware (häufig start.elf), wird die CPU bei vielen Bare-Metal-Installationen in EL2 (Hypervisor Mode) gestartet, wenn kein spezieller Wechsel zu einem bestimmten Berechtigungslevel vorgegeben wird.

Damit für uns alles gut funktioniert, müssen wir in den Level 1. Zuvor müssen wir uns noch um die verschiedenen CPUs kümmern. Zur Zeit Laufen alle vier Kerne des ARM-Prozessors parallel.

Umsetzung auf den Raspberry Pi 4 (64-Bit)

Bisher haben wir in unserem Boot-Code nicht viel geschrieben gehabt. Das einzige war, dass wir unsere Kernel-Stackpointer gesetzt haben und anschließend direkt in unseren Code gegangen sind.

Nun hat der Raspberry Pi 4 allerdings 4 Kerne, die unabhängig voneinander Programme ablaufen lassen können. Bei Interrupts, könnte das uns Probleme bereiten, wenn ein bestimmter Kern, den wir eigentlich nicht verwenden, eine Ausnahme verursacht. Dies könnte zu einem chaotischen Zustand des Raspberry Pi führen. Damit dies nicht passiert, werden wir unseren Code nur noch auf der Haupt-CPU ausführen und die übrigen CPUs schlafen legen:

.section .init
.global _start

_start:
  mrs x1, mpidr_el1  //Lies den Inhalt des Systemregisters MPIDR_EL1 (Multiprocessor Affinity Register) und speichere ihn in Register x1.
  and x1, x1, #3
  cbz x1, core0
core_sleep: // Core 1-3 schlafen legen
  wfe
  b core_sleep
core0:

Mit "mrs" lesen wir das Systemregister MPIDR_EL1 aus. Dies beinhaltet die Kennung des CPUs. Diese ID steht in den unteren 2 Bits und kann durch "and #3" herausextrahiert werden. Mit "cbz" Sprung, wenn "NULL" wird die CPU0 verwendet, die hier an Label "core0:" weiterspring. Alle anderen CPUs werden mit "wfe" schlafen gelegt und oder in die Dauerschleife "b core_sleep" versetzt.

Als nächstes prüfen wir das Level, um sicherzustellen, dass wir nicht schon bereits in EL1 sind.

  mrs x0, CurrentEL   //Überprüfen, ob bereits in EL1
  cmp x0, #4
  beq switch_to_el1

Auch hierfür gibt es ein Systemregister, in dem der Momentane Level steht. Mit "mrs" und "CurrentEL" können wir dieses Register lesen. Wenn hier der Wert 4 steht, sind wir bereits in EL1 und überspringen das Umschalten in Level1.

Wechsel von EL2 nach EL1

//Wechsel von EL2 nach EL1
  ldr x0, =EXCEPTION_STACK
  msr sp_el1, x0

  ldr x0, =VectorTable
  msr vbar_el2, x0

  mrs x0, cnthctl_el2
  orr x0, x0, #0x3
  msr cnthctl_el2, x0
  msr cntvoff_el2, xzr

  mrs x0, midr_el1
  mrs x1, mpidr_el1
  msr vpidr_el2, x0
  msr vmpidr_el2, x1

  mov x0, #0x33ff
  msr cptr_el2, x0
  msr hstr_el2, xzr

  mov x0, #3 << 20
  msr cpacr_el1, x0

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

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

  mov x0, #0x3c4
  msr spsr_el2, x0
  adr x0, el1_return
  msr elr_el2, x0
  eret
  • ldr x0, =EXCEPTION_STACK: Die Adresse des Exception-Stacks wird geladen.
  • msr sp_el1, x0: Der Stack des EL1 (sp_el1) wird gesetzt.

Dann folgen verschiedene Register-Konfigurationen, welche die Timer-Funktionalität sowie die virtuelle CPU-Profilierung einrichten:

  • msr cnthctl_el2, x0 und msr cntvoff_el2, xzr: Timer-Konfiguration.
  • msr vpidr_el2, x0 und msr vmpidr_el2, x1: Virtualisierungs-Konfiguration.

Weitere Einstellungen betreffen die Steuerregister und die HCR_EL2-Konfiguration:

  • msr cptr_el2, x0, msr hstr_el2, xzr: Steuerregister.
  • msr hcr_el2, x0: Hypervisor Configuration Register (HCR_EL2) wird konfiguriert.

Schließlich wird der Systemsteuer-Register SCTLR_EL1 initialisiert und das SPSR_EL2 gesetzt:

  • msr sctlr_el1, x0: Einstellung des System Control Register im EL1.
  • msr spsr_el2, x0: Setzen des Saved Program Status Register, damit eret weiß, wohin er springen muss.

Mit eret erfolgt der eigentliche Wechsel auf Exception Level 1:

  • eret: Return from exception und Wechsel auf EL1.

Initialisierung im EL1

el1_return:
switch_to_el1:
  ldr x1, =_start
  mov sp, x1          // Stack setzen

  ldr x0, =VectorTable
  msr vbar_el1, x0    // VectorTabelle setzen für Exceptions

  // BSS section reinigen
  ldr x1, =__bss_start
  ldr w2, =__bss_size
clean_bss_loop: 
  cbz w2, bss_clean_done    // Quit loop if zero
  str xzr, [x1], #8
  sub w2, w2, #1
  cbnz w2, clean_bss_loop   // Loop if non-zero

bss_clean_done:
  // Springe in unser Hauptprogramm
  b main
  • ldr x1,=_start: Lade die Startadresse.
  • mov sp,x1: Setzen des Stack-Pointers an die Startadresse.

Reinigung der BSS-Sektion (__bss_start bis __bss_size) um sicherzustellen, dass alle uninitialisierten Daten null sind.

Warum wird der Wechsel durchgeführt?

Der Wechsel von EL2 zu EL1 wird durchgeführt, um vom Hypervisor-Modus in den normalen Betriebsmodus des Systems zu wechseln. EL2 ist für Virtualisierung und Hypervisor-Funktionen reserviert, während EL1 für das OS oder Bare-Metal-Anwendungen wie hier vorgesehen ist. EL1 bietet die notwendige Kontrolle und Privilegien für die Initialisierung und den Betrieb des Systems. Der Hypervisor (EL2) wird meist nur zu Beginn für Konfigurationen benötigt oder bei niedriger privilegierten OS-Schichten.

Vektortabelle

Wie im Code zuvor ersichtlich, wird dem Prozessor ein Zeiger auf eine Vektortabelle übergeben. Diese benötigt der Prozessor, um zu sehen, was er machen soll, wenn bestimmte Ausnahmen (Exceptions), wie bereits beschrieben, bei der Ausführung entstehen.

Struktur einer Vektortabelle

Die ARMv8-Architektur, die im Raspberry Pi 4 verwendet wird, verfügt über eine spezielle Vektortabelle für verschiedene Arten von Ausnahmen. Die Vektortabelle ist eine wie folgt strukturierte Liste von Adressen, welche die Einstiegspunkte für unterschiedliche Ausnahmearten (Exceptions) definiert. Der Prozessor springt basierend auf der Art der Ausnahme an die relevante Adresse innerhalb der Tabelle.

  • Synchronous Exception - Lower Level
  • IRQ (Normal Interrupt) - Lower Level
  • FIQ (Fast Interrupt) - Lower Level
  • SError (System Error) - Lower Level
  • Synchronous Exception - Current Level
  • IRQ (Normal Interrupt) - Current Level
  • FIQ (Fast Interrupt) - Current Level
  • SError (System Error) - Current Level

Zusätzlich muss man beachten, dass der Prozessor theoretisch im AArch64 und im AArch32 Modus befinden kann. Hinweis: Zu beachten ist, dass AArch32-Handler nur verwendet werden können, wenn der Prozessor für den Kompatibilitätsmodus eingerichtet wurde. Andernfalls sollten alle Handler AArch64-kompatibel implementiert werden.

Beispiel einer Vektortabelle

Hier ist ein Beispiel, wie eine Vektortabelle für EL1 aussehen könnte:

.align	11
.globl	VectorTable
VectorTable:
// Vektoren für EL1t (Current Exception Level SP_el0)
    .align	7
    b sync_exception_el1t     // Synchronous Exception
    .align	7
    b irq_handler_el1t        // IRQ - Normal Interrupt
    .align	7
    b fiq_handler_el1t        // FIQ - Fast Interrupt
    .align	7
    b serror_handler_el1t     // SError - System Error
// Vektoren für EL1h (Current Exception Level SP_el1)
    .align	7
    b sync_exception_el1h     // Synchronous Exception
    .align	7
    b irq_handler_el1h        // IRQ - Normal Interrupt
    .align	7
    b fiq_handler_el1h        // FIQ - Fast Interrupt
    .align	7
    b serror_handler_el1h     // SError - System Error
// Vektoren für EL0 64-bit Modus
    .align	7
    b sync_invalid_el0_64     // Synchronous EL0 (64-bit)
    .align	7
    b irq_invalid_el0_64      // IRQ EL0 (64-bit)
    .align	7
    b fiq_invalid_el0_64      // FIQ EL0 (64-bit)
    .align	7
    b error_invalid_el0_64    // Error EL0 (64-bit)
// Vektoren für EL0 32-bit Modus
    .align	7
    b sync_invalid_el0_32     // Synchronous EL0 (32-bit)
    .align	7
    b irq_invalid_el0_32      // IRQ EL0 (32-bit)
    .align	7
    b fiq_invalid_el0_32      // FIQ EL0 (32-bit)
    .align	7
    b error_invalid_el0_32    // Error EL0 (32-bit)

Erläuterungen zu den Einträgen:

  • sync_invalid_*: Diese Handler werden für synchrone Ausnahmen verwendet, wie z.B. system calls oder undefinierte Instruktionen.
  • irq_invalid_*: Diese Handler werden für normale Interrupt-Anfragen verwendet.
  • fiq_invalid_*: Diese Handler werden für schnelle Interrupt-Anfragen verwendet.
  • error_invalid_*: Diese Handler werden für Systemfehler verwendet.

Wichtige Punkte zur Vektortabelle

  • Ausrichtung: Die Vektortabelle muss auf eine bestimmte Weise ausgerichtet sein, typischerweise 2^11 Byte (2048 Byte, align 11), um den Anforderungen des ARM-Prozessors zu entsprechen. Jeder Sprungbefehl muss mit align 7 (128 Bytes) ausgerichtet sein.
  • Reihenfolge: Die Reihenfolge der Einträge ist fix und muss den Spezifikationen entsprechend sein. Jeder Vektor muss die korrekte Entsprechung der Ausnahme-Adressen haben.
  • Handlers: Jeder Eintrag in der Vektortabelle verweist auf einen spezifischen Handler für jede Art von Ausnahme.

Was ist zu beachten bei Interrupts

Um zu verhindern, dass während der Bearbeitung einer Ausnahme (Interrupt Handling) eine weitere Ausnahme auftritt und zu unerwünschten Zuständen führt, gibt es mehrere Mechanismen, die verwendet werden können. Diese Mechanismen beinhalten das Maskieren von Interrupts, das Setzen von Flags und das Sicherstellen einer richtigen Reihenfolge und Priorität von Ausnahmebehandlungen.

Mechanismen zur Verhinderung von Ausnahmen während der Ausnahmebehandlung

  • Maskieren von Interrupts:

Beim Eintritt in einen Ausnahme-Handler kannst du alle weiteren Interrupts maskieren. Dies bedeutet, dass andere Interrupts so lange blockiert werden, bis der aktuelle Handler vollständig abgeschlossen ist. FIQs haben eine höhere Priorität als IRQs und sind daher für zeitkritischere Operationen gedacht. Während ein IRQ behandelt wird, können FIQs dennoch ausgelöst werden, es sei denn, sie werden explizit maskiert. Die ARM-Architektur stellt dafür spezielle Register (DAIF, CPSR) zur Verfügung, mit denen Interrupts individuell oder global maskiert werden können.

  • DAIF-Register:

Das DAIF-Register enthält Flags, um Debug-, SError-, IRQ- und FIQ-Interrupts zu maskieren. Um alle Interrupts zu maskieren, wird das DAIF-Register mit den entsprechenden Bits gesetzt.

Bei der Ausführung des Handlers

Da eine Ausnahme zu jeder Zeit geschehen kann, wird das laufende Programm unterbrochen. Die Register, die für das Ausführen des Programms verwendet werden, werden auch in der Ausnahme verwendet. Dies kann zu Problemen kommen, wenn der Code an der Ursprünglichen Adresse wieder ausgeführt wird. Aus diesem Grund ist es wichtig, wenn es um eine absichtliche Ausnahme geht, die Register zu sichern, bevor die Ausführung des entsprechenden Codes für die Ausnahme abläuft. Aus diesem Grund, werden sämtliche Register auf den Stack abgelegt. Da wir auch Gleitkomma erlauben, müssen auch diese Register entsprechend abgelegt werden. Bei erfolgreichen Abarbeiten der Ausnahme, werden diese Register wieder aus dem Stack gefüllt und das Programm kann seinen Dienst fortführen.

Während des ausführen einer Ausnahme sollte es dem System unterbunden werden, dass während der Bearbeitung weitere Ausnahmen entstehen. Dies kann zu einem katastrophalen System führen, welches nicht mehr händelbar ist. Hier kann durch das maskieren des DAIF-Registers das weitere auslösen von Ausnahmen verhindert werden. Siehe dazu auch den vorigen Absatz.


Hier Beispielhaft für einen "normalen" Interrupt:

.align	11
.globl	VectorTable
VectorTable:
// Vektoren für EL1t (Current Exception Level SP_el0)
    ...
    b irq_handler_el1t        // IRQ - Normal Interrupt
    ...
// Vektoren für EL1h (Current Exception Level SP_el1)
    ...
    b irq_handler_el1h        // IRQ - Normal Interrupt
    ...
// Vektoren für EL0 64-bit Modus
    ...
// Vektoren für EL0 32-bit Modus
    ...

...
irq_handler_el1t:
irq_handler_el1h:
	stp	x29, x30, [sp, #-16]!		// save x29, x30 onto stack 

	mrs	x29, elr_el1			// save elr_el1, spsr_el1 onto stack 
	mrs	x30, spsr_el1
	stp	x29, x30, [sp, #-16]!
	msr	DAIFClr, #1			// enable FIQ 

	stp	q30, q31, [sp, #-32]!		// save q0-q31 onto stack 
	stp	q28, q29, [sp, #-32]!
	stp	q26, q27, [sp, #-32]!
	stp	q24, q25, [sp, #-32]!
	stp	q22, q23, [sp, #-32]!
	stp	q20, q21, [sp, #-32]!
	stp	q18, q19, [sp, #-32]!
	stp	q16, q17, [sp, #-32]!
	stp	q14, q15, [sp, #-32]!
	stp	q12, q13, [sp, #-32]!
	stp	q10, q11, [sp, #-32]!
	stp	q8, q9, [sp, #-32]!
	stp	q6, q7, [sp, #-32]!
	stp	q4, q5, [sp, #-32]!
	stp	q2, q3, [sp, #-32]!
	stp	q0, q1, [sp, #-32]!

	stp	x27, x28, [sp, #-16]!		// save x0-x28 onto stack 
	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]!

	ldr	x0, =IRQReturnAddress		// store return address for profiling 
	str	x29, [x0]

	bl	InterruptHandler

	ldr	x0, [sp], #16			// restore x0-x28 from stack 
	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	q0, q1, [sp], #32		// restore q0-q31 from stack 
	ldp	q2, q3, [sp], #32
	ldp	q4, q5, [sp], #32
	ldp	q6, q7, [sp], #32
	ldp	q8, q9, [sp], #32
	ldp	q10, q11, [sp], #32
	ldp	q12, q13, [sp], #32
	ldp	q14, q15, [sp], #32
	ldp	q16, q17, [sp], #32
	ldp	q18, q19, [sp], #32
	ldp	q20, q21, [sp], #32
	ldp	q22, q23, [sp], #32
	ldp	q24, q25, [sp], #32
	ldp	q26, q27, [sp], #32
	ldp	q28, q29, [sp], #32
	ldp	q30, q31, [sp], #32

	msr	DAIFSet, #1			// disable FIQ 
	ldp	x29, x30, [sp], #16		// restore elr_el1, spsr_el1 from stack 
	msr	elr_el1, x29
	msr	spsr_el1, x30

	ldp	x29, x30, [sp], #16		// restore x29, x30 from stack 

	eret
...

Weiter Beispiel Timer-Interrupt (PI4)

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.