7. Multiple priorities

Used imports in this chapter

use cortex_m_rt::entry;
use defmt::{info, unwrap};
use embassy_executor::{Executor, InterruptExecutor, Spawner};
use embassy_futures::select::select4;
use embassy_futures::yield_now;
use embassy_nrf::gpio::{AnyPin, Input, Pull};
use embassy_nrf::interrupt;
use embassy_nrf::interrupt::{InterruptExt, Priority};
use embassy_nrf::peripherals::SPI3;
use embassy_nrf::{
    bind_interrupts,
    gpio::{Level, Output, OutputDrive},
    peripherals, spim,
};
use embassy_sync::signal::Signal;
use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex};
use embedded_graphics::pixelcolor::raw::RawU16;
use embedded_graphics::{
    image::{Image, ImageRaw, ImageRawLE},
    pixelcolor::Rgb565,
    prelude::*,
};
use static_cell::StaticCell;
use {defmt_rtt as _, panic_probe as _};

The reason why the buttons did not always feel that snappy is because the input logic needs to share time with the other tasks. As such, it needs to wait its turn before being able to actually do some work. This is however not always desired, as some tasks need a higher priority than other tasks. In our case we should put the input handler task on a higher priority.

The way this is done in Embassy is by using a different executor (in our case based on interrupts) configured with this higher priority. To do this, the first thing we should do is start managing the executors ourselves and get rid of the #[embassy_executor::main] attribute on main. As we are still on an embedded system, we still need to make sure that our device knows where to start. We will do this by adding a #[entry] attribute macro in its place. This macro is defined by the cortex_m_rt crate and is also used by the #[embassy_executor::main] macro.

With this in mind, we will need to perform the following steps:

  • Change the signature of main from async fn main(spawner: Spawner) to fn main() -> !
  • Statically define the executors: one interrupt based (HIGH), one thread based (LOW)
  • Define an interrupt handler to be used by Embassy to wake up the interrupt based executor needed for the higher priority
  • Initialize the high priority executor and spawn the input handling task
  • Initialize the low priority executor and spawn a task that will initiate the screen driver (needs async) and then spawn the driver task and main drawing task.

Using multiple cores.

Some embedded processors come with multiple physical cores. One way to use those, is by instantiating multiple executors that are launched each on one core. An example on how to do this with for example the rp2040 chip (Raspberry Pico) can be found at https://github.com/embassy-rs/embassy/blob/main/examples/rp/src/bin/multicore.rs.

Changing the signature of main

Change the signature of main:

// OLD
// #[embassy_executor::main]
// async fn main(spawner: Spawner) { }
// NEW
#[entry]
fn main() -> ! { }

Define an interrupt handler and configure the interrupt executor

First, we define the interrupt executor:

// Above main
static EXECUTOR_HIGH: InterruptExecutor = InterruptExecutor::new();

We follow this by adding the interrupt handler that will wake the executor.

/// Add interrupt, so the executor can wake from sleep
/// Forgetting to add this, results in no progress
#[interrupt]
unsafe fn SWI0_EGU0() {
    EXECUTOR_HIGH.on_interrupt();
}

Then in main right after setting up spi, res and rs, set the priority of the interrupt and attach it to the executor. Next replace the todo! by spawning the input_handler task.

// set priority (in main)
info!("Starting high priority executor");
interrupt::SWI0_EGU0.set_priority(Priority::P6);
let high_spawner = EXECUTOR_HIGH.start(interrupt::SWI0_EGU0);
todo!("Use high_spawner to spawn `input_handler`");
Solution
#[entry]
fn main() -> ! {
    let p = embassy_nrf::init(Default::default());
    let led = p.P0_13;

    let mut config = spim::Config::default();
    config.frequency = spim::Frequency::M32;
    let spi = Spim::new_txonly(p.SPI3, Irqs, p.P0_04, p.P0_28, config);
    let res = Output::new(p.P0_29.into(), Level::Low, OutputDrive::Standard);
    let rs =  Output::new(p.P0_30.into(), Level::Low, OutputDrive::Standard);

    // Initialize the display here, such that we have less types
    // to write when starting our tasks.
    let mut display = st7735_lcd_async::ST7735::new(spi, rs, res, true, false);
    static DISPLAY_DRIVER: StaticCell<DisplayType> = StaticCell::new();
    let static_driver = DISPLAY_DRIVER.init(display);

    // set priority
    info!("Starting high priority executor");
    interrupt::SWI0_EGU0.set_priority(Priority::P6);
    let high_spawner = EXECUTOR_HIGH.start(interrupt::SWI0_EGU0);
    unwrap!(high_spawner.spawn(input_handler((
        // set buttons to be high priority
        p.P0_11.into(),
        p.P0_12.into(),
        p.P0_24.into(),
        p.P0_25.into(),
    ))));

    // Next part comes here ...
}

