Using the non-blocking display
We now have a basic functioning snake game. But you might find that when the snake gets a bit longer, it can be difficult to tell the snake from the food, and to tell which direction the snake is heading, because all LEDs are the same brightness. Let's fix that.
The microbit
library makes available two different interfaces to the LED matrix: a basic, blocking interface, which
we have been using, and a non-blocking interface which allows you to customise the brightness of each LED. At the
hardware level, each LED is either "on" or "off", but the microbit::display::nonblocking
module simulates ten levels
of brightness for each LED by rapidly switching the LED on and off.
The code to interact with the non-blocking interface is pretty simple and will follow a similar structure to the code we used to interact with the buttons.
#![allow(unused)] fn main() { use core::cell::RefCell; use cortex_m::interrupt::{free, Mutex}; use microbit::display::nonblocking::Display; use microbit::gpio::DisplayPins; use microbit::pac; use microbit::pac::TIMER1; static DISPLAY: Mutex<RefCell<Option<Display<TIMER1>>>> = Mutex::new(RefCell::new(None)); pub(crate) fn init_display(board_timer: TIMER1, board_display: DisplayPins) { let display = Display::new(board_timer, board_display); free(move |cs| { *DISPLAY.borrow(cs).borrow_mut() = Some(display); }); unsafe { pac::NVIC::unmask(pac::Interrupt::TIMER1) } } }
First, we initialise a microbit::display::nonblocking::Display
struct representing the LED display, passing it the
board's TIMER1
and DisplayPins
peripherals. Then we store the display in a Mutex. Finally, we unmask the TIMER1
interrupt.
We then define a couple of convenience functions which allow us to easily set (or unset) the image to be displayed.
#![allow(unused)] fn main() { use tiny_led_matrix::Render; // ... /// Display an image. pub(crate) fn display_image(image: &impl Render) { free(|cs| { if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { display.show(image); } }) } /// Clear the display (turn off all LEDs). pub(crate) fn clear_display() { free(|cs| { if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { display.clear(); } }) } }
display_image
takes an image and tells the display to show it. Like the Display::show
method that it calls, this
function takes a struct that implements the tiny_led_matrix::Render
trait. That trait ensures that the struct contains
the data and methods necessary for the Display
to render it on the LED matrix. The two implementations of Render
provided by the microbit::display::nonblocking
module are BitImage
and GreyscaleImage
. In a BitImage
, each
"pixel" (or LED) is either illuminated or not (like when we used the blocking interface), whereas in a
GreyscaleImage
each "pixel" can have a different brightness.
clear_display
does exactly as the name suggests.
Finally, we use the interrupt
macro to define a handler for the TIMER1
interrupt. This interrupt fires many times a
second, and this is what allows the Display
to rapidly cycle the different LEDs on and off to give the illusion of
varying brightness levels. All our handler code does is call the Display::handle_display_event
method, which handles
this.
#![allow(unused)] fn main() { use microbit::pac::interrupt; // ... #[interrupt] fn TIMER1() { free(|cs| { if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { display.handle_display_event(); } }) } }
Now we just need to update our main
function to call init_display
and use the new functions we have defined to
interact with our fancy new display.
#![no_main] #![no_std] mod game; mod control; mod display; use cortex_m_rt::entry; use microbit::{ Board, hal::{prelude::*, Rng, Timer}, display::nonblocking::{BitImage, GreyscaleImage} }; use rtt_target::rtt_init_print; use panic_rtt_target as _; use crate::control::{get_turn, init_buttons}; use crate::display::{clear_display, display_image, init_display}; use crate::game::{Game, GameStatus}; #[entry] fn main() -> ! { rtt_init_print!(); let mut board = Board::take().unwrap(); let mut timer = Timer::new(board.TIMER0).into_periodic(); let mut rng = Rng::new(board.RNG); let mut game = Game::new(rng.random_u32()); init_buttons(board.GPIOTE, board.buttons); init_display(board.TIMER1, board.display_pins); loop { loop { // Game loop let image = GreyscaleImage::new(&game.game_matrix(6, 3, 9)); display_image(&image); timer.delay_ms(game.step_len_ms()); match game.status { GameStatus::Ongoing => game.step(get_turn(true)), _ => { for _ in 0..3 { clear_display(); timer.delay_ms(200u32); display_image(&image); timer.delay_ms(200u32); } clear_display(); display_image(&BitImage::new(&game.score_matrix())); timer.delay_ms(2000u32); break } } } game.reset(); } }