|
|
| (29 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.
| |
|
| |
|
| == Betriebsmodi und Interrupts ==
| | Der Raspberry Pi 4 nutzt einen '''ARM Cortex-A72'''-Prozessor mit 64-Bit-Architektur (AArch64). ARM unterscheidet verschiedene '''Exception Levels (EL)''': |
|
| |
|
| 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:
| | * '''EL0''': Anwendungen (wenig Rechte) |
| | * '''EL1''': Betriebssystem oder Bare-Metal-Kernel (hohe Rechte) |
| | * '''EL2''': Hypervisor (z. B. für Virtualisierung) |
| | * '''EL3''': Firmware (höchste Rechte) |
|
| |
|
| === Interrupt Enable Stufe (EL1 und niedrigere Ebenen) ===
| | '''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. |
|
| |
|
| 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.
| | == Interrupts aktivieren: Die DAIF-Flags == |
|
| |
|
| === DAIF-Flags (erlauben Interrupts) ===
| | Ob Interrupts erlaubt sind, wird über das '''DAIF-Register''' gesteuert. Es enthält vier Flags: |
|
| |
|
| {| class="wikitable" | | {| class="wikitable" |
| | D || Debug-Exceptions
| | ! Flag |
| | ! Bedeutung |
| |- | | |- |
| | A || SError-Interrupts | | | D |
| | | Debug-Exceptions sperren |
| |- | | |- |
| | I || IRQ-Interrupts | | | A |
| | | Asynchrone Fehler (SError) |
| |- | | |- |
| | F || FIQ-Interrupts | | | I |
| | | Normale Interrupts (IRQ) |
| | |- |
| | | F |
| | | Schnelle Interrupts (FIQ) |
| |} | | |} |
|
| |
|
| Diese Flags können im DAIF-Register eingestellt werden. Um IRQ-Interrupts zu ermöglich, sollte das I-Flag gelöscht werden.
| | Wenn ein Flag '''gesetzt''' ist (1), ist der entsprechende Interrupt '''gesperrt'''. |
| == Wichtige Befehle und Register ==
| | Um '''IRQs zu erlauben''', muss das '''I-Flag gelöscht''' werden (also auf 0 gesetzt). |
| | |
| Hier ein Überblick über die relevanten Register und Befehle:
| |
| | |
| Programmstatusregister (PSTATE): Kontrolliert u.a. die globalen Interrupts.
| |
|
| |
|
| === Beispiel (ARM-Assembly) zur Aktivierung von Interrupts === | | === Beispiel: IRQ-Interrupts aktivieren === |
|
| |
|
| Folgender Code zeigt, wie man die IRQ-Interrupts explizit zulässt:
| | <source lang="armasm"> |
| <syntaxhighlight lang="asm"> | |
| .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 |
| </syntaxhighlight> | | </source> |
| | |
| == 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 ===
| | '''Hinweis''': <code>bic</code> steht für „bit clear“. <code>(1<<7)</code> ist Bit 7 – das I-Flag im DAIF-Register. |
|
| |
|
| Der Bootloader (z. B. der von der GPU initialisierte Bootcode) lädt die Firmware und deinen Kernel.
| | == Vorbereitung: Nur ein Kern aktivieren == |
| Nach der Initialisierung durch die Firmware (häufig start.elf), wird die CPU in EL2 (Hypervisor Mode) gestartet, wenn kein spezieller Wechsel zu einem bestimmten Berechtigungslevel vorgegeben wird.
| |
|
| |
|
| == Umsetzung auf den Raspberry Pi 4 (64-Bit) ==
| | 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. |
| 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 Raspberrys führen. Damit dies nicht passiert, werden wir unseren Code nur noch auf der Haupt-CPU ausführen und die übrigen CPUs schlafen legen:
| | Daher: Wir erlauben nur '''Kern 0 (CPU0)''' weiterzulaufen. Die anderen Kerne legen wir „in den Schlaf“. |
|
| |
|
| <syntaxhighlight lang="asm"> | | <syntaxhighlight lang="asm"> |
| .section .init | | .section .init |
| .global _start | | .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 |
|
| |
|
| _start:
| | core_sleep: |
| mrs x1, mpidr_el1 //Lies den Inhalt des Systemregisters MPIDR_EL1 (Multiprocessor Affinity Register) und speichere ihn in Register x1.
| | wfe // Warte auf Ereignis (sleep) |
| and x1, x1, #3
| | b core_sleep // Dauerschleife |
| cbz x1, 2f
| |
| 1: // Core 1-3 schlafen legen
| |
| wfe
| |
| b 1b
| |
| 2:
| |
| </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 "2:" weiterspring. Alle anderen CPUs werden mit "wfe" schlafen gelegt und oder in die Dauerschleife "b 1b" versetzt.
| |
|
| |
|
| Als nächstes prüfen wir das Level, um sicherzustellen, dass wir nicht schon bereits in EL1 sind.
| | core0: |
| <syntaxhighlight lang="asm">
| | // Nur Core 0 kommt hierher |
| mrs x0, CurrentEL //Überprüfen, ob bereits in EL1
| |
| cmp x0, #4
| |
| beq 5f
| |
| </syntaxhighlight> | | </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 in EL1.
| |
|
| |
|
| | > '''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. |
|
| |
|
| mrs x0, CurrentEL: Das CurrentEL Register wird gelesen, welches das aktuelle Exception-Level anzeigt.
| | === Schritt 1: Prüfen, ob wir schon in EL1 sind === |
| cmp x0, #4: Wird verglichen, ob sich die CPU schon im EL2 befindet.
| |
| beq 5f: Falls das System bereits in EL1 ist, springt der Code zum Label 5: und überspringt den EL-Wechsel.
| |
| Wechsel von EL2 nach EL1
| |
| ldr x0, =EXCEPTION_STACK
| |
| msr sp_el1, x0
| |
|
| |
|
| mrs x0, cnthctl_el2 | | <syntaxhighlight lang="asm"> |
| orr x0, x0, #0x3
| | mrs x0, CurrentEL // Lese aktuelles Exception Level |
| msr cnthctl_el2, x0
| | cmp x0, #8 // EL1 hat Wert 8 |
| msr cntvoff_el2, xzr
| | beq switch_to_el1 // Wenn EL1, direkt weiter |
| | </syntaxhighlight> |
|
| |
|
| mrs x0, midr_el1
| | > <code>CurrentEL</code> gibt z. B. <code>8</code> für EL1. Der Wert ist <code>ELn << 2</code>. |
| mrs x1, mpidr_el1
| | > Also: EL1 = 8, EL2 = 16. |
| msr vpidr_el2, x0
| |
| msr vmpidr_el2, x1
| |
|
| |
|
| mov x0, #0x33ff
| | === Schritt 2: Wechsel von EL2 nach EL1 === |
| msr cptr_el2, x0
| |
| msr hstr_el2, xzr
| |
| mov x0, #3 << 20
| |
| msr cpacr_el1, x0
| |
|
| |
|
| mov x0, #(1 << 31)
| | <syntaxhighlight lang="asm"> |
| msr hcr_el2, x0 | | ldr x0, =EXCEPTION_STACK // Lade Stack für EL1 |
| | msr sp_el1, x0 // Setze Stackpointer für EL1 |
|
| |
|
| mov x0, #0x0800
| | ldr x0, =VectorTable // Adresse der Vektortabelle |
| movk x0, #0x30d0, lsl #16
| | msr vbar_el2, x0 // VBAR_EL2 zeigt auf Tabelle |
| msr sctlr_el1, x0 | |
|
| |
|
| mov x0, #0x3c4
| | // Timer-Einstellungen |
| msr spsr_el2, x0
| | mrs x0, cnthctl_el2 |
| adr x0, 5f
| | orr x0, x0, #0x3 |
| msr elr_el2, x0 | | msr cnthctl_el2, x0 |
| eret
| | msr cntvoff_el2, xzr |
|
| |
|
| ldr x0, =EXCEPTION_STACK: Die Adresse des Exception-Stacks wird geladen.
| | // Virtualisierung deaktivieren |
| msr sp_el1, x0: Der Stack des EL1 (sp_el1) wird gesetzt. | | mov x0, #0x33ff |
| | msr cptr_el2, x0 |
| | msr hstr_el2, xzr |
|
| |
|
| Dann folgen verschiedene Register-Konfigurationen, welche die Timer-Funktionalität sowie die virtuelle CPU-Profilierung einrichten:
| | mov x0, #(1 << 31) |
| | msr hcr_el2, x0 |
|
| |
|
| msr cnthctl_el2, x0 und msr cntvoff_el2, xzr: Timer-Konfiguration.
| | // CPACR_EL1 für Gleitkomma |
| msr vpidr_el2, x0 und msr vmpidr_el2, x1: Virtualisierungs-Konfiguration. | | mov x0, #3 << 20 |
| | msr cpacr_el1, x0 |
|
| |
|
| Weitere Einstellungen betreffen die Steuerregister und die HCR_EL2-Konfiguration:
| | // Systemsteuerregister EL1 |
| | mov x0, #0x0800 |
| | movk x0, #0x30d0, lsl #16 |
| | msr sctlr_el1, x0 |
|
| |
|
| msr cptr_el2, x0, msr hstr_el2, xzr: Steuerregister.
| | // SPSR für Rücksprung nach EL1 |
| msr hcr_el2, x0: Hypervisor Configuration Register (HCR_EL2) wird konfiguriert. | | mov x0, #0x3c4 |
| | msr spsr_el2, x0 |
|
| |
|
| Schließlich wird der Systemsteuer-Register SCTLR_EL1 initialisiert und das SPSR_EL2 gesetzt:
| | adr x0, el1_return |
| | msr elr_el2, x0 |
|
| |
|
| msr sctlr_el1, x0: Einstellung des System Control Register im EL1.
| | eret // Wechsel nach EL1 |
| msr spsr_el2, x0: Setzen des Saved Program Status Register.
| | </syntaxhighlight> |
|
| |
|
| Mit eret erfolgt der eigentliche Wechsel auf Exception Level 1:
| | > Nach <code>eret</code> springt der Prozessor zu <code>el1_return</code> – aber jetzt in EL1. |
|
| |
|
| eret: Return from exception und Wechsel auf EL1.
| | == Initialisierung in EL1 == |
| Initialisierung im EL1 | |
| 5:
| |
| ldr x1,=_start
| |
| mov sp,x1 // Stack setzen
| |
|
| |
|
| // BSS section reinigen
| | Nach dem Wechsel: |
| ldr x1, =__bss_start // Start address
| |
| ldr w2, =__bss_size // Size of the section
| |
| 3: cbz w2, 4f // Quit loop if zero
| |
| str xzr, [x1], #8
| |
| sub w2, w2, #1
| |
| cbnz w2, 3b // Loop if non-zero
| |
|
| |
|
| // springe in die SystemInitierung
| | <syntaxhighlight lang="asm"> |
| 4: bl sysinit
| | el1_return: |
| // Falls man zurückkommt, in die Dauerschleife
| |
| b 1b
| |
| | |
| ldr x1,=_start: Lade die Startadresse.
| |
| mov sp,x1: Setzen des Stack-Pointers.
| |
| 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.
| |
|
| |
|
| |
| | |
| | |
| | |
| Damit wir nun Interrupts verwenden können, müssen wir zunächst die CPU nach EL1 umschalten.
| |
| | |
| | |
| Hier im Beispiel zeige ich, wie wir die Initialisierung des Interrupts für einen Timer und das umschalten nach EL1 durchführen können:
| |
| | |
| | |
| | |
| | |
| | |
|
| |
| Dazu sind mehrere Dinge nötig:
| |
| * Der Stackpointer muss korrekt initialisiert werden
| |
| *
| |
| | |
| | |
| | |
| | |
| Setup und Initialisierung
| |
| | |
| Stack Pointer setzen: Stelle sicher, dass der Stack Pointer korrekt initialisiert ist, bevor Interrupts aktiviert werden.
| |
| | |
| Setzen des Vektortabellenpointers: Leitet den Prozessor zu der richtigen Vektortabelle, welche die Adressen der ISR enthält.
| |
| | |
| .global _start
| |
| _start:
| |
| // Setup Stack Pointer und andere Initialisierungen hier
| |
| //...
| |
| | |
| // Set Vector Base Address Register (VBAR)
| |
| LDR x0, =vector_table
| |
| MSR VBAR_EL1, x0 // VBAR auf die Vektortabelle setzen
| |
| | |
| // Interrupts aktivieren
| |
| BL enable_interrupts
| |
| | |
| // Hauptprogrammschleife
| |
| main_loop:
| |
| //...
| |
| B main_loop
| |
| | |
| .section .vectors
| |
| .align 11
| |
| vector_table:
| |
| b irq_handler // IRQ Interrupt Handler Adresse
| |
| // Weitere Vektoren je nach Bedarf
| |
| | |
| Zusammenfassung
| |
| 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:
| |
| | |
| Exception Levels (ELs):
| |
| | |
| EL0:
| |
| | |
| User Mode
| |
| 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:
| |
| | |
| Kernel Mode
| |
| Läuft im privilegierten Modus.
| |
| 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:
| |
| | |
| Hypervisor Mode
| |
| Ebenfalls als Virtualization Exception Level bekannt, das für die Ausführung eines Hypervisors verwendet wird, der virtuelle Maschinen verwaltet.
| |
| Es ermöglicht die Erstellung und Verwaltung mehrerer EL1-Umgebungen (z. B. verschiedene Betriebssysteme auf einer Hardware).
| |
| | |
| EL3:
| |
| | |
| Secure Monitor Mode
| |
| Das höchste privilegierte Level, das für sichere Anwendungen und das TrustZone-Sicherheits-Monitor-Firmware verwendet wird.
| |
| Hier können sicherheitskritische Funktionen ausgeführt werden, die von den anderen Levels abgeschottet sind.
| |
| 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:
| |
| | |
| .global _start
| |
| | |
| _start:
| |
| // Initialisierung der Exception Levels (wenn notwendig)
| |
| | |
| // Setze SP für EL1
| |
| LDR x1, =stack_top_el1
| |
| MSR SP_EL1, x1
| |
| | |
| // Wechsel zu EL1
| |
| 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
| |
| BL switch_to_el1
| |
| | |
| init_el:
| |
| // 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
| |
| BL enable_interrupts
| |
| | |
| // Hauptprogrammschleife
| |
| main_loop:
| |
| B main_loop
| |
| | |
| // Funktion um von EL3 zu EL1 zu wechseln
| |
| switch_to_el1: | | switch_to_el1: |
| MSR SCR_EL3, #0x6 // Konfiguriere Secure Configuration Register (SCR) für EL1 | | ldr x1, =_start |
| MRS x0, SPSR_EL3 // Sichere aktuelle SPSR_EL3 | | mov sp, x1 // Stack setzen |
| BIC x0, x0, #(0xF << 6) // Setze EL1 | | ldr x0, =VectorTable |
| ORR x0, x0, #0xD3 // Maskiere IRQ und FIQ und setze EL1h | | msr vbar_el1, x0 // Vektortabelle für EL1 aktivieren |
| 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:
| | // BSS-Segment löschen (uninitialisierte Daten auf 0 setzen) |
| // Hier beginnt die Ausführung auf EL1 | | ldr x1, =__bss_start |
| RET | | ldr w2, =__bss_size |
| | | clean_bss_loop: |
| .section .vectors, "a"
| | cbz w2, bss_clean_done |
| vector_table:
| | str xzr, [x1], #8 |
| B irq_handler // IRQ Adresse | | sub w2, w2, #1 |
| // Weitere Vektoren für verschiedene Interruptarten | | cbnz w2, clean_bss_loop |
| | bss_clean_done: |
|
| |
|
| // Dummy-Interrupt-Handler | | b main // Starte Hauptprogramm |
| irq_handler:
| | </syntaxhighlight> |
| // Interrupt Service Routine Logik hier
| |
| RET
| |
|
| |
|
| // Speicherplatz für Stack (zum Beispiel bei EL1)
| | == Die Vektortabelle == |
| .section .bss
| |
| .stack_area:
| |
| .skip 0x1000 // Speicherplatz reservieren
| |
| stack_top_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. |
|
| |
|
| In diesem Ansatz wird sichergestellt, dass das System im passenden Berechtigungslevel (EL1) gesetzt wird, in dem Interrupts verwaltet werden können.
| | === Aufbau der Tabelle (ARMv8) === |
|
| |
|
| 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.
| | Jeder Eintrag behandelt einen bestimmten Typ: |
|
| |
|
| Beim Raspberry Pi ist der typische Ablauf wie folgt:
| | * '''Synchronous''': Fehler oder Systemaufrufe |
| | * '''IRQ''': Normaler Interrupt |
| | * '''FIQ''': Schneller Interrupt (höhere Priorität) |
| | * '''SError''': Systemfehler (z. B. Speicherfehler) |
|
| |
|
| Der Bootloader (z. B. der von der GPU initialisierte Bootcode) lädt die Firmware und deinen Kernel.
| | Und jeweils für: |
| 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.
| | * Aktuelles Exception Level (z. B. EL1h) |
| | * Niedrigeres Level (z. B. EL0) |
|
| |
|
| Dies ist der Default-Startmodus des Raspberry Pi 4 nach der Initialisierung durch die Firmware.
| | === Beispiel: Vektortabelle für EL1 === |
|
| |
|
| Vorgehensweise nach dem Start
| | <syntaxhighlight lang="asm"> |
| | .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 |
|
| |
|
| 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.
| | // 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 |
|
| |
|
| Standardmäßiger Start in EL2
| | // 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 |
|
| |
|
| Hier sind die Schritte, um sicherzustellen, dass du zu EL1 wechselst, falls dies nicht schon passiert:
| | // 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> |
|
| |
|
| Beispielcode für den Wechsel von EL2 zu EL1:
| | '''Hinweis''': |
| .global _start | | * <code>.align 11</code> = 2048 Bytes für die gesamte Tabelle |
| | * <code>.align 7</code> = 128 Bytes pro Eintrag (Abstand zwischen Vektoren) |
|
| |
|
| _start:
| | == Ein Interrupt-Handler: Schritt für Schritt == |
| // Lese aktuelles Exception Level
| |
| MRS x0, CurrentEL
| |
| 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
| | Hier ein Beispiel für einen '''IRQ-Handler''': |
| 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:
| | <syntaxhighlight lang="asm"> |
| // EL1 Initialisierung | | irq_handler_el1t: |
| ADR x0, vector_table // Lade Adresse der Vektortabelle | | irq_handler_el1h: |
| MSR VBAR_EL1, x0 // Set VBAR_EL1 zum Zeiger auf die Vektortabelle | | 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]! |
|
| |
|
| // Setze Stack Pointer für EL1 | | msr DAIFSet, #1 // FIQs sperren (nur für IRQ) |
| LDR x1, =stack_top_el1
| |
| MSR SP_EL1, x1
| |
|
| |
|
| // Interrupts aktivieren | | // Alle Register sichern (inkl. Gleitkomma) |
| BL enable_interrupts | | stp q0, q1, [sp, #-32]! |
| | stp q2, q3, [sp, #-32]! |
| | // ... bis q30, q31 |
|
| |
|
| // Hauptprogrammschleife | | stp x1, x2, [sp, #-16]! |
| main_loop:
| | // ... bis x27, x28 |
| B main_loop | | str x0, [sp, #-16]! |
|
| |
|
| .vector_table:
| | bl InterruptHandler // Aufruf der C-Funktion |
| B irq_handler // IRQ Adresse | |
| // Additional vectors for different interrupts
| |
|
| |
|
| // Dummy Interrupt Handler | | // Register wiederherstellen |
| irq_handler:
| | ldr x0, [sp], #16 |
| // Interrupt Service Routine Logik hier | | ldp x1, x2, [sp], #16 |
| RET
| | // ... bis x27, x28 |
|
| |
|
| // Speicherplatz für Stack | | ldp q0, q1, [sp], #32 |
| .section .bss | | // ... bis q30, q31 |
| .stack_area: | |
| .skip 0x1000 // Reserviere Speicherplatz
| |
| stack_top_el1:
| |
|
| |
|
| Wichtige Punkte:
| | ldp x29, x30, [sp], #16 // elr_el1, spsr_el1 |
| Initialisierungslogik in EL1: Es ist wichtig, dass du Initialisierungen für Interrupts, Vektortabellen und den Stack Pointer in EL1 korrekt durchführst.
| | msr elr_el1, x29 |
| Interrupt-Freigabe: Aktivierung von Interrupts, wenn notwendig.
| | msr spsr_el1, x30 |
| 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.
| | ldp x29, x30, [sp], #16 // x29/x30 wiederherstellen |
|
| |
|
| | eret // Zurück zum unterbrochenen Code |
| | </syntaxhighlight> |
|
| |
|
| Ja, Vektortabellen müssen einer bestimmten Struktur und Reihenfolge entsprechen, damit sie korrekt funktionieren. Dies ist wichtig, um sicherzustellen, dass der Prozessor bei einem Interrupt den richtigen Handler anspringt.
| | == Wichtige Prinzipien beim Interrupt-Handling == |
|
| |
|
| Struktur einer Vektortabelle
| | # '''Register sichern''': |
| | 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. |
|
| |
|
| Die ARMv8-Architektur, die im Raspberry Pi 4 verwendet wird, verfügt über eine spezielle Vektortabelle für verschiedene Arten von Ausnahmen. Die Tabelle enthält Adressen für die folgenden Ausnahmen:
| | == Zusammenfassung == |
|
| |
|
| Synchronous Exception - Lower Level
| | * Interrupts unterbrechen den normalen Programmablauf. |
| IRQ (Normal Interrupt) - Lower Level
| | * Sie werden über die '''Vektortabelle''' behandelt. |
| FIQ (Fast Interrupt) - Lower Level
| | * Nur '''EL1 oder höher''' darf Interrupts verarbeiten. |
| SError (System Error) - Lower Level
| | * Mehrere Kerne? Nur '''einen aktivieren''', die anderen schlafen legen. |
| Synchronous Exception - Current Level
| | * '''DAIF-Register''' kontrolliert, welche Interrupts erlaubt sind. |
| IRQ (Normal Interrupt) - Current Level
| | * Im Handler: '''Register sichern''', '''Interrupts steuern''', '''schnell arbeiten''', '''eret''' benutzen. |
| FIQ (Fast Interrupt) - Current Level
| |
| SError (System Error) - Current Level
| |
| Beispiel einer Vektortabelle
| |
|
| |
|
| Hier ist ein Beispiel, wie eine Vektortabelle für EL1 aussehen könnte:
| | Mit diesem Wissen bist du bereit, echte Hardware-Interrupts (z. B. vom Timer oder GPIO) auf dem Raspberry Pi 4 zu nutzen! |
|
| |
|
| .align 11
| | Im nächsten Kapitel kümmern wir uns um den '''Systemtimer''', um den ersten echten Interrupt auszulösen. |
| .section .vectors, "a"
| |
| vector_table:
| |
| // Vektoren für EL1 (Current Level)
| |
| b sync_exception_el1 // Synchronous Exception
| |
| b irq_handler_el1 // IRQ - Normal Interrupt
| |
| b fiq_handler_el1 // FIQ - Fast Interrupt
| |
| b serror_handler_el1 // SError - System Error
| |
|
| |
|
| // Vektoren für EL0 (Lower Level)
| | ----- |
| b sync_exception_el0 // Synchronous Exception
| |
| b irq_handler_el0 // IRQ - Normal Interrupt
| |
| b fiq_handler_el0 // FIQ - Fast Interrupt
| |
| b serror_handler_el0 // SError - System Error
| |
|
| |
|
| Konfiguration der Vektortabelle
| | {| style="width: 100%; |
| | | | style="width: 33%;" | [[Systeminformationen (PI4)|< Zurück (Systeminformationen)]] |
| Die Vektortabelle muss dem Prozessor mitgeteilt werden, indem die Basisadresse der Tabelle in das VBAR_ELx-Register geschrieben wird:
| | | style="width: 33%; text-align:center;" | [[Hauptseite|< Hauptseite >]] |
| | | | style="width: 33%; text-align:right;" | [[Beispiel Timer-Interrupt (PI4)|Weiter (Beispiel Timer-Interrupt) >]] |
| .global _start
| | |} |
| | |
| _start:
| |
| // Setze Stack-Pointer für EL1
| |
| LDR x1, =stack_top_el1
| |
| MSR SP_EL1, x1
| |
| | |
| // Lade Adresse der Vektortabelle
| |
| ADR x0, vector_table
| |
| MSR VBAR_EL1, x0 // Setze VBAR_EL1 auf die Adresse der Vektortabelle
| |
| | |
| // Initialisiere Interrupts
| |
| BL enable_interrupts
| |
| | |
| // Hauptprogrammschleife
| |
| main_loop:
| |
| B main_loop
| |
| | |
| // Dummy-Handler
| |
| sync_exception_el1:
| |
| // Handler-Code für synchrone Ausnahmen in EL1
| |
| RET
| |
| | |
| irq_handler_el1:
| |
| // Handler-Code für IRQ-Interrupts in EL1
| |
| RET
| |
| | |
| fiq_handler_el1:
| |
| // Handler-Code für FIQ-Interrupts in EL1
| |
| RET
| |
| | |
| serror_handler_el1:
| |
| // Handler-Code für Systemfehler in EL1
| |
| RET
| |
| | |
| sync_exception_el0:
| |
| // Handler-Code für synchrone Ausnahmen in EL0/EL1
| |
| RET
| |
| | |
| irq_handler_el0:
| |
| // Handler-Code für IRQ-Interrupts in EL0
| |
| RET
| |
| | |
| fiq_handler_el0:
| |
| // Handler-Code für FIQ-Interrupts in EL0
| |
| RET
| |
| | |
| serror_handler_el0:
| |
| // Handler-Code für Systemfehler in EL0
| |
| RET
| |
| | |
| .section .bss
| |
| .stack_area:
| |
| .skip 0x1000 // Speicherplatz reservieren
| |
| stack_top_el1:
| |
| | |
| 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.
| |
| 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.
| |
| Zusammenfassung
| |
| | |
| Eine korrekt strukturierte Vektortabelle ist essentiell für die korrekte Handhabung von Interrupts und Ausnahmen. Stelle sicher, dass alle Einträge in der richtigen Reihenfolge vorliegen und dass die Ausrichtung der Tabelle korrekt ist. Setze die Basisadresse der Tabelle im entsprechenden VBAR-Register und implementiere die entsprechenden Handler für jede Ausnahmeart.
| |
| | |
| 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.
| |
| 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.
| |
| Beispielcode für das Maskieren von Interrupts in einem Ausnahme-Handler
| |
| .global exception_handler
| |
| | |
| exception_handler:
| |
| // Maskiere alle Interrupts
| |
| MRS x0, DAIF // Lese DAIF Register
| |
| ORR x0, x0, #0x3C0 // Setze die Bits zum Maskieren: D, A, I, F
| |
| MSR DAIF, x0 // Schreibe zurück zum DAIF Register
| |
| | |
| // Ausgangszustände retten
| |
| PUSH {x0-x30, lr}
| |
| | |
| // Handler Code hier
| |
| | |
| // Ausgangszustände wiederherstellen
| |
| POP {x0-x30, lr}
| |
| | |
| // Entmaske alle Interrupts
| |
| MRS x0, DAIF // Lese DAIF Register
| |
| BIC x0, x0, #0x3C0 // Lösche die Bits zum Entmaskieren: D, A, I, F
| |
| MSR DAIF, x0 // Schreibe zurück zum DAIF Register
| |
| | |
| RET
| |
| | |
| Schritt-für-Schritt Erklärung
| |
| | |
| Maskieren von Interrupts:
| |
| | |
| MRS x0, DAIF liest den aktuellen Zustand des DAIF-Registers und speichert ihn in x0.
| |
| ORR x0, x0, #0x3C0 setzt die Bits 8 (D), 9 (A), 6 (I), und 7 (F) auf 1, um Debug, SError, IRQ und FIQ Interrupts zu maskieren.
| |
| MSR DAIF, x0 schreibt den modifizierten Wert zurück in das DAIF-Register und maskiert somit alle diese Interrupts.
| |
| | |
| Ausführung des Handlers:
| |
| | |
| Speicherungen der Registersätze (x0-x30, lr), um das Rückkehren nach der Ausnahme sicherzustellen.
| |
| Ausführen der eigentlichen Ausnahmebehandlung.
| |
| | |
| Wiederherstellen Ausgangszustände und Entmaske Interrupts:
| |
| | |
| POP {x0-x30, lr} stellt die gespeicherten Register wieder her.
| |
| MRS x0, DAIF liest den aktuellen Zustand des DAIF-Registers.
| |
| BIC x0, x0, #0x3C0 löscht die Bits 8 (D), 9 (A), 6 (I), und 7 (F), um die Interruptmaskierung aufzuheben.
| |
| MSR DAIF, x0 schreibt den modifizierten Wert zurück in das DAIF-Register, um die Interrupts wieder zu erlauben.
| |
| Priority und Reihenfolge von Ausnahmen
| |
| | |
| Zusätzlich zum Maskieren von Interrupts können auch Priorisieren und die richtige Verwaltung der Reihenfolge helfen:
| |
| | |
| Priorisieren von Interrupts:
| |
| Bestimmte Interrupts können kritisch und hoch-priorisierter sein, während andere niederpriorisiert behandelt werden.
| |
| Der interrupt controller kann verwendet werden, um Prioritäten der Interrupts zu verwalten.
| |
| Zusammenfassung
| |
| | |
| Interrupt-Maskierung und richtige Priorisierung sind die Schlüssel, um zu verhindern, dass während des Ausnahme-Handlings weitere Ausnahmen auftreten. Das Maskieren der Interrupts kann über das DAIF-Register erfolgen und sollte vor Austritt aus dem Ausnahme-Handler aufgehoben werden. Diese Mechanismen stellen sicher, dass die Ausnahmebehandlung ohne Störungen abgeschlossen werden kann.
| |
| | |
| Ja, es gibt Unterschiede im Aufbau der Vektortabellen, insbesondere wenn verschiedene Betriebsmodi und Ausnahmelevel berücksichtigt werden. In der ARMv8-Architektur werden normalerweise 16 Einträge verwendet, um den verschiedenen Ausnahmebedingungen innerhalb der verschiedenen Exception Levels gerecht zu werden. Dies bezieht sich sowohl auf 64-Bit- als auch auf 32-Bit-Modi und auf die verschiedenen Zustände eines Exception Levels.
| |
| | |
| Erklärung der 16 Einträge
| |
| | |
| Die ARMv8-Architektur teilt die Exception Level Vektortabelle in vier verschiedene Ausnahmearten und zwei verschiedene Zustände jeder Ausnahmeart (z. B. EL1t und EL1h), und dies sowohl für 64-Bit- als auch für 32-Bit-Modi. Hier ist die detaillierte Aufschlüsselung:
| |
| | |
| Ausnahmearten und Zustände:
| |
| Synchronous: Sorgt für synchrone Ausnahmen, z.B. Software-Fehler.
| |
| IRQ: Normaler Interrupt Request.
| |
| FIQ: Schneller Interrupt Request.
| |
| SError: System Error.
| |
| | |
| Diese Ausnahmen haben jeweils einen Zustand für:
| |
| | |
| Current Exception Level with SP_el0 (_el1t)
| |
| Wenn der Stack Pointer SP_el0 verwendet wird.
| |
| Current Exception Level with SP_elx (_el1h)
| |
| Wenn der Stack Pointer von SP_elx verwendet wird (z.B. SP_el1).
| |
| | |
| Zusätzlich dazu müssen die Einträge sowohl für 64-Bit als auch 32-Bit-Untermodi der Ausnahmebehandlung berücksichtigt werden. Dies kommt typischerweise bei der Arbeit in der 64-Bit-Arbeitsumgebung vor. Im Allgemeinen:
| |
| | |
| 64-Bit Mode EL0 (_el0_64)
| |
| 32-Bit Mode EL0 (_el0_32)
| |
| Beispiel einer vollständigen 16-Eintrag-Vektortabelle: | |
| .align 11
| |
| .section .vectors, "a"
| |
| | |
| vector_table:
| |
| | |
| // Einträge für EL1t (Current Exception Level SP_el0)
| |
| ventry sync_invalid_el1t // Synchronous EL1t
| |
| ventry irq_invalid_el1t // IRQ EL1t
| |
| ventry fiq_invalid_el1t // FIQ EL1t
| |
| ventry error_invalid_el1t // Error EL1t
| |
| | |
| // Einträge für EL1h (Current Exception Level SP_el1)
| |
| ventry sync_invalid_el1h // Synchronous EL1h
| |
| ventry handle_el1_irq // IRQ EL1h
| |
| ventry fiq_invalid_el1h // FIQ EL1h
| |
| ventry error_invalid_el1h // Error EL1h
| |
| | |
| // Einträge für EL0 64-bit Modus
| |
| ventry sync_invalid_el0_64 // Synchronous EL0 (64-bit)
| |
| ventry irq_invalid_el0_64 // IRQ EL0 (64-bit)
| |
| ventry fiq_invalid_el0_64 // FIQ EL0 (64-bit)
| |
| ventry error_invalid_el0_64 // Error EL0 (64-bit)
| |
| | |
| // Einträge für EL0 32-bit Modus
| |
| ventry sync_invalid_el0_32 // Synchronous EL0 (32-bit)
| |
| ventry irq_invalid_el0_32 // IRQ EL0 (32-bit)
| |
| ventry fiq_invalid_el0_32 // FIQ EL0 (32-bit)
| |
| ventry error_invalid_el0_32 // Error EL0 (32-bit)
| |
| | |
| .macro ventry label
| |
| b \label
| |
| .endm
| |
| | |
| 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.
| |
| Verwendung und Initialisierung der Vektortabelle:
| |
| | |
| Die Registerzüge VBAR_EL1, VBAR_EL2 und VBAR_EL3 müssen entsprechend initialisiert werden, um die Adresse dieser Vektortabellenstruktur zu verwenden. Hier ist ein Beispiel für die Initialisierung auf EL1:
| |
| | |
| .global _start
| |
| | |
| _start:
| |
| // Initialisiere Stack Pointer für EL1
| |
| LDR x1, =stack_top_el1
| |
| MSR SP_EL1, x1
| |
| | |
| // Lade die Adresse der Vektortabelle
| |
| ADR x0, vector_table
| |
| MSR VBAR_EL1, x0 // Setze VBAR_EL1 zur Adresse der Vektortabelle
| |
| | |
| // Initialise Interrupts
| |
| BL enable_interrupts
| |
| | |
| // Hauptprogrammschleife
| |
| main_loop:
| |
| B main_loop
| |
| | |
| sync_invalid_el1t:
| |
| // Handler für synchronous exception in EL1t
| |
| RET
| |
| | |
| irq_invalid_el1t:
| |
| // Handler für IRQ in EL1t
| |
| RET
| |
| | |
| fiq_invalid_el1t:
| |
| // Handler für FIQ in EL1t
| |
| RET
| |
| | |
| error_invalid_el1t:
| |
| // Handler für System Error in EL1t
| |
| RET
| |
| | |
| sync_invalid_el1h:
| |
| // Handler für synchronous exception in EL1h
| |
| RET
| |
| | |
| handle_el1_irq:
| |
| // Hauptinterrupt Handler für IRQ in EL1h
| |
| RET
| |
| | |
| fiq_invalid_el1h:
| |
| // Handler für FIQ in EL1h
| |
| RET
| |
| | |
| error_invalid_el1h:
| |
| // Handler für System Error in EL1h
| |
| RET
| |
| | |
| sync_invalid_el0_64:
| |
| // Handler für synchronous exception in 64-bit EL0
| |
| RET
| |
| | |
| irq_invalid_el0_64:
| |
| // Handler für IRQ in 64-bit EL0
| |
| RET
| |
| | |
| fiq_invalid_el0_64:
| |
| // Handler für FIQ in 64-bit EL0
| |
| RET
| |
| | |
| error_invalid_el0_64:
| |
| // Handler für System Error in 64-bit EL0
| |
| RET
| |
| | |
| sync_invalid_el0_32:
| |
| // Handler für synchronous exception in 32-bit EL0
| |
| RET
| |
| | |
| irq_invalid_el0_32:
| |
| // Handler für IRQ in 32-bit EL0
| |
| RET
| |
| | |
| fiq_invalid_el0_32:
| |
| // Handler für FIQ in 32-bit EL0
| |
| RET
| |
| | |
| error_invalid_el0_32:
| |
| // Handler für System Error in 32-bit EL0
| |
| RET
| |
| | |
| .section .bss
| |
| .stack_area:
| |
| .skip 0x1000 // Speicherplatz reservieren
| |
| stack_top_el1:
| |
| | |
| Zusammenfassung
| |
| | |
| In der ARMv8-Architektur gibt es eine erweiterte Struktur für Vektortabellen mit 16 Einträgen, um verschiedene Arten und Zustände von Ausnahmen vollständig zu unterstützen. Bei der Ausnahmebehandlung können unterschiedliche Modi (64-Bit und 32-Bit) und verschiedene Betriebsarten (_el1t, _el1h, _el0_64, _el0_32) gleichzeitig berücksichtigt werden. Eine korrekte Initialisierung und Einhaltung der Reihenfolge dieser Einträge ist für die zuverlässige Ausnahmebehandlung unerlässlich.
| |
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:
Zum Beispiel: Tastatur, Maus, Timer, GPIO-Pins.
Netzwerkadapter, Speichercontroller, Sensoren (Temperatur, Beschleunigung).
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
- Register sichern:
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 DAIFSet 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 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.