Skip to content

Commit

Permalink
Merge pull request #555 from bunburya/snake-game
Browse files Browse the repository at this point in the history
adding chapter 11 (snake game)
  • Loading branch information
eldruin authored Feb 5, 2024
2 parents d5c0248 + f9a6796 commit 9fce2cd
Show file tree
Hide file tree
Showing 17 changed files with 1,531 additions and 7 deletions.
1 change: 1 addition & 0 deletions microbit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"src/08-i2c",
"src/09-led-compass",
"src/10-punch-o-meter",
"src/11-snake-game",
]

[profile.release]
Expand Down
4 changes: 4 additions & 0 deletions microbit/src/11-snake-game/.cargo/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = [
"-C", "link-arg=-Tlink.x",
]
30 changes: 30 additions & 0 deletions microbit/src/11-snake-game/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "snake-game"
version = "0.1.0"
authors = ["Alan Bunbury <[email protected]>"]
edition = "2018"

[dependencies.microbit-v2]
version = "0.13.0"
optional = true

# NOTE: We define a dependency for v1 here so that CI checks pass, and to facilitate future porting of the snake game
# to the micro:bit v1. However, the code has not been written for, or tested on, the v1 and may not work.
[dependencies.microbit]
version = "0.13.0"
optional = true

[dependencies]
cortex-m = "0.7.3"
cortex-m-rt = "0.7.0"
rtt-target = { version = "0.3.1", features = ["cortex-m"] }
panic-rtt-target = { version = "0.1.2", features = ["cortex-m"] }
lsm303agr = "0.2.2"
nb = "1.0.0"
libm = "0.2.1"
heapless = "0.8.0"
tiny-led-matrix = "1.0.1"

[features]
v2 = ["microbit-v2"]
v1 = ["microbit"]
11 changes: 11 additions & 0 deletions microbit/src/11-snake-game/Embed.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[default.general]
chip = "nrf52833_xxAA" # micro:bit V2

[default.reset]
halt_afterwards = false

[default.rtt]
enabled = true

[default.gdb]
enabled = false
19 changes: 19 additions & 0 deletions microbit/src/11-snake-game/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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.

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.

> **NOTE** This chapter has been developed for the micro:bit v2 only, not the v1. Contributions to port the code to the
> v1 are welcome.
> **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.
201 changes: 201 additions & 0 deletions microbit/src/11-snake-game/controls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# 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 (**G**eneral **P**urpose **I**nput/**O**utput **T**asks and **E**vents) 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.

```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));
```

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.

First, we will initialise the buttons.

```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);
});
}
```

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

```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;
}
});
}
```

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.

```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
})
}
```

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 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!
Loading

0 comments on commit 9fce2cd

Please sign in to comment.