Rust embedded. Stopwatch.

Hello everyone. This is my new article about Rust for embedded systems. I decided to start writing articles in English to attract a large audience (I apologize to the Russian audience). This article is about creating a stopwatch using Rust and the embedded-graphics library. Today I’ve made for you an example of creating a digital and analog stopwatch. For this example, you need stm32f429i-disco (development kit) and ssd1306 (SPI). This article is a spin-off of the stopwatch from the second article on Rust for Embedded Systems. I will briefly describe the work of our stopwatch:

  1. You need to connect ssd1306 to the stm32f429i-disco board. You should follow the following pinout. We’ll use SPI4.
    1. Pin SCK (ssd1306) – PE2
    2. Pin MISO (ssd1306) – PE5
    3. Pin MOSI (ssd1306) – PE6
    4. Pin CS (ssd1306) – PE4
    5. Pin DC (ssd1306) – PE3
  2. There are two LEDs LD3 (PG13) and LD4 (PG14) on this development board. The LD3 LED is on when the stopwatch is ready for use. The LD 3 LED blinks when the stopwatch is running. The red LED is on when the stopwatch is stopped.
  3. My example is based on the example I took from here.
  4. When the stopwatch is stopped, you could clear the LCD or continue to work. To clear and restore the screen to its original state, press the user button (the blue button) and hold it for two seconds. To continue the stopwatch, briefly press the blue button.
  5. You could watch the video below to understand how this works in more detail.

First of all, we will describe this Cargo.toml file. The Cargo.toml file for each package is called its manifest. This manifest file consists of the following sections.

cargo-features = ["default-run"]

[package]
authors = ["Roman Shulenkov postmaster@mcu.by"]
categories = ["embedded", "no-std"]
name = "stm32f429i_disco"
version = "0.1.0"
edition = "2018"

[dependencies]
cortex-m = "0.6.2"
cortex-m-rt = "0.6.12"
panic-halt = "0.2.0"
panic-semihosting = "0.5.3"
cortex-m-semihosting = "0.3.5"
ssd1306 = "0.3.1"
embedded-graphics = "0.6.2"
arrayvec = { version = "0.5.1", default-features = false }
nb = "0.1.2"
rand_core = "0.5.1"
micromath = "1.0.0"

[dependencies.stm32f4]
version = "0.10.0"
features = ["stm32f429", "rt"]

[dependencies.stm32f4xx-hal]
version = "0.7"
features = ["rt", "stm32f429"]

First, we need to set up the internal clock (RCC). I decided to run this example at a core frequency of 180 MHz.

fn setup_clocks(rcc: Rcc) -> Clocks {
    return rcc
        .cfgr
        .hclk(180.mhz())
        .sysclk(180.mhz())
        .pclk1(45.mhz())
        .pclk2(90.mhz())
        .freeze();
}

This project uses two interrupt handlers. This is an interrupt handler for the user button EXTI0 and an interrupt handler for the timer TIM2. Interrupts differ from exceptions in a variety of ways but their operation and use are largely similar and they are also handled by the same interrupt controller. Whereas exceptions are defined by the Cortex-M architecture, interrupts are always vendor (and often even chip) specific implementations, both in naming and functionality. Interrupt handlers look like plain functions (except for the lack of arguments) similar to exception handlers. However, they can not be called directly by other parts of the firmware due to the special calling conventions. It is, however, possible to generate interrupt requests in software to trigger a diversion to the interrupt handler.

