A basic application

So you’ve got one of the examples running, but what now? Let’s go through a simple application for the BBC:microbit to understand it better.

The application is a simple game: As a cursor traverses the LED matrix, use the button to toggle the 'set' state of each individual LED. Once all LEDs are lit, you win!

Main

The full example can be found here.

Rust Nightly

The first thing you’ll notice is a few declarations stating that the application requires some nightly features:

#![no_std]
#![no_main]
#![macro_use]
#![feature(type_alias_impl_trait)]
#![feature(async_fn_in_trait)]

Dealing with errors

Then, what follows are some declarations on how to deal with panics and faults. During development, a good practice is to rely on defmt-rtt and panic-probe to print diagnostics to the terminal:

/// A simple game where the led matrix is traversed at a fixed interval and you press the button
/// to light a red. You win when the whole board is lit.

Main

The main entry point is defined using the #[embassy::main] macro. The entry point is also required to take a Spawner argument.

The Spawner is the way the main application spawns tasks.

For some boards, a Board Support Package (BSP) helps locating peripherals with a board-specific name. The board simply helps configuring the peripherals and giving them a name that maps to your particular board. In our case, we can use the Microbit board.

We’ll model the game as an async task called run_game, which we will spawn from main. We then wait for button events and pass that to the game using a Channel.

};

type CS = embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;

#[embassy_executor::main]
async fn main(spawner: embassy_executor::Spawner) {
    // Using a board support package to simplify setup
    let board = Microbit::default();

    // Channel for game events, buffering up to 10 events
    static EVENTS: Channel<CS, GameMessage, 10> = Channel::new();

    // Start the game logic
    spawner
        .spawn(run_game(board.display, EVENTS.receiver().into()))
        .unwrap();

    // Wait for button presses and submit game events
    let mut button = board.btn_a;
    loop {
        button.wait_for_any_edge().await;
        if button.is_low() {
            // Best effort delivery, to ensure the game is responsive
            let _ = EVENTS.try_send(GameMessage::Toggle);
        }
    }
}

/// A message for the game logic based on external input
#[derive(Clone)]

We define a GameMessage type that our task will expect to be notified of whenever the button is pressed. The DynamicReceiver type is a handle that you can use to receive events that get sent to a Channel.

Game logic

Having the setup code out of the way, we can now focus on our application. The game logic can be described as follows:

Game logic
  1. Define a set of coordinates in a 5x5 matrix

  2. Initialize cursor at (0, 0)

  3. Initialize coordinates as not being visited

  4. While not all coordinates visited

    1. Lit the LED at the cursor coordinate

    2. Wait for a GameMessage to arrive within a timeout

    3. If a GameMessage::Toggle arrived, toggle the visited status at the cursor coordinate

    4. Unlit the LED at the cursor coordinate, if not visited

    5. Update cursor

The game logic is implemented here:

pub enum GameMessage {
    Toggle,
}

#[embassy_executor::task]
async fn run_game(mut matrix: LedMatrix, events: DynamicReceiver<'static, GameMessage>) {
    defmt::info!("Starting game! Press the 'A' button to lit the LED at the cursor.");
    let speed = Duration::from_millis(200);

    let mut coordinates: [[bool; 5]; 5] = [[false; 5]; 5];
    let mut cursor = 0;
    let (mut x, mut y) = (0, 0);
    let mut done = false;

    let mut render = Ticker::every(Duration::from_millis(5));
    while !done {
        matrix.on(x, y);
        // Race timeout and button press
        let timeout = Timer::after(speed);
        let event = events.recv();

        let mut logic = select(timeout, event);
        loop {
            let tick = render.next();
            match select(tick, &mut logic).await {
                Either::First(_) => {
                    matrix.render();
                }
                Either::Second(f) => match f {
                    Either::First(_) => {
                        break;
                    }
                    Either::Second(_) => {
                        // Set/unset
                        coordinates[y][x] = !coordinates[y][x];
                        break;
                    }
                },
            }
        }

        // Unlit only if we're not set
        if !coordinates[y][x] {
            matrix.off(x, y)
        }

        // Check if game is done
        done = true;
        for x in 0..5 {
            for y in 0..5 {
                if !coordinates[y][x] {
                    done = false;
                    break;
                }
            }
        }

        x = cursor % 5;
        y = (cursor / 5) % 5;
        cursor += 1;
        matrix.render();
    }
}

The LedMatrix has an on and off method to toggle the LED at a given coordinate. In order to render the current LedMatrix state, we need to call a render function at the desired refresh interval. The "outer" loop does just that until one of the 'game logic' futures are completing.

The events.recv() is an async funtion that completes when an event have been received (i.e. button has been pressed).

The Timer type can be used to asynchronously wait for an event. The select function can be used to "race" two async functions for completion.

Therefore, the outer select races the rending loop and the events.recv() and timer, whereas they are raced against eachother. If there is an event or timeout, we update the game state.

Summary

We’ve gone through a basic application written for the BBC micro:bit. We’ve created an async task to drive the game logic, and wired it together with a channel receiving events from the main task.