ノンブロッキングディスプレイを使う
次に、MB2 画面の LED にヘビと食べ物を表示します。これまでは、LED を最大輝度で点灯するか消灯するかのどちらかにする ブロッキングインターフェースを使ってきました。これでも、基本的に動作する snake ゲームを作ることは可能です。ですが、ヘビが少し 長くなってくると、ヘビと食べ物を見分けることや、ヘビがどちらの方向に進んでいるのかを判断することが難しくなるかもしれません。 LED の明るさを変えられるようにする方法を考えてみましょう。ヘビの胴体を少し暗くすれば、ごちゃついた表示を整理しやすくなります。
microbit ライブラリでは、LED マトリクスに対する 2 種類の異なるインターフェースが利用できます。1 つは、
これまでの章ですでに見てきたブロッキングインターフェースです。もう 1 つはノンブロッキングインターフェースで、
各 LED の明るさをカスタマイズできます。ハードウェアレベルでは各 LED は「点灯」か「消灯」かのどちらかですが、
microbit::display::nonblocking モジュールは、LED を高速にオン/オフすることで、各 LED について 10 段階の明るさを
シミュレートします。
(microbit ライブラリクレートの 2 つの表示モードが、別々になっていて別々のコードを使う必要がある大きな理由は
ありません。より完全な設計であれば、単一のディスプレイ API について、ユーザーが明るさレベルやリフレッシュレートを指定したうえで、
ノンブロッキングまたはブロッキングのどちらの使い方もできるようにするでしょう。手渡されたものが完成されているとか、
完成に近いとさえ思い込んではいけません。常に、自分なら何を違うやり方でできるかを考えてください。とはいえ今は、
当面の目的には十分な、今あるものを使って進めます。)
ノンブロッキングインターフェースとやり取りするコード(src/display.rs)はかなり単純で、ボタンとやり取りするために使ったコードと
似た構造になります。今回はトップレベルから始めましょう。
ディスプレイモジュール
#![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) }
}
}
まず、LED ディスプレイを表す microbit::display::nonblocking::Display 構造体を初期化し、
ボードの TIMER1 と DisplayPins ペリフェラルを渡します。次に、そのディスプレイを Mutex に格納します。
最後に、TIMER1 割り込みをアンマスクします。
Display API
次に、表示するイメージを簡単に設定(または解除)できるようにするための、いくつかの便利な関数を定義します
(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 はイメージを受け取り、それを表示するようディスプレイに指示します。これが呼び出す Display::show メソッドと同様に、
この関数は tiny_led_matrix::Render トレイトを実装した構造体を受け取ります。このトレイトは、その構造体が、
Display がそれを LED マトリクス上にレンダリングするために必要なデータとメソッドを備えていることを保証します。
microbit::display::nonblocking モジュールが提供する Render の実装は、BitImage と GreyscaleImage の 2 つです。
BitImage では各「ピクセル」(または LED)は点灯しているかしていないかのどちらかですが(ブロッキングインターフェースを使ったときと同様)、
GreyscaleImage では各「ピクセル」がそれぞれ異なる明るさを持てます。
clear_display は、その名前が示すとおりのことを行います。
ディスプレイ割り込み処理
最後に、interrupt マクロを使って TIMER1 割り込みのハンドラを定義します。この割り込みは 1 秒間に何度も発生し、
これによって Display は異なる LED を高速に順番にオン/オフし、明るさが変化しているような錯覚を与えられます。
このハンドラのコードが行うことは、これを処理する Display::handle_display_event メソッドを呼び出すことだけです
(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();
}
})
}
}
これで、main 関数がどのように表示を行うのかがわかります。init_display を呼び出し、
新たに定義した関数を使ってこれとやり取りします。