Ausnahmen (Exceptions)
Ausnahmen und Interrupts sind ein Hardwaremechanismus, mit dem der Prozessor asynchrone Ereignisse und schwerwiegende Fehler (z. B. die Ausführung einer ungültigen Anweisung) behandelt. Ausnahmen implizieren Präemption und erfordern Ausnahmebehandlungsroutinen, die als Reaktion auf das auslösende Signal ausgeführt werden.
Die cortex-m-rt-Crate bietet das Attribut exception zur Deklaration von Ausnahmebehandlungsroutinen.
// Ausnahmebehandlungsroutine fuer die SysTick-Ausnahme (System-Timer)
#[exception]
fn SysTick() {
// ..
}
Abgesehen vom exception-Attribut sehen Exception-Handler wie gewöhnliche Funktionen aus, doch es gibt noch einen weiteren Unterschied: exception-Handler können nicht per Software aufgerufen werden. Bezogen auf das vorherige Beispiel würde die Anweisung SysTick(); zu einem Kompilierfehler führen.
Dieses Verhalten ist durchaus beabsichtigt und notwendig, um eine bestimmte Eigenschaft zu gewährleisten: static mut-Variablen, die innerhalb von exception-Handlern deklariert werden, sind sicher in der Verwendung.
#[exception]
fn SysTick() {
static mut COUNT: u32 = 0;
// `COUNT` wurde in den Typ `&mut u32` umgewandelt und ist sicher in der
// Verwendung.
*COUNT += 1;
Wie Ihnen vielleicht bekannt ist, führt die Verwendung von static mut-Variablen in einer Funktion dazu, dass diese nicht wiedereintrittsfähig ist. Es stellt undefiniertes Verhalten dar, eine nicht wiedereintrittsfähige Funktion direkt oder indirekt aus mehr als einem Ausnahme- oder Interrupt-Handler oder aus main und einem oder mehreren Ausnahme- oder Interrupt-Handlern heraus aufzurufen.
Safe Rust darf niemals zu undefiniertem Verhalten führen; daher müssen nicht-wiedereintrittsfähige Funktionen als unsafe gekennzeichnet werden. Dennoch habe ich gerade erwähnt, dass Ausnahme-Handler (exception handlers) sicher static mut-Variablen verwenden können. Wie ist das möglich? Dies ist möglich, da Ausnahme-Handler nicht per Software aufgerufen werden können und somit kein Wiedereintritt (Reentrancy) stattfinden kann. Diese Handler werden von der Hardware selbst aufgerufen, bei der davon ausgegangen wird, dass sie physisch keine Nebenläufigkeit aufweist.
Im Kontext von Ausnahme-Handlern in eingebetteten Systemen stellt das Ausbleiben gleichzeitiger Aufrufe desselben Handlers folglich sicher, dass keine Probleme mit der Wiedereintrittsfähigkeit auftreten – selbst dann nicht, wenn der Handler veränderbare statische Variablen verwendet
In einem Multicore-System, in dem mehrere Prozessorkerne gleichzeitig Code ausführen, gewinnt die Problematik der Wiedereintrittsfähigkeit (Reentrancy) erneut an Bedeutung – selbst innerhalb von Exception-Handlern. Auch wenn jeder Kern über eigene Exception-Handler verfügen mag, kann es dennoch zu Situationen kommen, in denen mehrere Kerne versuchen, denselben Exception-Handler simultan auszuführen. Um dieser Herausforderung in einer Multicore-Umgebung zu begegnen, müssen innerhalb der Exception-Handler geeignete Synchronisationsmechanismen eingesetzt werden; so wird sichergestellt, dass der Zugriff auf gemeinsam genutzte Ressourcen zwischen den Kernen korrekt koordiniert wird. Dies geschieht typischerweise durch den Einsatz von Techniken wie Locks, Semaphoren oder atomaren Operationen, um Race Conditions zu vermeiden und die Datenintegrität zu wahren.
Beachten Sie, dass das AttributexceptionDefinitionen statischer Variablen innerhalb der Funktion umwandelt, indem es sie inunsafe-Blöcke einschließt und uns neue, passende Variablen des Typs&mutmit demselben Namen zur Verfügung stellt. Auf diese Weise können wir die Referenz mittels*dereferenzieren, um auf die Werte der Variablen zuzugreifen, ohne sie selbst in einenunsafe-Block einschließen zu müssen.
Ein vollständiges Beispiel
Hier ist ein Beispiel, das den System-Timer verwendet, um etwa jede Sekunde eine SysTick-Exception auszulösen. Der SysTick-Exception-Handler protokolliert in der Variablen COUNT, wie oft er aufgerufen wurde, und gibt anschließend den Wert von COUNT mittels Semihosting auf der Host-Konsole aus.
HINWEIS: Sie können dieses Beispiel auf jedem Cortex-M-Gerät ausführen; es lässt sich auch unter QEMU ausführen.
#![deny(unsafe_code)]
#![no_main]
#![no_std]
use panic_halt as _;
use core::fmt::Write;
use cortex_m::peripheral::syst::SystClkSource;
use cortex_m_rt::{entry, exception};
use cortex_m_semihosting::{
debug,
hio::{self, HostStream},
};
#[entry]
fn main() -> ! {
let p = cortex_m::Peripherals::take().unwrap();
let mut syst = p.SYST;
// konfiguriert den System-Timer so, dass er jede Sekunde eine SysTick-
// Exception ausloest
syst.set_clock_source(SystClkSource::Core);
// Dies ist für den LM3S6965 konfiguriert, der einen Standard-CPU-Takt von
// 12 MHz hat
syst.set_reload(12_000_000);
syst.clear_current();
syst.enable_counter();
syst.enable_interrupt();
loop {}
}
#[exception]
fn SysTick() {
static mut COUNT: u32 = 0;
static mut STDOUT: Option<HostStream> = None;
*COUNT += 1;
// Verzoegerte Initialisierung
if STDOUT.is_none() {
*STDOUT = hio::hstdout().ok();
}
if let Some(hstdout) = STDOUT.as_mut() {
write!(hstdout, "{}", *COUNT).ok();
}
// WICHTIG: Lassen Sie diesen `if`-Block weg, wenn Sie das Programm auf
// echter Hardware ausfuehren, da sich Ihr Debugger sonst in
// einem inkonsistenten Zustand befinden wird.
if *COUNT == 9 {
// Dies beendet den QEMU-Prozess.
debug::exit(debug::EXIT_SUCCESS);
}
}
tail -n5 Cargo.toml[dependencies]
cortex-m = "0.5.7"
cortex-m-rt = "0.6.3"
panic-halt = "0.2.0"
cortex-m-semihosting = "0.3.1"$ cargo run --release
Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb (..)
123456789Wenn Sie dies auf dem Discovery-Board ausführen, sehen Sie die Ausgabe in der OpenOCD-Konsole. Außerdem hält das Programm nicht an, wenn der Zählerstand 9 erreicht.
Der Standard-Exception-Handler
Das exception-Attribut bewirkt eigentlich, dass der Standard-Exception-Handler für eine bestimmte Exception überschrieben wird. Wenn Sie den Handler für eine bestimmte Exception nicht überschreiben, wird sie von der Funktion DefaultHandler behandelt, die standardmäßig Folgendes tut:
fn DefaultHandler() {
loop {}
}Diese Funktion wird vom cortex-m-rt-Crate bereitgestellt und ist mit #[no_mangle] markiert, sodass Sie einen Breakpoint auf „DefaultHandler“ setzen und unbehandelte Exceptions abfangen können.
Es ist möglich, diesen DefaultHandler mithilfe des exception-Attributs zu überschreiben:
#[exception]
fn DefaultHandler(irqn: i16) {
// benutzerdefinierter Standard-Handler
}
Das Argument irqn gibt an, welche Exception gerade bearbeitet wird. Ein negativer Wert zeigt an, dass eine Cortex-M-Exception bearbeitet wird, während ein Wert von null oder ein positiver Wert darauf hinweist, dass eine gerätespezifische Exception – auch als Interrupt bezeichnet – bearbeitet wird.
Der “Schwere Fehler”-Handler
Die HardFault-Exception nimmt eine Sonderstellung ein. Sie wird ausgelöst, wenn das Programm in einen ungültigen Zustand gerät; daher darf ihr Handler nicht zurückkehren, da dies zu undefiniertem Verhalten führen könnte. Zudem führt die Runtime-Crate einige Vorarbeiten durch, bevor der benutzerdefinierte HardFault-Handler aufgerufen wird, um die Fehlersuche (Debugging) zu erleichtern.
Daraus ergibt sich, dass der HardFault-Handler folgende Signatur aufweisen muss: fn(&ExceptionFrame) -> !. Das Argument des Handlers ist ein Zeiger auf Register, die beim Auftreten der Exception auf den Stack gesichert wurden. Diese Register stellen eine Momentaufnahme des Prozessorzustands zum Zeitpunkt der Auslösung der Exception dar und sind für die Diagnose eines Hard Faults hilfreich.
Hier ist ein Beispiel für eine unzulässige Operation: der Lesezugriff auf eine nicht existierende Speicheradresse.
HINWEIS: Dieses Programm wird unter QEMU nicht fehlschlagen (d. h. es stürzt nicht ab), daqemu-system-arm -machine lm3s6965evbkeine Überprüfung von Speicherzugriffen durchführt und beim Lesen von ungültigem Speicher problemlos den Wert0zurückgibt.
#![no_main]
#![no_std]
use panic_halt as _;
use core::fmt::Write;
use core::ptr;
use cortex_m_rt::{entry, exception, ExceptionFrame};
use cortex_m_semihosting::hio;
#[entry]
fn main() -> ! {
// liest einen nicht existierenden Speicherbereich
unsafe {
ptr::read_volatile(0x3FFF_0000 as *const u32);
}
loop {}
}
#[exception]
fn HardFault(ef: &ExceptionFrame) -> ! {
if let Ok(mut hstdout) = hio::hstdout() {
writeln!(hstdout, "{:#?}", ef).ok();
}
loop {}
}
Der HardFault-Handler gibt den Wert des ExceptionFrame aus. Wenn Sie dies ausführen, sehen Sie auf der OpenOCD-Konsole eine Ausgabe wie diese.
$ openocd
(..)
ExceptionFrame {
r0: 0x3fff0000,
r1: 0x00000003,
r2: 0x080032e8,
r3: 0x00000000,
r12: 0x00000000,
lr: 0x080016df,
pc: 0x080016e2,
xpsr: 0x61000000,
}Der Wert pc ist der Wert des Programmzählers zum Zeitpunkt der Ausnahme und verweist auf die Anweisung, die die Ausnahme ausgelöst hat.
Wenn Sie sich den Disassembler des Programms ansehen:
$ cargo objdump --bin app --release -- -d --no-show-raw-insn --print-imm-hex
(..)
ResetTrampoline:
8000942: movw r0, #0xfffe
8000946: movt r0, #0x3fff
800094a: ldr r0, [r0]
800094c: b #-0x4 <ResetTrampoline+0xa>Den Wert des Programmzählers 0x0800094a können Sie in der Disassemblierung nachschlagen.
Sie werden sehen, dass eine Ladeoperation (ldr r0, [r0]) die Ausnahme verursacht hat.
Das Feld r0 von ExceptionFrame gibt an, dass der Wert des Registers r0 zu diesem Zeitpunkt 0x3fff_fffe war.