Неблокирующий вывод на дисплей
Далее выведем змейку и еду на светодиоды экрана MB2. До сих пор мы пользовались блокирующим интерфейсом, который позволяет светодиодам либо гореть на полную яркость, либо быть выключенными. С ним вполне можно сделать простую рабочую игру «Змейка». Но если змейка станет немного длиннее, будет трудно отличить её от еды и понять, куда она движется. Разберёмся, как сделать яркость светодиодов переменной: например, сделать тело змейки чуть тусклее, и это поможет разобраться в картинке.
Крейт microbit предоставляет два разных интерфейса к светодиодной матрице. Есть блокирующий интерфейс, с которым мы уже работали в предыдущих главах. Есть и неблокирующий интерфейс, который позволяет настраивать яркость каждого светодиода. На уровне железа каждый светодиод либо «включён», либо «выключен», но модуль microbit::display::nonblocking имитирует десять уровней яркости для каждого светодиода, быстро включая и выключая его.
(Нет особой причины, по которой два режима вывода в крейте microbit должны быть разделены и использовать разный код. Более полная конструкция позволила бы использовать один API дисплея как в блокирующем, так и в неблокирующем режиме, с уровнями яркости и частотой обновления, которые задаёт пользователь. Никогда не считайте, что то, что вам передали, отточено или хотя бы близко к этому. Всегда думайте о том, что можно было бы сделать иначе. Но пока будем работать с тем, что есть, — этого достаточно для нашей текущей задачи.)
Код для работы с неблокирующим интерфейсом (src/display.rs) довольно прост и устроен примерно так же, как код для работы с кнопками. На этот раз начнём с верхнего уровня.
Модуль display
#![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) }
}
}
Сначала создаём структуру microbit::display::nonblocking::Display, которая представляет светодиодный дисплей, передавая ей периферию TIMER1 платы и DisplayPins. Затем сохраняем дисплей в Mutex. Наконец, разрешаем прерывание TIMER1.
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;
/// Вывести изображение.
pub fn display_image(image: &impl Render) {
interrupt_free(|cs| {
if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() {
display.show(image);
}
})
}
/// Очистить дисплей (выключить все светодиоды).
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 для вывода на светодиодную матрицу. Две реализации Render, которые предоставляет модуль microbit::display::nonblocking, — это BitImage и GreyscaleImage. В BitImage каждый «пиксель» (или светодиод) либо светится, либо нет (как в блокирующем интерфейсе), тогда как в GreyscaleImage каждый «пиксель» может иметь свою яркость.
clear_display делает именно то, что следует из названия.
Обработка прерываний дисплея
Наконец, с помощью макроса interrupt определяем обработчик прерывания TIMER1. Это прерывание срабатывает много раз в секунду, и именно оно позволяет Display быстро включать и выключать разные светодиоды, создавая иллюзию переменной яркости. Весь наш код обработчика просто вызывает метод 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 и будем использовать новые функции, чтобы с ним взаимодействовать.