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

Код на embedded Rust для управления светодиодной матрицей MAX7219 на Raspberry Pi Pico 2

Перейдём к части с кодом. В этой программе мы нарисуем квадрат на светодиодной матрице 8x8 с помощью embedded-graphics.

Tip

Рисование квадрата — лишь пример, но, освоив основы, можно делать гораздо больше. Можно прокручивать текст, выводить иконки или смайлики и даже анимировать объекты. Примеры можно посмотреть здесь: https://github.com/ImplFerris/max7219-examples. Также можно попробовать собрать простые часы из последовательно соединённых матриц.

Как обычно, создайте новый проект с помощью предоставленного шаблона.

#![allow(unused)]
fn main() {
cargo generate --git https://github.com/ImplFerris/pico2-template.git --tag v0.3.2
}

Когда появится запрос, задайте проекту имя, например «hello-max7219», и выберите embassy в качестве HAL.

Дополнительные крейты

Добавим следующие крейты вместе с уже существующими зависимостями:

max7219-display = { version = "0.1.5", features = ["led-matrix", "graphics"] }
embedded-graphics = "0.8.0"
embedded-hal-bus = "0.3.0"

Крейт max7219-display предоставляет драйвер, который используется в этой программе. Драйвер поддерживает разные виды модулей дисплеев, например 7-сегментные дисплеи и светодиодные матрицы. В нашем случае используется светодиодная матрица 8x8, поэтому мы включаем feature led-matrix. Поскольку мы хотим рисовать фигуры, а не управлять отдельными светодиодами вручную, мы также включаем feature graphics, который реализует для драйвера трейты embedded-graphics.

Крейт embedded-graphics используется для создания фигур и текста в структурированном виде. Он предоставляет примитивы вроде прямоугольников, точек и стилей, что значительно упрощает рисование на светодиодной матрице.

embedded-hal-bus

Крейт embedded-hal определяет общие трейты для периферии вроде SPI, чтобы драйверы могли работать на разных микроконтроллерах. Чтобы понять, зачем нужен embedded-hal-bus, полезно посмотреть, как организованы эти трейты.

Embedded HAL разделяет SPI на два отдельных понятия: шину SPI (представляет общие линии тактирования и данных) и устройство SPI (представляет отдельную периферию со своей линией выбора микросхемы, chip select). HAL микроконтроллеров обычно предоставляют реализацию шины SPI, но драйверы устройств пишутся для работы с устройствами SPI.

Крейт embedded-hal-bus закрывает этот разрыв. Он предоставляет адаптеры, которые оборачивают шину SPI и управляют выводом chip select, создавая интерфейс устройства SPI, который ожидают драйверы. В этой главе мы используем ExclusiveDevice — самый простой адаптер. Он рассчитан на сценарии, когда шиной SPI пользуется только одно устройство: он автоматически активирует chip select в начале каждой транзакции и деактивирует его по завершении.

Дополнительные импорты

Теперь добавим импорты, необходимые для SPI, драйвера дисплея MAX7219 и рисования с помощью embedded-graphics.

#![allow(unused)]
fn main() {
// Для MAX7219
use embedded_hal_bus::spi::ExclusiveDevice;
use max7219_display::led_matrix::display::SingleMatrix;

// Для рисования фигур
use embedded_graphics::pixelcolor::BinaryColor;
use embedded_graphics::prelude::*;
use embedded_graphics::primitives::{PrimitiveStyleBuilder, Rectangle};

// Для SPI
use embassy_rp::spi::{Config as SpiConfig, Spi};

// Для вывода CS
use embassy_rp::gpio::{Level, Output};
}

Настройка SPI и chip select

Начнём с создания вывода chip select. MAX7219 использует активно-низкий сигнал chip select, поэтому инициализируем вывод в высоком состоянии. Так устройство остаётся неактивным, пока не начнётся передача по SPI.

#![allow(unused)]
fn main() {
let cs_pin = Output::new(p.PIN_13, Level::High);
}

Далее назначаем выводы тактирования SPI и MOSI. Поскольку MAX7219 работает только на запись, нужны только эти два сигнала. В этой схеме вывод MISO не используется.

#![allow(unused)]
fn main() {
let clk = p.PIN_14;
let mosi = p.PIN_15;
}

Выбрав выводы, создаём периферию SPI. Мы используем блокирующий экземпляр SPI только на передачу, так как вся связь идёт от Pico к дисплею.

#![allow(unused)]
fn main() {
let spi_bus = Spi::new_blocking_txonly(p.SPI1, clk, mosi, SpiConfig::default());
}

На этом этапе у нас есть шина SPI, но она ещё не управляет chip select. Чтобы это обеспечить, оборачиваем шину в ExclusiveDevice. Это превращает шину SPI в устройство SPI, автоматически управляя выводом chip select для каждой передачи.

#![allow(unused)]
fn main() {
let spi_dev =
        ExclusiveDevice::new_no_delay(spi_bus, cs_pin).expect("Failed to get exclusive device");
}

После этого шага устройство SPI готово к использованию драйвером дисплея MAX7219.

Создание экземпляра дисплея

Когда устройство SPI готово, создаём экземпляр светодиодной матрицы. Здесь мы работаем с одной матрицей 8x8, а не с последовательным соединением. Драйвер дисплея берёт устройство SPI во владение и использует его для отправки команд и данных о пикселях в MAX7219.

#![allow(unused)]
fn main() {
// Создаём экземпляр дисплея для одной светодиодной матрицы 8x8 (без последовательного соединения)
let mut display = SingleMatrix::from_spi(spi_dev).expect("display count 1 should not panic");
}

Настройка яркости дисплея