Define and initialize the low priority thread executor

First, we define, immediately after our previous executor, our thread executor.

static EXECUTOR_LOW: StaticCell<Executor> = StaticCell::new();

Now add the following code, under where we configured our previous executor. Then move everything left in the original main to where it is indicated with the todo!, except for the construction of the display. As moving peripherals over an async boundary can be tedious, we will instantiate the display driver before we start our low-priority executor. The initialization however needs an async executor, so this will need to be passed down to the async part.

info!("Starting low priority executor");

// Initialize the display -> DO NOT MOVE
let mut config = spim::Config::default();
config.frequency = spim::Frequency::M32;
let spi = Spim::new_txonly(p.SPI3, Irqs, p.P0_04, p.P0_28, config);
let res = Output::new(p.P0_29, Level::Low, OutputDrive::Standard);
let rs = Output::new(p.P0_30, Level::Low, OutputDrive::Standard);

// Keep this part
let display = st7735_lcd_async::ST7735::new(spi, rs, res, true, false);
static DISPLAY_DRIVER: StaticCell<DisplayType> = StaticCell::new();
let static_driver: DisplayRef = DISPLAY_DRIVER.init(display);

// Everything after this point is now moved to the async main_task.
// This includes the actual startup code of the screen, but we keep
// the allocation here such that we have less typing to do.

let low_spawner = EXECUTOR_LOW.init(Executor::new()); // init executor
// Starts the executor
// NOTE: the run method never returns
low_spawner.run(move |spawner| { 
    // We cannot yet run futures directly in this closure, so we should create 
    // a new task whose sole purpose is to setup the peripherals and spawn the 
    // other low priority tasks.
    #[embassy_executor::task]
    async fn main_task(spawner: Spawner, static_driver: DisplayRef, led: AnyPin) {
        todo!("Everything left in the original main should come here");
    }

    unwrap!(spawner.spawn(main_task(spawner, static_driver, led.into())));
});
// everything after here should be moved, or will never be executed

If you came this far, you should now have the same application as at the end of chapter 6, except the inputs should be much more responsive. Try it out! If not, here follows the entire code of main.rs up until now.

#![no_std]
#![no_main]

mod frame_buffer;
mod game;

use cortex_m_rt::entry;
use defmt::{info, unwrap};
use embassy_executor::{Executor, InterruptExecutor, Spawner};
use embassy_futures::select::select4;
use embassy_futures::yield_now;
use embassy_nrf::gpio::{AnyPin, Input, Pull};
use embassy_nrf::interrupt;
use embassy_nrf::interrupt::{InterruptExt, Priority};
use embassy_nrf::peripherals::SPI3;
use embassy_nrf::{
    bind_interrupts,
    gpio::{Level, Output, OutputDrive},
    peripherals, spim,
};
use embassy_sync::signal::Signal;
use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex};
use embedded_graphics::pixelcolor::raw::RawU16;
use embedded_graphics::{
    image::{Image, ImageRaw, ImageRawLE},
    pixelcolor::Rgb565,
    prelude::*,
};
use static_cell::StaticCell;

use embassy_101::framebuffer::FrameBuffer;
use {defmt_rtt as _, panic_probe as _};

bind_interrupts!(struct Irqs {
    SPIM3 => spim::InterruptHandler<peripherals::SPI3>;
});

static EXECUTOR_HIGH: InterruptExecutor = InterruptExecutor::new();
static EXECUTOR_LOW: StaticCell<Executor> = StaticCell::new();

/// Add interrupt, so the executor can wake from sleep
/// Forgetting to add this, results in no progress
#[interrupt]
unsafe fn SWI0_EGU0() {
    EXECUTOR_HIGH.on_interrupt();
}

pub type FrameBufferType = Mutex<NoopRawMutex, FrameBuffer>;
pub type FrameBufferRef = &'static FrameBufferType;
pub type SignalType = Signal<NoopRawMutex, ()>;
pub type SignalRef = &'static SignalType;
pub type DisplayType = st7735_lcd_async::ST7735<
    spim::Spim<'static, SPI3>,
    Output<'static, peripherals::P0_30>,
    Output<'static, peripherals::P0_29>,
>;
pub type DisplayRef = &'static mut DisplayType;