#[interrupt]
fn EXTI0() {
    free(|cs| {
        let mut btn_ref = BUTTON.borrow(cs).borrow_mut();
        if let Some(ref mut btn) = btn_ref.deref_mut() {
            // We cheat and don't bother checking _which_ exact interrupt line fired - there's only
            // ever going to be one in this example.
            btn.clear_interrupt_pending_bit();

            let state = STATE.borrow(cs).get();
            // Run the state machine in an ISR - probably not something you want to do in most
            // cases but this one only starts and stops TIM2 interrupts
            match state {
                StopwatchState::Ready => {
                    ELAPSED_RESET_MS.borrow(cs).replace(0);                    
                    stopwatch_start(cs);
                    STATE.borrow(cs).replace(StopwatchState::Running);
                }
                StopwatchState::Running => {
                    ELAPSED_RESET_MS.borrow(cs).replace(0);
                    stopwatch_stop(cs);
                    stopwatch_reset_start(cs);
                    STATE.borrow(cs).replace(StopwatchState::Stopped);
                }
                StopwatchState::Stopped => {
                    let cell_reset = ELAPSED_RESET_MS.borrow(cs);
                    let val_reset = cell_reset.get();
                    
                    if val_reset > 500_u32{
                        ELAPSED_MS.borrow(cs).replace(0);
                        stopwatch_reset_stop(cs);
                        STATE.borrow(cs).replace(StopwatchState::Ready);
                    } else{
                        stopwatch_reset_stop(cs);
                        //stopwatch_reset_start(cs);    
                        stopwatch_continue(cs);
                        STATE.borrow(cs).replace(StopwatchState::Running);
                    }                             
                }                
            }
        }
    });
}

#[interrupt]
fn TIM2() {
    free(|cs| {
        if let Some(ref mut tim2) = TIMER_TIM2.borrow(cs).borrow_mut().deref_mut() {
            tim2.clear_interrupt(Event::TimeOut);
        }

        let cell = ELAPSED_MS.borrow(cs);
        let cell_reset = ELAPSED_RESET_MS.borrow(cs);
        let val = cell.get();        
        let val_reset = cell_reset.get();

        match STATE.borrow(cs).get() {
            StopwatchState::Ready => {                    
                cell.replace(val + 1);
            }
            StopwatchState::Running => {
                cell.replace(val + 1);
            }
            StopwatchState::Stopped => {

                let mut btn_ref = BUTTON.borrow(cs).borrow_mut();
                if let Some(ref mut btn) = btn_ref.deref_mut() {
                    if btn.is_high().unwrap() {
                        cell_reset.replace(val_reset + 1);    
                    }
                }
            }
        }
    });
}

By the way, we need to initialize the SPI4 for the screen. We can do this as follows.

    let sck = gpioe.pe2.into_alternate_af5();
    let miso = gpioe.pe5.into_alternate_af5();
    let mosi = gpioe.pe6.into_alternate_af5();

    let spi = Spi::spi4(dp.SPI4, (sck, miso, mosi), Mode {
        polarity: Polarity::IdleLow,
        phase: Phase::CaptureOnFirstTransition,
    }, stm32f4xx_hal::time::KiloHertz(2000).into(),clocks);

We also need to set a global state for our variables. We can do this as follows.

static ELAPSED_MS: Mutex = Mutex::new(Cell::new(0u32));
static ELAPSED_RESET_MS: Mutex = Mutex::new(Cell::new(0u32));
static TIMER_TIM2: Mutex<RefCell> = Mutex::new(RefCell::new(None));
static STATE: Mutex = Mutex::new(Cell::new(StopwatchState::Ready));
static BUTTON: Mutex<RefCell<option<PA0>>> = Mutex::new(RefCell::new(None));

For synchronization, we will use mutexes. These constructs ensure exclusive access to a variable, such as our counter. A thread can attempt to lock (or acquire) the mutex, and either succeed immediately, or blocks waiting for the lock to be acquired, or returns an error that the mutex could not be locked. While that thread holds the lock, it is granted access to the protected data. When the thread is done, it unlocks (or releases) the mutex, allowing another thread to lock it. Using a mutex with interrupt handlers can be tricky: it is not normally acceptable for the interrupt handler to block, and it would be especially disastrous for it to block waiting for the main thread to release a lock since we would then deadlock (the main thread will never release the lock because execution stays in the interrupt handler). Deadlocking is not considered unsafe: it is possible to even in safe Rust.

