Using the non-blocking display

We will next display the snake and food on the LEDs of the MB2 screen. So far, we have used the blocking interface, which provides for LEDs to be either maximally bright or turned off. With this, a basic functioning snake game would be possible. But you might find that when the snake got a bit longer, it would be difficult to tell the snake from the food, and to tell which direction the snake was heading. Let's figure out how to allow the LED brightness to vary: we can make the snake's body a bit dimmer, which will help sort out the clutter.

The microbit library makes available two different interfaces to the LED matrix. There is the blocking interface we've already seen in previous chapters. There is also 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.

(There is no great reason the two display modes of the microbit library crate have to be separate and use separate code. A more complete design would allow either non-blocking or blocking use of a single display API with variable brightness levels and refresh rates specified by the user. Never assume that the stuff you have been handed is perfected, or even close. Always think about what you might do differently. For now, though, we'll work with what we have, which is adequate for our immediate purpose.)

The code to interact with the non-blocking interface (src/display.rs) is pretty simple and will follow a similar structure to the code we used to interact with the buttons. This time we'll start at the top level.

Display module

#![allow(unused)]
fn main() {
pub mod interrupt;
pub mod show;

pub use show::{clear_display, display_image};

use core::cell::RefCell;
use cortex_m::interrupt::{free as 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 fn init_display(board_timer: TIMER1, board_display: DisplayPins) {
    let display = Display::new(board_timer, board_display);

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

Display API

We then define a couple of convenience functions which allow us to easily set (or unset) the image to be displayed (src/display/show.rs).

#![allow(unused)]
fn main() {
use super::DISPLAY;

use cortex_m::interrupt::free as interrupt_free;

use tiny_led_matrix::Render;

/// Display an image.
pub fn display_image(image: &impl Render) {
    interrupt_free(|cs| {
        if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() {
            display.show(image);
        }
    })
}

/// Clear the display (turn off all LEDs).
pub fn clear_display() {
    interrupt_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.

Display interrupt handling

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 (src/display/interrupt.rs).

#![allow(unused)]
fn main() {
use super::DISPLAY;

use cortex_m::interrupt::free as interrupt_free;
use microbit::pac::{self, interrupt};

#[pac::interrupt]
fn TIMER1() {
    interrupt_free(|cs| {
        if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() {
            display.handle_display_event();
        }
    })
}
}

Now we can understand how our main function will do display: we will call init_display and use the new functions we have defined to interact with it.