После создания экземпляра дисплея задаём уровень яркости. MAX7219 поддерживает несколько уровней интенсивности, и здесь мы настраиваем единственное устройство с индексом 0. Низкого значения интенсивности обычно достаточно, и оно не даёт излишней яркости.

#![allow(unused)]
fn main() {
// Задаём яркость (уровень интенсивности) единственного устройства с индексом 0
    display
        .driver()
        .set_intensity(0, 1)
        .expect("failed to set intensity");
}

Рисуем прямоугольник

Теперь нарисуем простую фигуру с помощью embedded-graphics. Вместо заливки всей области мы рисуем полый прямоугольник, так что включаются только пиксели по границе. Сначала я попробовал залитый прямоугольник, но он выглядел плохо, поэтому эту версию я закомментировал.

Сначала определяем стиль рисования. Этот стиль включает пиксели и задаёт толщину линии в один пиксель, что хорошо подходит для дисплея 8x8.

#![allow(unused)]
fn main() {
// ---- Рисуем прямоугольник ----
// let rect = Rectangle::new(Point::new(1, 1), Size::new(6, 6)).into_styled(
//     embedded_graphics::primitives::PrimitiveStyle::with_fill(BinaryColor::On),
// );
let hollow_rect_style = PrimitiveStyleBuilder::new()
    .stroke_color(BinaryColor::On) // Рисуем только границу
    .stroke_width(1) // Толщина границы — 1 пиксель
    .build();
}

Далее определяем сам прямоугольник. Левый верхний угол находится в точке x = 1 и y = 1, а прямоугольник занимает 6 пикселей по ширине и высоте. Это оставляет отступ в один пиксель от краёв, поэтому граница не касается крайних светодиодов.

#![allow(unused)]
fn main() {
let rect = Rectangle::new(Point::new(1, 1), Size::new(6, 6)).into_styled(hollow_rect_style);
}

Когда мы рисуем прямоугольник, он записывается в буфер дисплея. На этом этапе на аппаратуру ещё ничего не отправлено.

#![allow(unused)]
fn main() {
rect.draw(&mut display).expect("failed to draw the shape");
}

Чтобы фактически обновить светодиодную матрицу, выгружаем кадровый буфер (framebuffer). Это отправляет данные пикселей по SPI в MAX7219.

#![allow(unused)]
fn main() {
display.flush().expect("failed to send to the device");
}

Полный код

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_rp as hal;
use embassy_rp::block::ImageDef;
use embassy_time::Timer;

// Для MAX7219
use embedded_hal_bus::spi::ExclusiveDevice;
use max7219_display::led_matrix::display::SingleMatrix;

// Для рисования фигур
use embedded_graphics::pixelcolor::BinaryColor;
use embedded_graphics::prelude::*;
use embedded_graphics::primitives::{PrimitiveStyleBuilder, Rectangle};

// Для SPI
use embassy_rp::spi::{Config as SpiConfig, Spi};

// Для вывода CS
use embassy_rp::gpio::{Level, Output};

//Обработчик паники
use panic_probe as _;
// Логирование через defmt
use defmt_rtt as _;

/// Сообщаем Boot ROM о нашем приложении
#[unsafe(link_section = ".start_block")]
#[used]
pub static IMAGE_DEF: ImageDef = hal::block::ImageDef::secure_exe();

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_rp::init(Default::default());

    let cs_pin = Output::new(p.PIN_13, Level::High);

    let clk = p.PIN_14;
    let mosi = p.PIN_15;

    let spi_bus = Spi::new_blocking_txonly(p.SPI1, clk, mosi, SpiConfig::default());
    let spi_dev =
        ExclusiveDevice::new_no_delay(spi_bus, cs_pin).expect("Failed to get exclusive device");

    // Создаём экземпляр дисплея для одной светодиодной матрицы 8x8 (без последовательного соединения)
    let mut display = SingleMatrix::from_spi(spi_dev).expect("display count 1 should not panic");

    // Задаём яркость (уровень интенсивности) единственного устройства с индексом 0
    display
        .driver()
        .set_intensity(0, 1)
        .expect("failed to set intensity");

    // ---- Рисуем прямоугольник ----
    // let rect = Rectangle::new(Point::new(1, 1), Size::new(6, 6)).into_styled(
    //     embedded_graphics::primitives::PrimitiveStyle::with_fill(BinaryColor::On),
    // );
    let hollow_rect_style = PrimitiveStyleBuilder::new()
        .stroke_color(BinaryColor::On) // Рисуем только границу
        .stroke_width(1) // Толщина границы — 1 пиксель
        .build();
    let rect = Rectangle::new(Point::new(1, 1), Size::new(6, 6)).into_styled(hollow_rect_style);
    rect.draw(&mut display).expect("failed to draw the shape");

    display.flush().expect("failed to send to the device");

    loop {
        Timer::after_millis(100).await;
    }
}

// Метаданные программы для `picotool info`.
// Это не обязательно, но рекомендуется добавить эти минимальные записи.
#[unsafe(link_section = ".bi_entries")]
#[used]
pub static PICOTOOL_ENTRIES: [embassy_rp::binary_info::EntryAddr; 4] = [
    embassy_rp::binary_info::rp_program_name!(c"hello-max7219"),
    embassy_rp::binary_info::rp_program_description!(c"your program description"),
    embassy_rp::binary_info::rp_cargo_version!(),
    embassy_rp::binary_info::rp_program_build_attribute!(),
];

// Конец файла

Клонируйте существующий проект

Можно клонировать проект, который я создал, и перейти в папку hello-max7219.

git clone https://github.com/ImplFerris/pico2-embassy-projects
cd pico2-embassy-projects/max7219/hello-max7219