Embedded-graphics is a 2D graphics library that is focused on memory-constrained embedded devices. It contains built-in items that make it easy to draw 2D graphics primitives. To support many different kinds of display embedded-graphics doesn’t include any drivers directly but provides an API that can be implemented by external crates. This example shows some more advanced usage of Embedded Graphics. It draws a round clock face with a second hand. A digital clock is drawn at the top of the clock.

/// Draw a circle and 12 tics as a simple clock face
fn draw_face() -> impl Iterator> {
    let tic_len = 3.0;

    // Use the circle macro to create the outer face
    let face = egcircle!(
        center = CENTER,
        radius = SIZE,
        style = primitive_style!(stroke_color = BinaryColor::On, stroke_width = 1)
    );

    // Create 12 `Line`s starting from the outer edge and drawing inwards by `tic_len` pixels
    let tics = (0..12).into_iter().map(move |index| {
        // Start angle around the circle, in radians
        let angle = START + (PI * 2.0 / 12.0) * index as f32;

        // Start point on circumference
        let start = polar(angle, SIZE as f32);

        // End point; start point offset by `tic_len` pixels towards the circle center
        let end = polar(angle, SIZE as f32 - tic_len);

        Line::new(start, end)
            .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1))
            .into_iter()
    });

    // Create a single iterator of pixels, first iterating over the circle, then over the 12 lines
    // generated
    face.into_iter().chain(tics.flatten())
}

/// Draw the seconds hand given a seconds value (0 - 59)
fn draw_seconds_hand(seconds: u32) -> impl Iterator> {
    // Convert seconds into a position around the circle in radians
    let seconds_radians = ((seconds as f32 / 60.0) * 2.0 * PI) + START;

    let end = polar(seconds_radians, SIZE as f32);

    // Basic line hand
    let hand = Line::new(CENTER, end).into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1));

    // Decoration position
    let decoration_position = polar(seconds_radians, SIZE as f32 - 23.0);

    // Decoration style
    let decoration_style = PrimitiveStyleBuilder::new()
        .fill_color(BinaryColor::Off)
        .stroke_color(BinaryColor::On)
        .stroke_width(1)
        .build();

    // Add a fancy circle near the end of the hand
    let decoration = Circle::new(decoration_position, 3).into_styled(decoration_style);

    hand.into_iter().chain(&decoration)
}

The source code of the project.

#![no_std]
#![no_main]

extern crate cortex_m;
extern crate cortex_m_rt as rt;
extern crate panic_semihosting;
extern crate stm32f4xx_hal as hal;

use crate::hal::{
    prelude::*,
    spi::Spi, 
    interrupt,
    gpio::{gpioa::PA0, Edge, ExtiPin, Input, PullDown},
    timer::{Event, Timer},
    rcc::{Clocks, Rcc},
    stm32,
};

use arrayvec::ArrayString;
use core::cell::{Cell, RefCell};
use core::fmt;
use core::ops::DerefMut;
use cortex_m::interrupt::{free, CriticalSection, Mutex};

use hal::spi::{Mode, Phase, Polarity};

use micromath::F32Ext;
use cortex_m_rt::{ExceptionFrame, entry, exception};
use embedded_graphics::{
    egcircle,
    prelude::*,
    fonts::{Font6x8, Text}, 
    primitive_style,
    primitives::{Circle, Line},
    pixelcolor::BinaryColor,  
    style::TextStyleBuilder,
    style::{PrimitiveStyle, PrimitiveStyleBuilder},
};
use core::f32::consts::{FRAC_PI_2, PI};

use ssd1306::{ mode::GraphicsMode, Builder as SSD1306Builder};
use stm32f4::stm32f429;

// Set up global state. It's all mutexed up for concurrency safety.
static ELAPSED_MS: Mutex = Mutex::new(Cell::new(0u32));
static ELAPSED_RESET_MS: Mutex = Mutex::new(Cell::new(0u32));
static TIMER_TIM2: Mutex<RefCell> = Mutex::new(RefCell::new(None));
static STATE: Mutex = Mutex::new(Cell::new(StopwatchState::Ready));
static BUTTON: Mutex<RefCell<option<PA0>>> = Mutex::new(RefCell::new(None));

