はじめに
普段はQtの話ばかりな底辺系フリーランスのhermit4です。ごきげんよう。
このたび合同会社シグナルスロット様のご厚意で、次週11/20から11/22までみなとみらいで行われるEdge Tech+2024でのSlintの展示と紹介をお手伝いさせていただけることになりました。会場へお越しになる方はぜひ遊びにきてください。
1つ前の記事で、printerdemo_mcu以外のSlintアプリケーションをRaspberry Pi Picoで動かす手順を書きました。しかし、これだとWaveShare 2.8インチLCD以外が利用できません。
組込機器というのは千差万別です。アプリケーションはもちろんですが、それを動かすためのボードサポート部分も重要です。Slintのpicoサポートもあくまでexampleの下に生えているわけで、好きな構成を使うためにはここも自分で変更が必要です。
幸い、手元に非常によく似たマイコンボードがあるので、slintのコードを参考にカスタマイズしてみたいと思います。
Raspberry Pi Pico2について
Raspberry Pi Pico2は、つい最近新しく出たばかりのマイコンボードです。
- CPU : Dual ARM Cortex-M33 or Dual RISC-V Hazard3
- Memory : 520KB SRAM
- Flash : 4MB QSPI Flash
新しく出たばかりでRustのHALを含めてまだこなれていないチップですがPicoと比べると非常に性能が良いので、こちらでもSlintを動かしたいところです。アーキテクチャからして違うわけですが、せっかくですからがんばりましょう。
Slint MCU Boad Supportを見る
まずはコードを入手します。
git clone https://github.com/slint-ui/slint.git -b v1.8.0
Picoで利用したボードサポートは以下の場所にあります。
[lib]
path = "lib.rs"
[features]
pico-st7789 = ["slint/unsafe-single-threaded", "rp-pico", "embedded-hal", "embedded-hal-nb", "cortex-m-rt", "embedded-alloc", "fugit", "cortex-m", "display-interface", "display-interface-spi", "mipidsi", "defmt", "defmt-rtt", "slint/libm", "embedded-dma", "embedded-graphics", "euclid/libm"]
stm32h735g = ["slint/unsafe-single-threaded", "cortex-m/critical-section-single-core", "cortex-m-rt","embedded-alloc", "embedded-time", "stm32h7xx-hal/stm32h735", "defmt", "defmt-rtt", "embedded-display-controller", "ft5336", "panic-probe", "slint/libm", "getrandom"]
esp32-s2-kaluga-1 = ["slint/unsafe-single-threaded", "esp-hal/esp32s2", "embedded-hal", "embedded-hal-bus", "esp-alloc", "esp-println/esp32s2", "display-interface", "display-interface-spi", "mipidsi", "embedded-graphics-core", "slint/libm"]
esp32-s3-box = ["slint/unsafe-single-threaded", "esp-hal/esp32s3", "esp-hal/embedded-hal-02", "embedded-hal", "embedded-hal-bus", "esp-alloc", "esp-println/esp32s3", "esp-backtrace/esp32s3", "display-interface", "display-interface-spi", "mipidsi", "embedded-graphics-core", "slint/libm", "tt21100"]
[dependencies]
slint = { version = "=1.8.0", path = "../../api/rs/slint", default-features = false, features = ["compat-1-2", "renderer-software"] }
i-slint-core-macros = { version = "=1.8.0", path = "../../internal/core-macros" }
derive_more = "0.99.5"
embedded-graphics = { version = "0.8", optional = true }
once_cell = { version = "1.9", default-features = false, features = ["alloc"] }
pin-weak = { version = "1", default-features = false }
rgb = "0.8.27"
cfg-if = "1"
embedded-alloc = { version = "0.5", optional = true }
cortex-m-rt = { version = "0.7", optional = true }
cortex-m = { version = "0.7.2", optional = true }
display-interface = { version = "0.5.0", optional = true }
embedded-hal = { version = "1.0.0", optional = true }
embedded-hal-nb = { version = "1.0.0", optional = true }
embedded-hal-bus = { version = "0.2", optional = true }
embedded-dma = { version = "0.2.0", optional = true }
rp-pico = { version = "0.9.0", optional = true }
fugit = { version = "0.3.6", optional = true }
euclid = { version = "0.22", default-features = false, optional = true }
stm32h7xx-hal = { version = "0.16.0", optional = true, features = ["log-rtt", "ltdc", "xspi"] }
getrandom = { version = "0.2", optional = true, default-features = false, features = ["custom"] }
embedded-time = { version = "0.12.0", optional = true }
embedded-display-controller = { version = "0.2.0", optional = true }
ft5336 = { version = "0.2", optional = true }
esp-hal = { version = "0.19", optional = true }
display-interface-spi = { version = "0.5", optional = true }
esp-alloc = { version = "0.4", optional = true }
esp-println = { version = "0.11.0", optional = true }
esp-backtrace = { version = "0.14.0", optional = true, features = ["panic-handler", "println"] }
tt21100 = { version = "0.1", optional = true }
mipidsi = { version = "0.8.0", optional = true }
embedded-graphics-core = { version = "0.4", optional = true }
defmt-rtt = { version = "0.4.0", optional = true }
defmt = { version = "0.3.0", optional = true }
panic-probe = { version = "0.3.0", optional = true, features = ["print-defmt"] }
[build-dependencies]
cfg-if = "1.0.0"
先の記事で、features = "pico-st7789"しました。ですので以下の行が有効になります(読みやすいように改行しています)。
pico-st7789 = [
"slint/unsafe-single-threaded",
"rp-pico",
"embedded-hal",
"embedded-hal-nb",
"cortex-m-rt",
"embedded-alloc",
"fugit",
"cortex-m",
"display-interface",
"display-interface-spi",
"mipidsi",
"defmt",
"defmt-rtt",
"slint/libm",
"embedded-dma",
"embedded-graphics",
"euclid/libm"
]
この機能を有効にしながら依存関係を見ると以下のようになることがわかります。
[dependencies]
slint = { version="1.8.0" , default-features = false , features = ["compat-1-2", "renderer-software", "unsafe-single-threaded", "libm"]}
i-slint-core-macros = "1.8.0"
cfg-if = "1"
cortex-m = "0.7.2"
cortex-m-rt = "0.7"
defmt = "0.3.0"
defmt-rtt = "0.4.0"
derive_more = "0.99.5"
display-interface-spi = "0.5"
display-interface = "0.5.0"
embedded-alloc = "0.5"
embedded-dma = "0.2.0"
embedded-graphics = "0.8"
embedded-hal = "1.0.0"
embedded-hal-nb = "1.0.0"
euclid = { version = "0.22", default-features = false, features = ["libm"]}
fugit = "0.3.6"
mipidsi = "0.8.0"
once_cell = { version = "1.9", default-features = false, features = ["alloc"] }
pin-weak = { version = "1", default-features = false }
rgb = "0.8.27"
rp-pico = "0.9.0"
ソースコードを見ると
#![doc = include_str!("README.md")]
#![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")]
#![no_std]
extern crate alloc;
#[cfg(feature = "pico-st7789")]
mod pico_st7789;
#[cfg(feature = "pico-st7789")]
pub use pico_st7789::*;
#[cfg(feature = "stm32h735g")]
mod stm32h735g;
#[cfg(feature = "stm32h735g")]
pub use stm32h735g::*;
#[cfg(feature = "esp32-s2-kaluga-1")]
mod esp32_s2_kaluga_1;
#[cfg(feature = "esp32-s2-kaluga-1")]
pub use esp32_s2_kaluga_1::*;
#[cfg(feature = "esp32-s3-box")]
mod esp32_s3_box;
#[cfg(feature = "esp32-s3-box")]
pub use esp32_s3_box::*;
#[cfg(not(any(
feature = "pico-st7789",
feature = "stm32h735g",
feature = "esp32-s2-kaluga-1",
feature = "esp32-s3-box"
)))]
pub use i_slint_core_macros::identity as entry;
#[cfg(not(any(
feature = "pico-st7789",
feature = "stm32h735g",
feature = "esp32-s2-kaluga-1",
feature = "esp32-s3-box"
)))]
pub fn init() {}
実際に使われているのは以下の通りです。
- pico_st7789.rs
- pico_st7789/board_config.toml
- pico_st7789/memory.x
ローカルに移してみよう
先の記事で作成したslint-helloの下に、利用していたボードサポートのコードを移してしまいましょう。memory.xはトップディレクトリに、pico_st7789.rsはsrc下に移動します。
- Cargo.toml
- build.rs
- memory.x
- src/main.rs
- src/pico_st7789.rs
- ui/app-window.slint
次にmemory.xを利用すること、ついでにターゲットについても設定してしまいます。もともとは、pico_st7789/board_config.tomlにあった設定を.cargoを作って、そこにおいておきます。
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "elf2uf2-rs -d"
rustflags = [
"-C", "link-arg=--nmagic",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
]
[build]
target = "thumbv6m-none-eabi"
[env]
DEFMT_LOG = "debug"
mcu-board-supportを使わなくなるので、一部修正します。
#![no_std]
#![no_main]
mod pico_st7789;
use pico_st7789::entry;
slint::include_modules!();
#[entry]
fn main() -> ! {
pico_st7789::init();
let ui = AppWindow::new().unwrap();
ui.on_request_increase_value({
let ui_handle = ui.as_weak();
move || {
let ui = ui_handle.unwrap();
ui.set_counter(ui.get_counter() + 1);
}
});
ui.run().unwrap();
panic!("The MCU demo should not quit")
}
あとは、bootselを押しながらLinuxマシンにUSB接続をし
udisksctl mount -b /dev/sdb1
cargo run --release
とすると、ビルドして実機へ転送し起動されるはずです。
これで、HW制御周りのコードが自由になる状態となりました。
Raspberry Pi Pico2で動かしてみよう
ターゲットの追加
コンパイルに必要な環境を整えます。
rustup target add thumbv8m.main-none-eabihf
通常インストールに利用していた elf2uf2-rs が利用できない時期があったので、picotoolを使うことにしています。まぁ、セキュリティとか考え始めるとこちらのツールの知識も必要なので触っておいた方が無難です。
パッケージ追加
sudo apt install pkg-config libusb-1.0-0-dev
Pico SDKの取得
git clone https://github.com/raspberrypi/pico-sdk
cd pico-sdk
git submodule update --init
export PICO_SDK_PATH=`pwd`
cd -
pico toolのインストール
git clone https://github.com/raspberrypi/picotool
cd picotool
cmake -B build -G"Ninja"
cmake --build build
sudo cmake --install build
sudo cp udev/99-picotool.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger
Memoryの構成
/*
* source: https://github.com/rp-rs/rp-hal/blob/main/rp235x-hal-examples/memory.x
*/
MEMORY {
FLASH : ORIGIN = 0x10000000, LENGTH = 2048K
RAM : ORIGIN = 0x20000000, LENGTH = 512K
SRAM4 : ORIGIN = 0x20080000, LENGTH = 4K
SRAM5 : ORIGIN = 0x20081000, LENGTH = 4K
}
SECTIONS {
.start_block : ALIGN(4)
{
__start_block_addr = .;
KEEP(*(.start_block));
KEEP(*(.boot_info));
} > FLASH
} INSERT AFTER .vector_table;
_stext = ADDR(.start_block) + SIZEOF(.start_block);
SECTIONS {
.bi_entries : ALIGN(4)
{
__bi_entries_start = .;
KEEP(*(.bi_entries));
. = ALIGN(4);
__bi_entries_end = .;
} > FLASH
} INSERT AFTER .text;
SECTIONS {
.end_block : ALIGN(4)
{
__end_block_addr = .;
KEEP(*(.end_block));
} > FLASH
} INSERT AFTER .uninit;
PROVIDE(start_to_end = __end_block_addr - __start_block_addr);
PROVIDE(end_to_start = __start_block_addr - __end_block_addr);
Cargoが使うパラメータ類の設定も修正します。
i```
```toml:Cargo.toml
[package]
name = "slint-hello"
version = "0.1.0"
edition = "2021"
[dependencies]
display-interface = "0.5.0"
display-interface-spi = "0.5.0"
embedded-hal = "1.0.0"
embedded-hal-nb = "1.0.0"
embedded-hal-bus = "0.2.0"
embedded-dma = "0.2.0"
embedded-alloc = "0.6.0"
embedded-graphics = "0.8.1"
embedded-graphics-core = "0.4.0"
embedded-display-controller = "0.2.0"
embedded-time = "0.12.1"
rp235x-hal= { version = "0.2.0", features = [
"binary-info",
"critical-section-impl",
"defmt",
]}
rp235x-pac = "0.1.0"
panic-halt = "0.2.0"
panic-probe = "0.3.2"
cortex-m = "0.7.2"
cortex-m-rt = "0.7"
mipidsi = "0.8.0"
euclid = { version = "0.22", default-features = false}
slint = { version = "1.8", default-features = false, features = ["compat-1-2", "renderer-software", "unsafe-single-threaded", "libm"] }
fugit = "0.3.6"
critical-section = {optional = true, version = "1.0.0"}
[build-dependencies]
slint-build = "1.8.0"
あとはまぁ、Pico2向けのHALを使って書き換えていくだけです。本当は名前とか変えるか、Featureで変更できるようにするかうまくやるべきなんですが、展示会で動けばよいかの精神で・・・・。
extern crate alloc;
mod xpt2046;
use panic_halt as _;
use alloc::boxed::Box;
use alloc::rc::Rc;
use alloc::vec;
use core::cell::RefCell;
use core::convert::Infallible;
use cortex_m::interrupt::Mutex;
use cortex_m::peripheral::NVIC;
use cortex_m::singleton;
use embedded_alloc::LlffHeap as Heap;
use embedded_hal::digital::{InputPin, OutputPin};
use embedded_hal::spi::{ErrorType, Operation, SpiBus, SpiDevice};
use fugit::{Hertz, RateExtU32};
use rp235x_hal::{ self as hal, dma, gpio, pac, timer, Clock, pac::interrupt };
pub use hal::entry;
pub use hal::binary_info;
use timer::{Timer,Alarm,Alarm0,CopyableTimer0};
use gpio::{Interrupt as GpioInterrupt};
use dma::{SingleChannel, WriteTarget, DMAExt};
use slint::platform::{software_renderer as renderer, PointerEventButton, WindowEvent};
use renderer::Rgb565Pixel;
static TIMER: Mutex<RefCell<Option<Timer<CopyableTimer0>>>> = Mutex::new(RefCell::new(None));
static ALARM0: Mutex<RefCell<Option<Alarm0<CopyableTimer0>>>> = Mutex::new(RefCell::new(None));
type IrqPin = gpio::Pin<gpio::bank0::Gpio17, gpio::FunctionSio<gpio::SioInput>, gpio::PullUp>;
static IRQ_PIN: Mutex<RefCell<Option<IrqPin>>> = Mutex::new(RefCell::new(None));
const HEAP_SIZE: usize = 200 * 1024;
static mut HEAP: [u8; HEAP_SIZE] = [0; HEAP_SIZE];
const DISPLAY_SIZE: slint::PhysicalSize = slint::PhysicalSize::new(320, 240);
const XTAL_FREQ_HZ: u32 = 12_000_000u32;
const SPI_ST7789VW_MAX_FREQ: Hertz<u32> = Hertz::<u32>::Hz(62_500_000);
type SpiPins = (
gpio::Pin<gpio::bank0::Gpio11, gpio::FunctionSpi, gpio::PullDown>,
gpio::Pin<gpio::bank0::Gpio12, gpio::FunctionSpi, gpio::PullDown>,
gpio::Pin<gpio::bank0::Gpio10, gpio::FunctionSpi, gpio::PullDown>,
);
type EnabledSpi = hal::Spi<hal::spi::Enabled, pac::SPI1, SpiPins, 8>;
type SpiRefCell = RefCell<(EnabledSpi, Hertz<u32>)>;
#[derive(Clone)]
struct SharedSpiWithFreq<CS> {
refcell: &'static SpiRefCell,
cs: CS,
freq: Hertz<u32>,
}
impl<CS> ErrorType for SharedSpiWithFreq<CS> {
type Error = <EnabledSpi as ErrorType>::Error;
}
impl<CS: OutputPin<Error = Infallible>> SpiDevice for SharedSpiWithFreq<CS> {
#[inline]
fn transaction(&mut self, operations: &mut [Operation<u8>]) -> Result<(), Self::Error> {
let mut borrowed = self.refcell.borrow_mut();
if borrowed.1 != self.freq {
borrowed.0.flush()?;
// the touchscreen and the LCD have different frequencies
borrowed.0.set_baudrate(125_000_000u32.Hz(), self.freq);
borrowed.1 = self.freq;
}
self.cs.set_low()?;
for op in operations {
match op {
Operation::Read(words) => borrowed.0.read(words),
Operation::Write(words) => borrowed.0.write(words),
Operation::Transfer(read, write) => borrowed.0.transfer(read, write),
Operation::TransferInPlace(words) => borrowed.0.transfer_in_place(words),
Operation::DelayNs(_) => unimplemented!(),
}?;
}
borrowed.0.flush()?;
drop(borrowed);
self.cs.set_high()?;
Ok(())
}
}
type TargetPixel = Rgb565Pixel;
type Display<DI, RST> = mipidsi::Display<DI, mipidsi::models::ST7789, RST>;
struct PartialReadBuffer(&'static mut [Rgb565Pixel], core::ops::Range<usize>);
unsafe impl embedded_dma::ReadBuffer for PartialReadBuffer {
type Word = u8;
unsafe fn read_buffer(&self) -> (*const <Self as embedded_dma::ReadBuffer>::Word, usize) {
let act_slice = &self.0[self.1.clone()];
(act_slice.as_ptr() as *const u8, act_slice.len() * core::mem::size_of::<Rgb565Pixel>())
}
}
enum PioTransfer<TO: WriteTarget, CH: SingleChannel> {
Idle(CH, &'static mut [TargetPixel], TO),
Running(hal::dma::single_buffer::Transfer<CH, PartialReadBuffer, TO>),
}
impl<TO: WriteTarget<TransmittedWord = u8>, CH: SingleChannel> PioTransfer<TO, CH> {
fn wait(self) -> (CH, &'static mut [TargetPixel], TO) {
match self {
PioTransfer::Idle(a, b, c) => (a, b, c),
PioTransfer::Running(dma) => {
let (a, b, to) = dma.wait();
(a, b.0, to)
}
}
}
}
struct DrawBuffer<Display, PioTransfer, Stolen> {
display: Display,
buffer: &'static mut [TargetPixel],
pio: Option<PioTransfer>,
stolen_pin: Stolen,
}
impl<
DI: display_interface::WriteOnlyDataCommand,
RST: OutputPin<Error = Infallible>,
TO: WriteTarget<TransmittedWord = u8>,
CH: SingleChannel,
DC_: OutputPin<Error = Infallible>,
CS_: OutputPin<Error = Infallible>,
> renderer::LineBufferProvider
for &mut DrawBuffer<Display<DI, RST>, PioTransfer<TO, CH>, (DC_, CS_)>
{
type TargetPixel = TargetPixel;
fn process_line(
&mut self,
line: usize,
range: core::ops::Range<usize>,
render_fn: impl FnOnce(&mut [TargetPixel]),
) {
render_fn(&mut self.buffer[range.clone()]);
// convert from little to big endian before sending to the DMA channel
for x in &mut self.buffer[range.clone()] {
*x = Rgb565Pixel(x.0.to_be())
}
let (ch, mut b, spi) = self.pio.take().unwrap().wait();
core::mem::swap(&mut self.buffer, &mut b);
// We send empty data just to get the device in the right window
self.display
.set_pixels(
range.start as u16,
line as _,
range.end as u16,
line as u16,
core::iter::empty(),
)
.unwrap();
self.stolen_pin.1.set_low().unwrap();
self.stolen_pin.0.set_high().unwrap();
let mut dma = hal::dma::single_buffer::Config::new(ch, PartialReadBuffer(b, range), spi);
dma.pace(hal::dma::Pace::PreferSink);
self.pio = Some(PioTransfer::Running(dma.start()));
}
}
impl<
DI: display_interface::WriteOnlyDataCommand,
RST: OutputPin<Error = Infallible>,
TO: WriteTarget<TransmittedWord = u8> + embedded_hal_nb::spi::FullDuplex,
CH: SingleChannel,
DC_: OutputPin<Error = Infallible>,
CS_: OutputPin<Error = Infallible>,
> DrawBuffer<Display<DI, RST>, PioTransfer<TO, CH>, (DC_, CS_)>
{
fn flush_frame(&mut self) {
let (ch, b, mut spi) = self.pio.take().unwrap().wait();
self.stolen_pin.1.set_high().unwrap();
// After the DMA operated, we need to empty the receive FIFO, otherwise the touch screen
// driver will pick wrong values.
// Continue to read as long as we don't get a Err(WouldBlock)
while !spi.read().is_err() {}
self.pio = Some(PioTransfer::Idle(ch, b, spi));
}
}
struct PicoBackend<DrawBuffer, Touch> {
window: RefCell<Option<Rc<renderer::MinimalSoftwareWindow>>>,
buffer_provider: RefCell<DrawBuffer>,
touch: RefCell<Touch>,
}
impl<
DI: display_interface::WriteOnlyDataCommand,
RST: OutputPin<Error = Infallible>,
TO: WriteTarget<TransmittedWord = u8> + embedded_hal_nb::spi::FullDuplex,
CH: SingleChannel,
DC_: OutputPin<Error = Infallible>,
CS_: OutputPin<Error = Infallible>,
IRQ: InputPin<Error = Infallible>,
SPI: SpiDevice,
> slint::platform::Platform
for PicoBackend<
DrawBuffer<Display<DI, RST>, PioTransfer<TO, CH>, (DC_, CS_)>,
xpt2046::XPT2046<IRQ, SPI>,
>
{
fn create_window_adapter(
&self,
) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
let window = renderer::MinimalSoftwareWindow::new(renderer::RepaintBufferType::ReusedBuffer);
self.window.replace(Some(window.clone()));
Ok(window)
}
fn duration_since_start(&self) -> core::time::Duration {
let counter = cortex_m::interrupt::free(|cs| {
TIMER.borrow(cs).borrow().as_ref().map(|t| t.get_counter().ticks()).unwrap_or_default()
});
core::time::Duration::from_micros(counter)
}
fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
let mut last_touch = None;
self.window.borrow().as_ref().unwrap().set_size(DISPLAY_SIZE);
loop {
slint::platform::update_timers_and_animations();
if let Some(window) = self.window.borrow().clone() {
window.draw_if_needed(|renderer| {
let mut buffer_provider = self.buffer_provider.borrow_mut();
renderer.render_by_line(&mut *buffer_provider);
buffer_provider.flush_frame();
});
// handle touch event
let button = PointerEventButton::Left;
if let Some(event) = self
.touch
.borrow_mut()
.read()
.map_err(|_| ())
.unwrap()
.map(|point| {
let position = slint::PhysicalPosition::new(
(point.x * DISPLAY_SIZE.width as f32) as _,
(point.y * DISPLAY_SIZE.height as f32) as _,
)
.to_logical(window.scale_factor());
match last_touch.replace(position) {
Some(_) => WindowEvent::PointerMoved { position },
None => WindowEvent::PointerPressed { position, button },
}
})
.or_else(|| {
last_touch
.take()
.map(|position| WindowEvent::PointerReleased { position, button })
})
{
let is_pointer_release_event =
matches!(event, WindowEvent::PointerReleased { .. });
window.dispatch_event(event);
// removes hover state on widgets
if is_pointer_release_event {
window.dispatch_event(WindowEvent::PointerExited);
}
// Don't go to sleep after a touch event that forces a redraw
continue;
}
if window.has_active_animations() {
continue;
}
}
let sleep_duration = match slint::platform::duration_until_next_timer_update() {
None => None,
Some(d) => {
let micros = d.as_micros() as u32;
if micros < 10 {
// Cannot wait for less than 10µs, or `schedule()` panics
continue;
} else {
Some(fugit::MicrosDurationU32::micros(micros))
}
}
};
cortex_m::interrupt::free(|cs| {
if let Some(duration) = sleep_duration {
ALARM0.borrow(cs).borrow_mut().as_mut().unwrap().schedule(duration).unwrap();
}
IRQ_PIN
.borrow(cs)
.borrow()
.as_ref()
.unwrap()
.set_interrupt_enabled(GpioInterrupt::LevelLow, true);
});
cortex_m::asm::wfe();
}
}
}
mod xpt2046 {
use core::cell::RefCell;
use cortex_m::interrupt::Mutex;
use embedded_hal::digital::InputPin;
use embedded_hal::spi::SpiDevice;
use euclid::default::Point2D;
use fugit::Hertz;
pub const SPI_FREQ: Hertz<u32> = Hertz::<u32>::Hz(3_000_000);
pub enum Error<PinE, TransferE> {
Pin(PinE),
Transfer(TransferE),
}
pub struct XPT2046<IRQ: InputPin + 'static, SPI: SpiDevice> {
irq: &'static Mutex<RefCell<Option<IRQ>>>,
spi: SPI,
pressed: bool,
}
impl<PinE, IRQ: InputPin<Error = PinE>, SPI: SpiDevice> XPT2046<IRQ, SPI> {
pub fn new(irq: &'static Mutex<RefCell<Option<IRQ>>>, spi: SPI) -> Result<Self, PinE> {
Ok(Self { irq, spi, pressed: false })
}
pub fn read(&mut self) -> Result<Option<Point2D<f32>>, Error<PinE, SPI::Error>> {
const PRESS_THRESHOLD: i32 = -25_000;
const RELEASE_THRESHOLD: i32 = -30_000;
let threshold = if self.pressed { RELEASE_THRESHOLD } else { PRESS_THRESHOLD };
self.pressed = false;
if cortex_m::interrupt::free(|cs| {
self.irq.borrow(cs).borrow_mut().as_mut().unwrap().is_low()
})
.map_err(|e| Error::Pin(e))?
{
const CMD_X_READ: u8 = 0b10010000;
const CMD_Y_READ: u8 = 0b11010000;
const CMD_Z1_READ: u8 = 0b10110000;
const CMD_Z2_READ: u8 = 0b11000000;
// These numbers were measured approximately.
const MIN_X: u32 = 1900;
const MAX_X: u32 = 30300;
const MIN_Y: u32 = 2300;
const MAX_Y: u32 = 30300;
macro_rules! xchg {
($byte:expr) => {{
let mut b = [0, $byte, 0, 0];
self.spi.transfer_in_place(&mut b).map_err(|e| Error::Transfer(e))?;
let [_, _, h, l] = b;
((h as u32) << 8) | (l as u32)
}};
}
let z1 = xchg!(CMD_Z1_READ);
let z2 = xchg!(CMD_Z2_READ);
let z = z1 as i32 - z2 as i32;
if z < threshold {
return Ok(None);
}
let mut point = Point2D::new(0u32, 0u32);
for _ in 0..10 {
let y = xchg!(CMD_Y_READ);
let x = xchg!(CMD_X_READ);
point += euclid::vec2(i16::MAX as u32 - x, y)
}
point /= 10;
let z1 = xchg!(CMD_Z1_READ);
let z2 = xchg!(CMD_Z2_READ);
let z = z1 as i32 - z2 as i32;
if z < RELEASE_THRESHOLD {
return Ok(None);
}
self.pressed = true;
Ok(Some(euclid::point2(
point.x.saturating_sub(MIN_X) as f32 / (MAX_X - MIN_X) as f32,
point.y.saturating_sub(MIN_Y) as f32 / (MAX_Y - MIN_Y) as f32,
)))
} else {
Ok(None)
}
}
}
}
#[interrupt]
fn IO_IRQ_BANK0() {
cortex_m::interrupt::free(|cs| {
let mut pin = IRQ_PIN.borrow(cs).borrow_mut();
let pin = pin.as_mut().unwrap();
pin.set_interrupt_enabled(GpioInterrupt::LevelLow, false);
pin.clear_interrupt(GpioInterrupt::LevelLow);
});
}
#[interrupt]
fn TIMER0_IRQ_0() {
cortex_m::interrupt::free(|cs| {
ALARM0.borrow(cs).borrow_mut().as_mut().unwrap().clear_interrupt();
});
}
#[global_allocator]
static ALLOCATOR: Heap = Heap::empty();
pub fn init() {
let mut pac = hal::pac::Peripherals::take().unwrap();
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
let clocks = hal::clocks::init_clocks_and_plls(
XTAL_FREQ_HZ,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();
unsafe { ALLOCATOR.init(core::ptr::addr_of_mut!(HEAP) as usize, HEAP_SIZE) }
let mut timer = Timer::new_timer0(pac.TIMER0, &mut pac.RESETS, &clocks);
let sio = hal::sio::Sio::new(pac.SIO);
let pins = hal::gpio::Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);
let uart_pins = (
pins.gpio0.into_function::<hal::gpio::FunctionUart>(),
pins.gpio1.into_function::<hal::gpio::FunctionUart>(),
);
let uart = hal::uart::UartPeripheral::new(pac.UART0, uart_pins, &mut pac.RESETS)
.enable(
hal::uart::UartConfig::new(9600.Hz(), hal::uart::DataBits::Eight, None, hal::uart::StopBits::One),
clocks.peripheral_clock.freq(),
).unwrap();
uart.write_full_blocking(b"Hello World!\n");
let mut touch_cs = pins.gpio16.into_push_pull_output();
touch_cs.set_high().unwrap();
let touch_irq = pins.gpio17.into_pull_up_input();
touch_irq.set_interrupt_enabled(GpioInterrupt::LevelLow, true);
cortex_m::interrupt::free(|cs| {
IRQ_PIN.borrow(cs).replace(Some(touch_irq));
});
let rst = pins.gpio15.into_push_pull_output();
let mut backlight = pins.gpio13.into_push_pull_output();
let dc = pins.gpio8.into_push_pull_output();
let cs = pins.gpio9.into_push_pull_output();
let spi_sclk = pins.gpio10.into_function::<gpio::FunctionSpi>();
let spi_mosi = pins.gpio11.into_function::<gpio::FunctionSpi>();
let spi_miso = pins.gpio12.into_function::<gpio::FunctionSpi>();
let spi_device = pac.SPI1;
let spi_pin_layout = (spi_mosi, spi_miso, spi_sclk);
let spi = hal::Spi::<_,_,_,8>::new(spi_device, spi_pin_layout).init(
&mut pac.RESETS,
clocks.peripheral_clock.freq(),
SPI_ST7789VW_MAX_FREQ,
&embedded_hal::spi::MODE_3,
);
// SAFETY: This is not safe :-( But we need to access the SPI and its control pins for the PIO
let (dc_copy, cs_copy) =
unsafe { (core::ptr::read(&dc as *const _), core::ptr::read(&cs as *const _)) };
let stolen_spi = unsafe { core::ptr::read(&spi as *const _) };
let spi = singleton!(:SpiRefCell = SpiRefCell::new((spi, 0.Hz()))).unwrap();
let display_spi = SharedSpiWithFreq { refcell: spi, cs, freq: SPI_ST7789VW_MAX_FREQ };
let di = display_interface_spi::SPIInterface::new(display_spi, dc);
let display = mipidsi::Builder::new(mipidsi::models::ST7789, di)
.reset_pin(rst)
.display_size(DISPLAY_SIZE.height as _, DISPLAY_SIZE.width as _)
.orientation(mipidsi::options::Orientation::new().rotate(mipidsi::options::Rotation::Deg90))
.invert_colors(mipidsi::options::ColorInversion::Inverted)
.init(&mut timer)
.unwrap();
backlight.set_high().unwrap();
let touch = xpt2046::XPT2046::new(
&IRQ_PIN,
SharedSpiWithFreq { refcell: spi, cs: touch_cs, freq: xpt2046::SPI_FREQ },
)
.unwrap();
let mut alarm0 = timer.alarm_0().unwrap();
alarm0.enable_interrupt();
cortex_m::interrupt::free(|cs| {
ALARM0.borrow(cs).replace(Some(alarm0));
TIMER.borrow(cs).replace(Some(timer));
});
unsafe {
NVIC::unmask(hal::pac::Interrupt::IO_IRQ_BANK0);
NVIC::unmask(hal::pac::Interrupt::TIMER0_IRQ_0);
}
let dma = pac.DMA.split(&mut pac.RESETS);
let pio = PioTransfer::Idle(
dma.ch0,
vec![Rgb565Pixel::default(); DISPLAY_SIZE.width as _].leak(),
stolen_spi,
);
let buffer_provider = DrawBuffer {
display,
buffer: vec![Rgb565Pixel::default(); DISPLAY_SIZE.width as _].leak(),
pio: Some(pio),
stolen_pin: (dc_copy, cs_copy),
};
slint::platform::set_platform(Box::new(PicoBackend {
window: Default::default(),
buffer_provider: buffer_provider.into(),
touch: touch.into(),
}))
.unwrap();
}
#[link_section = ".start_block"]
#[used]
pub static IMAGE_DEF: hal::block::ImageDef = hal::block::ImageDef::secure_exe();
で、picotolを使うにはBinary Infoが必要なので
#![no_std]
#![no_main]
mod pico_st7789;
use pico_st7789::entry;
use pico_st7789::binary_info;
slint::include_modules!();
#[entry]
fn main() -> ! {
pico_st7789::init();
let ui = AppWindow::new().unwrap();
ui.on_request_increase_value({
let ui_handle = ui.as_weak();
move || {
let ui = ui_handle.unwrap();
ui.set_counter(ui.get_counter() + 1);
}
});
ui.run().unwrap();
panic!("The MCU demo should not quit")
}
/// Program metadata for `picotool info`
#[link_section = ".bi_entries"]
#[used]
pub static PICOTOOL_ENTRIES: [binary_info::EntryAddr; 3] = [
// binary_info::rp_program_name!(),
binary_info::rp_cargo_version!(),
binary_info::rp_program_description!(c"Slint hello world."),
// binary_info::rp_program_url!(),
binary_info::rp_program_build_attribute!(),
];
あとは、USBSEL押しながらケーブルつないで、cargo runしてあげれば、Pico2のうえでもSlint動きます。
#まとめ
なんというか、「Qiitaはコード置き場じゃない」とか「githubに入れてくれ」とか怨嗟の声が聞こえる気がするけど、展示会前で死にかけてるんだよ、察してくれよと誤魔化しながら、駆け足てPico2で動かしてみたを書いてみました。
展示会終わったら整理します。というわけで、お時間のある方は、EdgeTech+2024でシグナルスロット社のブースまでお越しください。お待ちしてます。
蛇足。動作動画です。
はーい。 pic.twitter.com/vejEFBXpDr
— 緑野翁 (@hermit4) November 17, 2024