A look into ways to implement and share data with interrupt handlers in Rust (Update 1)
- 4338 words
- 22 min
In this blog entry I will explain a bit what interrupts are and they work in embedded systems and compare various interrupt implementation and sharing methods in Rust.
Update 2020-03-06: @jamesmunns has provided a new version of cmim which
allows to use the SysTick
exception and an adaption of the example for it so
I updated this post accordingly.
Interrupt handling
Interrupt
An interrupt is a hardware method to react to special events recognized by the system to interrupt regular program flow and do some special handling of the event which caused the interrupt. This mechanism does not only exist for micro controllers but is also present on regular CPUs. On the latter it is used especially by external components, e.g. extension cards or connected external peripherals which can notify the system about events (e.g. data to be processed is available) by generating an interrupt signal. The other common case is that interrupts are generated internally in the processing core by configuration of certain trigger events, e.g. if some memory access violation happened, some timer expired or the transmit buffer is empty.
To enable the use of interrupts on a system there's a special hardware element called an interrupt controller which can be programmed to handle such special events happening in the system. Interrupts have a defined or configurable priority and whenever an interrupt is generated with a higher priority than what the current execution priority, then the ongoing execution flow will be suspended and execution continues at a special function called an interrupt handler...
Interrupt handler
An interrupt handler is a special function which is designed to handle one or multiple of the interrupt types an interrupt controller can support. During the execution of an interrupt handler the regular program execution is suspended and all processing resources are dedicated to the interrupt handler. Most interrupt controllers are so called nested interrupt controllers which means that the execution of an interrupt handler can also be suspended to work on an even higher priority interrupt, should one occur.
As you can imagine, in a system where multiple priority levels exist, this can be problematic. If a part of the program is working with a resource, gets suspended and then an interrupt handler takes over and works with the same resource, this can easily cause race conditions.
There're multiple ways around this and we'll talk about this later but for now I'll briefly mention the two simplest solution for single-core systems (multi-core systems can execute code in parallel which causes additional problems):
- Let everything run on the same priority: Without preemption there can't be data races (between interrupt handlers, at least)
- Disable interrupts temporarily. This is called a Critical Section (or CS) and we'll see this a few times in a bit.
At the end of the interrupt handler, execution will continue at the last suspension point.
Resource sharing
There're essentially two pieces of resources you might you might want to "share" between interrupt handlers and regular code. It is essential to appropriately manage access from different parts of the program to them, especially for a language like Rust which takes safety very seriously.
Resource types
There're two important classes of data which we need to consider:
Data
Data can be any structure located at some address in random access memory. Those can be simply variables but also more complex structures like buffers where data piles up until it can be processed by a different piece of code.
Peripherals
In typical micro controllers all peripherals are accessible via memory mapped addresses which means that these magic addresses do interact directly with hardware when written to or read from.
Access management
There're 3 essential mechanisms to manage access to those resources:
Uniqueness
Every function, interrupt handler, memory address, etc. can only exist once. For the most part Rust already guarantees that this is the case, however it can not do this for raw pointers to memory addresses which is the reason why it is unsafe to dereference them into concrete types. However we do want to have access to those memory locations but still obtain unique handles to those resources which Rust will help us to deal with, so how do we do this?
The trick is that we're mapping those memory mapped addresses into concrete types which give us the required abstraction to safely use those and in addition we wrap them into a singleton so it's only possible to (safely) obtain the whole set of resources exactly once and let the Rust compiler handle most of that for us.
Sharing
Usually sharing resources in Rust is simple, you just pass borrows around and the Rust borrow checker will ensure that the lifetimes of the borrowed resources are not mixed up and that a mutable borrow and a regular borrow are mutually exclusive.
However things get more tricky if there're non-linear program flows involved as
it is the case with threads and interrupt handlers. As soon as there's an
external party involved which can interfere with program execution flow like a
operating system scheduler or interrupt controller, all shared resources require
special protection which know how to deal with a particular case. For threads we
have the Sync
trait to cover concurrent access however this assumes that there
is an overseeing party which makes sure that some over-the-top guarantees are
upheld, however there're cases in embedded where this cannot not work.
Moving
Similarly to sharing, moving now becomes a problem because the Rust compiler cannot track resources which are supposed to move into e.g. an interrupt handler. To the Rust compilers' eye those are not even part of the program itself but more a kind of standalone functions which are never even called and for some weird reason just happen to be defined in the same code base are the "real" program. Silly developers!
So special precautions need to be applied to a resource which is initialized in once part of the "real" program but then supposed to move to a different location which is not seen as part of the "real" program but a weird function which seemingly unused.
Implementation approaches
Over the last couple of years a few mechanisms have been developed and evolved to deal with the implementation and handling of the before mentioned shenanigans in a sane way.
For the rest of the post I will look at different approaches to handle interrupts, share and move resources. For this I've developed a simple example application for a Cortex-M0 MCU and we will look at the different implementations with the different approaches, compare the generated code sizes and I will give you my opinion about the pros and cons of the various methods.
So this is what the end result will have to look like:
All of the code can be found in the interrupt-comparison repo under the
flashing
workspace.
So these are the contenders:
Cortex-m-rt with delay
This is almost the most basic approach one could take without interrupts or an kind of sharing and thus our primitive "baseline":
- Set up the hardware and a timer
- Busy-wait for the timer to expire
- Turn LED on or off
Code
#![no_main]
#![no_std]
use panic_halt as _;
use stm32f0xx_hal::{delay::Delay, prelude::*, stm32};
use cortex_m::peripheral::Peripherals;
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
if let (Some(mut p), Some(cp)) = (stm32::Peripherals::take(), Peripherals::take()) {
let mut state: u8 = 0;
let (mut led, mut delay) = cortex_m::interrupt::free(|cs| {
// Configure clock to 48 MHz (i.e. the maximum) and freeze it
let mut rcc = p.RCC.configure().sysclk(48.mhz()).freeze(&mut p.FLASH);
// Obtain resources from GPIO port A
let gpioa = p.GPIOA.split(&mut rcc);
// (Re-)configure PA5 as output
let led = gpioa.pa5.into_push_pull_output(cs);
// Get delay provider
let delay = Delay::new(cp.SYST, &rcc);
(led, delay)
});
loop {
if state < 10 {
// Turn off the LED
led.set_low().ok();
state += 1;
} else {
// Turn on the LED
led.set_high().ok();
state = 0;
}
delay.delay_ms(100_u16);
}
}
loop {
continue;
}
}
Binary results
Optimized
Bloat for example flashing_delay
File .text Size Crate Name
0.2% 74.5% 328B flashing_delay flashing_delay::__cortex_m_rt_main
0.0% 14.5% 64B cortex_m_rt Reset
0.0% 1.4% 6B [Unknown] main
0.0% 0.5% 2B cortex_m_rt HardFault_
0.0% 0.5% 2B cortex_m_rt DefaultPreInit
0.0% 0.5% 2B cortex_m_rt DefaultHandler_
0.0% 0.0% 0B And 0 smaller methods. Use -n N to show more.
0.3% 100.0% 440B .text section size, the file size is 154.7KiB
Section sizes:
text data bss dec hex filename
632 0 4 636 27c flashing_delay
Unoptimized
Bloat for example flashing_delay
File .text Size Crate Name
0.5% 15.2% 1.4KiB stm32f0xx_hal stm32f0xx_hal::rcc::CFGR::freeze
0.2% 6.1% 592B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_clock
0.2% 5.0% 482B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_pll
0.1% 2.9% 280B stm32f0xx_hal stm32f0xx_hal::rcc::CFGR::freeze::{{closure}}
0.1% 2.8% 276B stm32f0xx_hal stm32f0xx_hal::gpio::gpioa::PA5<MODE>::into_push_pull_output
0.1% 2.1% 206B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_clock::{{closure}}
0.1% 2.1% 204B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_clock::{{closure}}
0.1% 1.9% 180B stm32f0xx_hal <stm32f0xx_hal::delay::Delay as embedded_hal::blocking::delay::DelayUs<u32>>::delay_us
0.1% 1.9% 180B stm32f0xx_hal stm32f0xx_hal::rcc::CFGR::freeze::{{closure}}
0.1% 1.7% 162B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_pll::{{closure}}
1.7% 57.8% 5.5KiB And 109 smaller methods. Use -n N to show more.
3.0% 100.0% 9.5KiB .text section size, the file size is 312.7KiB
Section size:
text data bss dec hex filename
11628 0 4 11632 2d70 flashing_delay
Pros
- Doesn't get any simpler than that
Cons
- Only useful to build simple reactive systems, no hard real-time possible
- Asynchronous handling is really tedious
Cortex-m-rt with interrupts and moved resources
So compared to our baseline, here we'll also:
- Explicitly set up the interrupt handling
- Define a
Mutex
protected resource for the LED so we can use the hardware safely from the interrupt handler - Define static resources in our interrupt handler which exclusively are accessible from there
- Move out the LED pin from the
Mutex
protected resource into our local static resource on first execution of the interrupt handler
Code
#![no_main]
#![no_std]
use panic_halt as _;
use stm32f0xx_hal::{gpio::*, prelude::*, stm32};
use cortex_m::{interrupt::Mutex, peripheral::syst::SystClkSource::Core, Peripherals};
use cortex_m_rt::{entry, exception};
use core::cell::RefCell;
// Mutex protected structure for our shared GPIO pin
static GPIO: Mutex<RefCell<Option<flashing::LEDPIN>>> = Mutex::new(RefCell::new(None));
#[entry]
fn main() -> ! {
if let (Some(mut p), Some(mut cp)) = (stm32::Peripherals::take(), Peripherals::take()) {
cortex_m::interrupt::free(move |cs| {
// Configure clock to 48 MHz (i.e. the maximum) and freeze it
let mut rcc = p.RCC.configure().sysclk(48.mhz()).freeze(&mut p.FLASH);
// Get access to individual pins in the GPIO port
let gpioa = p.GPIOA.split(&mut rcc);
// (Re-)configure the pin connected to our LED as output
let led = gpioa.pa5.into_push_pull_output(cs);
// Transfer GPIO into a shared structure
*GPIO.borrow(cs).borrow_mut() = Some(led);
// Set source for SysTick counter, here full operating frequency (== 48MHz)
cp.SYST.set_clock_source(Core);
// Set reload value, i.e. timer delay 48 MHz/4 Mcounts == 12Hz or 83ms
cp.SYST.set_reload(4_000_000 - 1);
// Start counting
cp.SYST.enable_counter();
// Enable interrupt generation
cp.SYST.enable_interrupt();
});
}
loop {
continue;
}
}
// Define an exception handler, i.e. function to call when exception occurs. Here, if our SysTick
// timer generates an exception the following handler will be called
#[exception]
fn SysTick() -> () {
// Our moved LED pin
static mut LED: Option<flashing::LEDPIN> = None;
// Exception handler state variable
static mut STATE: u8 = 0;
// If LED pin was moved into the exception handler, just use it
if let Some(led) = LED {
// Check state variable, keep LED off most of the time and turn it on every 10th tick
if *STATE < 10 {
// Turn off the LED
led.set_low().ok();
// And now increment state variable
*STATE += 1;
} else {
// Turn on the LED
led.set_high().ok();
// And set new state variable back to 0
*STATE = 0;
}
}
// Otherwise move it out of the Mutex protected shared region into our exception handler
else {
// Enter critical section
cortex_m::interrupt::free(|cs| {
// Move LED pin here, leaving a None in its place
LED.replace(GPIO.borrow(cs).replace(None).unwrap());
});
}
}
Binary results
Optimized
Bloat for example flashing_rt_move
File .text Size Crate Name
0.2% 52.9% 296B flashing_rt_move flashing_rt_move::__cortex_m_rt_main
0.1% 22.9% 128B [Unknown] SysTick
0.0% 11.4% 64B cortex_m_rt Reset
0.0% 1.1% 6B std core::result::unwrap_failed
0.0% 1.1% 6B std core::panicking::panic_fmt
0.0% 1.1% 6B std core::panicking::panic
0.0% 1.1% 6B [Unknown] main
0.0% 0.4% 2B cortex_m_rt HardFault_
0.0% 0.4% 2B panic_halt rust_begin_unwind
0.0% 0.4% 2B cortex_m_rt DefaultPreInit
0.0% 0.4% 2B cortex_m_rt DefaultHandler_
0.0% 0.0% 0B And 0 smaller methods. Use -n N to show more.
0.3% 100.0% 560B .text section size, the file size is 169.3KiB
Section sizes:
text data bss dec hex filename
796 0 12 808 328 flashing_rt_move
Unoptimized
Bloat for example flashing_rt_move
File .text Size Crate Name
0.4% 12.0% 1.4KiB stm32f0xx_hal stm32f0xx_hal::rcc::CFGR::freeze
0.3% 9.6% 1.1KiB std core::fmt::Formatter::pad
0.2% 4.8% 592B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_clock
0.1% 3.9% 482B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_pll
0.1% 2.3% 280B stm32f0xx_hal stm32f0xx_hal::rcc::CFGR::freeze::{{closure}}
0.1% 2.3% 276B stm32f0xx_hal stm32f0xx_hal::gpio::gpioa::PA5<MODE>::into_push_pull_output
0.1% 1.7% 210B std core::ptr::swap_nonoverlapping_bytes
0.1% 1.7% 206B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_clock::{{closure}}
0.1% 1.7% 204B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_clock::{{closure}}
0.0% 1.5% 180B stm32f0xx_hal stm32f0xx_hal::rcc::CFGR::freeze::{{closure}}
1.9% 58.0% 6.9KiB And 150 smaller methods. Use -n N to show more.
3.3% 100.0% 12.0KiB .text section size, the file size is 357.7KiB
Section size:
text data bss dec hex filename
14180 0 12 14192 3770 flashing_rt_move
Pros
- Easy to understand and straight-forward "text-book" syntax
Cons
- Requires use of
static
Mutex
which have annoyingly exact type specification requirements Mutex
require critical sections to access the values which block all interrupt processing
IRQ
This uses the irq crate from Jonas Schievink which takes a slightly different
approach than cortex-m-rt but still uses it under the hood to do the interrupt
handling. Instead of defining the interrupt handlers manually the irq
crate
provides a scoped_interrupts
macro to define the interrupts which are turned
into regular cortex-m-rt
interrupt handler functions. The real handlers are
defined as closures within the main program flow and parsed to a special scope
function while will register the real handler closure. As long as the scope
is
active any triggered interrupt handler internally created by scoped_interrupts
will call the registered closure to do the real processing.
Code
#![no_main]
#![no_std]
use panic_halt as _;
use stm32f0xx_hal::{gpio::*, prelude::*, stm32};
use cortex_m::{peripheral::syst::SystClkSource::Core, Peripherals};
use cortex_m_rt::{entry, exception};
use irq::{handler, scope, scoped_interrupts};
// Hook `SysTick` using the `#[exception]` attribute
scoped_interrupts! {
enum Exception {
SysTick,
}
use #[exception];
}
#[entry]
fn main() -> ! {
if let (Some(mut p), Some(mut cp)) = (stm32::Peripherals::take(), Peripherals::take()) {
let mut led = cortex_m::interrupt::free(move |cs| {
// Configure clock to 48 MHz (i.e. the maximum) and freeze it
let mut rcc = p.RCC.configure().sysclk(48.mhz()).freeze(&mut p.FLASH);
// Get access to individual pins in the GPIO port
let gpioa = p.GPIOA.split(&mut rcc);
// (Re-)configure the pin connected to our LED as output
let led = gpioa.pa5.into_push_pull_output(cs);
// Set source for SysTick counter, here full operating frequency (== 48MHz)
cp.SYST.set_clock_source(Core);
// Set reload value, i.e. timer delay 48 MHz/4 Mcounts == 12Hz or 83ms
cp.SYST.set_reload(4_000_000 - 1);
// Start counting
cp.SYST.enable_counter();
// Enable interrupt generation
cp.SYST.enable_interrupt();
led
});
// State variable
let mut state: u8 = 0;
handler!(
systick = || {
// Check state variable, keep LED off most of the time and turn it on every 10th tick
if state < 10 {
// Turn off the LED
led.set_low().ok();
// And now increment state variable
state += 1;
} else {
// Turn on the LED
led.set_high().ok();
// And set new state variable back to 0
state = 0;
}
}
);
// Create a scope and register the handlers
scope(|scope| {
scope.register(Exception::SysTick, systick);
loop {
continue;
}
});
}
loop {
continue;
}
}
Binary results
Optimized
Bloat for example flashing_irq
File .text Size Crate Name
0.2% 55.7% 312B flashing_irq flashing_irq::__cortex_m_rt_main
0.0% 11.4% 64B cortex_m_rt Reset
0.0% 7.1% 40B flashing_irq flashing_irq::__cortex_m_rt_main::{{closure}}
0.0% 7.1% 40B [Unknown] SysTick
0.0% 6.4% 36B std core::ops::function::FnOnce::call_once{{vtable.shim}}
0.0% 1.1% 6B std core::panicking::panic_fmt
0.0% 1.1% 6B std core::panicking::panic
0.0% 1.1% 6B [Unknown] main
0.0% 0.4% 2B cortex_m_rt HardFault_
0.0% 0.4% 2B panic_halt rust_begin_unwind
0.0% 0.4% 2B cortex_m_rt DefaultPreInit
0.0% 0.4% 2B cortex_m_rt DefaultHandler_
0.0% 0.4% 2B std core::ptr::real_drop_in_place
0.0% 0.0% 0B And 0 smaller methods. Use -n N to show more.
0.3% 100.0% 560B .text section size, the file size is 162.4KiB
Section sizes:
text data bss dec hex filename
808 0 12 820 334 flashing_irq
Unoptimized
Bloat for example flashing_irq
File .text Size Crate Name
0.4% 14.7% 1.4KiB stm32f0xx_hal stm32f0xx_hal::rcc::CFGR::freeze
0.2% 5.9% 592B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_clock
0.1% 4.8% 482B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_pll
0.1% 2.8% 280B stm32f0xx_hal stm32f0xx_hal::rcc::CFGR::freeze::{{closure}}
0.1% 2.8% 276B stm32f0xx_hal stm32f0xx_hal::gpio::gpioa::PA5<MODE>::into_push_pull_output
0.1% 2.1% 206B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_clock::{{closure}}
0.1% 2.0% 204B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_clock::{{closure}}
0.1% 1.8% 180B stm32f0xx_hal stm32f0xx_hal::rcc::CFGR::freeze::{{closure}}
0.0% 1.6% 162B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_pll::{{closure}}
0.0% 1.5% 148B stm32f0xx_hal stm32f0xx_hal::rcc::CFGR::freeze::{{closure}}
1.8% 59.3% 5.8KiB And 121 smaller methods. Use -n N to show more.
3.0% 100.0% 9.8KiB .text section size, the file size is 321.0KiB
Section size:
text data bss dec hex filename
12148 0 8 12156 2f7c flashing_irq
Pros
- All data is in the same single scope so rustc can do its work properly
- No overhead for resource transfer and local storage
- Can change interrupt handlers on the fly
Cons
- Overhead for the handler indirection
cortex-m-rtfm
cortex-m-rtfm is a very clever toolbox combining a macro based domain specific language annotation with static analysis to provide ideal results with calculable worst time interrupt execution times and lock-free operation.
Code
#![no_main]
#![no_std]
use panic_halt as _;
use stm32f0xx_hal::{gpio::*, prelude::*, stm32};
use cortex_m::peripheral::syst::SystClkSource::Core;
use rtfm::app;
#[app(device = crate::stm32, peripherals = true)]
const APP: () = {
// Late resources
struct Resources {
led: flashing::LEDPIN,
}
#[init]
fn init(cx: init::Context) -> init::LateResources {
// Cortex-M peripherals
let mut core = cx.core;
// Device specific peripherals
let mut device = cx.device;
// Configure clock to 48 MHz (i.e. the maximum) and freeze it
let mut rcc = device
.RCC
.configure()
.sysclk(48.mhz())
.freeze(&mut device.FLASH);
// Get access to individual pins in the GPIO port
let gpioa = device.GPIOA.split(&mut rcc);
let led = cortex_m::interrupt::free(move |cs| {
// (Re-)configure the pin connected to our LED as output
gpioa.pa5.into_push_pull_output(cs)
});
// Set source for SysTick counter, here full operating frequency (== 48MHz)
core.SYST.set_clock_source(Core);
// Set reload value, i.e. timer delay 48 MHz/4 Mcounts == 12Hz or 83ms
core.SYST.set_reload(4_000_000 - 1);
// Start counting
core.SYST.enable_counter();
// Enable interrupt generation
core.SYST.enable_interrupt();
init::LateResources { led }
}
// Define an exception handler, i.e. function to call when exception occurs. Here, if our SysTick
// timer generates an exception the following handler will be called
#[task(binds = SysTick, priority = 1, resources = [led])]
fn systick(c: systick::Context) {
// Exception handler state variable
static mut STATE: u8 = 0;
// If LED pin was moved into the exception handler, just use it
// Check state variable, keep LED off most of the time and turn it on every 10th tick
if *STATE < 10 {
// Turn off the LED
c.resources.led.set_low().ok();
// And now increment state variable
*STATE += 1;
} else {
// Turn on the LED
c.resources.led.set_high().ok();
// And set new state variable back to 0
*STATE = 0;
}
}
};
Binary results
Optimized
Bloat for example flashing_rtfm
File .text Size Crate Name
0.1% 61.5% 236B [Unknown] main
0.0% 16.7% 64B cortex_m_rt Reset
0.0% 10.4% 40B [Unknown] SysTick
0.0% 0.5% 2B cortex_m_rt HardFault_
0.0% 0.5% 2B cortex_m_rt DefaultPreInit
0.0% 0.5% 2B cortex_m_rt DefaultHandler_
0.0% 0.0% 0B And 0 smaller methods. Use -n N to show more.
0.2% 100.0% 384B .text section size, the file size is 154.8KiB
Section sizes:
text data bss dec hex filename
576 0 4 580 244 flashing_rtfm
Unoptimized
Bloat for example flashing_rtfm
File .text Size Crate Name
0.4% 12.8% 1.4KiB stm32f0xx_hal stm32f0xx_hal::rcc::CFGR::freeze
0.3% 8.9% 1018B std core::fmt::Formatter::pad_integral
0.2% 5.2% 592B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_clock
0.1% 4.2% 482B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_pll
0.1% 2.8% 320B std core::fmt::num::imp::fmt_u32
0.1% 2.4% 280B stm32f0xx_hal stm32f0xx_hal::rcc::CFGR::freeze::{{closure}}
0.1% 2.4% 276B stm32f0xx_hal stm32f0xx_hal::gpio::gpioa::PA5<MODE>::into_push_pull_output
0.1% 1.8% 206B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_clock::{{closure}}
0.1% 1.8% 204B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_clock::{{closure}}
0.1% 1.8% 204B cortex_m cortex_m::peripheral::scb::<impl cortex_m::peripheral::SCB>::set_priority::{{closure}}
1.9% 55.5% 6.2KiB And 117 smaller methods. Use -n N to show more.
3.4% 100.0% 11.2KiB .text section size, the file size is 331.1KiB
Section size:
text data bss dec hex filename
14100 0 4 14104 3718 flashing_rtfm
Pros
- Look ma! No locks!
- Extremely powerful possibilities, especially when different priorities are used
- Great compile time checks and guarantees
- Incredibly efficient binary code
Cons
- Custom syntax feels the least idiomatic of all approaches
- Additional implementation complexity even if the full functionality is not required
cmim (Added 2020-03-06)
cmim is crate which aims to simplify the move of resources into an interrupt
handler in order to avoid having to use a critical section to gain access to a
resource protected by a Mutex
. Unlike a Mutex
it keeps internal track of
the state of protected resource so it can handle accesses to it accordingly.
Code
#![no_main]
#![no_std]
use panic_halt as _;
use stm32f0xx_hal::{gpio::*, prelude::*, stm32};
use cortex_m::{peripheral::syst::SystClkSource::Core, Peripherals};
use cortex_m_rt::{entry, exception};
use cmim::{
Move,
Context,
Exception,
};
// Mutex protected structure for our shared GPIO pin
static GPIO: Move<flashing::LEDPIN, stm32::Interrupt> = Move::new_uninitialized(Context::Exception(Exception::SysTick));
#[entry]
fn main() -> ! {
if let (Some(mut p), Some(mut cp)) = (stm32::Peripherals::take(), Peripherals::take()) {
cortex_m::interrupt::free(move |cs| {
// Configure clock to 48 MHz (i.e. the maximum) and freeze it
let mut rcc = p.RCC.configure().sysclk(48.mhz()).freeze(&mut p.FLASH);
// Get access to individual pins in the GPIO port
let gpioa = p.GPIOA.split(&mut rcc);
// (Re-)configure the pin connected to our LED as output
let led = gpioa.pa5.into_push_pull_output(cs);
// Transfer GPIO into a shared structure
GPIO.try_move(led).ok();
// Set source for SysTick counter, here full operating frequency (== 48MHz)
cp.SYST.set_clock_source(Core);
// Set reload value, i.e. timer delay 48 MHz/4 Mcounts == 12Hz or 83ms
cp.SYST.set_reload(4_000_000 - 1);
// Start counting
cp.SYST.enable_counter();
// Enable interrupt generation
cp.SYST.enable_interrupt();
});
}
loop {
continue;
}
}
// Define an exception handler, i.e. function to call when exception occurs. Here, if our SysTick
// timer generates an exception the following handler will be called
#[exception]
fn SysTick() -> () {
// Exception handler state variable
static mut STATE: u8 = 0;
GPIO.try_lock(|led| {
// Check state variable, keep LED off most of the time and turn it on every 10th tick
if *STATE < 10 {
// Turn off the LED
led.set_low().ok();
// And now increment state variable
*STATE += 1;
} else {
// Turn on the LED
led.set_high().ok();
// And set new state variable back to 0
*STATE = 0;
}
}).ok();
}
Binary results
Optimized
Bloat for example flashing_cmim
File .text Size Crate Name
0.2% 61.2% 328B flashing_cmim flashing_cmim::__cortex_m_rt_main
0.1% 17.9% 96B [Unknown] SysTick
0.0% 11.9% 64B cortex_m_rt Reset
0.0% 1.1% 6B [Unknown] main
0.0% 0.4% 2B cortex_m_rt HardFault_
0.0% 0.4% 2B cortex_m_rt DefaultPreInit
0.0% 0.4% 2B cortex_m_rt DefaultHandler_
0.0% 0.0% 0B And 0 smaller methods. Use -n N to show more.
0.3% 100.0% 536B .text section size, the file size is 163.0KiB
Section sizes:
text data bss dec hex filename
728 0 4 732 2dc flashing_cmim
Unoptimized
Bloat for example flashing_cmim
File .text Size Crate Name
0.4% 12.6% 1.4KiB stm32f0xx_hal stm32f0xx_hal::rcc::CFGR::freeze
0.2% 5.1% 592B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_clock
0.1% 4.1% 482B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_pll
0.1% 2.7% 316B stm32f0? <stm32f0::stm32f0x2::Interrupt as bare_metal::Nr>::nr
0.1% 2.4% 280B stm32f0xx_hal stm32f0xx_hal::rcc::CFGR::freeze::{{closure}}
0.1% 2.4% 276B stm32f0xx_hal stm32f0xx_hal::gpio::gpioa::PA5<MODE>::into_push_pull_output
0.1% 2.0% 236B cmim cmim::Move<T,I>::try_lock
0.1% 1.8% 216B cortex_m cortex_m::peripheral::scb::<impl cortex_m::peripheral::SCB>::vect_active
0.1% 1.8% 210B std core::ptr::swap_nonoverlapping_bytes
0.1% 1.8% 206B stm32f0xx_hal stm32f0xx_hal::rcc::inner::enable_clock::{{closure}}
2.1% 63.0% 7.2KiB And 136 smaller methods. Use -n N to show more.
3.3% 100.0% 11.4KiB .text section size, the file size is 345.6KiB
Section size:
text data bss dec hex filename
13816 4 4 13824 3600 flashing_cmim
Pros
- No panic code in binary
- Easy and straight forward to use
Cons
- It's not quite obvious what happens behind the scenes and under which conditions this would fall apart
Epilog
Exploring the various options to use interrupts was quite a fun ride and also quite enlightening. I've spent a bit of time to look under the hood of the various approaches in order to understand what they're doing and whether there might be potential to combine approaches or improve on them for future work.
As can be seen each of the approaches comes with a their own set of drawbacks
and it would be great if there was a way to combine them in a way that combines
the best of all worlds: The straight-forwardness of cortex-m
with the
simplicity of irq
but the power, efficiency and compile time guarantees of
cortex-m-rtfm
.
Also it would be great to have an approach which covers more architectures than ARM Cortex-M. However to cover all of Cortex-M I specifically opted to use a Cortex-M0 which lacks e.g. compare-and-swap options and thus resembles the lowest possible denominator for this particular vendor.
You can find all the code and tools used to conduct the tests in my repository at interrupt-comparison. Feel free to experiment and let me know your experiences. Also let me know if you have another approach you feel should be covered here.
I was trying to include cmim in this comparison but I failed to get the
SysTick
exception to work and failed. If this gets addressed I might update
this blog post with another implementation.
Update 2020-03-06: cmim is now included
Thanks for sticking with me for this long and I hope to see you another time around.