Controls

Our protagonist will be controlled by the two buttons on the front of the micro:bit. Button A will turn to the snake's left, and button B will turn to the snake's right.

We will use the microbit::pac::interrupt macro to handle button presses in a concurrent way. The interrupt will be generated by the MB2's General Purpose Input/Output Tasks and Events (GPIOTE) peripheral.

The controls module

We will need to keep track of two separate pieces of global mutable state: A reference to the GPIOTE peripheral, and a record of the selected direction to turn next.

Shared data is wrapped in a RefCell to permit interior mutability and locking. You can learn more about RefCell by reading the RefCell documentation and the interior mutability chapter of the Rust Book]. The RefCell is, in turn, wrapped in a cortex_m::interrupt::Mutex to allow safe access. The Mutex provided by the cortex_m crate uses the concept of a critical section. Data in a Mutex can only be accessed from within a function or closure passed to cortex_m::interrupt:free (renamed here to interrupt_free for clarity), which ensures that the code in the function or closure cannot itself be interrupted.

Initialization

First, we will initialise the buttons (src/controls/init.rs).

#![allow(unused)]
fn main() {
use super::{Buttons, GPIO};

use cortex_m::interrupt::free as interrupt_free;
use microbit::{
    hal::{
        gpio::{Floating, Input, Pin},
        gpiote::{Gpiote, GpioteChannel},
    },
    pac,
};

/// Initialise the buttons and enable interrupts.
pub fn init_buttons(board_gpiote: pac::GPIOTE, board_buttons: Buttons) {
    let gpiote = Gpiote::new(board_gpiote);

    fn init_channel(channel: &GpioteChannel<'_>, button: &Pin<Input<Floating>>) {
        channel.input_pin(button).hi_to_lo().enable_interrupt();
        channel.reset_events();
    }

    let channel0 = gpiote.channel0();
    init_channel(&channel0, &board_buttons.button_a.degrade());

    let channel1 = gpiote.channel1();
    init_channel(&channel1, &board_buttons.button_b.degrade());

    interrupt_free(move |cs| {
        *GPIO.borrow(cs).borrow_mut() = Some(gpiote);

        unsafe {
            pac::NVIC::unmask(pac::Interrupt::GPIOTE);
        }
        pac::NVIC::unpend(pac::Interrupt::GPIOTE);
    });
}
}

The GPIOTE peripheral on the nRF52 has 8 "channels", each of which can be connected to a GPIO pin and configured to respond to certain events, including rising edge (transition from low to high signal) and falling edge (high to low signal). A button is a GPIO pin which has high signal when not pressed and low signal otherwise. Therefore, a button press is a falling edge.

Note the awkward use of the function init_channel() in initialization to avoid copy-pasting the button initialization code. The types that the various embedded crates for the MB2 have been hiding from you are sometimes a bit scary. I would encourage you to explore the type structure of the HAL and PAC crates at some point, as it is a bit odd and takes getting used to. In particular, note that each pin on the microbit has its own unique type. The purpose of the degrade() function in initialization is to convert these to a common type that can reasonably be used as an argument to init_channel() and thence to input_pin().

We connect channel0 to button_a and channel1 to button_b. In each case, we set the button up to generate events on a falling edge (hi_to_lo). We store a reference to our GPIOTE peripheral in the GPIO Mutex. We then unmask GPIOTE interrupts, allowing them to be propagated by the hardware, and call unpend to clear any interrupts with pending status (which may have been generated prior to the interrupts being unmasked).

Interrupt handler

Next, we write the code that handles the interrupt. We use the interrupt macro re-exported from the nrf52833_hal crate. We define a function with the same name as the interrupt we want to handle (you can see them all here) and annotate it with #[interrupt] (src/controls/interrupt.rs).

#![allow(unused)]
fn main() {
use super::{Turn, GPIO, TURN};

use cortex_m::interrupt::free as interrupt_free;
use microbit::pac::{self, interrupt};

#[pac::interrupt]
fn GPIOTE() {
    interrupt_free(|cs| {
        if let Some(gpiote) = GPIO.borrow(cs).borrow().as_ref() {
            let a_pressed = gpiote.channel0().is_event_triggered();
            let b_pressed = gpiote.channel1().is_event_triggered();

            let turn = match (a_pressed, b_pressed) {
                (true, false) => Turn::Left,
                (false, true) => Turn::Right,
                _ => Turn::None,
            };

            gpiote.channel0().reset_events();
            gpiote.channel1().reset_events();

            *TURN.borrow(cs).borrow_mut() = turn;
        }
    });
}
}

When a GPIOTE interrupt is generated, we check each button to see whether it has been pressed. If only button A has been pressed, we record that the snake should turn to the left. If only button B has been pressed, we record that the snake should turn to the right. In any other case, we record that the snake should not make any turn. (Having both buttons pressed "at the same time" is exceedingly unlikely: button presses are noted almost instantly, and this interrupt handler runs very fast — it would be hard to get both buttons down in time for this to happen. Similarly, it would be hard to press a button for a short enough time for this code to miss it and report that neither button is pressed. Still, Rust enforces that you plan for these unexpected cases: the code will not compile unless you check all the possibilities.) The relevant turn is stored in the TURN Mutex. All of this happens within an interrupt_free block, to ensure that we cannot be interrupted by some other event while handling this interrupt.

Finally, we expose a simple function to get the next turn (src/controls.rs).

#![allow(unused)]
fn main() {
mod init;
mod interrupt;

pub use init::init_buttons;

use crate::game::Turn;
use core::cell::RefCell;
use cortex_m::interrupt::{free as interrupt_free, Mutex};
use microbit::{board::Buttons, hal::gpiote::Gpiote};
pub static GPIO: Mutex<RefCell<Option<Gpiote>>> = Mutex::new(RefCell::new(None));
pub static TURN: Mutex<RefCell<Turn>> = Mutex::new(RefCell::new(Turn::None));

/// Get the next turn (ie, the turn corresponding to the most recently pressed button).
pub fn get_turn(reset: bool) -> Turn {
    interrupt_free(|cs| {
        let turn = *TURN.borrow(cs).borrow();
        if reset {
            *TURN.borrow(cs).borrow_mut() = Turn::None
        }
        turn
    })
}
}

This function simply returns the current value of the TURN Mutex. It takes a single boolean argument, reset. If reset is true, the value of TURN is reset, i.e., set to Turn::None.

Next we will build support for a high-fidelity game display.