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 micro:bit's GPIOTE (General Purpose Input/Output Tasks and Events) peripheral.
The controls
module
Code in this section should be placed in a separate file, controls.rs
, in our src
directory.
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.
#![allow(unused)] fn main() { use core::cell::RefCell; use cortex_m::interrupt::Mutex; use microbit::hal::gpiote::Gpiote; use crate::game::Turn; // ... static GPIO: Mutex<RefCell<Option<Gpiote>>> = Mutex::new(RefCell::new(None)); static TURN: Mutex<RefCell<Turn>> = Mutex::new(RefCell::new(Turn::None)); }
The data is wrapped in a RefCell
to permit interior mutability. You can learn more about RefCell
by reading
its documentation and the relevant 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
, which
ensures that the code in the function or closure cannot itself be interrupted.
First, we will initialise the buttons.
#![allow(unused)] fn main() { use cortex_m::interrupt::free; use microbit::{ board::Buttons, pac::{self, GPIOTE} }; // ... /// Initialise the buttons and enable interrupts. pub(crate) fn init_buttons(board_gpiote: GPIOTE, board_buttons: Buttons) { let gpiote = Gpiote::new(board_gpiote); let channel0 = gpiote.channel0(); channel0 .input_pin(&board_buttons.button_a.degrade()) .hi_to_lo() .enable_interrupt(); channel0.reset_events(); let channel1 = gpiote.channel1(); channel1 .input_pin(&board_buttons.button_b.degrade()) .hi_to_lo() .enable_interrupt(); channel1.reset_events(); 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.
We connect channel0
to button_a
and channel1
to button_b
and, in each case, tell them 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).
Next, we write the code that handles the interrupt. We use the interrupt
macro provided by microbit::pac
(in the
case of the v2, it is 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]
.
#![allow(unused)] fn main() { use microbit::pac::interrupt; // ... #[interrupt] fn GPIOTE() { 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. The relevant turn is
stored in the TURN
Mutex. All of this happens within a free
block, to ensure that we cannot be interrupted again
while handling this interrupt.
Finally, we expose a simple function to get the next turn.
#![allow(unused)] fn main() { /// Get the next turn (i.e., the turn corresponding to the most recently pressed button). pub fn get_turn(reset: bool) -> Turn { 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
.
Updating the main
file
Returning to our main
function, we need to add a call to init_buttons
before our main loop, and in the game loop,
replace our placeholder Turn::None
argument to the game.step
method with the value returned by get_turn
.
#![no_main] #![no_std] mod game; mod control; use cortex_m_rt::entry; use microbit::{ Board, hal::{prelude::*, Rng, Timer}, display::blocking::Display }; use rtt_target::rtt_init_print; use panic_rtt_target as _; use crate::game::{Game, GameStatus}; use crate::control::{init_buttons, get_turn}; #[entry] fn main() -> ! { rtt_init_print!(); let mut board = Board::take().unwrap(); let mut timer = Timer::new(board.TIMER0); let mut rng = Rng::new(board.RNG); let mut game = Game::new(rng.random_u32()); let mut display = Display::new(board.display_pins); init_buttons(board.GPIOTE, board.buttons); loop { // Main loop loop { // Game loop let image = game.game_matrix(9, 9, 9); // The brightness values are meaningless at the moment as we haven't yet // implemented a display capable of displaying different brightnesses display.show(&mut timer, image, game.step_len_ms()); match game.status { GameStatus::Ongoing => game.step(get_turn(true)), _ => { for _ in 0..3 { display.clear(); timer.delay_ms(200u32); display.show(&mut timer, image, 200); } display.clear(); display.show(&mut timer, game.score_matrix(), 1000); break } } } game.reset(); } }
Now we can control the snake using the micro:bit's buttons!