使用非阻塞显示

我们现在有一个基本功能齐全的贪吃蛇游戏。 但你可能发现,当蛇变长一些时,很难区分蛇和食物,以及判断蛇的行进方向,因为所有LED的亮度都是一样的。 让我们来解决这个问题。

microbit库提供了两种不同的LED矩阵接口:一种是我们一直在使用的、基本的、阻塞式接口,另一种是非阻塞接口,它允许你自定义每个LED的亮度。 在硬件层面上,每个LED要么是“开”的,要么是“关”的,但microbit::display::nonblocking模块通过快速开关LED来模拟每个LED的十个亮度级别。

与非阻塞接口交互的代码非常简单,并且会遵循与我们用于与按钮交互的代码类似的结构。

#![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)
    }
}
}

首先,我们初始化一个代表LED显示的microbit::display::nonblocking::Display结构体,将其传递给板子的TIMER1DisplayPins外设。 然后我们将显示存储在一个互斥锁中。 最后,我们取消TIMER1中断的屏蔽。

然后我们定义了几个便利函数,允许我们轻松地设置(或取消设置)要显示的图像。

#![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函数接收一张图像,并指示显示屏显示它。 就像它调用的Display::show方法一样,这个函数接收一个实现了tiny_led_matrix::Render特性的结构体。 这个特性确保了结构体包含了Display在LED矩阵上渲染它所需的数据和方法。 microbit::display::nonblocking模块提供的Render的两种实现是BitImageGreyscaleImage。 在BitImage中,每个“像素”(或LED)要么被点亮,要么没有(就像我们使用阻塞式接口时一样),而在GreyscaleImage中,每个“像素”可以有不同的亮度。

clear_display函数正如其名,做的就是清除显示。

最后,我们使用interrupt宏来定义TIMER1中断的处理程序。 这个中断每秒触发多次,这就是允许Display快速循环不同LED的开和关,以产生不同亮度级别的错觉。 我们处理程序代码所做的就是调用Display::handle_display_event方法,它处理这个。

#![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();
        }
    })
}
}

现在我们只需要更新我们的main函数,调用init_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();
    }
}