ノンブロッキングディスプレイを使う
これで、基本的に動作するスネークゲームができました。しかし、ヘビが少し長くなると、 ヘビとエサを見分けたり、ヘビがどちらの方向に進んでいるのかを判断したりするのが難しくなるかもしれません。すべての LED が 同じ明るさだからです。これを直しましょう。
microbit ライブラリは LED マトリクスに対する 2 種類の異なるインターフェースを提供しています。これまで使ってきた基本的な
ブロッキングインターフェースと、各 LED の明るさをカスタマイズできるノンブロッキングインターフェースです。ハードウェアレベルでは各
LED は「オン」か「オフ」かのどちらかですが、microbit::display::nonblocking モジュールは LED を高速にオン・オフ
することで、各 LED に対して 10 段階の明るさをシミュレートします。
ノンブロッキングインターフェースとやり取りするコードはかなりシンプルで、ボタンとやり取りするために使ったコードと似た構成に なります。
#![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 構造体を初期化し、そこに
board の TIMER1 と DisplayPins ペリフェラルを渡します。次にディスプレイを Mutex に格納します。最後に、TIMER1
割り込みをアンマスクします。
次に、表示する画像を簡単に設定(または解除)できるようにする便利関数を 2 つ定義します。
#![allow(unused)]
fn main() {
use tiny_led_matrix::Render;
// ...
/// 画像を表示する。
pub(crate) fn display_image(image: &impl Render) {
free(|cs| {
if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() {
display.show(image);
}
})
}
/// ディスプレイをクリアする(すべての LED を消灯する)。
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 トレイトを実装した構造体を受け取ります。そのトレイトは、その構造体が LED マトリクス上に
Display がそれを描画するために必要なデータとメソッドを備えていることを保証します。microbit::display::nonblocking
モジュールが提供する Render の 2 つの実装は、BitImage と GreyscaleImage です。BitImage では、各
「ピクセル」(つまり LED)は点灯しているかしていないかのどちらかであり(ブロッキングインターフェースを使ったときと同様です)、一方で
GreyscaleImage では各「ピクセル」に異なる明るさを持たせることができます。
clear_display は名前のとおりの動作をします。
最後に、interrupt マクロを使って TIMER1 割り込みのハンドラを定義します。この割り込みは 1 秒間に何度も発生し、これによって
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 { // ゲームループ
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();
}
}