/// The center of the clock face
const CENTER: Point = Point::new(64, 40);

/// The radius of the clock face
const SIZE: u32 = 23;

/// Start at the top of the circle
const START: f32 = -FRAC_PI_2;

#[derive(Clone, Copy)]
enum StopwatchState {
    Ready,
    Running,
    Stopped,
}

#[entry]
fn main() -> ! {

    let mut dp = stm32f429::Peripherals::take().unwrap();
    let cp = cortex_m::peripheral::Peripherals::take().unwrap();    
    dp.RCC.apb2enr.write(|w| w.syscfgen().enabled());

    let rcc = dp.RCC.constrain();

    let clocks = setup_clocks(rcc);

    let gpioa = dp.GPIOA.split();
    let gpioe = dp.GPIOE.split();    

    let mut board_btn = gpioa.pa0.into_pull_down_input();
    board_btn.make_interrupt_source(&mut dp.SYSCFG);
    board_btn.enable_interrupt(&mut dp.EXTI);
    board_btn.trigger_on_edge(&mut dp.EXTI, Edge::FALLING);

    //spi4
    //sck  - pe2
    //miso - pe5
    //mosi - pe6
    //cs - pe4
    //dc - pe3

    let sck = gpioe.pe2.into_alternate_af5();
    let miso = gpioe.pe5.into_alternate_af5();
    let mosi = gpioe.pe6.into_alternate_af5();

    let spi = Spi::spi4(dp.SPI4, (sck, miso, mosi), Mode {
        polarity: Polarity::IdleLow,
        phase: Phase::CaptureOnFirstTransition,
    }, stm32f4xx_hal::time::KiloHertz(2000).into(),clocks);

    // Set up the LEDs. On the stm32f429i-disco they are connected to pin PG13 and PG14.
    let gpiog = dp.GPIOG.split();
    let mut led3 = gpiog.pg13.into_push_pull_output();
    let mut led4 = gpiog.pg14.into_push_pull_output();

    let dc = gpioe.pe3.into_push_pull_output();
    let mut ss = gpioe.pe4.into_push_pull_output();
    let mut delay = hal::delay::Delay::new(cp.SYST, clocks);

    ss.set_high().unwrap();
    delay.delay_ms(100_u32);
    ss.set_low().unwrap();

    // Set up the display
    let mut disp: GraphicsMode<_> = SSD1306Builder::new().connect_spi(spi, dc).into();
    disp.init().unwrap();
    disp.flush().unwrap();

     // Create a 1ms periodic interrupt from TIM2
     let mut timer = Timer::tim2(dp.TIM2, 1.khz(), clocks);
     timer.listen(Event::TimeOut);

     free(|cs| {
         TIMER_TIM2.borrow(cs).replace(Some(timer));
         BUTTON.borrow(cs).replace(Some(board_btn));
     });

     // Enable interrupts
     stm32::NVIC::unpend(hal::stm32::Interrupt::TIM2);
     stm32::NVIC::unpend(hal::stm32::Interrupt::EXTI0);
     unsafe {
         stm32::NVIC::unmask(hal::stm32::Interrupt::EXTI0);
     };          

    let mut state_led = false;
    loop{

        let elapsed = free(|cs| ELAPSED_MS.borrow(cs).get());

        let mut format_buf = ArrayString::<[u8; 10]>::new();
        format_elapsed(&mut format_buf, elapsed);

        disp.clear();

        let state = free(|cs| STATE.borrow(cs).get());
        let state_msg = match state {
            StopwatchState::Ready => "Ready",
            StopwatchState::Running => "Running",
            StopwatchState::Stopped => "Stopped",
        };

        state_led = !state_led;

        match state {
            StopwatchState::Ready => {
                led3.set_high().unwrap();
                led4.set_low().unwrap();
            },
            StopwatchState::Running => {
                if state_led {
                    led4.set_low().unwrap();
                    led3.set_high().unwrap();
                } else {
                    led4.set_low().unwrap();
                    led3.set_low().unwrap();   
                }
            },
            StopwatchState::Stopped => {
                led3.set_low().unwrap();
                led4.set_high().unwrap();
            },
        };

        let text_style = TextStyleBuilder::new(Font6x8)
            .text_color(BinaryColor::On)
            .build();
        
        Text::new(state_msg, Point::new(0, 0))
            .into_styled(text_style)
            .draw(&mut disp).unwrap();
                
        Text::new(format_buf.as_str(), Point::new((128 / 2) - 1, 0 ))
            .into_styled(text_style)
            .draw(&mut disp).unwrap();

        draw_face().draw(&mut disp).unwrap();
        draw_seconds_hand(elapsed_to_s(elapsed)).draw(&mut disp).unwrap();
            
        disp.flush().unwrap();

        delay.delay_ms(100u32);
    }
}

