Interrupt Teil 2 (PI4): Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
KKeine Bearbeitungszusammenfassung
Zeile 107: Zeile 107:
...
...
}   
}   
 
</syntaxhighlight>
== Hypervisor Call ==
== Hypervisor Call ==
Diesen fangen wir hier ab, da es unter umständen dazu kommen könnte, dass ein Anwendungsprogramm einen Hypervisors Zugriff benötigt. Damit aber unser Programm weiter sinnvoll weiterläuft, müssen wir das spsr_el2 Register reinigen:
Diesen fangen wir hier ab, da es unter umständen dazu kommen könnte, dass ein Anwendungsprogramm einen Hypervisors Zugriff benötigt. Damit aber unser Programm weiter sinnvoll weiterläuft, müssen wir das spsr_el2 Register reinigen:
Zeile 119: Zeile 119:
eret
eret
</syntaxhighlight>
</syntaxhighlight>
== Standard Interrupt (IRQ)==
== Standard Interrupt (IRQ)==
Da wir nun etwas mehr erlauben, als nur einen Timer-Interrupt, müssen wir etwas mehr tun, damit unser System stabil bleibt.
Da wir nun etwas mehr erlauben, als nur einen Timer-Interrupt, müssen wir etwas mehr tun, damit unser System stabil bleibt.

Version vom 28. Juli 2025, 13:26 Uhr

Im vorhergehenden Kapitel haben wir einen Interrupt für den Timer erstellt. Dies ist zur Zeit auch der einzige, der in unserem System, welches wir entwickeln, zur Verfügung haben. Im nächsten Kapitel wird es dann aufwendig. Wir werden uns um den USB-Eingang kümmern und zunächst einen Treiber für die Tatstatur und anschließend noch auf Mausbewegungen reagieren.

Dazu werden wir dann auch Interrupts benötigen, die ausgelöst werden, wenn jemand auf die Tastatur verwendet, oder dann später auch die Maus bedient.

Damit wir dann grundsätzlich Interrupts programmieren können, werden wir unseren Code etwas erweitern und alle Arten von Interrupts erlauben. Leider werden wir hier nicht viel sehen, aber es ist essentiell, um später unser System zu erweitern.

Vektortabelle

.align	11
.globl	VectorTable
VectorTable:
// Vektoren für EL1t (Current Exception Level SP_el0)
    .align	7
    b sync_exception          // Synchronous Exception
    .align	7
    b irq_handler             // IRQ - Normal Interrupt
    .align	7
    b fiq_handler            // FIQ - Fast Interrupt
    .align	7
    b serror_handler         // SError - System Error
// Vektoren für EL1h (Current Exception Level SP_el1)
    .align	7
    b sync_exception          // Synchronous Exception
    .align	7
    b irq_handler             // IRQ - Normal Interrupt
    .align	7
    b fiq_handler             // FIQ - Fast Interrupt
    .align	7
    b serror_handler          // SError - System Error
// Vektoren für EL0 64-bit Modus
    .align	7
    b hvc_handler             // Synchronous EL0 (64-bit), Hypervisor Call
    .align	7
    b not_used                // IRQ EL0 (64-bit)
    .align	7
    b not_used                // FIQ EL0 (64-bit)
    .align	7
    b not_used                // Error EL0 (64-bit)
// Vektoren für EL0 32-bit Modus
    .align	7
    b not_used                // Synchronous EL0 (32-bit)
    .align	7
    b not_used                // IRQ EL0 (32-bit)
    .align	7
    b not_used                // FIQ EL0 (32-bit)
    .align	7
    b not_used                // Error EL0 (32-bit)

Bisher haben wir "nur" den normalen Interrupt unterstützt. Mit diesem Beispiel werden wir auch das "fast Interrupt" programmieren. Genaus gehen wir auch auf Fehler (sync_exception und serror_handler) ein, wenn etwas nicht so läuft wie wir es wollen.

Einige Vectoren verwenden wir nicht. Hier springen wir zu "not_used".

Exceptions

Fangen wir zunächst mit den Exceptions (Abweichungen) an. Da das System hier auf einen Fehler läuft, werden wir in diesem auf eine Sicherung der Register verzichten und direkt in die Fehlerbehandlung gehen:

sync_exception:
    mov x0,#0 //Fehlercode
    b ExceptionHandler
serror_handler:
    mov x0,#1 //Fehlercode
    b ExceptionHandler
not_used:
    mov x0,#2 //Fehlercode
    b ExceptionHandler

