Interrupts (PI4)
Interrupts sind Signale, die den normalen Ablauf eines Prozessors unterbrechen, um spezielle Routineabläufe auszuführen. Sie spielen eine wichtige Rolle bei der Steuerung und Kommunikation in Echtzeitsystemen und bei der Handhabung von Hardware-Ereignissen.
Definition von Interrupts
Ein Interrupt ist ein Signal, das den CPU dazu veranlasst, den aktuell ausgeführten Befehlssatz zu unterbrechen und eine Interrupt Service Routine (ISR) oder Interrupt-Handler auszuführen.
Auslöser für Interrupts
Interrupts können durch verschiedene Ereignisse ausgelöst werden:
Externe Hardware-Ereignisse
- Tastatureingaben: Wenn eine Taste gedrückt wird.
- Mausbewegungen: Wenn die Maus bewegt wird oder eine Taste gedrückt wird.
- Timer: Timer können so konfiguriert werden, dass sie periodische Interrupts auslösen.
Interne Hardware-Ereignisse
- Peripheriegeräte: Ereignisse von Geräten wie Netzwerkadaptern, Festplatten, usw.
- Signale von Sensoren: Daten von Temperatursensoren, Beschleunigungssensoren, usw.
Betriebssystem-Ereignisse
- Systemaufrufe: Bestimmte Anfragen von Software an das Betriebssystem.
- Fehlermeldungen: Wenn Fehler wie Division durch Null oder Speicherzugriffsfehler auftreten.
Typen von Interrupts
Interrupts können auch kategorisiert werden in:
- Maskierbare Interrupts: Diese können vom CPU vorübergehend ignoriert oder maskiert werden.
- Nicht maskierbare Interrupts (NMI): Diese können nicht ignoriert werden und haben höchste Priorität.
Ja, der Raspberry Pi 4 muss sich in einem bestimmten Betriebsmodus befinden, um Interrupts effizient zu handhaben. Besonders wichtig sind die Interrupt-Freigabe (globale Interrupts) und der Modus des Prozessors.
Betriebsmodi und Interrupts
Der ARM-Cortex-A72-Prozessor, der im Raspberry Pi 4 verwendet wird, unterstützt verschiedene Ebenen und Modi, die sich auf das Verhalten von Interrupts auswirken können:
Interrupt Enable Stufe (EL1 und niedrigere Ebenen):
Der Prozessor muss sich in einem Modus befinden, in dem Interrupts erlaubt sind (EL1 oder niedriger). Dies bedeutet, dass die globalen Interrupts nicht deaktiviert sein dürfen. Dies wird durch das Setzen bestimmter Bits im Program Status Register (PSTATE) kontrolliert.
DAIF-Flags (erlauben Interrupts):
D: Debug-Exceptions A: SError-Interrupts I: IRQ-Interrupts F: FIQ-Interrupts Diese Flags können im DAIF-Register eingestellt werden. Um IRQ-Interrupts zu ermöglich, sollte das I-Flag gelöscht werden. Wichtige Befehle und Register
Hier ein Überblick über die relevanten Register und Befehle:
Programmstatusregister (PSTATE): Kontrolliert u.a. die globalen Interrupts. Beispiel (ARM-Assembly) zur Aktivierung von Interrupts
Folgender Code zeigt, wie man die IRQ-Interrupts explizit zulässt:
.global enable_interrupts
enable_interrupts:
mrs x0, DAIF // DAIF Register lesen bic x0, x0, #(1<<7) // IRQ-Freigabe (Setze I-Flag auf 0) msr DAIF, x0 // DAIF Register zurückschreiben ret
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:
MSR SCR_EL3, #0x6 // Konfiguriere Secure Configuration Register (SCR) für EL1 MRS x0, SPSR_EL3 // Sichere aktuelle SPSR_EL3 BIC x0, x0, #(0xF << 6) // Setze EL1 ORR x0, x0, #0xD3 // Maskiere IRQ und FIQ und setze EL1h MSR SPSR_EL3, x0 // Schreibe das veränderte Register zurück ADR x0, el1_entry_point // Lade Einstiegspunkt für EL1 MSR ELR_EL3, x0 // Setze ELR_EL3 auf Einstiegspunkt ERET // Ausnahme zurückgeben über Exception Return
el1_entry_point:
// Hier beginnt die Ausführung auf EL1 RET
.section .vectors, "a" vector_table:
B irq_handler // IRQ Adresse // Weitere Vektoren für verschiedene Interruptarten
// Dummy-Interrupt-Handler irq_handler:
// Interrupt Service Routine Logik hier RET
// Speicherplatz für Stack (zum Beispiel bei EL1) .section .bss .stack_area:
.skip 0x1000 // Speicherplatz reservieren
stack_top_el1:
In diesem Ansatz wird sichergestellt, dass das System im passenden Berechtigungslevel (EL1) gesetzt wird, in dem Interrupts verwaltet werden können.
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.
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 in EL2 (Hypervisor Mode) gestartet, wenn du keinen speziellen Wechsel zu einem bestimmten Berechtigungslevel vorgibst.
Dies ist der Default-Startmodus des Raspberry Pi 4 nach der Initialisierung durch die Firmware.
Vorgehensweise nach dem Start
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.
Standardmäßiger Start in EL2
Hier sind die Schritte, um sicherzustellen, dass du zu EL1 wechselst, falls dies nicht schon passiert:
Beispielcode für den Wechsel von EL2 zu EL1: .global _start
_start:
// 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 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:
// EL1 Initialisierung ADR x0, vector_table // Lade Adresse der Vektortabelle MSR VBAR_EL1, x0 // Set VBAR_EL1 zum Zeiger auf die Vektortabelle
// Setze Stack Pointer für EL1 LDR x1, =stack_top_el1 MSR SP_EL1, x1
// Interrupts aktivieren BL enable_interrupts
// Hauptprogrammschleife
main_loop:
B main_loop
.vector_table:
B irq_handler // IRQ Adresse // Additional vectors for different interrupts
// Dummy Interrupt Handler irq_handler:
// Interrupt Service Routine Logik hier RET
// Speicherplatz für Stack .section .bss .stack_area:
.skip 0x1000 // Reserviere Speicherplatz
stack_top_el1:
Wichtige Punkte: Initialisierungslogik in EL1: Es ist wichtig, dass du Initialisierungen für Interrupts, Vektortabellen und den Stack Pointer in EL1 korrekt durchführst. Interrupt-Freigabe: Aktivierung von Interrupts, wenn notwendig. Zusammenfassung
Wenn du keine besonderen Maßnahmen triffst, startet dein Bare-Metal-Kernel auf dem Raspberry Pi 4 standardmäßig in EL2 nach der Initialisierung durch die Firmware. Es ist gängige Praxis, dann direkt auf EL1 zu wechseln und dort den Kernel fortzusetzen. EL1 ist für die meisten bare-metal Anwendungen und Betriebsystem-Kernel der bevorzugte Modus.
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.
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 Tabelle enthält Adressen für die folgenden Ausnahmen:
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 Beispiel einer Vektortabelle
Hier ist ein Beispiel, wie eine Vektortabelle für EL1 aussehen könnte:
.align 11 .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
Die Vektortabelle muss dem Prozessor mitgeteilt werden, indem die Basisadresse der Tabelle in das VBAR_ELx-Register geschrieben wird:
.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.