Exceptions

Exceptions, and interrupts, are a hardware mechanism by which the processor handles asynchronous events and fatal errors (e.g. executing an invalid instruction). 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 exception handlers.

  1. // Exception handler for the SysTick (System Timer) exception
  2. #[exception]
  3. fn SysTick() {
  4. // ..
  5. }

Other than the exception attribute exception handlers look like plain functions but there’s one more difference: exception handlers can not be called 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.

  1. #[exception]
  2. fn SysTick() {
  3. static mut COUNT: u32 = 0;
  4. // `COUNT` has transformed to type `&mut u32` and it's safe to use
  5. *COUNT += 1;
  6. }

As you may know, using static mut variables in a function makes it non-reentrant). It’s undefined behavior to call a non-reentrant function, directly or indirectly, from more than one exception / interrupt handler or from main and one or more exception / interrupt handlers.

Safe Rust must never result in undefined behavior so non-reentrant functions must be marked as unsafe. Yet I just told that exception handlers can safely use static mut variables. How is this possible? This is possible because exception handlers can not be called by software thus reentrancy is not possible.

Note that the exception attribute transforms definitions of static variables inside the function by wrapping them into unsafe blocks and providing us with new appropriate variables of type &mut of the same name. Thus we can derefence the reference via * to access the values of the variables without needing to wrap them in an unsafe block.

A complete example

Here’s an example that uses the system timer to raise a SysTick exception roughly every second. The SysTick exception handler keeps track of how many times it has been called in the COUNT variable and then prints the value of COUNT to the host console using semihosting.

NOTE: You can run this example on any Cortex-M device; you can also run it on QEMU

  1. #![deny(unsafe_code)]
  2. #![no_main]
  3. #![no_std]
  4. use panic_halt as _;
  5. use core::fmt::Write;
  6. use cortex_m::peripheral::syst::SystClkSource;
  7. use cortex_m_rt::{entry, exception};
  8. use cortex_m_semihosting::{
  9. debug,
  10. hio::{self, HStdout},
  11. };
  12. #[entry]
  13. fn main() -> ! {
  14. let p = cortex_m::Peripherals::take().unwrap();
  15. let mut syst = p.SYST;
  16. // configures the system timer to trigger a SysTick exception every second
  17. syst.set_clock_source(SystClkSource::Core);
  18. // this is configured for the LM3S6965 which has a default CPU clock of 12 MHz
  19. syst.set_reload(12_000_000);
  20. syst.clear_current();
  21. syst.enable_counter();
  22. syst.enable_interrupt();
  23. loop {}
  24. }
  25. #[exception]
  26. fn SysTick() {
  27. static mut COUNT: u32 = 0;
  28. static mut STDOUT: Option<HStdout> = None;
  29. *COUNT += 1;
  30. // Lazy initialization
  31. if STDOUT.is_none() {
  32. *STDOUT = hio::hstdout().ok();
  33. }
  34. if let Some(hstdout) = STDOUT.as_mut() {
  35. write!(hstdout, "{}", *COUNT).ok();
  36. }
  37. // IMPORTANT omit this `if` block if running on real hardware or your
  38. // debugger will end in an inconsistent state
  39. if *COUNT == 9 {
  40. // This will terminate the QEMU process
  41. debug::exit(debug::EXIT_SUCCESS);
  42. }
  43. }
  1. tail -n5 Cargo.toml
  1. [dependencies]
  2. cortex-m = "0.5.7"
  3. cortex-m-rt = "0.6.3"
  4. panic-halt = "0.2.0"
  5. cortex-m-semihosting = "0.3.1"
  1. $ cargo run --release
  2. Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb (..)
  3. 123456789

If you run this on the Discovery board you’ll see the output on the OpenOCD console. 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 exception handler for a specific exception. If you don’t override the handler for a particular exception it will be handled by the DefaultHandler function, which defaults to:

  1. fn DefaultHandler() {
  2. loop {}
  3. }

This function is provided by the cortex-m-rt crate and marked as #[no_mangle] so you can put a breakpoint on “DefaultHandler” and catch unhandled exceptions.

It’s possible to override this DefaultHandler using the exception attribute:

  1. #[exception]
  2. fn DefaultHandler(irqn: i16) {
  3. // custom default handler
  4. }

The irqn argument indicates which exception is being serviced. A negative value indicates that a Cortex-M exception is being serviced; and zero or a positive value indicate that a device specific exception, AKA interrupt, is being serviced.

The hard fault handler

The HardFault exception is a bit special. This exception is fired when the program enters an invalid state so its handler can not return as that could result in undefined behavior. Also, the runtime crate does a bit of work before the 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 to registers that were pushed into the stack by the exception. These registers are a snapshot of the processor state at the moment the exception was triggered and are useful to diagnose a hard fault.

Here’s an example that performs an illegal operation: a read to a nonexistent memory 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 will happily return 0 on reads to invalid memory.

  1. #![no_main]
  2. #![no_std]
  3. use panic_halt as _;
  4. use core::fmt::Write;
  5. use core::ptr;
  6. use cortex_m_rt::{entry, exception, ExceptionFrame};
  7. use cortex_m_semihosting::hio;
  8. #[entry]
  9. fn main() -> ! {
  10. // read a nonexistent memory location
  11. unsafe {
  12. ptr::read_volatile(0x3FFF_FFFE as *const u32);
  13. }
  14. loop {}
  15. }
  16. #[exception]
  17. fn HardFault(ef: &ExceptionFrame) -> ! {
  18. if let Ok(mut hstdout) = hio::hstdout() {
  19. writeln!(hstdout, "{:#?}", ef).ok();
  20. }
  21. loop {}
  22. }

The HardFault handler prints the ExceptionFrame value. If you run this you’ll see something like this on the OpenOCD console.

  1. $ openocd
  2. (..)
  3. ExceptionFrame {
  4. r0: 0x3ffffffe,
  5. r1: 0x00f00000,
  6. r2: 0x20000000,
  7. r3: 0x00000000,
  8. r12: 0x00000000,
  9. lr: 0x080008f7,
  10. pc: 0x0800094a,
  11. xpsr: 0x61000000
  12. }

The pc value is the value of the Program Counter at the time of the exception and it points to the instruction that triggered the exception.

If you look at the disassembly of the program:

  1. $ cargo objdump --bin app --release -- -d --no-show-raw-insn --print-imm-hex
  2. (..)
  3. ResetTrampoline:
  4. 8000942: movw r0, #0xfffe
  5. 8000946: movt r0, #0x3fff
  6. 800094a: ldr r0, [r0]
  7. 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.