Wir übergeben jeweils in x0 (Erster Parameter für die C-Funktion) einen Fehlercode, den wir verwenden können, um uns dann in C entsprechend zu reagieren. Dazu schreiben wir ein kleines C-Programm:

void ExceptionHandler (int code)
{
  const char *Exmsg;
  switch (code)
  {
    case 0:
      Exmsg = "sync_exception";
      break;
    case 1:
      Exmsg = "serror_handler";
      break;
    case 2:
      Exmsg = "not used";
      break;
    default:
      Exmsg = "Unbekannter Code\n";
      break;
  }
  printf ("Error: Exception '%s' ausgelöst\n", Exmsg);
  Stop();	
}

FastInterrupt (FIQ)

Obwohl unsere Vectortabelle dies ermöglicht, verzichten wir zunächst darauf. Allerdings werden wir in den kommenden Code-Abschnitte, dort wo es eventuell wichtig ist, darauf achten, dass wir richtig handeln und diese Art von Interrupt zunächst verbieten.

Damit wir aber einen Überblick haben, ob dennoch irgendwo ein FastInterrupt entstanden ist, werden wir diesen wie eine Exception behandeln:

fiq_handler:
    mov x0,#3 //Fehlercode
    b ExceptionHandler

Unserer C-Funktion für die Exceptionverwaltung erweitern wir mit:

void ExceptionHandler (int code)
{
...
    case 3:
      Exmsg = "FastInterrupt (FIQ)";
      break;
...
}

Hypervisor Call

Diesen fangen wir hier ab, da es unter umständen dazu kommen könnte, dass ein Anwendungsprogramm einen Hypervisors Zugriff benötigt. Damit aber unser Programm weiter sinnvoll weiterläuft, müssen wir das spsr_el2 Register reinigen:

hvc_handler:					// Hypervisor Call, return to EL2h mode 
	mrs	x0, spsr_el2
	bic	x0, x0, #0xF
	mov	x1, #9
	orr	x0, x0, x1
	msr	spsr_el2, x0
	eret

Standard Interrupt (IRQ)

Da wir nun etwas mehr erlauben, als nur einen Timer-Interrupt, müssen wir etwas mehr tun, damit unser System stabil bleibt. Zunächst hier der Code:

