Управление
Нашим главным героем будут управлять две кнопки на передней стороне micro:bit. Кнопка A будет поворачивать змейку влево, а кнопка B — вправо.
Для обработки нажатий кнопок конкурентным образом мы используем макрос microbit::pac::interrupt. Прерывание будет генерироваться периферией General Purpose Input/Output Tasks and Events (GPIOTE) MB2.
Модуль controls
Нам понадобятся два отдельных глобальных изменяемых состояния: ссылка на периферию GPIOTE и запись о выбранном направлении следующего поворота.
Общие данные оборачиваются в RefCell, чтобы обеспечить внутреннюю изменяемость и блокировку. Подробнее о RefCell можно прочитать в документации RefCell и в главе о внутренней изменяемости книги Rust. Сам RefCell, в свою очередь, оборачивается в cortex_m::interrupt::Mutex, чтобы обеспечить безопасный доступ. Mutex из крейта cortex_m опирается на понятие критической секции. Данные в Mutex можно использовать только внутри функции или замыкания, переданного в cortex_m::interrupt::free (для ясности здесь переименованной в interrupt_free), что гарантирует: код этой функции или замыкания не может быть прерван сам по себе.
Инициализация
Сначала инициализируем кнопки (src/controls/init.rs).
#![allow(unused)]
fn main() {
use super::{Buttons, GPIO};
use cortex_m::interrupt::free as interrupt_free;
use microbit::{
hal::{
gpio::{Floating, Input, Pin},
gpiote::{Gpiote, GpioteChannel},
},
pac,
};
/// Инициализировать кнопки и разрешить прерывания.
pub fn init_buttons(board_gpiote: pac::GPIOTE, board_buttons: Buttons) {
let gpiote = Gpiote::new(board_gpiote);
fn init_channel(channel: &GpioteChannel<'_>, button: &Pin<Input<Floating>>) {
channel.input_pin(button).hi_to_lo().enable_interrupt();
channel.reset_events();
}
let channel0 = gpiote.channel0();
init_channel(&channel0, &board_buttons.button_a.degrade());
let channel1 = gpiote.channel1();
init_channel(&channel1, &board_buttons.button_b.degrade());
interrupt_free(move |cs| {
*GPIO.borrow(cs).borrow_mut() = Some(gpiote);
unsafe {
pac::NVIC::unmask(pac::Interrupt::GPIOTE);
}
pac::NVIC::unpend(pac::Interrupt::GPIOTE);
});
}
}
Периферия GPIOTE в nRF52 имеет 8 «каналов», каждый из которых можно подключить к выводу GPIO и настроить на реакцию на определённые события, в том числе на нарастающий фронт (переход сигнала из низкого уровня в высокий) и на спадающий фронт (из высокого в низкий). Кнопка — это вывод GPIO, который в ненажатом состоянии имеет высокий сигнал, а в нажатом — низкий. Поэтому нажатие кнопки — это спадающий фронт.
Обратите внимание на немного неуклюжее использование функции init_channel() при инициализации: она позволяет не копировать код инициализации кнопок. Типы, которые различные встраиваемые крейты для MB2 от вас скрывают, иногда немного пугают. Рекомендую однажды внимательно изучить структуру типов крейтов HAL и PAC: она немного странная и требует привыкания. В частности, обратите внимание, что каждый вывод micro:bit имеет собственный уникальный тип. Функция degrade() при инициализации нужна, чтобы привести их к общему типу, который можно разумно передать в init_channel() и далее в input_pin().
Мы подключаем channel0 к button_a, а channel1 к button_b. В каждом случае настраиваем кнопку на генерацию событий по спадающему фронту (hi_to_lo). Ссылку на нашу периферию GPIOTE сохраняем в Mutex GPIO. Затем вызываем unmask для прерываний GPIOTE, позволяя аппаратуре их передавать, и unpend, чтобы сбросить все прерывания в состоянии ожидания (они могли появиться до разрешения прерываний).
Обработчик прерывания
Далее пишем код, обрабатывающий прерывание. Мы используем макрос interrupt, реэкспортированный из крейта nrf52833_hal. Определяем функцию с тем же именем, что и прерывание, которое хотим обработать (все их можно увидеть здесь), и помечаем её атрибутом #[interrupt] (src/controls/interrupt.rs).
#![allow(unused)]
fn main() {
use super::{Turn, GPIO, TURN};
use cortex_m::interrupt::free as interrupt_free;
use microbit::pac::{self, interrupt};
#[pac::interrupt]
fn GPIOTE() {
interrupt_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, записываем, что направо. Во всех остальных случаях записываем, что поворота не будет. (Нажатие обеих кнопок «одновременно» крайне маловероятно: нажатия фиксируются почти мгновенно, а этот обработчик прерывания работает очень быстро, так что одновременно нажать обе кнопки успеть будет трудно. Точно так же трудно нажать кнопку так коротко, чтобы этот код её пропустил и сообщил, что ни одна кнопка не нажата. Тем не менее Rust требует предусмотреть и такие неожиданные случаи: код не скомпилируется, пока вы не проверите все варианты.) Нужный поворот сохраняется в Mutex TURN. Всё это происходит внутри блока interrupt_free, чтобы гарантировать, что другое событие не прервёт обработку этого прерывания.
Наконец, открываем простую функцию для получения следующего поворота (src/controls.rs).
#![allow(unused)]
fn main() {
mod init;
mod interrupt;
pub use init::init_buttons;
use crate::game::Turn;
use core::cell::RefCell;
use cortex_m::interrupt::{free as interrupt_free, Mutex};
use microbit::{board::Buttons, hal::gpiote::Gpiote};
pub static GPIO: Mutex<RefCell<Option<Gpiote>>> = Mutex::new(RefCell::new(None));
pub static TURN: Mutex<RefCell<Turn>> = Mutex::new(RefCell::new(Turn::None));
/// Получить следующий поворот (то есть поворот, соответствующий последней нажатой кнопке).
pub fn get_turn(reset: bool) -> Turn {
interrupt_free(|cs| {
let turn = *TURN.borrow(cs).borrow();
if reset {
*TURN.borrow(cs).borrow_mut() = Turn::None
}
turn
})
}
}
Эта функция просто возвращает текущее значение Mutex TURN. Она принимает один логический аргумент reset. Если reset равен true, значение TURN сбрасывается, то есть устанавливается в Turn::None.
Далее добавим поддержку «высококачественного» игрового дисплея.