Exceptions
Exceptions, and interrupts, are a hardware mechanism by which the processorhandles asynchronous events and fatal errors (e.g. executing an invalidinstruction). Exceptions imply preemption and involve exception handlers,subroutines executed in response to the signal that triggered the event.
The cortex-m-rt
crate provides an exception
attribute to declare exceptionhandlers.
// Exception handler for the SysTick (System Timer) exception
#[exception]
fn SysTick() {
// ..
}
Other than the exception
attribute exception handlers look like plainfunctions but there's one more difference: exception
handlers can not becalled by software. Following the previous example, the statement SysTick();
would result in a compilation error.
This behavior is pretty much intended and it's required to provide a feature:static mut
variables declared inside exception
handlers are safe to use.
#[exception]
fn SysTick() {
static mut COUNT: u32 = 0;
// `COUNT` has type `&mut u32` and it's safe to use
*COUNT += 1;
}
As you may know, using static mut
variables in a function makes itnon-reentrant). It's undefined behavior to call a non-reentrant function,directly or indirectly, from more than one exception / interrupt handler or frommain
and one or more exception / interrupt handlers.
Safe Rust must never result in undefined behavior so non-reentrant functionsmust be marked as unsafe
. Yet I just told that exception
handlers can safelyuse static mut
variables. How is this possible? This is possible becauseexception
handlers can not be called by software thus reentrancy is notpossible.
A complete example
Here's an example that uses the system timer to raise a SysTick
exceptionroughly every second. The SysTick
exception handler keeps track of how manytimes it has been called in the COUNT
variable and then prints the value ofCOUNT
to the host console using semihosting.
NOTE: You can run this example on any Cortex-M device; you can also run iton QEMU
#![deny(unsafe_code)]
#![no_main]
#![no_std]
extern crate panic_halt;
use core::fmt::Write;
use cortex_m::peripheral::syst::SystClkSource;
use cortex_m_rt::{entry, exception};
use cortex_m_semihosting::{
debug,
hio::{self, HStdout},
};
#[entry]
fn main() -> ! {
let p = cortex_m::Peripherals::take().unwrap();
let mut syst = p.SYST;
// configures the system timer to trigger a SysTick exception every second
syst.set_clock_source(SystClkSource::Core);
// this is configured for the LM3S6965 which has a default CPU clock of 12 MHz
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<HStdout> = None;
*COUNT += 1;
// Lazy initialization
if STDOUT.is_none() {
*STDOUT = hio::hstdout().ok();
}
if let Some(hstdout) = STDOUT.as_mut() {
write!(hstdout, "{}", *COUNT).ok();
}
// IMPORTANT omit this `if` block if running on real hardware or your
// debugger will end in an inconsistent state
if *COUNT == 9 {
// This will terminate the QEMU process
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 (..)
123456789
If you run this on the Discovery board you'll see the output on the OpenOCDconsole. Also, the program will not stop when the count reaches 9.
The default exception handler
What the exception
attribute actually does is override the default exceptionhandler for a specific exception. If you don't override the handler for aparticular exception it will be handled by the DefaultHandler
function, whichdefaults to:
fn DefaultHandler() {
loop {}
}
This function is provided by the cortex-m-rt
crate and marked as#[nomangle]
so you can put a breakpoint on "DefaultHandler" and catch_unhandled exceptions.
It's possible to override this DefaultHandler
using the exception
attribute:
#[exception]
fn DefaultHandler(irqn: i16) {
// custom default handler
}
The irqn
argument indicates which exception is being serviced. A negativevalue indicates that a Cortex-M exception is being serviced; and zero or apositive value indicate that a device specific exception, AKA interrupt, isbeing serviced.
The hard fault handler
The HardFault
exception is a bit special. This exception is fired when theprogram enters an invalid state so its handler can not return as that couldresult in undefined behavior. Also, the runtime crate does a bit of work beforethe user defined HardFault
handler is invoked to improve debuggability.
The result is that the HardFault
handler must have the following signature:fn(&ExceptionFrame) -> !
. The argument of the handler is a pointer toregisters that were pushed into the stack by the exception. These registers area snapshot of the processor state at the moment the exception was triggered andare useful to diagnose a hard fault.
Here's an example that performs an illegal operation: a read to a nonexistentmemory location.
NOTE: This program won't work, i.e. it won't crash, on QEMU because
qemu-system-arm -machine lm3s6965evb
doesn't check memory loads and willhappily return0
on reads to invalid memory.
#![no_main]
#![no_std]
extern crate panic_halt;
use core::fmt::Write;
use core::ptr;
use cortex_m_rt::{entry, exception, ExceptionFrame};
use cortex_m_semihosting::hio;
#[entry]
fn main() -> ! {
// read a nonexistent memory location
unsafe {
ptr::read_volatile(0x3FFF_FFFE as *const u32);
}
loop {}
}
#[exception]
fn HardFault(ef: &ExceptionFrame) -> ! {
if let Ok(mut hstdout) = hio::hstdout() {
writeln!(hstdout, "{:#?}", ef).ok();
}
loop {}
}
The HardFault
handler prints the ExceptionFrame
value. If you run thisyou'll see something like this on the OpenOCD console.
$ openocd
(..)
ExceptionFrame {
r0: 0x3ffffffe,
r1: 0x00f00000,
r2: 0x20000000,
r3: 0x00000000,
r12: 0x00000000,
lr: 0x080008f7,
pc: 0x0800094a,
xpsr: 0x61000000
}
The pc
value is the value of the Program Counter at the time of the exceptionand it points to the instruction that triggered the exception.
If you look at the disassembly of the program:
$ 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>
You can lookup the value of the program counter 0x0800094a
in the dissassembly.You'll see that a load operation (ldr r0, [r0]
) caused the exception.The r0
field of ExceptionFrame
will tell you the value of register r0
was 0x3fff_fffe
at that time.