.globl	GenericIRQHandler
GenericIRQHandler:
	stp	x29, x30, [sp, #-16]!		//Zunächst x29, x30 sichern 

	mrs	x29, elr_el1			//Lies das Exception Link Register von EL1 
	mrs	x30, spsr_el1           //Lies das Saved Program Status Register von EL1
	stp	x29, x30, [sp, #-16]!   //Speichere `elr_el1` und `spsr_el1` auf dem Stack
	msr	DAIFClr, #1			    //Deaktiviere FIQ-Maskierung (ermögliche Fast Interrupts) 

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

	stp	x27, x28, [sp, #-16]!		// speichert x0-x28 auf den Stack 
	stp	x25, x26, [sp, #-16]!
	stp	x23, x24, [sp, #-16]!
	stp	x21, x22, [sp, #-16]!
	stp	x19, x20, [sp, #-16]!
	stp	x17, x18, [sp, #-16]!
	stp	x15, x16, [sp, #-16]!
	stp	x13, x14, [sp, #-16]!
	stp	x11, x12, [sp, #-16]!
	stp	x9, x10, [sp, #-16]!
	stp	x7, x8, [sp, #-16]!
	stp	x5, x6, [sp, #-16]!
	stp	x3, x4, [sp, #-16]!
	stp	x1, x2, [sp, #-16]!
	str	x0, [sp, #-16]!

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

	bl	InterruptHandler          //Sprung in die Interruptroutine

	ldr	x0, [sp], #16			// Reaktivieren der Register x0-x28 vom 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		// Reaktivieren der Register q0-q31 vom 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			//Reaktiviere FIQ-Maskierung (sperre Fast Interrupts)
	ldp	x29, x30, [sp], #16		// Reaktivieren der Register elr_el1, spsr_el1 vom Stack 
	msr	elr_el1, x29
	msr	spsr_el1, x30

	ldp	x29, x30, [sp], #16		// Reaktivieren der Register x29, x30 vom Stack 

	eret                        //Zurück zum "normalen" Programmablauf

Der gezeigte GenericIRQHandler implementiert die Verarbeitung eines Standard Interrupts (IRQ). Abgesehen von der Sicherung der allgemeinen Register (z. B. x0-x28 und q0-q31) führt der Code zusätzliche Aktionen aus, um sicherzustellen, dass der Interrupt sicher und ohne Seiteneffekte verarbeitet wird.

Zusätzliche Schritte und deren Gründe

Speichern von `elr_el1` und `spsr_el1` (Programmcounter und Statusregister)

mrs x29, elr_el1            // Lies den Exception Link Register von EL1
mrs x30, spsr_el1           // Lies den Saved Program Status Register von EL1
stp x29, x30, [sp, #-16]!   // Speichere `elr_el1` und `spsr_el1` auf dem Stack
  • elr_el1: Speichert die Adresse des Befehls, der unterbrochen wurde. Nach Abschluss des Interrupt-Handlers muss der Prozessor an diese Stelle zurückspringen, um die reguläre Ausführung des Programms fortzusetzen.
  • spsr_el1: Enthält den gespeicherten Prozessorstatus (z. B. Flags, Interrupt-Status), der wiederhergestellt werden muss, damit der Zustand des Gastsystems nicht verändert wird.

Zusammenhang mit den Interrupts:

Interrupts können überall auftreten – der Prozessor muss daher sicherstellen, dass er nach der Bearbeitung an genau der gleichen Stelle und im genau gleichen Zustand weitermacht.

Das Speichern dieser Register stellt sicher, dass alle Informationen zum zuvor ausgeführten Kontext gesichert sind.

Interrupt-Maskierung: Aktivieren/Deaktivieren von Interrupts

msr DAIFClr, #1  // Deaktiviere FIQ-Maskierung (ermögliche Fast Interrupts)
msr DAIFSet, #1  // Reaktiviere FIQ-Maskierung (sperre Fast Interrupts)
  • FIQ aktivieren: Vor dem Betreten des Interrupt-Handlers werden in manchen Fällen FIQs (Fast Interrupt Requests) wieder aktiviert. Dadurch können zeitkritische Operationen durch spezielle, priorisierte Interrupts verarbeitet werden.
  • FIQ deaktivieren: Vor Rückkehr aus dem IRQ-Handler werden FIQs abgeschaltet, um mögliche Unterbrechungen während des Kontextwechsels zu verhindern.

Durch selektives Aktivieren/Deaktivieren der Interrupts wird sichergestellt, dass ein Interrupt sicher verarbeitet werden kann, ohne dass derselbe oder ein anderer Interrupt erneut eingreift.

Speichern der Gleitkomma-/SIMD-Register (`q0-q31`)

stp q30, q31, [sp, #-32]!   // Sichere die letzten NEON-/SIMD-Register
...
stp q0, q1, [sp, #-32]!

Warum die SIMD-Register speichern?

In ARMv8 speichern die Gleitkomma-/SIMD-Register (NEON-Register, q0-q31) Werte, die in parallelen Berechnungen (z. B. Vektorrechnungen, Mediendatenverarbeitung) oder Gleitkommaberechnungen verwendet werden. Ein IRQ kann zu einem beliebigen Zeitpunkt auftreten, auch während einer Berechnung oder eines Programms, das diese Register verwendet.

Ziel:

Durch das Speichern aller Gleitkomma-/SIMD-Register wird sichergestellt, dass kein Kontextverlust (z. B. Änderungen der Gleitkommadaten oder SIMD-Daten) stattfindet.

Speichern aller allgemeinen Register (`x0-x28`)

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

Warum alle Register sichern?

Selbst wenn in einer Unterbrechung (IRQ) nur bestimmte Register verwendet werden, sichert man in einem generischen IRQ-Handler alle General-Purpose-Register (x0-x31). Dies verhindert Probleme durch unvorhergesehene Änderungen oder Seiteneffekte im laufenden Code.

Kontext-Sicherheit:

Der Interrupt kann nicht kontrollieren, welche Register zum Zeitpunkt der Unterbrechung gerade in Benutzung sind. Das Sichern des gesamten CPU-Registersatzes garantiert einen vollständigen Schutz, bis der IRQ vollständig abgeschlossen ist.

Reaktivieren der Register und Fortsetzung

ldr x0, [sp], #16             // Stelle `x0-x28` wieder her
...
ldp q0, q1, [sp], #32         // Stelle `q0-q31` wieder her
...
msr elr_el1, x29              // Wiederherstellen des Programmcounters (ELR_EL1)
msr spsr_el1, x30             // Wiederherstellen des Prozessorstatus
eret                          // Rückkehr zum unterbrochenen Kontext

Reaktivierung:

Nach Abschluss des Interrupts wird der Prozessorzustand wiederhergestellt, indem alle zuvor gesicherten Register und Statusinformationen zurückgeschrieben werden.