#[entry]
fn main() -> ! {
    let p = embassy_nrf::init(Default::default());
    let led = p.P0_13;

    let mut config = spim::Config::default();
    config.frequency = spim::Frequency::M32;
    let spi = spim::Spim::new_txonly(p.SPI3, Irqs, p.P0_04, p.P0_28, config);
    let res = Output::new(p.P0_29, Level::Low, OutputDrive::Standard);
    let rs = Output::new(p.P0_30, Level::Low, OutputDrive::Standard);

    let display = st7735_lcd_async::ST7735::new(spi, rs, res, true, false);
    static DISPLAY_DRIVER: StaticCell<DisplayType> = StaticCell::new();
    let static_driver: DisplayRef = DISPLAY_DRIVER.init(display);

    // set priority
    info!("Starting high priority executor");
    interrupt::SWI0_EGU0.set_priority(Priority::P6);
    let high_spawner = EXECUTOR_HIGH.start(interrupt::SWI0_EGU0);
    unwrap!(high_spawner.spawn(input_handler((
        // set buttons to be high priority
        p.P0_11.into(),
        p.P0_12.into(),
        p.P0_24.into(),
        p.P0_25.into(),
    ))));

    info!("Starting low priority executor");
    let low_spawner = EXECUTOR_LOW.init(Executor::new());
    low_spawner.run(move |spawner| {
        // we need an async context for the driver to start initializing -> add a main task
        #[embassy_executor::task]
        async fn main_task(spawner: Spawner, static_driver: DisplayRef, led: AnyPin) {
            info!("Starting main task");

            let mut delay = embassy_time::Delay;
            static_driver.init(&mut delay).await.unwrap();
            static_driver
                .set_orientation(&st7735_lcd_async::Orientation::Landscape)
                .await
                .unwrap();

            static BACK_BUFFER: StaticCell<FrameBufferType> = StaticCell::new();
            let fb_ref = BACK_BUFFER.init(Mutex::new(FrameBuffer::new()));

            static SIGNAL_REF: StaticCell<SignalType> = StaticCell::new();
            let signal_ref = SIGNAL_REF.init(Signal::new());

            unwrap!(spawner.spawn(async_driver(fb_ref, display, signal_ref)));
            unwrap!(spawner.spawn(draw_task(fb_ref, led, signal_ref)));
        }

        unwrap!(spawner.spawn(main_task(spawner, static_driver, led.into())));
    });
}

#[embassy_executor::task]
async fn input_handler(btns: (AnyPin, AnyPin, AnyPin, AnyPin)) {
    let mut btn1 = Input::new(btns.0, Pull::Up);
    let mut btn2 = Input::new(btns.1, Pull::Up);
    let mut btn3 = Input::new(btns.2, Pull::Up);
    let mut btn4 = Input::new(btns.3, Pull::Up);

    loop {
        select4(
            btn1.wait_for_any_edge(),
            btn2.wait_for_any_edge(),
            btn3.wait_for_any_edge(),
            btn4.wait_for_any_edge(),
        )
        .await;

        info!(
            "Detected input change 1: {}, 2: {}, 3: {}, 4: {}",
            btn1.is_low(),
            btn2.is_low(),
            btn3.is_low(),
            btn4.is_low()
        );
    }
}

#[embassy_executor::task]
async fn draw_task(back_buffer: FrameBufferRef, led: AnyPin, signal: SignalRef) {
    let image_raw: ImageRawLE<Rgb565> =
        ImageRaw::new(include_bytes!("../../assets/ferris.raw"), 86);
    let mut led = Output::new(led, Level::Low, OutputDrive::Standard);

    let interval = embassy_time::Duration::from_millis(500);
    let mut x = 0;
    loop {
        led.set_high();
        info!("Hello world");

        let image = Image::new(&image_raw, Point::new(x, 0));
        {
            let mut fb = back_buffer.lock().await;
            let fb = &mut *fb; // unpack guard
            unwrap!(fb.fill_solid(&fb.bounding_box(), Rgb565::BLACK));
            unwrap!(image.draw(fb));
            signal.signal(()); // signal to driver that draw has been called
        } // Guard is dropped here

        // update animation
        x += 10;
        x %= 80;

        yield_now().await;
        led.set_low();

        embassy_time::Timer::after(interval).await;
    }
}

#[embassy_executor::task]
async fn async_driver(back_buffer: FrameBufferRef, static_driver: DisplayRef, signal: SignalRef) {
    loop {
        if !signal.signaled() {
            signal.wait().await;
        }
        {
            let fb = back_buffer.lock().await;
            let fb = &*fb; // unpack the guard, the guard will only drop at the end of the scope

            let drawable_area = fb.size();
            unwrap!(
                static_driver
                    .set_pixels_buffered(
                        0,
                        0,
                        drawable_area.width as u16 - 1,
                        drawable_area.height as u16 - 1,
                        fb.as_ref().iter().map(|&c| RawU16::from(c).into_inner()),
                    )
                    .await
            );
        } // drop mutex guard
        signal.reset();
        yield_now().await;
    }
}