fn setup_clocks(rcc: Rcc) -> Clocks {
    return rcc
        .cfgr
        .hclk(180.mhz())
        .sysclk(180.mhz())
        .pclk1(45.mhz())
        .pclk2(90.mhz())
        .freeze();
}

#[interrupt]
fn EXTI0() {
    free(|cs| {
        let mut btn_ref = BUTTON.borrow(cs).borrow_mut();
        if let Some(ref mut btn) = btn_ref.deref_mut() {
            // We cheat and don't bother checking _which_ exact interrupt line fired - there's only
            // ever going to be one in this example.
            btn.clear_interrupt_pending_bit();

            let state = STATE.borrow(cs).get();
            // Run the state machine in an ISR - probably not something you want to do in most
            // cases but this one only starts and stops TIM2 interrupts
            match state {
                StopwatchState::Ready => {
                    ELAPSED_RESET_MS.borrow(cs).replace(0);                    
                    stopwatch_start(cs);
                    STATE.borrow(cs).replace(StopwatchState::Running);
                }
                StopwatchState::Running => {
                    ELAPSED_RESET_MS.borrow(cs).replace(0);
                    stopwatch_stop(cs);
                    stopwatch_reset_start(cs);
                    STATE.borrow(cs).replace(StopwatchState::Stopped);
                }
                StopwatchState::Stopped => {
                    let cell_reset = ELAPSED_RESET_MS.borrow(cs);
                    let val_reset = cell_reset.get();
                    
                    if val_reset > 500_u32{
                        ELAPSED_MS.borrow(cs).replace(0);
                        stopwatch_reset_stop(cs);
                        STATE.borrow(cs).replace(StopwatchState::Ready);
                    } else{
                        stopwatch_reset_stop(cs);
                        //stopwatch_reset_start(cs);    
                        stopwatch_continue(cs);
                        STATE.borrow(cs).replace(StopwatchState::Running);
                    }                             
                }                
            }
        }
    });
}

#[interrupt]
fn TIM2() {
    free(|cs| {
        if let Some(ref mut tim2) = TIMER_TIM2.borrow(cs).borrow_mut().deref_mut() {
            tim2.clear_interrupt(Event::TimeOut);
        }

        let cell = ELAPSED_MS.borrow(cs);
        let cell_reset = ELAPSED_RESET_MS.borrow(cs);
        let val = cell.get();        
        let val_reset = cell_reset.get();

        match STATE.borrow(cs).get() {
            StopwatchState::Ready => {                    
                cell.replace(val + 1);
            }
            StopwatchState::Running => {
                cell.replace(val + 1);
            }
            StopwatchState::Stopped => {

                let mut btn_ref = BUTTON.borrow(cs).borrow_mut();
                if let Some(ref mut btn) = btn_ref.deref_mut() {
                    if btn.is_high().unwrap() {
                        cell_reset.replace(val_reset + 1);    
                    }
                }
            }
        }
    });
}

fn stopwatch_start<'cs>(cs: &'cs CriticalSection) {
    ELAPSED_MS.borrow(cs).replace(0);
    unsafe {
        stm32::NVIC::unmask(hal::stm32::Interrupt::TIM2);
    }
}

