In the previous post, we created a state machine and implemented it in Rust as a combination of interrupt handlers and structs.
In this post, I want to cover converting over to RTIC, which is a sort of lightweight real-time OS/framework that focuses on making it easier to handle concurrency on cortex-m
devices. The main thing RTIC is going to give us is better abstractions so that we can focus more on the “business logic” of our device.
I’ll also touch on some refactoring to improve our interrupt usage and Timer
handling.
RTIC
Before moving up an abstraction level, I like making sure I understand things well enough at the previous level so I can generally understand what the next level is giving us and how it does so. Now that we’ve seen the basic abstractions provided to us by the cortex-m
crate, it’s a good time to move “up” to RTIC.
Migrating over was a very straightforward process since the way we structured everything maps really well to RTIC’s abstractions. By the way, there’s a lot more details available in the RTIC book so I won’t go into everything here.
The main improvement we’ll see is RTIC managing the tedious static Mutex<RefCell<Option<T>>>
s for us as well as avoiding both the noisy critical sections and the borrow
, as_ref
, unwrap
chains necessary to use these shared values.
The idea behind RTIC is that we have a set of Resources
that are initialized and then shared between Tasks
. We declare all of these things inside an item with the [#app]
attribute (a const
module-level item, for reasons, but just imagine a mod
or crate-level attribute). We also provide the PAC crate we’re using and ask for access to the device-specific peripherals.
⮟ src/main.rs
#[app(device = stm32l0::stm32l0x2, peripherals = true)]
const APP: () = {
We can convert our shared resources:
⮟ src/main.rs
static STATUS: Mutex<RefCell<Option<status::StatusLights<PBOut, PBOut, PBOut>>>> =
Mutex::new(RefCell::new(None));
static BUTTON: Mutex<RefCell<Option<gpiob::PB2<Input<PullUp>>>>> = Mutex::new(RefCell::new(None));
static MORSE: Mutex<RefCell<Option<machine::MorseMachine>>> = Mutex::new(RefCell::new(None));
static TIMER: Mutex<RefCell<Option<Timer<TIM2>>>> = Mutex::new(RefCell::new(None));
into the following Resources
struct:
⮟ src/main.rs
struct Resources {
: gpiob::PB2<Input<PullUp>>,
button: status::StatusLights<PBOut, PBOut, PBOut>,
status: Timer<TIM2>,
timer#[init(machine::MorseMachine::new(DOT_TICKS))]
: machine::MorseMachine,
morse}
#[init(...)]
allows us to statically initialize a variable, which we can do with const fn machine::MorseMachine::new
(I updated new
in between this and the last post to be const
, it could now also be used directly Mutex<RefCell<T>>
to avoid the Option
).
init
The init
function is where we initialize whatever parts of our Resources
weren’t statically initialized. In our case, it will be identical to our main
function except we have direct access to the core
peripherals and the device
peripherals. Finally, we return init::LateResources
which is made up of exactly the values needed to fill in our Resources
struct. The magic of code generation!
⮟ src/main.rs
#[init]
fn init(init::Context { core, device }: init::Context) -> init::LateResources {
// Configure the clock at the default speed
let mut rcc = device.RCC.freeze(hal::rcc::Config::default());
⮟ src/main.rs
init::LateResources {
,
button,
status: device.TIM2.timer(TICK_LENGTH, &mut rcc),
timer}
task
We mark each interrupt handler as a task
and tell RTIC
which resources we need:
⮟ src/main.rs
#[task(binds = TIM2, resources = [status, timer, morse])]
fn timer(
Our function is then called with those resources passed in:
⮟ src/main.rs
fn timer(
timer::Context {
:
resourcestimer::Resources {
,
status,
timer,
morse..
},
}: timer::Context,
{ )
From then on our code remains the same except we have direct access to the resources and don’t have to go through Mutex
es and RefCell
s (there are cases where we need to use a lock
to get access to the resource, but RTIC
will enforce this if and only if necessary).
⮟ src/main.rs
fn TIM2() {
static mut FLASH: Option<u8> = None;
const FLASH_TICKS: u8 = 2;
cortex_m::interrupt::free(|cs| {
let mut mm = MORSE.borrow(cs).borrow_mut();
let mut status = STATUS.borrow(cs).borrow_mut();
let mut timer = TIMER.borrow(cs).borrow_mut();
.as_mut().unwrap().clear_irq();
timerif let Some(state_change) = mm.as_mut().unwrap().tick() {
⮟ src/main.rs
static mut FLASH: Option<u8> = None;
.clear_irq();
timerif let Some(state_change) = morse.tick() {
match state_change {
machine::Transition::Long => status.as_mut().unwrap().on_long(),
machine::Transition::VeryLong => status.as_mut().unwrap().busy(),
machine::Transition::Long => status.on_long(),
machine::Transition::VeryLong => status.busy(),
machine::Transition::Transmit => {
// send letters
Our button
handler is improved similarly.
Fewer interrupts
In the previous post we implemented our morse code machine by counting timer “ticks”, to avoid having to deal with setting different timers. The downside of this is that we run an interrupt every tick and most of the time nothing happens.
Timeless morse machine
Instead of counting, we can set up our Timer
to countdown to exactly the time we need, depending on our current state, so that we only tick once per transition. I.e. a tick moves us directly from short to long, long to very long, or waiting to idle.
The new MorseTimelessMachine
now no longer uses Button
to count but instead uses State
directly. We also wrap the new machine with a MorseTimingMachine
that handles manipulating our timer peripheral and dealing with interrupts for us.
⮟ src/machine.rs
impl PressType {
pub fn tick(&self) -> Self {
match self {
Self::Short => Self::Long,
Self::Long => Self::VeryLong,
Self::VeryLong => Self::VeryLong,
}
}
⮟ src/machine.rs
impl State {
fn release(&mut self) -> Self {
Self::WaitingOnPress
}
fn press(&mut self) -> Self {
Self::Press(PressType::Short)
}
fn tick(&self) -> Self {
match self {
Self::WaitingOnPress => Self::Idle,
Self::Press(p) => Self::Press(p.tick()),
Self::Idle => Self::Idle,
}
}
In MorseTimelessMachine
, use State::tick
in combination with the current
MorseCode
:
⮟ src/machine.rs
fn next_state(&self) -> State {
match (self.current.is_empty(), self.state) {
false, State::Press(PressType::Long)) => self.state,
(, s) => s.tick(),
(_}
}
Finally, we move all Timer
handling into MorseTimingMachine
, which is the struct
we expose for main.rs
:
⮟ src/machine.rs
impl MorseTimingMachine {
pub const fn new(dot_length: MicroSeconds) -> Self {
Self {
: dot_length,
long_press: MicroSeconds(2 * dot_length.0),
very_long_press: MicroSeconds(3 * dot_length.0),
timeout: MorseTimelessMachine::new(),
machine}
}
pub fn press(&mut self, timer: &mut Timer<TIM2>) {
self.machine.press();
.clear_irq();
timer.start(self.long_press);
timer.listen();
timer}
pub fn release(&mut self, timer: &mut Timer<TIM2>) {
self.machine.release();
.clear_irq();
timerlet timeout = if self.machine.current == morse::TRANSMIT {
1.ms()
} else {
self.timeout
};
.start(timeout);
timer.listen();
timer}
pub fn tick(&mut self, timer: &mut Timer<TIM2>) -> Option<Transition> {
.clear_irq();
timerlet transition = self.machine.tick();
if let Some(ref state_change) = transition {
match state_change {
Transition::Long => timer.start(self.very_long_press),
Transition::VeryLong | Transition::Transmit | Transition::Character(_) => {
.unlisten()
timer}
}
}
transition}
}
The upside of the Timeless
/Timing
distinction is that testing is significantly easier and as a state machine, Timeless
is much more similar to the abstract machine from last post.
⮟ src/machine.rs
#[test]
fn test_morse_multiple() {
let mut machine = MorseTimelessMachine::new();
.press().release().press().tick();
machineassert_eq!(
.release().tick(),
machineSome(Transition::Character('a' as u8))
;
)}
The less testable Timer
handling is left up to MorseTimingMachine
.
Flashing lights
Instead of having a static mut
local to our timer
task, we can put the flash functionality into StatusLights
. Similar to the new Machine
, we let the Flasher
handle our Timer
.
⮟ src/status.rs
self.green.set_low().unwrap();
self.blue.set_low().unwrap();
}
pub fn flash_busy(&mut self, timer: &mut Timer<TIM2>) {
self.busy();
self.flasher.flash(timer);
}
pub fn flash_tick(&mut self, timer: &mut Timer<TIM2>) {
if self.flasher.tick(timer) {
self.off();
}
}
}
struct Flasher {
: bool,
flashing}
impl Flasher {
const fn new() -> Self {
{ flashing: false }
Flasher }
fn flash(&mut self, timer: &mut Timer<TIM2>) {
self.flashing = true;
.start(10.ms());
timer.listen();
timer}
fn tick(&mut self, timer: &mut Timer<TIM2>) -> bool {
let turn_off = self.flashing;
if self.flashing {
.unlisten();
timerself.flashing = false;
}
turn_off}
}
Improvements
The tasks in src/main.rs
are now significantly easier to read, due to making our various state machines much more explicit. We can immediately see each transition as well as what triggers them just from looking at the interrupt handlers/tasks.
⮟ src/main.rs
// send letters
}
machine::Transition::Character(ch) => {
*FLASH = Some(FLASH_TICKS);
.busy();
status.flash_busy(timer);
status// handle letter
}
}
} else if let Some(flash_count) = *FLASH {
if flash_count == 0 {
*FLASH = None;
.unlisten();
timer.off();
status} else {
*FLASH = Some(flash_count - 1);
}
} else {
.flash_tick(timer);
status}
}
⮟ src/main.rs
{
) Exti::unpend(GpioLine::from_raw_line(button.pin_number()).unwrap());
if button.is_low().unwrap() {
.press();
morse.listen();
timer.press(timer);
morse.on_short();
status} else {
.release();
morse.release(timer);
morse.off();
status}
}
This post was focused on refactoring in embedded Rust. Hopefully it’s interesting to see an example of using RTIC for a real device as well as how to best design interactions between state machines and hardware.
The next post will (really) cover keeping a stack of inputted characters and handling the TRANSMIT
value to broadcast over the radio.