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.