-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0303290
commit 7d5ea0d
Showing
21 changed files
with
581 additions
and
1,176 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
Oops, something went wrong.