Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

操作

主人公は micro:bit の前面にある 2 つのボタンで操作します。ボタン A は(ヘビを) 左に曲げ、ボタン B は(ヘビを)右に曲げます。

ボタンの押下を並行的に処理するために、microbit::pac::interrupt マクロを使用します。割り込みは micro:bit の GPIOTE(General Purpose Input/Output Tasks and Events)ペリフェラルによって生成されます。

controls モジュール

このセクションのコードは、src ディレクトリ内の別ファイル controls.rs に配置してください。

2 つの独立したグローバルな可変状態を追跡する必要があります。1 つは GPIOTE ペリフェラルへの参照、もう 1 つは 次に曲がる向きの選択結果を記録したものです。

#![allow(unused)]
fn main() {
use core::cell::RefCell;
use cortex_m::interrupt::Mutex;
use microbit::hal::gpiote::Gpiote;
use crate::game::Turn;

// ...

static GPIO: Mutex<RefCell<Option<Gpiote>>> = Mutex::new(RefCell::new(None));
static TURN: Mutex<RefCell<Turn>> = Mutex::new(RefCell::new(Turn::None));
}

内部可変性を可能にするため、データは RefCell でラップされています。RefCell の詳細については、 そのドキュメントThe Rust Book の該当章を読んでください。 さらに RefCell は、安全にアクセスできるよう cortex_m::interrupt::Mutex でラップされています。 cortex_m クレートが提供する Mutex は、クリティカルセクション の概念を使用します。 Mutex 内のデータには、cortex_m::interrupt:free に渡された関数またはクロージャの内部からしかアクセスできず、これにより その関数またはクロージャ内のコード自体が割り込まれないことが保証されます。

まず、ボタンを初期化します。

#![allow(unused)]
fn main() {
use cortex_m::interrupt::free;
use microbit::{
    board::Buttons,
    pac::{self, GPIOTE}
};

// ...

/// ボタンを初期化し、割り込みを有効化します。
pub(crate) fn init_buttons(board_gpiote: GPIOTE, board_buttons: Buttons) {
    let gpiote = Gpiote::new(board_gpiote);

    let channel0 = gpiote.channel0();
    channel0
        .input_pin(&board_buttons.button_a.degrade())
        .hi_to_lo()
        .enable_interrupt();
    channel0.reset_events();

    let channel1 = gpiote.channel1();
    channel1
        .input_pin(&board_buttons.button_b.degrade())
        .hi_to_lo()
        .enable_interrupt();
    channel1.reset_events();

    free(move |cs| {
        *GPIO.borrow(cs).borrow_mut() = Some(gpiote);

        unsafe {
            pac::NVIC::unmask(pac::Interrupt::GPIOTE);
        }
        pac::NVIC::unpend(pac::Interrupt::GPIOTE);
    });
}
}

nRF52 上の GPIOTE ペリフェラルには 8 つの「チャネル」があり、それぞれを GPIO ピンに接続して、 立ち上がりエッジ(信号が low から high に遷移)や立ち下がりエッジ(high から low への 遷移)などの特定のイベントに応答するよう設定できます。ボタンは、押されていないときには high 信号で、 押されると low 信号になる GPIO ピンです。したがって、ボタンの押下は立ち下がりエッジです。

channel0button_a に、channel1button_b に接続し、それぞれについて 立ち下がりエッジ(hi_to_lo)でイベントを生成するよう設定します。GPIOTE ペリフェラルへの参照は GPIO Mutex に保存します。 その後、GPIOTE 割り込みを unmask してハードウェアが伝播できるようにし、さらに unpend を呼び出して pending 状態の 割り込みをすべてクリアします(これらは割り込みが unmask される前に生成されていた可能性があります)。

次に、割り込みを処理するコードを書きます。microbit::pac が提供する interrupt マクロを使用します(v2 の 場合は、nrf52833_hal クレートから再エクスポートされています)。処理したい割り込みと同じ名前の関数を 定義し(一覧は こちら で確認できます)、#[interrupt] を付けます。

#![allow(unused)]
fn main() {
use microbit::pac::interrupt;

// ...

#[interrupt]
fn GPIOTE() {
    free(|cs| {
        if let Some(gpiote) = GPIO.borrow(cs).borrow().as_ref() {
            let a_pressed = gpiote.channel0().is_event_triggered();
            let b_pressed = gpiote.channel1().is_event_triggered();

            let turn = match (a_pressed, b_pressed) {
                (true, false) => Turn::Left,
                (false, true) => Turn::Right,
                _ => Turn::None
            };

            gpiote.channel0().reset_events();
            gpiote.channel1().reset_events();

            *TURN.borrow(cs).borrow_mut() = turn;
        }
    });
}
}

GPIOTE 割り込みが生成されると、各ボタンが押されたかどうかを確認します。ボタン A だけが押されていれば、 ヘビは左に曲がるべきだと記録します。ボタン B だけが押されていれば、ヘビは右に曲がるべきだと記録します。 それ以外の場合は、ヘビは曲がらないものとして記録します。該当する曲がる方向は TURN Mutex に保存されます。 これらはすべて free ブロック内で行われ、これによりこの割り込みの処理中に再度割り込まれないことが保証されます。

最後に、次の曲がる方向を取得するためのシンプルな関数を公開します。

#![allow(unused)]
fn main() {
/// 次の曲がる方向(つまり、直前に押されたボタンに対応する方向)を取得します。
pub fn get_turn(reset: bool) -> Turn {
    free(|cs| {
        let turn = *TURN.borrow(cs).borrow();
        if reset {
            *TURN.borrow(cs).borrow_mut() = Turn::None
        }
        turn
    })
}
}

この関数は、TURN Mutex の現在の値を返すだけです。引数として 1 つの真偽値 reset を取ります。resettrue の場合、TURN の値はリセットされ、つまり Turn::None に設定されます。

main ファイルの更新

main 関数に戻ると、メインループの前に init_buttons の呼び出しを追加し、ゲームループでは、 game.step メソッドに渡していたプレースホルダーの Turn::None 引数を、get_turn が返す値に置き換える必要があります。

#![no_main]
#![no_std]

mod game;
mod control;

use cortex_m_rt::entry;
use microbit::{
    Board,
    hal::{prelude::*, Rng, Timer},
    display::blocking::Display
};
use rtt_target::rtt_init_print;
use panic_rtt_target as _;

use crate::game::{Game, GameStatus};
use crate::control::{init_buttons, get_turn};

#[entry]
fn main() -> ! {
    rtt_init_print!();
    let mut board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0);
    let mut rng = Rng::new(board.RNG);
    let mut game = Game::new(rng.random_u32());

    let mut display = Display::new(board.display_pins);

    init_buttons(board.GPIOTE, board.buttons);

    loop {  // メインループ
        loop {  // ゲームループ
            let image = game.game_matrix(9, 9, 9);
            // 明るさの値は、異なる明るさを表示できるディスプレイをまだ
            // 実装していないため、現時点では意味がありません
            display.show(&mut timer, image, game.step_len_ms());
            match game.status {
                GameStatus::Ongoing => game.step(get_turn(true)),
                _ => {
                    for _ in 0..3 {
                        display.clear();
                        timer.delay_ms(200u32);
                        display.show(&mut timer, image, 200);
                    }
                    display.clear();
                    display.show(&mut timer, game.score_matrix(), 1000);
                    break
                }
            }
        }
        game.reset();
    }
}

これで、micro:bit のボタンを使ってヘビを操作できるようになりました!