Skip to content

Commit 9fce2cd

Browse files
authored
Merge pull request #555 from bunburya/snake-game
adding chapter 11 (snake game)
2 parents d5c0248 + f9a6796 commit 9fce2cd

File tree

17 files changed

+1531
-7
lines changed

17 files changed

+1531
-7
lines changed

microbit/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ members = [
66
"src/08-i2c",
77
"src/09-led-compass",
88
"src/10-punch-o-meter",
9+
"src/11-snake-game",
910
]
1011

1112
[profile.release]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
2+
rustflags = [
3+
"-C", "link-arg=-Tlink.x",
4+
]

microbit/src/11-snake-game/Cargo.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[package]
2+
name = "snake-game"
3+
version = "0.1.0"
4+
authors = ["Alan Bunbury <dev@bunburya.eu>"]
5+
edition = "2018"
6+
7+
[dependencies.microbit-v2]
8+
version = "0.13.0"
9+
optional = true
10+
11+
# NOTE: We define a dependency for v1 here so that CI checks pass, and to facilitate future porting of the snake game
12+
# to the micro:bit v1. However, the code has not been written for, or tested on, the v1 and may not work.
13+
[dependencies.microbit]
14+
version = "0.13.0"
15+
optional = true
16+
17+
[dependencies]
18+
cortex-m = "0.7.3"
19+
cortex-m-rt = "0.7.0"
20+
rtt-target = { version = "0.3.1", features = ["cortex-m"] }
21+
panic-rtt-target = { version = "0.1.2", features = ["cortex-m"] }
22+
lsm303agr = "0.2.2"
23+
nb = "1.0.0"
24+
libm = "0.2.1"
25+
heapless = "0.8.0"
26+
tiny-led-matrix = "1.0.1"
27+
28+
[features]
29+
v2 = ["microbit-v2"]
30+
v1 = ["microbit"]

microbit/src/11-snake-game/Embed.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[default.general]
2+
chip = "nrf52833_xxAA" # micro:bit V2
3+
4+
[default.reset]
5+
halt_afterwards = false
6+
7+
[default.rtt]
8+
enabled = true
9+
10+
[default.gdb]
11+
enabled = false

microbit/src/11-snake-game/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Snake game
2+
3+
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
4+
display and its two buttons as controls. In doing so, we will build on some of the concepts covered in the earlier
5+
chapters of this book, and also learn about some new peripherals and concepts.
6+
7+
In particular, we will be using the concept of hardware interrupts to allow our program to interact with multiple
8+
peripherals at once. Interrupts are a common way to implement concurrency in embedded contexts. There is a good
9+
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
10+
before proceeding.
11+
12+
> **NOTE** This chapter has been developed for the micro:bit v2 only, not the v1. Contributions to port the code to the
13+
> v1 are welcome.
14+
15+
> **NOTE** In this chapter, we are going to use later versions of certain libraries that have been used in previous
16+
> chapters. We are going to use version 0.13.0 of the `microbit` library (the preceding chapters have used 0.12.0).
17+
> 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
18+
> version 0.8.0 of the `heapless` library (previous chapters used version 0.7.10), which allows us to use certain of its
19+
> data structures with structs that implement Rust's `core::Hash` trait.
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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

Comments
 (0)