Skip to content

Commit

Permalink
edited ch 11
Browse files Browse the repository at this point in the history
  • Loading branch information
BartMassey committed Jul 22, 2024
1 parent 0303290 commit 7d5ea0d
Show file tree
Hide file tree
Showing 21 changed files with 581 additions and 1,176 deletions.
33 changes: 21 additions & 12 deletions mdbook/src/11-snake-game/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
# Snake game

We're now going to implement a basic [snake](https://en.wikipedia.org/wiki/Snake_(video_game_genre)) game that you can play on a micro:bit v2 using its 5x5 LED matrix as a
display and its two buttons as controls. In doing so, we will build on some of the concepts covered in the earlier
chapters of this book, and also learn about some new peripherals and concepts.
We're now going to implement a basic [snake](https://en.wikipedia.org/wiki/Snake_(video_game_genre))
game that you can play on an MB2 using its 5×5 LED matrix as a display and its two buttons as
controls. In doing so, we will build on some of the concepts covered in the earlier chapters of this
book, and also learn about some new peripherals and concepts.

In particular, we will be using the concept of hardware interrupts to allow our program to interact with multiple
peripherals at once. Interrupts are a common way to implement concurrency in embedded contexts. There is a good
introduction to concurrency in an embedded context in the [Embedded Rust Book](https://docs.rust-embedded.org/book/concurrency/index.html) that I suggest you read through
before proceeding.
In particular, we will be using the concept of hardware interrupts to allow our program to interact
with multiple peripherals at once. Interrupts are a common way to implement concurrency in embedded
contexts. There is a good introduction to concurrency in an embedded context [here] that
you might read through before proceeding.

> **NOTE** In this chapter, we are going to use later versions of certain libraries that have been used in previous
> chapters. We are going to use version 0.13.0 of the `microbit` library (the preceding chapters have used 0.12.0).
> Version 0.13.0 fixes a couple of bugs in the non-blocking display code that we will be using. We are also going to use
> version 0.8.0 of the `heapless` library (previous chapters used version 0.7.10), which allows us to use certain of its
> data structures with structs that implement Rust's `core::Hash` trait.
[here]: https://docs.rust-embedded.org/book/concurrency/index.html

## Modularity

The source code here is more modular than it probably should be. This fine-grained modularity allows
us to look at the source code a little at a time. We will build the code bottom-up: we will first
build three modules — `game`, `controls` and `display`, and then compose these to build the final
program. Each module will have a top-level source file and one or more included source files: for
example, the `game` module will consist of `src/game.rs`, `src/game/coords.rs`,
`src/game/movement.rs`, etc. The Rust `mod` statement is used to combine the various components of
the module. *The Rust Programming Language* has a good [description] of Rust's module system.

[description]: https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html
241 changes: 63 additions & 178 deletions mdbook/src/11-snake-game/controls.md
Original file line number Diff line number Diff line change
@@ -1,202 +1,87 @@
# 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.
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 (**G**eneral **P**urpose **I**nput/**O**utput **T**asks and **E**vents) peripheral.
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

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.

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.

```rust
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));
```
[RefCell documentation]: https://doc.rust-lang.org/std/cell/struct.RefCell.html
[interior mutability chapter]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html
[critical section]: https://en.wikipedia.org/wiki/Critical_section

The data is wrapped in a `RefCell` to permit interior mutability. You can learn more about `RefCell` by reading
[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).
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](https://en.wikipedia.org/wiki/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.
### Initialization

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

```rust
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);
});
}
{{#include src/controls/init.rs}}
```

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 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](https://docs.rs/nrf52833-hal/latest/nrf52833_hal/pac/enum.Interrupt.html)) and annotate it with `#[interrupt]`.
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](https://docs.rs/nrf52833-hal/latest/nrf52833_hal/pac/enum.Interrupt.html)) and annotate it
with `#[interrupt]` (`src/controls/interrupt.rs`).

```rust
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;
}
});
}
{{#include src/controls/interrupt.rs}}
```

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.
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.
Finally, we expose a simple function to get the next turn (`src/controls.rs`).

```rust
/// 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
})
}
{{#include src/controls.rs}}
```

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`.

```rust
#![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 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();
}
}
```
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`.

Now we can control the snake using the micro:bit's buttons!
Next we will build support for a high-fidelity game display.
30 changes: 30 additions & 0 deletions mdbook/src/11-snake-game/final-assembly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Snake game: final assembly

The code in our `src/main.rs` file brings all the previously-discussed machinery together to make
our final game.

```rust
{{#include src/main.rs}}
```

After initialising the board and its timer and RNG peripherals, we initialise a `Game` struct and a
`Display` from the `microbit::display::blocking` module.

In our "game loop" (which runs inside of the "main loop" we place in our `main` function), we
repeatedly perform the following steps:

1. Get a 5×5 array of bytes representing the grid. The `Game::get_matrix` method takes three integer
arguments (which should be between 0 and 9, inclusive) which will, eventually, represent how
brightly the head, tail and food should be displayed.

2. Display the matrix, for an amount of time determined by the `Game::step_len_ms` method. As
currently implemented, this method basically provides for 1 second between steps, reducing by
200ms every time the player scores 5 points (eating 1 piece of food = 1 point), subject to a
floor of 200ms.

3. Check the game status. If it is `Ongoing` (which is its initial value), run a step of the game
and update the game state (including its `status` property). Otherwise, the game is over, so
flash the current image three times, then show the player's score (represented as a number of
illuminated LEDs corresponding to the score), and exit the game loop.

Our main loop just runs the game loop repeatedly, resetting the game's state after each iteration.
Loading

0 comments on commit 7d5ea0d

Please sign in to comment.