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:
-
Define a set of coordinates in a 5x5 matrix
-
Initialize cursor at (0, 0)
-
Initialize coordinates as not being visited
-
While not all coordinates visited
-
Lit the LED at the cursor coordinate
-
Wait for a GameMessage to arrive within a timeout
-
If a GameMessage::Toggle arrived, toggle the visited status at the cursor coordinate
-
Unlit the LED at the cursor coordinate, if not visited
-
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.