|
| 1 | +# Controls |
| 2 | + |
| 3 | +Our protagonist will be controlled by the two buttons on the front of the micro:bit. Button A will turn to the (snake's) |
| 4 | +left, and button B will turn to the (snake's) right. |
| 5 | + |
| 6 | +We will use the `microbit::pac::interrupt` macro to handle button presses in a concurrent way. The interrupt will be |
| 7 | +generated by the micro:bit's GPIOTE (**G**eneral **P**urpose **I**nput/**O**utput **T**asks and **E**vents) peripheral. |
| 8 | + |
| 9 | +## The `controls` module |
| 10 | + |
| 11 | +Code in this section should be placed in a separate file, `controls.rs`, in our `src` directory. |
| 12 | + |
| 13 | +We will need to keep track of two separate pieces of global mutable state: A reference to the `GPIOTE` peripheral, and a |
| 14 | +record of the selected direction to turn next. |
| 15 | + |
| 16 | +```rust |
| 17 | +use core::cell::RefCell; |
| 18 | +use cortex_m::interrupt::Mutex; |
| 19 | +use microbit::hal::gpiote::Gpiote; |
| 20 | +use crate::game::Turn; |
| 21 | + |
| 22 | +// ... |
| 23 | + |
| 24 | +static GPIO: Mutex<RefCell<Option<Gpiote>>> = Mutex::new(RefCell::new(None)); |
| 25 | +static TURN: Mutex<RefCell<Turn>> = Mutex::new(RefCell::new(Turn::None)); |
| 26 | +``` |
| 27 | + |
| 28 | +The data is wrapped in a `RefCell` to permit interior mutability. You can learn more about `RefCell` by reading |
| 29 | +[its documentation](https://doc.rust-lang.org/std/cell/struct.RefCell.html) and the relevant chapter of [the Rust Book](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html). |
| 30 | +The `RefCell` is, in turn, wrapped in a `cortex_m::interrupt::Mutex` to allow safe access. |
| 31 | +The Mutex provided by the `cortex_m` crate uses the concept of a [critical section](https://en.wikipedia.org/wiki/Critical_section). |
| 32 | +Data in a Mutex can only be accessed from within a function or closure passed to `cortex_m::interrupt:free`, which |
| 33 | +ensures that the code in the function or closure cannot itself be interrupted. |
| 34 | + |
| 35 | +First, we will initialise the buttons. |
| 36 | + |
| 37 | +```rust |
| 38 | +use cortex_m::interrupt::free; |
| 39 | +use microbit::{ |
| 40 | + board::Buttons, |
| 41 | + pac::{self, GPIOTE} |
| 42 | +}; |
| 43 | + |
| 44 | +// ... |
| 45 | + |
| 46 | +/// Initialise the buttons and enable interrupts. |
| 47 | +pub(crate) fn init_buttons(board_gpiote: GPIOTE, board_buttons: Buttons) { |
| 48 | + let gpiote = Gpiote::new(board_gpiote); |
| 49 | + |
| 50 | + let channel0 = gpiote.channel0(); |
| 51 | + channel0 |
| 52 | + .input_pin(&board_buttons.button_a.degrade()) |
| 53 | + .hi_to_lo() |
| 54 | + .enable_interrupt(); |
| 55 | + channel0.reset_events(); |
| 56 | + |
| 57 | + let channel1 = gpiote.channel1(); |
| 58 | + channel1 |
| 59 | + .input_pin(&board_buttons.button_b.degrade()) |
| 60 | + .hi_to_lo() |
| 61 | + .enable_interrupt(); |
| 62 | + channel1.reset_events(); |
| 63 | + |
| 64 | + free(move |cs| { |
| 65 | + *GPIO.borrow(cs).borrow_mut() = Some(gpiote); |
| 66 | + |
| 67 | + unsafe { |
| 68 | + pac::NVIC::unmask(pac::Interrupt::GPIOTE); |
| 69 | + } |
| 70 | + pac::NVIC::unpend(pac::Interrupt::GPIOTE); |
| 71 | + }); |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +The `GPIOTE` peripheral on the nRF52 has 8 "channels", each of which can be connected to a `GPIO` pin and configured to |
| 76 | +respond to certain events, including rising edge (transition from low to high signal) and falling edge (high to low |
| 77 | +signal). A button is a `GPIO` pin which has high signal when not pressed and low signal otherwise. Therefore, a button |
| 78 | +press is a falling edge. |
| 79 | + |
| 80 | +We connect `channel0` to `button_a` and `channel1` to `button_b` and, in each case, tell them to generate events on a |
| 81 | +falling edge (`hi_to_lo`). We store a reference to our `GPIOTE` peripheral in the `GPIO` Mutex. We then `unmask` `GPIOTE` |
| 82 | +interrupts, allowing them to be propagated by the hardware, and call `unpend` to clear any interrupts with pending |
| 83 | +status (which may have been generated prior to the interrupts being unmasked). |
| 84 | + |
| 85 | +Next, we write the code that handles the interrupt. We use the `interrupt` macro provided by `microbit::pac` (in the |
| 86 | +case of the v2, it is re-exported from the `nrf52833_hal` crate). We define a function with the same name as the |
| 87 | +interrupt we want to handle (you can see them all [here](https://docs.rs/nrf52833-hal/latest/nrf52833_hal/pac/enum.Interrupt.html)) and annotate it with `#[interrupt]`. |
| 88 | + |
| 89 | +```rust |
| 90 | +use microbit::pac::interrupt; |
| 91 | + |
| 92 | +// ... |
| 93 | + |
| 94 | +#[interrupt] |
| 95 | +fn GPIOTE() { |
| 96 | + free(|cs| { |
| 97 | + if let Some(gpiote) = GPIO.borrow(cs).borrow().as_ref() { |
| 98 | + let a_pressed = gpiote.channel0().is_event_triggered(); |
| 99 | + let b_pressed = gpiote.channel1().is_event_triggered(); |
| 100 | + |
| 101 | + let turn = match (a_pressed, b_pressed) { |
| 102 | + (true, false) => Turn::Left, |
| 103 | + (false, true) => Turn::Right, |
| 104 | + _ => Turn::None |
| 105 | + }; |
| 106 | + |
| 107 | + gpiote.channel0().reset_events(); |
| 108 | + gpiote.channel1().reset_events(); |
| 109 | + |
| 110 | + *TURN.borrow(cs).borrow_mut() = turn; |
| 111 | + } |
| 112 | + }); |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +When a `GPIOTE` interrupt is generated, we check each button to see whether it has been pressed. If only button A has been |
| 117 | +pressed, we record that the snake should turn to the left. If only button B has been pressed, we record that the snake |
| 118 | +should turn to the right. In any other case, we record that the snake should not make any turn. The relevant turn is |
| 119 | +stored in the `TURN` Mutex. All of this happens within a `free` block, to ensure that we cannot be interrupted again |
| 120 | +while handling this interrupt. |
| 121 | + |
| 122 | +Finally, we expose a simple function to get the next turn. |
| 123 | + |
| 124 | +```rust |
| 125 | +/// Get the next turn (i.e., the turn corresponding to the most recently pressed button). |
| 126 | +pub fn get_turn(reset: bool) -> Turn { |
| 127 | + free(|cs| { |
| 128 | + let turn = *TURN.borrow(cs).borrow(); |
| 129 | + if reset { |
| 130 | + *TURN.borrow(cs).borrow_mut() = Turn::None |
| 131 | + } |
| 132 | + turn |
| 133 | + }) |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +This function simply returns the current value of the `TURN` Mutex. It takes a single boolean argument, `reset`. If |
| 138 | +`reset` is `true`, the value of `TURN` is reset, i.e., set to `Turn::None`. |
| 139 | + |
| 140 | +## Updating the `main` file |
| 141 | + |
| 142 | +Returning to our `main` function, we need to add a call to `init_buttons` before our main loop, and in the game loop, |
| 143 | +replace our placeholder `Turn::None` argument to the `game.step` method with the value returned by `get_turn`. |
| 144 | + |
| 145 | +```rust |
| 146 | +#![no_main] |
| 147 | +#![no_std] |
| 148 | + |
| 149 | +mod game; |
| 150 | +mod control; |
| 151 | + |
| 152 | +use cortex_m_rt::entry; |
| 153 | +use microbit::{ |
| 154 | + Board, |
| 155 | + hal::{prelude::*, Rng, Timer}, |
| 156 | + display::blocking::Display |
| 157 | +}; |
| 158 | +use rtt_target::rtt_init_print; |
| 159 | +use panic_rtt_target as _; |
| 160 | + |
| 161 | +use crate::game::{Game, GameStatus}; |
| 162 | +use crate::control::{init_buttons, get_turn}; |
| 163 | + |
| 164 | +#[entry] |
| 165 | +fn main() -> ! { |
| 166 | + rtt_init_print!(); |
| 167 | + let mut board = Board::take().unwrap(); |
| 168 | + let mut timer = Timer::new(board.TIMER0); |
| 169 | + let mut rng = Rng::new(board.RNG); |
| 170 | + let mut game = Game::new(rng.random_u32()); |
| 171 | + |
| 172 | + let mut display = Display::new(board.display_pins); |
| 173 | + |
| 174 | + init_buttons(board.GPIOTE, board.buttons); |
| 175 | + |
| 176 | + loop { // Main loop |
| 177 | + loop { // Game loop |
| 178 | + let image = game.game_matrix(9, 9, 9); |
| 179 | + // The brightness values are meaningless at the moment as we haven't yet |
| 180 | + // implemented a display capable of displaying different brightnesses |
| 181 | + display.show(&mut timer, image, game.step_len_ms()); |
| 182 | + match game.status { |
| 183 | + GameStatus::Ongoing => game.step(get_turn(true)), |
| 184 | + _ => { |
| 185 | + for _ in 0..3 { |
| 186 | + display.clear(); |
| 187 | + timer.delay_ms(200u32); |
| 188 | + display.show(&mut timer, image, 200); |
| 189 | + } |
| 190 | + display.clear(); |
| 191 | + display.show(&mut timer, game.score_matrix(), 1000); |
| 192 | + break |
| 193 | + } |
| 194 | + } |
| 195 | + } |
| 196 | + game.reset(); |
| 197 | + } |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +Now we can control the snake using the micro:bit's buttons! |
0 commit comments