fn stopwatch_continue<'cs>(_cs: &'cs CriticalSection) {
    unsafe {
        stm32::NVIC::unmask(hal::stm32::Interrupt::TIM2);
    }
}

fn stopwatch_stop<'cs>(_cs: &'cs CriticalSection) {
    stm32::NVIC::mask(hal::stm32::Interrupt::TIM2);
}

fn stopwatch_reset_start<'cs>(cs: &'cs CriticalSection) {
    ELAPSED_RESET_MS.borrow(cs).replace(0);
    unsafe {
        stm32::NVIC::unmask(hal::stm32::Interrupt::TIM2);
    }
}

fn stopwatch_reset_stop<'cs>(_cs: &'cs CriticalSection) {
    stm32::NVIC::mask(hal::stm32::Interrupt::TIM2);
}

// Formatting requires the arrayvec crate
fn format_elapsed(buf: &mut ArrayString<[u8; 10]>, elapsed: u32) {
    let minutes = elapsed_to_m(elapsed);
    let seconds = elapsed_to_s(elapsed);
    let millis = elapsed_to_ms(elapsed);
    fmt::write(
        buf,
        format_args!("{}:{:02}.{:03}", minutes, seconds, millis),
    )
    .unwrap();
}

fn elapsed_to_ms(elapsed: u32) -> u32 {
    return elapsed % 1000;
}

fn elapsed_to_s(elapsed: u32) -> u32 {
    return (elapsed - elapsed_to_ms(elapsed)) % 60000 / 1000;
}

fn elapsed_to_m(elapsed: u32) -> u32 {
    return elapsed / 60000;
}

/// Convert a polar coordinate (angle/distance) into an (X, Y) coordinate centered around `CENTER`
fn polar(angle: f32, radius: f32) -> Point {
    CENTER + Point::new((angle.cos() * radius) as i32, (angle.sin() * radius) as i32)
}

/// Draw a circle and 12 tics as a simple clock face
fn draw_face() -> impl Iterator> {
    let tic_len = 3.0;

    // Use the circle macro to create the outer face
    let face = egcircle!(
        center = CENTER,
        radius = SIZE,
        style = primitive_style!(stroke_color = BinaryColor::On, stroke_width = 1)
    );

    // Create 12 `Line`s starting from the outer edge and drawing inwards by `tic_len` pixels
    let tics = (0..12).into_iter().map(move |index| {
        // Start angle around the circle, in radians
        let angle = START + (PI * 2.0 / 12.0) * index as f32;

        // Start point on circumference
        let start = polar(angle, SIZE as f32);

        // End point; start point offset by `tic_len` pixels towards the circle center
        let end = polar(angle, SIZE as f32 - tic_len);

        Line::new(start, end)
            .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1))
            .into_iter()
    });

    // Create a single iterator of pixels, first iterating over the circle, then over the 12 lines
    // generated
    face.into_iter().chain(tics.flatten())
}

/// Draw the seconds hand given a seconds value (0 - 59)
fn draw_seconds_hand(seconds: u32) -> impl Iterator> {
    // Convert seconds into a position around the circle in radians
    let seconds_radians = ((seconds as f32 / 60.0) * 2.0 * PI) + START;

    let end = polar(seconds_radians, SIZE as f32);

    // Basic line hand
    let hand = Line::new(CENTER, end).into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1));

    // Decoration position
    let decoration_position = polar(seconds_radians, SIZE as f32 - 23.0);

    // Decoration style
    let decoration_style = PrimitiveStyleBuilder::new()
        .fill_color(BinaryColor::Off)
        .stroke_color(BinaryColor::On)
        .stroke_width(1)
        .build();

    // Add a fancy circle near the end of the hand
    let decoration = Circle::new(decoration_position, 3).into_styled(decoration_style);

    hand.into_iter().chain(&decoration)
}

#[exception]
fn HardFault(ef: &ExceptionFrame) -> ! {
    panic!("{:#?}", ef);
}

You can download the project sources as follows:
git clone https://github.com/mcuby/rust-stm32f429i-disco.git