In the previous post, we worked on getting the display powered up and displaying a splash on startup.
Today we’ll start translating into morse code using timers and state machines. We talked a little about timers in the last post where they were an implementation detail of Delay
. Today we’ll use them again, except this time we’ll control them directly.
State machines
Devices that interact with the real world can often be modelled through an abstraction called a state machine. We can usually think of devices as being in a state at any specific moment. They remain in this state until some event occurs, like a button press or a change in input signal, at which point they transition to another state. The behavior of our device can be expressed as a set of states, each of which has a set of possible outgoing transitions to some state. A specific “use” of our device can be modeled as a sequence of transitions.
Let’s model the behavior of our device this way:
Enumerating the components of our state and our transitions like this is a great way to make sure we’ve covered everything, including edge cases.
The idea is that as we press and hold the button, we move from a short press to a long press. When we release, we push a new dot/dash onto the list of dots/dashes and when we haven’t held the button in a longer time, the current sequence of dots/dashes (𝑣) is transformed into a letter which is appended to the sequence of letters. When we hold a very long press, we transmit the sequence of letters (𝑤), but only if we don’t have any dots/dashes yet.
The state of our device can be described by the LED state, the sequence of letters (𝑤) as well as the current accumulated sequence of dots/dashes (𝑣).
The transitions in our case are time passing and presses/releases of the button.
Creating a MorseMachine
We already have a type that holds our status lights. Let’s create a new struct
that we’ll use in src/main.rs
to hold and manipulate the state of the current morse code, i.e. the sequence of dots/dashes.
⮟ src/machine.rs
pub struct MorseMachine {}
pub enum Transition {}
impl MorseMachine {
fn new() -> Self;
fn press(&mut self);
fn release(&mut self);
fn tick(&mut self) -> Option<Transition>;
}
The idea will be to tell the struct when an external event occurs. We’ll call tick
every time our timer interrupt fires and call press
/release
when our GPIO interrupt fires.
Calling tick
can cause more than one kind of transition which we’re going to handle in main
so let’s have it return a Transition
. Notice press
always leads to the “short press” state and although release
modifies the current sequence, i.e. the internal state of MorseMachine
, it always leads to a “waiting” state.
Looking again at our state machine we can see that a tick
of the clock can lead to the following state transitions:
⮟ src/machine.rs
pub struct MorseMachine {}
#[derive(PartialEq, Eq)]
pub enum Transition {
,
Long,
VeryLongu8),
Character(,
Transmit}
pub enum Transition {}
pub struct MorseMachine {}
impl MorseMachine {
fn new() -> Self;
Hooking interrupts to MorseMachine
We’ll put a MorseMachine
into a Mutex<RefCell<_>>
to share between our interrupts:
⮟ 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));
#[entry]
fn main() -> ! {
⮟ src/main.rs
cortex_m::interrupt::free(|cs| {
*STATUS.borrow(cs).borrow_mut() = Some(status);
*BUTTON.borrow(cs).borrow_mut() = Some(button);
*MORSE.borrow(cs).borrow_mut() = Some(machine::MorseMachine::new());
});
unsafe {
Let’s hook it up to the EXTI2_3
interrupt handler and get rid of our dummy code from the last chapter:
⮟ src/main.rs
.map(|p| (p.pin_number(), p.is_low().unwrap()))
.unwrap();
Exti::unpend(GpioLine::from_raw_line(pin_number).unwrap());
let mut mm = MORSE.borrow(cs).borrow_mut();
let mut status = STATUS.borrow(cs).borrow_mut();
if is_low {
.as_mut().unwrap().off();
status.as_mut().unwrap().press();
mm} else {
.as_mut().unwrap().on_short();
status.as_mut().unwrap().release();
mm}
})
}
Our microcontroller reference describes a general-purpose timer peripheral called TIM2
whose interrupt handler we can use to call tick
:
⮟ src/main.rs
loop {}
}
#[allow(non_snake_case)]
#[interrupt]
fn TIM2() {
cortex_m::interrupt::free(|cs| {
let mut mm = MORSE.borrow(cs).borrow_mut();
let mut status = STATUS.borrow(cs).borrow_mut();
if let Some(state_change) = mm.as_mut().unwrap().tick() {
// handle state_change
}
})
}
#[allow(non_snake_case)]
#[interrupt]
fn EXTI2_3() {
Our HAL crate provides a Timer
struct to encapsulate the timer which we’ll access through a Mutex<RefCell<_>>
.
⮟ src/main.rs
,
delayexti::{Exti, ExtiLine, GpioLine, TriggerEdge},
gpio::*,
pac::{self, interrupt},
pac::{self, interrupt, TIM2},
prelude::*,
,
syscfgtimer::Timer,
};
use core::cell::RefCell;
use cortex_m::interrupt::Mutex;
⮟ src/main.rs
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));
#[entry]
fn main() -> ! {
Let’s now hook up the state changes from press
/release
as well as tick
to our status LEDs and timer. We start the timer with listen
whenever we get a button press.
⮟ src/main.rs
Exti::unpend(GpioLine::from_raw_line(pin_number).unwrap());
let mut mm = MORSE.borrow(cs).borrow_mut();
let mut status = STATUS.borrow(cs).borrow_mut();
let mut timer = TIMER.borrow(cs).borrow_mut();
if is_low {
.as_mut().unwrap().press();
mm.as_mut().unwrap().listen();
timer.as_mut().unwrap().on_short();
status} else {
.as_mut().unwrap().release();
mm.as_mut().unwrap().off();
status}
})
}
We always clear the interrupt request with clear_irq
and deactivate the timer with unlisten
when we get a new letter in tick
:
⮟ src/main.rs
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() {
// handle state_change
match state_change {
machine::Transition::Long => status.as_mut().unwrap().on_long(),
machine::Transition::VeryLong => status.as_mut().unwrap().busy(),
machine::Transition::Transmit => {
// send letters
},
machine::Transition::Character(ch) => {
.as_mut().unwrap().unlisten();
timer// handle letter
}
}
}
})
}
Flash on timeout
One UX optimization we can make is to flash our LED when the timeout is triggered, so the user knows they can start the next letter. It’s not really a part of our morse code state machine so we’ll track it in the TIM2
interrupt handler. The LED turns on for a few ticks then off again and unlisten
ing the timer instead of directly at timeout:
⮟ src/main.rs
#[allow(non_snake_case)]
#[interrupt]
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();
⮟ src/main.rs
// send letters
},
machine::Transition::Character(ch) => {
.as_mut().unwrap().unlisten();
timer*FLASH = Some(FLASH_TICKS);
.as_mut().unwrap().busy();
status// handle letter
}
}
} else if let Some(flash_count) = *FLASH {
if flash_count == 0 {
*FLASH = None;
.as_mut().unwrap().unlisten();
timer.as_mut().unwrap().off();
status} else {
*FLASH = Some(flash_count - 1);
}
}
})
}
Finishing up the machine
Let’s implement the state tracking and transitions in MorseMachine
.
Button state
We’re going to maintain the state of our button inside the MorseMachine
as well, so let’s create an enum
with the different possible states:
⮟ src/machine.rs
#[derive(Clone, Copy, PartialEq, Eq)]
enum PressType {
,
Short,
Long,
VeryLong}
#[derive(PartialEq, Eq)]
enum State {
,
Press(PressType),
WaitingOnPress,
Idle}
Internally, we really only need a count of how many ticks have passed as well as whether the button is being held. We offer a state
method to return a State
as well as tick
to advance the tick count.
⮟ src/machine.rs
struct Button {
: u32,
long_press: u32,
very_long_press: u32,
timeout: u32,
count: bool,
pressed}
impl Button {
fn new(long_press: u32, very_long_press: u32, timeout: u32) -> Self {
Self {
,
long_press,
very_long_press,
timeout: 0,
count: false,
pressed}
}
fn release(&mut self) {
self.count = 0;
self.pressed = false;
}
fn press(&mut self) {
self.count = 0;
self.pressed = true;
}
fn state(&self) -> State {
if !self.pressed {
if self.count > self.timeout {
State::Idle
} else {
State::WaitingOnPress
}
} else {
State::Press(if self.count > self.very_long_press {
PressType::VeryLong
} else if self.count > self.long_press {
PressType::Long
} else {
PressType::Short
})
}
}
fn tick(&mut self) {
self.count += 1;
}
}
#[derive(PartialEq, Eq)]
pub enum Transition {
, Long
Standard morse code uses a 1:3 ratio of dot:letter-spacing duration so we set that in new
and leave the transmit press duration the same as the letter-spacing:
⮟ src/machine.rs
,
Transmit}
pub struct MorseMachine {}
pub struct MorseMachine {
: Button,
button}
impl MorseMachine {
fn new() -> Self;
fn press(&mut self);
fn release(&mut self);
fn tick(&mut self) -> Option<Transition>;
pub fn new(dot_ticks: u32) -> Self {
Self {
: Button::new(dot_ticks, 3 * dot_ticks, 3 * dot_ticks),
button}
}
pub fn press(&mut self) {
self.button.press();
}
pub fn release(&mut self) {
self.button.release();
}
pub fn tick(&mut self) -> Option<Transition> {
self.button.tick();
None
}
}
press
Looking again at our state machine we can see that press
is complete because a press always leads to a short press state and doesn’t affect our morse code.
release
When the button is released, our ouput state definitely depends on whether we were holding a short, long or very long press and we update the current morse code. Let’s sketch out an interface for a morse code value, i.e. a dot/dash accumulator. I’m going to leave the implementation out for now but we’ll briefly go over it at the end of this post.
⮟ src/morse.rs
#[derive(PartialEq)]
pub struct MorseCode {}
pub const TRANSMIT: MorseCode;
impl MorseCode {
pub fn empty() -> Self;
pub fn is_empty(&self) -> bool;
pub fn append_dot(&mut self) -> Self;
pub fn append_dash(&mut self) -> Self;
}
Import to note here is that we’ve expanded the alphabet with a sentinel value to mean “ready to transmit”.
In fact, our Button::state
function from earlier wasn’t correct. We only want to enter a very long press if we haven’t started accumulating dots and dashes, so let’s fix that up:
⮟ src/machine.rs
self.pressed = true;
}
fn state(&self) -> State {
fn state(&self, code_empty: bool) -> State {
if !self.pressed {
if self.count > self.timeout {
State::Idle
⮟ src/machine.rs
State::WaitingOnPress
}
} else {
State::Press(if self.count > self.very_long_press {
State::Press(if self.count > self.very_long_press && code_empty {
PressType::VeryLong
} else if self.count > self.long_press {
PressType::Long
We’ll track a MorseCode
in our MorseMachine
:
⮟ src/machine.rs
pub struct MorseMachine {
: Button,
button: morse::MorseCode,
current}
impl MorseMachine {
pub fn new(dot_ticks: u32) -> Self {
Self {
: Button::new(dot_ticks, 3 * dot_ticks, 3 * dot_ticks),
button: morse::MorseCode::empty(),
current}
}
Now we can complete release
and update our MorseCode
according to our State
:
⮟ src/machine.rs
}
pub fn release(&mut self) {
if let State::Press(p) = self.button.state(self.current.is_empty()) {
self.current = match p {
PressType::Short => self.current.append_dot(),
PressType::Long => self.current.append_dash(),
PressType::VeryLong => morse::TRANSMIT,
}
}
self.button.release();
if self.current == morse::TRANSMIT {
self.button.timeout();
}
}
pub fn tick(&mut self) -> Option<Transition> {
We make a little optimization to immediately timeout our button when the user enters a TRANSMIT
character.
⮟ src/machine.rs
fn timeout(&mut self) {
self.count = self.timeout;
}
tick
For tick
we’re going to have to come up with an Option<Transition>
. The idea is to tick our button
and see if there’s been a change in State
, these are all the transitions which are triggered by time passing.
The interesting part is the “waiting on dot/dash timeout” transition where we lookup our accumulated dots/dashes into a u8
(an ASCII character).
⮟ src/machine.rs
}
pub fn tick(&mut self) -> Option<Transition> {
let is_empty = self.current.is_empty();
let previous_state = self.button.state(is_empty);
self.button.tick();
None
let current_state = self.button.state(is_empty);
match (previous_state, current_state) {
, n) if p == n => None,
(p, State::Idle) => {
(_let character = self.current;
self.current = morse::MorseCode::empty();
Some(if character == morse::TRANSMIT {
Transition::Transmit
} else {
Transition::Character(character.lookup())
})
}
, State::Press(PressType::VeryLong)) => Some(Transition::VeryLong),
(_, State::Press(PressType::Long)) => Some(Transition::Long),
(_=> None,
_ }
}
}
MorseMachine
initialization
Finally we can setup MorseMachine
from main
and pick a tick of 10ms with a dot to dash transition of 20*10ms, which feels pretty comfortable.
⮟ src/main.rs
let line = GpioLine::from_raw_line(button.pin_number()).unwrap();
.listen_gpio(&mut syscfg, button.port(), line, TriggerEdge::Both);
exti
let timer = dp.TIM2.timer(10.ms(), &mut rcc);
cortex_m::interrupt::free(|cs| {
*STATUS.borrow(cs).borrow_mut() = Some(status);
*BUTTON.borrow(cs).borrow_mut() = Some(button);
*MORSE.borrow(cs).borrow_mut() = Some(machine::MorseMachine::new());
*TIMER.borrow(cs).borrow_mut() = Some(timer);
*MORSE.borrow(cs).borrow_mut() = Some(machine::MorseMachine::new(20));
});
unsafe {
NVIC::unmask(line.interrupt());
NVIC::unmask(Interrupt::TIM2);
}
loop {}
That concludes everything we need to start speaking morse code, check it out!
Hopefully this third post served as an introductory but not completely trivial example of translating the behavior of an embedded device into a state machine and then into code.
The next post covers moving over to RTIC and some refactors.
MorseCode
implementation
There are multiple ways to implement the accumulation and lookup of morse code. I chose to do it by essentially encoding a binary tree of possible morse code values into an array and keeping a u8
pointer into the tree, moving left or right down the branches as new dots/dashes come in. Check it out on Github.
Edits
- Improved
machine::Transition
variants.