|
|
| (11 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.
| | <span style="background:#FF0000; color:#FFFFFF;"> Wichtig! Durch die Entwicklung zur USB-Unterstützung wurde einige Funktionen, die wir bisher genutzt haben, überarbeitet. Diese "neuen" Funktionen sind ab hier eingebaut. Darunter zählt zum Beispiel die printf-Funktion, die hier komplett überarbeitet ist. Zusätzlich wurden einige Screen-Funktionen überarbeitet. </span> |
|
| |
|
| == Definition von Interrupts == | | == Interrupts und Exceptions auf dem Raspberry Pi 4 (Bare Metal) == |
|
| |
|
| 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.
| | Interrupts (Unterbrechungen) und Exceptions (Ausnahmen) sind das Nervensystem jedes Betriebssystems. |
| | Sie sorgen dafür, dass der Prozessor nicht blind Code abarbeitet, sondern auf Ereignisse reagieren kann – sei es, |
| | dass ein Timer abläuft, eine Taste gedrückt wird oder ein schwerwiegender Programmierfehler auftritt. |
|
| |
|
| == Auslöser für Interrupts ==
| | In diesem Kapitel lernst du, wie die Ausnahmebehandlung im ARMv8 (AArch64) funktioniert, |
| | wie wir die Vektortabelle einrichten und wie wir mit einem einfachen "Blindtest" sicherstellen, |
| | dass unser System bereit für echte Hardware-Interrupts ist. |
|
| |
|
| Interrupts können durch verschiedene Ereignisse ausgelöst werden: | | == Exceptions vs. Interrupts: Was ist der Unterschied? == |
|
| |
|
| === Externe Hardware-Ereignisse ===
| | Obwohl die Begriffe oft synonym verwendet werden, gibt es einen feinen, aber wichtigen Unterschied: |
| | * Exceptions (Synchron): Treten '''direkt''' durch die Ausführung eines Befehls auf. Beispiele: Ein ungültiger Speicherzugriff, eine Division durch Null oder ein bewusster Systemaufruf (wie `svc #0`). Der Prozessor weiß '''genau''', welcher Befehl die Ausnahme ausgelöst hat. |
| | * Interrupts (Asynchron): Treten '''unabhängig''' vom aktuellen Befehl auf, ausgelöst durch externe Hardware. Beispiele: Der System-Timer, eine USB-Maus oder ein GPIO-Pin. |
|
| |
|
| * Tastatureingaben, wie das Drücken einer Taste..
| | ''Tipp für die OS-Entwicklung'': Synchronous Exceptions (wie unsere `SynchError`-Handler) sind deine besten Freunde beim Debuggen! Wenn dein Code versehentlich in einen `SynchError`-Handler springt, weißt du sofort: Hier ist ein schwerer Fehler (z. B. Sprung an eine ungültige Adresse oder falscher Befehl). Wir werden das gleich gezielt ausnutzen. |
| * 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 === | | == Die Spielregeln: Exception Levels (EL) und DAIF == |
|
| |
|
| * Peripheriegeräte: Ereignisse von Geräten wie Netzwerkadaptern, Festplatten, usw. | | Der ARM Cortex-A72 kennt verschiedene Privilegienstufen, die '''Exception Levels (EL)''': |
| * Signale von Sensoren: Daten von Temperatursensoren, Beschleunigungssensoren, usw. | | * '''EL0:''' User-Mode (Anwendungen, wenig Rechte) |
| | * '''EL1:''' Kernel-Mode (Unser Bare-Metal-Code, volle Hardware-Kontrolle) |
| | * '''EL2:''' Hypervisor (Virtualisierung, wird vom Bootloader kurz genutzt) |
| | * '''EL3:''' Secure Monitor (Highest Privilege, z. B. ARM Trusted Firmware) |
|
| |
|
| === Betriebssystem-Ereignisse ===
| | '''Wichtig:''' Um Interrupts korrekt zu verarbeiten, müssen wir uns in '''EL1''' befinden. Da der Raspberry Pi 4 oft in EL2 startet, ist unser erster Schritt der Wechsel nach EL1. |
|
| |
|
| * Systemaufrufe: Bestimmte Anfragen von Software an das Betriebssystem. | | Zudem kontrolliert das '''DAIF'''-Register, welche Interrupts gerade erlaubt sind: |
| * Fehlermeldungen: Wenn Fehler wie Division durch Null oder Speicherzugriffsfehler auftreten. | | * '''D''' (Debug): Debug-Exceptions sperren |
| | * '''A''' (SError): Asynchrone Systemfehler sperren |
| | * '''I''' (IRQ): Normale Interrupts sperren |
| | * '''F''' (FIQ): Schnelle Interrupts (Fast IRQs) sperren |
|
| |
|
| == Typen von Interrupts ==
| | Ist ein Bit auf `1` gesetzt, ist der Interrupt '''gesperrt'''. Um IRQs zu erlauben, müssen wir das I-Bit auf `0` setzen (clear). |
|
| |
|
| Interrupts können in verschiedene Kategorien eingeteilt werden::
| | == Vorbereitung: Nur ein Kern darf laufen == |
|
| |
|
| * Maskierbare Interrupts: Diese können vom CPU vorübergehend ignoriert oder maskiert werden.
| | Der Pi 4 hat 4 Kerne. Beim Start führen alle denselben Code aus. Würden alle versuchen, Interrupts zu bearbeiten oder den Stack zu nutzen, würde das System sofort abstürzen. Wir lassen nur '''Core 0''' arbeiten und schicken die anderen in einen energiesparenden Schlafmodus (`WFE` = Wait For Event). |
| * 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) ===
| |
|
| |
| {| class="wikitable"
| |
| | 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:
| |
| <syntaxhighlight lang="asm"> | | <syntaxhighlight lang="asm"> |
| .global enable_interrupts | | // In boot.S |
| | _start: |
| | mrs x1, mpidr_el1 // Core-ID lesen |
| | and x1, x1, #3 // Nur die unteren 2 Bits (0-3) behalten |
| | cbz x1, core0 // Wenn Core 0, weiter zum Setup |
|
| |
|
| enable_interrupts:
| | core_sleep: |
| mrs x0, DAIF // DAIF Register lesen | | wfe // Warte auf ein Event (Sleep) |
| bic x0, x0, #(1<<7) // IRQ-Freigabe (Setze I-Flag auf 0)
| | b core_sleep // Endlosschleife für Core 1-3 |
| msr DAIF, x0 // DAIF Register zurückschreiben | |
| ret
| |
| </syntaxhighlight>
| |
| | |
| == 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:
| |
| | |
| <syntaxhighlight lang="asm">
| |
| .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: | | core0: |
| | // Ab hier läuft nur noch Core 0 |
| </syntaxhighlight> | | </syntaxhighlight> |
| 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.
| |
| <syntaxhighlight lang="asm">
| |
| mrs x0, CurrentEL //Überprüfen, ob bereits in EL1
| |
| cmp x0, #4
| |
| beq switch_to_el1
| |
| </syntaxhighlight>
| |
| 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 ===
| |
| <syntaxhighlight lang="asm">
| |
| //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
| |
| </syntaxhighlight>
| |
|
| |
| * 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 ===
| |
| <syntaxhighlight lang="asm">
| |
| 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
| |
| </syntaxhighlight>
| |
|
| |
| * 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:
| |
| <syntaxhighlight lang="asm">
| |
| .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)
| |
| </syntaxhighlight>
| |
| 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 == | | == Der Wechsel von EL2 nach EL1 == |
| 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.
| | Da wir ein Kernel schreiben, gehört unser Platz in EL1. Der folgende Code konfiguriert EL1, setzt den Stackpointer und bereitet den Rücksprung vor. |
|
| |
|
|
| |
| Hier Beispielhaft für einen "normalen" Interrupt:
| |
| <syntaxhighlight lang="asm"> | | <syntaxhighlight lang="asm"> |
| .align 11
| | // 1. Prüfen, ob wir schon in EL1 sind (CurrentEL = 8 für EL1) |
| .globl VectorTable
| | mrs x0, CurrentEL |
| VectorTable:
| | cmp x0, #8 |
| // Vektoren für EL1t (Current Exception Level SP_el0)
| | beq switch_to_el1 |
| ...
| |
| 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
| |
| ...
| |
|
| |
|
| ... | | // 2. EL1 Stackpointer setzen |
| irq_handler_el1t:
| | ldr x0, =EXCEPTION_STACK |
| irq_handler_el1h:
| | msr sp_el1, x0 |
| stp x29, x30, [sp, #-16]! // save x29, x30 onto stack
| |
|
| |
|
| mrs x29, elr_el1 // save elr_el1, spsr_el1 onto stack
| | // 3. Vektortabelle für EL2 setzen (für den Fall, dass während des Wechsels was passiert) |
| mrs x30, spsr_el1
| | ldr x0, =VectorTable |
| stp x29, x30, [sp, #-16]!
| | msr vbar_el2, x0 |
| msr DAIFClr, #1 // enable FIQ
| |
|
| |
|
| stp q30, q31, [sp, #-32]! // save q0-q31 onto stack
| | // ... (Hier folgen Timer- und Virtualisierungs-Einstellungen, siehe boot.S) ... |
| 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
| | // 4. Rücksprung-Adresse und Status für EL1 vorbereiten |
| stp x25, x26, [sp, #-16]!
| | mov x0, #0x3c4 // SPSR_EL2: EL1h, Interrupts (DAIF) sind hier noch maskiert |
| 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
| |
| ...
| |
| </syntaxhighlight>
| |
| 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:
| |
| | |
| {| class="wikitable"
| |
| ! 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 ===
| |
| | |
| <source lang="armasm">
| |
| .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
| |
| </source>
| |
| | |
| > '''Hinweis''': <code>bic</code> steht für „bit clear“. <code>(1<<7)</code> 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“.
| |
| | |
| <source lang="armasm">
| |
| .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
| |
| </source>
| |
| | |
| > '''Tipp''': <code>mpidr_el1</code> enthält die Core-ID.
| |
| > <code>wfe</code> (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 ===
| |
| | |
| <source lang="armasm">
| |
| mrs x0, CurrentEL // Lese aktuelles Exception Level
| |
| cmp x0, #8 // EL1 hat Wert 8 (nicht 4!)
| |
| beq switch_to_el1 // Wenn EL1, direkt weiter
| |
| </source>
| |
| | |
| > '''Korrektur''': <code>CurrentEL</code> gibt z. B. <code>8</code> für EL1 (nicht <code>4</code>). Der Wert ist <code>ELn << 2</code>.
| |
| > Also: EL1 = 8, EL2 = 16.
| |
| | |
| === Schritt 2: Wechsel von EL2 nach EL1 ===
| |
| | |
| <source lang="armasm">
| |
| 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 | | msr spsr_el2, x0 |
| | | adr x0, el1_return // Adresse, zu der wir nach dem Wechsel springen |
| adr x0, el1_return | |
| msr elr_el2, x0 | | msr elr_el2, x0 |
|
| |
|
| eret // Wechsel nach EL1 | | eret // Exception Return: Wechselt nach EL1 zu 'el1_return' |
| </source>
| |
|
| |
|
| > Nach <code>eret</code> springt der Prozessor zu <code>el1_return</code> – aber jetzt in EL1.
| |
|
| |
| == Initialisierung in EL1 ==
| |
|
| |
| Nach dem Wechsel:
| |
|
| |
| <source lang="armasm">
| |
| el1_return: | | el1_return: |
| switch_to_el1: | | switch_to_el1: |
| ldr x1, =_start | | // Wir sind jetzt in EL1! |
| mov sp, x1 // Stack setzen
| |
| ldr x0, =VectorTable | | ldr x0, =VectorTable |
| msr vbar_el1, x0 // Vektortabelle für EL1 aktivieren | | msr vbar_el1, x0 // Vektortabelle für EL1 aktivieren |
|
| |
|
| // BSS-Segment löschen (uninitialisierte Daten auf 0 setzen) | | // BSS-Segment (uninitialisierte Variablen) mit Nullen füllen |
| ldr x1, =__bss_start | | ldr x1, =__bss_start |
| ldr w2, =__bss_size | | ldr w2, =__bss_size |
| Zeile 604: |
Zeile 100: |
| bss_clean_done: | | bss_clean_done: |
|
| |
|
| b main // Starte Hauptprogramm | | b main // Ab ins C-Hauptprogramm! |
| </source> | | </syntaxhighlight> |
|
| |
|
| == Die Vektortabelle == | | == Die Exception Vector Table (VBT) == |
|
| |
|
| 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. | | Die VBT ist das "Telefonbuch" des Prozessors. Bei einer Ausnahme schaut die CPU in dieses Register (`vbar_el1`) und springt zu der dort hinterlegten Adresse. |
|
| |
|
| === Aufbau der Tabelle (ARMv8) ===
| | Die Tabelle muss exakt '''2048 Byte (2 KB)''' groß sein (''.align 11''). Sie ist in 16 Einträge unterteilt (4 Ausnahmetypen × 4 Ausführungsstatus). Jeder Eintrag muss exakt '''128 Byte''' (''.align 7'') voneinander entfernt sein. |
|
| |
|
| Jeder Eintrag behandelt einen bestimmten Typ:
| | <syntaxhighlight lang="asm"> |
| | | // In vector.S |
| * '''Synchronous''': Fehler oder Systemaufrufe
| | .align 11 // 2^11 = 2048 Byte Ausrichtung für die ganze Tabelle |
| * '''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 ===
| |
| | |
| <source lang="armasm"> | |
| .align 11 // Muss auf 2048-Byte-Grenze | |
| .globl VectorTable | | .globl VectorTable |
| VectorTable: | | VectorTable: |
| // EL1 mit SP_EL0 (Benutzermodus) | | // 1. Aktuelles EL, Stackpointer ist SP_EL0 (User-Mode, nutzen wir nicht) |
| .align 7 | | .align 7 |
| b sync_exception_el1t | | b SynchError1 |
| .align 7 | | .align 7 |
| b irq_handler_el1t | | b GenericIRQHandler |
| .align 7 | | .align 7 |
| b fiq_handler_el1t | | b FastInterruptHandler |
| .align 7 | | .align 7 |
| b serror_handler_el1t | | b SErrorStub1 |
|
| |
|
| // EL1 mit SP_EL1 (Kernelmodus) | | // 2. Aktuelles EL, Stackpointer ist SP_ELx (Kernel-Mode, EL1h) -> DAS NUTZEN WIR! |
| .align 7 | | .align 7 |
| b sync_exception_el1h | | b SynchError2 // Synchronous Exception (z.B. svc #0, Memory Abort) |
| .align 7 | | .align 7 |
| b irq_handler_el1h | | b GenericIRQHandler // Normaler Hardware-Interrupt (IRQ) |
| .align 7 | | .align 7 |
| b fiq_handler_el1h | | b FastInterruptHandler // FIQ |
| .align 7 | | .align 7 |
| b serror_handler_el1h | | b SErrorStub2 // System Error |
|
| |
|
| // EL0 64-Bit (ungültig – nicht erlaubt) | | // 3. Niedrigeres EL (EL0), 64-Bit (Auch hier leiten wir auf unsere Handler um) |
| .align 7 | | .align 7 |
| b sync_invalid_el0_64 | | b SynchError1 |
| .align 7 | | .align 7 |
| b irq_invalid_el0_64 | | b GenericIRQHandler |
| .align 7 | | // ... (weitere Einträge siehe vector.S) |
| b fiq_invalid_el0_64
| | </syntaxhighlight> |
| .align 7
| | |
| b error_invalid_el0_64
| | 🛠️ '''Debugging-Hinweis:''' Die ''SynchError''-Handler sind extrem wertvoll. Wenn dein Programm unerwartet in `SynchError2` landet, weißt du: Ein Befehl in EL1 war ungültig oder hat einen Fehler verursacht. Wir werden das gleich gezielt provozieren, um zu prüfen, ob die Tabelle funktioniert. |
| | |
| | == Der Blindtest: `svc #0` (Supervisor Call) == |
| | |
| | Bevor wir uns mit komplexer Hardware (wie dem GIC-Interrupt-Controller) herumschlagen, müssen wir sicherstellen, dass unser Exception-Mechanismus grundlegend funktioniert. |
| | |
| | Der Befehl ''svc #0'' (Supervisor Call) löst absichtlich eine '''synchrone Exception''' aus. Da wir uns in EL1 befinden, sollte die CPU in den Eintrag ''SynchError2'' unserer Vektortabelle springen. |
|
| |
|
| // EL0 32-Bit (ebenfalls ungültig)
| | Füge diesen Test vorübergehend in deine `main.c` ein, *bevor* du `printf` oder andere Initialisierungen machst: |
| .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
| |
| </source>
| |
|
| |
|
| > '''Hinweis''': | | <syntaxhighlight lang="C"> |
| > * <code>.align 11</code> = 2048 Bytes für die gesamte Tabelle
| | // In main.c |
| > * <code>.align 7</code> = 128 Bytes pro Eintrag (Abstand zwischen Vektoren)
| | #include "printf.h" |
| | #include "sync.h" // Für Stop() |
|
| |
|
| == Ein Interrupt-Handler: Schritt für Schritt ==
| | int main(void) |
| | { |
| | // --- BLINDTEST FÜR EXCEPTIONS --- |
| | printf("Starte Exception-Blindtest...\n"); |
| | |
| | // Dieser Befehl löst absichtlich eine synchrone Exception aus. |
| | // Die CPU sollte in vector.S -> SynchError2 -> ExceptionHandler springen. |
| | asm volatile("svc #0"); |
| | |
| | // Diese Zeile sollte NICHT erreicht werden! |
| | printf("FEHLER: svc #0 wurde nicht abgefangen!\n"); |
| | Stop(); |
| | // ---------------------------------- |
|
| |
|
| Hier ein Beispiel für einen '''IRQ-Handler''':
| | // ... Rest deiner Initialisierung (Interrupt_Initialize, Timer, etc.) ... |
| | } |
| | </syntaxhighlight> |
|
| |
|
| <source lang="armasm">
| | **Erwartetes Ergebnis:** |
| irq_handler_el1t:
| | Auf dem Bildschirm sollte erscheinen: |
| irq_handler_el1h:
| | `Starte Exception-Blindtest...` |
| stp x29, x30, [sp, #-16]! // x29/x30 sichern
| | `Error: Exception 'SynchError2' ausgelöst` |
| mrs x29, elr_el1 // Rückkehradresse sichern
| | Und das Programm bleibt stehen (dank `Stop()` im `ExceptionHandler`). |
| mrs x30, spsr_el1 // Statusregister sichern
| |
| stp x29, x30, [sp, #-16]!
| |
|
| |
|
| msr DAIFSet, #1 // FIQs sperren (nur für IRQ)
| | **Wenn das funktioniert, hast du den schwersten Teil bereits geschafft:** Der Wechsel nach EL1, das Setzen von `vbar_el1` und die korrekte Ausrichtung der Vektortabelle sind verifiziert! Du kannst den `svc #0`-Test danach wieder entfernen. |
|
| |
|
| // Alle Register sichern (inkl. Gleitkomma)
| | ## 7. Der IRQ-Handler: Den Zustand retten und wiederherstellen |
| stp q0, q1, [sp, #-32]!
| |
| stp q2, q3, [sp, #-32]!
| |
| // ... bis q30, q31
| |
|
| |
|
| stp x1, x2, [sp, #-16]!
| | Wenn ein echter Hardware-Interrupt (z. B. vom Timer) kommt, springt die CPU zu `GenericIRQHandler`. Da der Interrupt *jederzeit* passieren kann, weiß der Handler nicht, welche Register gerade benutzt wurden. |
| // ... bis x27, x28
| |
| str x0, [sp, #-16]!
| |
|
| |
|
| bl InterruptHandler // Aufruf der C-Funktion
| | Die goldene Regel lautet: **Rette alles, was du veränderst, und stelle es exakt so wieder her.** |
|
| |
|
| // Register wiederherstellen | | <syntaxhighlight lang="asm"> |
| | // In vector.S (vereinfachte Darstellung des GenericIRQHandler) |
| | .globl GenericIRQHandler |
| | GenericIRQHandler: |
| | // 1. Frame-Pointer und Link-Register retten |
| | stp x29, x30, [sp, #-16]! |
| | |
| | // 2. Rücksprungadresse (elr_el1) und Status (spsr_el1) retten |
| | mrs x29, elr_el1 |
| | mrs x30, spsr_el1 |
| | stp x29, x30, [sp, #-16]! |
| | |
| | // 3. FIQs erlauben (falls sie gesperrt waren), IRQs bleiben durch Eintritt automatisch gesperrt |
| | msr DAIFClr, #1 |
| | |
| | // 4. ALLE General Purpose Register (x0-x28) auf den Stack sichern |
| | stp x27, x28, [sp, #-16]! |
| | // ... (weitere stp Befehle bis x0) ... |
| | str x0, [sp, #-16]! |
| | |
| | // 5. ALLE NEON/FP-Register (q0-q31) sichern (wichtig für C-Code mit Floats!) |
| | stp q30, q31, [sp, #-32]! |
| | // ... (weitere stp Befehle bis q0) ... |
| | |
| | // 6. Jetzt ist der Stack sauber. Wir können sicher C-Code aufrufen! |
| | bl InterruptHandler |
| | |
| | // 7. Epilog: Alles in EXAKT umgekehrter Reihenfolge wiederherstellen! |
| | ldp q0, q1, [sp], #32 |
| | // ... (bis q30, q31) |
| | |
| ldr x0, [sp], #16 | | ldr x0, [sp], #16 |
| ldp x1, x2, [sp], #16 | | ldp x1, x2, [sp], #16 |
| // ... bis x27, x28 | | // ... (bis x27, x28) |
| | | |
| ldp q0, q1, [sp], #32 | | ldp x29, x30, [sp], #16 |
| // ... bis q30, q31
| |
| | |
| ldp x29, x30, [sp], #16 // elr_el1, spsr_el1 | |
| msr elr_el1, x29 | | msr elr_el1, x29 |
| msr spsr_el1, x30 | | msr spsr_el1, x30 |
| | |
| | ldp x29, x30, [sp], #16 |
| | |
| | // 8. Zurück zum unterbrochenen Code |
| | eret |
| | </syntaxhighlight> |
|
| |
|
| ldp x29, x30, [sp], #16 // x29/x30 wiederherstellen
| | ## 8. Zusammenfassung |
|
| |
|
| eret // Zurück zum unterbrochenen Code
| | 1. **Nur ein Kern:** Wir schicken Core 1-3 in den Schlaf (`WFE`), um Chaos zu vermeiden. |
| </source>
| | 2. **EL1 ist Pflicht:** Wir wechseln von EL2 nach EL1, um Kernel-Rechte zu haben. |
| | 3. **Vektortabelle:** Sie muss auf 2 KB ausgerichtet sein und zeigt der CPU den Weg zu den Handlern. |
| | 4. **Blindtest:** Ein `svc #0` ist der perfekte, sichere Weg, um zu prüfen, ob Exceptions korrekt abgefangen werden, bevor man Hardware-IRQs konfiguriert. |
| | 5. **State Saving:** Der Assembly-Handler muss *jedes* Register sichern, bevor er C-Code (`InterruptHandler`) aufruft, und es danach exakt wiederherstellen. |
|
| |
|
| == Wichtige Prinzipien beim Interrupt-Handling ==
| | ## Ausblick |
|
| |
|
| # '''Register sichern''':
| | Nun, da das Fundament steht und wir wissen, dass unsere Exception-Tabelle funktioniert, können wir uns im nächsten Kapitel dem **Generic Interrupt Controller (GIC-400)** und dem **System Timer** widmen. Dort werden wir lernen, wie wir einen echten Hardware-Interrupt (IRQ 27) aktivieren, um unseren ersten periodischen "Rotor" auf dem Bildschirm zu animieren! |
| 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.
| |
|
| |
|
| == Zusammenfassung == | | {| style="width: 100%; |
| | | | style="width: 33%;" | [[Systeminformationen (PI4)|< Zurück (Systeminformationen)]] |
| * Interrupts unterbrechen den normalen Programmablauf.
| | | style="width: 33%; text-align:center;" | [[Hauptseite|< Hauptseite >]] |
| * Sie werden über die '''Vektortabelle''' behandelt.
| | | style="width: 33%; text-align:right;" | [[Beispiel Timer-Interrupt (PI4)|Weiter (Beispiel Timer-Interrupt) >]] |
| * 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.
| |
Wichtig! Durch die Entwicklung zur USB-Unterstützung wurde einige Funktionen, die wir bisher genutzt haben, überarbeitet. Diese "neuen" Funktionen sind ab hier eingebaut. Darunter zählt zum Beispiel die printf-Funktion, die hier komplett überarbeitet ist. Zusätzlich wurden einige Screen-Funktionen überarbeitet.
Interrupts und Exceptions auf dem Raspberry Pi 4 (Bare Metal)
Interrupts (Unterbrechungen) und Exceptions (Ausnahmen) sind das Nervensystem jedes Betriebssystems.
Sie sorgen dafür, dass der Prozessor nicht blind Code abarbeitet, sondern auf Ereignisse reagieren kann – sei es,
dass ein Timer abläuft, eine Taste gedrückt wird oder ein schwerwiegender Programmierfehler auftritt.
In diesem Kapitel lernst du, wie die Ausnahmebehandlung im ARMv8 (AArch64) funktioniert,
wie wir die Vektortabelle einrichten und wie wir mit einem einfachen "Blindtest" sicherstellen,
dass unser System bereit für echte Hardware-Interrupts ist.
Exceptions vs. Interrupts: Was ist der Unterschied?
Obwohl die Begriffe oft synonym verwendet werden, gibt es einen feinen, aber wichtigen Unterschied:
- Exceptions (Synchron): Treten direkt durch die Ausführung eines Befehls auf. Beispiele: Ein ungültiger Speicherzugriff, eine Division durch Null oder ein bewusster Systemaufruf (wie `svc #0`). Der Prozessor weiß genau, welcher Befehl die Ausnahme ausgelöst hat.
- Interrupts (Asynchron): Treten unabhängig vom aktuellen Befehl auf, ausgelöst durch externe Hardware. Beispiele: Der System-Timer, eine USB-Maus oder ein GPIO-Pin.
Tipp für die OS-Entwicklung: Synchronous Exceptions (wie unsere `SynchError`-Handler) sind deine besten Freunde beim Debuggen! Wenn dein Code versehentlich in einen `SynchError`-Handler springt, weißt du sofort: Hier ist ein schwerer Fehler (z. B. Sprung an eine ungültige Adresse oder falscher Befehl). Wir werden das gleich gezielt ausnutzen.
Die Spielregeln: Exception Levels (EL) und DAIF
Der ARM Cortex-A72 kennt verschiedene Privilegienstufen, die Exception Levels (EL):
- EL0: User-Mode (Anwendungen, wenig Rechte)
- EL1: Kernel-Mode (Unser Bare-Metal-Code, volle Hardware-Kontrolle)
- EL2: Hypervisor (Virtualisierung, wird vom Bootloader kurz genutzt)
- EL3: Secure Monitor (Highest Privilege, z. B. ARM Trusted Firmware)
Wichtig: Um Interrupts korrekt zu verarbeiten, müssen wir uns in EL1 befinden. Da der Raspberry Pi 4 oft in EL2 startet, ist unser erster Schritt der Wechsel nach EL1.
Zudem kontrolliert das DAIF-Register, welche Interrupts gerade erlaubt sind:
- D (Debug): Debug-Exceptions sperren
- A (SError): Asynchrone Systemfehler sperren
- I (IRQ): Normale Interrupts sperren
- F (FIQ): Schnelle Interrupts (Fast IRQs) sperren
Ist ein Bit auf `1` gesetzt, ist der Interrupt gesperrt. Um IRQs zu erlauben, müssen wir das I-Bit auf `0` setzen (clear).
Vorbereitung: Nur ein Kern darf laufen
Der Pi 4 hat 4 Kerne. Beim Start führen alle denselben Code aus. Würden alle versuchen, Interrupts zu bearbeiten oder den Stack zu nutzen, würde das System sofort abstürzen. Wir lassen nur Core 0 arbeiten und schicken die anderen in einen energiesparenden Schlafmodus (`WFE` = Wait For Event).
// In boot.S
_start:
mrs x1, mpidr_el1 // Core-ID lesen
and x1, x1, #3 // Nur die unteren 2 Bits (0-3) behalten
cbz x1, core0 // Wenn Core 0, weiter zum Setup
core_sleep:
wfe // Warte auf ein Event (Sleep)
b core_sleep // Endlosschleife für Core 1-3
core0:
// Ab hier läuft nur noch Core 0
Der Wechsel von EL2 nach EL1
Da wir ein Kernel schreiben, gehört unser Platz in EL1. Der folgende Code konfiguriert EL1, setzt den Stackpointer und bereitet den Rücksprung vor.
// 1. Prüfen, ob wir schon in EL1 sind (CurrentEL = 8 für EL1)
mrs x0, CurrentEL
cmp x0, #8
beq switch_to_el1
// 2. EL1 Stackpointer setzen
ldr x0, =EXCEPTION_STACK
msr sp_el1, x0
// 3. Vektortabelle für EL2 setzen (für den Fall, dass während des Wechsels was passiert)
ldr x0, =VectorTable
msr vbar_el2, x0
// ... (Hier folgen Timer- und Virtualisierungs-Einstellungen, siehe boot.S) ...
// 4. Rücksprung-Adresse und Status für EL1 vorbereiten
mov x0, #0x3c4 // SPSR_EL2: EL1h, Interrupts (DAIF) sind hier noch maskiert
msr spsr_el2, x0
adr x0, el1_return // Adresse, zu der wir nach dem Wechsel springen
msr elr_el2, x0
eret // Exception Return: Wechselt nach EL1 zu 'el1_return'
el1_return:
switch_to_el1:
// Wir sind jetzt in EL1!
ldr x0, =VectorTable
msr vbar_el1, x0 // Vektortabelle für EL1 aktivieren
// BSS-Segment (uninitialisierte Variablen) mit Nullen füllen
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 // Ab ins C-Hauptprogramm!
Die Exception Vector Table (VBT)
Die VBT ist das "Telefonbuch" des Prozessors. Bei einer Ausnahme schaut die CPU in dieses Register (`vbar_el1`) und springt zu der dort hinterlegten Adresse.
Die Tabelle muss exakt 2048 Byte (2 KB) groß sein (.align 11). Sie ist in 16 Einträge unterteilt (4 Ausnahmetypen × 4 Ausführungsstatus). Jeder Eintrag muss exakt 128 Byte (.align 7) voneinander entfernt sein.
// In vector.S
.align 11 // 2^11 = 2048 Byte Ausrichtung für die ganze Tabelle
.globl VectorTable
VectorTable:
// 1. Aktuelles EL, Stackpointer ist SP_EL0 (User-Mode, nutzen wir nicht)
.align 7
b SynchError1
.align 7
b GenericIRQHandler
.align 7
b FastInterruptHandler
.align 7
b SErrorStub1
// 2. Aktuelles EL, Stackpointer ist SP_ELx (Kernel-Mode, EL1h) -> DAS NUTZEN WIR!
.align 7
b SynchError2 // Synchronous Exception (z.B. svc #0, Memory Abort)
.align 7
b GenericIRQHandler // Normaler Hardware-Interrupt (IRQ)
.align 7
b FastInterruptHandler // FIQ
.align 7
b SErrorStub2 // System Error
// 3. Niedrigeres EL (EL0), 64-Bit (Auch hier leiten wir auf unsere Handler um)
.align 7
b SynchError1
.align 7
b GenericIRQHandler
// ... (weitere Einträge siehe vector.S)
🛠️ Debugging-Hinweis: Die SynchError-Handler sind extrem wertvoll. Wenn dein Programm unerwartet in `SynchError2` landet, weißt du: Ein Befehl in EL1 war ungültig oder hat einen Fehler verursacht. Wir werden das gleich gezielt provozieren, um zu prüfen, ob die Tabelle funktioniert.
Der Blindtest: `svc #0` (Supervisor Call)
Bevor wir uns mit komplexer Hardware (wie dem GIC-Interrupt-Controller) herumschlagen, müssen wir sicherstellen, dass unser Exception-Mechanismus grundlegend funktioniert.
Der Befehl svc #0 (Supervisor Call) löst absichtlich eine synchrone Exception aus. Da wir uns in EL1 befinden, sollte die CPU in den Eintrag SynchError2 unserer Vektortabelle springen.
Füge diesen Test vorübergehend in deine `main.c` ein, *bevor* du `printf` oder andere Initialisierungen machst:
// In main.c
#include "printf.h"
#include "sync.h" // Für Stop()
int main(void)
{
// --- BLINDTEST FÜR EXCEPTIONS ---
printf("Starte Exception-Blindtest...\n");
// Dieser Befehl löst absichtlich eine synchrone Exception aus.
// Die CPU sollte in vector.S -> SynchError2 -> ExceptionHandler springen.
asm volatile("svc #0");
// Diese Zeile sollte NICHT erreicht werden!
printf("FEHLER: svc #0 wurde nicht abgefangen!\n");
Stop();
// ----------------------------------
// ... Rest deiner Initialisierung (Interrupt_Initialize, Timer, etc.) ...
}
Auf dem Bildschirm sollte erscheinen:
`Starte Exception-Blindtest...`
`Error: Exception 'SynchError2' ausgelöst`
Und das Programm bleibt stehen (dank `Stop()` im `ExceptionHandler`).
- Wenn das funktioniert, hast du den schwersten Teil bereits geschafft:** Der Wechsel nach EL1, das Setzen von `vbar_el1` und die korrekte Ausrichtung der Vektortabelle sind verifiziert! Du kannst den `svc #0`-Test danach wieder entfernen.
- 7. Der IRQ-Handler: Den Zustand retten und wiederherstellen
Wenn ein echter Hardware-Interrupt (z. B. vom Timer) kommt, springt die CPU zu `GenericIRQHandler`. Da der Interrupt *jederzeit* passieren kann, weiß der Handler nicht, welche Register gerade benutzt wurden.
Die goldene Regel lautet: **Rette alles, was du veränderst, und stelle es exakt so wieder her.**
// In vector.S (vereinfachte Darstellung des GenericIRQHandler)
.globl GenericIRQHandler
GenericIRQHandler:
// 1. Frame-Pointer und Link-Register retten
stp x29, x30, [sp, #-16]!
// 2. Rücksprungadresse (elr_el1) und Status (spsr_el1) retten
mrs x29, elr_el1
mrs x30, spsr_el1
stp x29, x30, [sp, #-16]!
// 3. FIQs erlauben (falls sie gesperrt waren), IRQs bleiben durch Eintritt automatisch gesperrt
msr DAIFClr, #1
// 4. ALLE General Purpose Register (x0-x28) auf den Stack sichern
stp x27, x28, [sp, #-16]!
// ... (weitere stp Befehle bis x0) ...
str x0, [sp, #-16]!
// 5. ALLE NEON/FP-Register (q0-q31) sichern (wichtig für C-Code mit Floats!)
stp q30, q31, [sp, #-32]!
// ... (weitere stp Befehle bis q0) ...
// 6. Jetzt ist der Stack sauber. Wir können sicher C-Code aufrufen!
bl InterruptHandler
// 7. Epilog: Alles in EXAKT umgekehrter Reihenfolge wiederherstellen!
ldp q0, q1, [sp], #32
// ... (bis q30, q31)
ldr x0, [sp], #16
ldp x1, x2, [sp], #16
// ... (bis x27, x28)
ldp x29, x30, [sp], #16
msr elr_el1, x29
msr spsr_el1, x30
ldp x29, x30, [sp], #16
// 8. Zurück zum unterbrochenen Code
eret
- 8. Zusammenfassung
1. **Nur ein Kern:** Wir schicken Core 1-3 in den Schlaf (`WFE`), um Chaos zu vermeiden.
2. **EL1 ist Pflicht:** Wir wechseln von EL2 nach EL1, um Kernel-Rechte zu haben.
3. **Vektortabelle:** Sie muss auf 2 KB ausgerichtet sein und zeigt der CPU den Weg zu den Handlern.
4. **Blindtest:** Ein `svc #0` ist der perfekte, sichere Weg, um zu prüfen, ob Exceptions korrekt abgefangen werden, bevor man Hardware-IRQs konfiguriert.
5. **State Saving:** Der Assembly-Handler muss *jedes* Register sichern, bevor er C-Code (`InterruptHandler`) aufruft, und es danach exakt wiederherstellen.
- Ausblick
Nun, da das Fundament steht und wir wissen, dass unsere Exception-Tabelle funktioniert, können wir uns im nächsten Kapitel dem **Generic Interrupt Controller (GIC-400)** und dem **System Timer** widmen. Dort werden wir lernen, wie wir einen echten Hardware-Interrupt (IRQ 27) aktivieren, um unseren ersten periodischen "Rotor" auf dem Bildschirm zu animieren!