diff --git a/Cargo.toml b/Cargo.toml index afa96dc0..ce932c24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,7 @@ members = [ "apis/sensors/temperature", "apis/storage/key_value", "demos/st7789", + "demos/st7789-slint", "panic_handlers/debug_panic", "panic_handlers/small_panic", "platform", diff --git a/Makefile b/Makefile index 59421d5b..397a7fd5 100644 --- a/Makefile +++ b/Makefile @@ -111,7 +111,7 @@ EXCLUDE_RUNTIME := --exclude libtock --exclude libtock_runtime \ --exclude libtock_debug_panic --exclude libtock_small_panic # Arguments to pass to cargo to exclude demo crates. -EXCLUDE_RUNTIME := $(EXCLUDE_RUNTIME) --exclude st7789 +EXCLUDE_RUNTIME := $(EXCLUDE_RUNTIME) --exclude st7789 --exclude st7789-slint # Arguments to pass to cargo to exclude crates that cannot be tested by Miri. In # addition to excluding libtock_runtime, Miri also cannot test proc macro crates @@ -134,6 +134,7 @@ test: examples LIBTOCK_PLATFORM=hifive1 cargo clippy $(EXCLUDE_STD) \ --target=riscv32imac-unknown-none-elf --workspace $(MAKE) apollo3-st7789 + $(MAKE) apollo3-st7789-slint cd nightly && \ MIRIFLAGS="-Zmiri-strict-provenance -Zmiri-symbolic-alignment-check" \ cargo miri test $(EXCLUDE_MIRI) --manifest-path=../Cargo.toml \ @@ -227,6 +228,14 @@ $(1)-st7789: toolchain mkdir -p target/tbf/$(1) cp demos/st7789/target/$(1)/$(2)/release/st7789.{tab,tbf} \ target/tbf/$(1) + +.PHONY: $(1)-st7789-slint +$(1)-st7789-slint: toolchain + cd demos/st7789-slint && LIBTOCK_PLATFORM=$(1) cargo run $(features) \ + $(release) --target=$(2) --target-dir=target/$(1) + mkdir -p target/tbf/$(1) + cp demos/st7789-slint/target/$(1)/$(2)/release/st7789-slint.{tab,tbf} \ + target/tbf/$(1) endef # Creates the `make flash- EXAMPLE=` targets. Arguments: @@ -243,6 +252,12 @@ flash-$(1)-st7789: toolchain cd demos/st7789 && LIBTOCK_PLATFORM=$(1) cargo run $(features) \ $(release) --target=$(2) --target-dir=target/flash-$(1) -- \ --deploy=tockloader + +.PHONY: flash-$(1)-st7789-slint +flash-$(1)-st7789-slint: toolchain + cd demos/st7789-slint && LIBTOCK_PLATFORM=$(1) cargo run $(features) \ + $(release) --target=$(2) --target-dir=target/flash-$(1) -- \ + --deploy=tockloader endef $(eval $(call platform_build,apollo3,thumbv7em-none-eabi)) diff --git a/build_scripts/libtock_layout.ld b/build_scripts/libtock_layout.ld index 14029fd8..4103b57b 100644 --- a/build_scripts/libtock_layout.ld +++ b/build_scripts/libtock_layout.ld @@ -144,7 +144,7 @@ SECTIONS { .data ALIGN(4) : { data_ram_start = .; /* .sdata is the RISC-V small data section */ - *(.sdata .data) + *(.sdata .data*) /* Pad to word alignment so the relocation loop can use word-sized * copies. */ diff --git a/demos/st7789-slint/Cargo.toml b/demos/st7789-slint/Cargo.toml new file mode 100644 index 00000000..79be8592 --- /dev/null +++ b/demos/st7789-slint/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "st7789-slint" +version = "0.1.0" +edition = "2021" +rust-version.workspace = true +authors = ["Alistair Francis "] +description = """A demo to use the Slint GUI library with a ST7789 display via SPI using libtock-rs.""" +license = "Apache-2.0 OR MIT" + +[dependencies] +libtock = { path = "../../", features = ["rust_embedded"] } + +embedded-hal = "1.0" + +mipidsi = "0.8.0" +display-interface-spi = "0.5" +embedded-graphics = "0.8" + +# The heap allocator and portable atomics +embedded-alloc = "0.5.1" +critical-section = "1.0" + +slint = { git = "https://github.com/slint-ui/slint", default-features = false, features = ["libm", "unsafe-single-threaded"] } +mcu-board-support = { git = "https://github.com/slint-ui/slint" } + +display-interface = "0.5" + +[build-dependencies] +libtock_build_scripts = { path = "../../build_scripts" } + +slint-build = { git = "https://github.com/slint-ui/slint" } diff --git a/demos/st7789-slint/build.rs b/demos/st7789-slint/build.rs new file mode 100644 index 00000000..7ee9f923 --- /dev/null +++ b/demos/st7789-slint/build.rs @@ -0,0 +1,8 @@ +fn main() { + libtock_build_scripts::auto_layout(); + + let config = slint_build::CompilerConfiguration::new() + .embed_resources(slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer); + slint_build::compile_with_config("ui/appwindow.slint", config).unwrap(); + slint_build::print_rustc_flags().unwrap(); +} diff --git a/demos/st7789-slint/src/main.rs b/demos/st7789-slint/src/main.rs new file mode 100644 index 00000000..64833e3f --- /dev/null +++ b/demos/st7789-slint/src/main.rs @@ -0,0 +1,187 @@ +//! This sample demonstrates displaying a slint GUI on a ST7789 display +//! using a rust-embedded based crate + +#![no_main] +#![no_std] + +extern crate alloc; + +use core::fmt::Write; +use libtock::alarm::{Alarm, Milliseconds}; +use libtock::console::Console; +use libtock::gpio::Gpio; +use libtock::platform::ErrorCode; +use libtock::runtime::{set_main, stack_size}; +use libtock::spi_controller::EmbeddedHalSpi; + +use critical_section::RawRestoreState; +use embedded_alloc::Heap; + +use display_interface_spi::SPIInterface; +use embedded_hal::digital::OutputPin; +use mipidsi::{models::ST7789, options::ColorInversion, Builder, Display}; + +slint::include_modules!(); + +set_main! {main} +stack_size! {0x1400} + +#[global_allocator] +static HEAP: Heap = Heap::empty(); + +struct MyCriticalSection; +critical_section::set_impl!(MyCriticalSection); + +unsafe impl critical_section::Impl for MyCriticalSection { + unsafe fn acquire() -> RawRestoreState { + // Tock is single threaded, so this can only be preempted by interrupts + // The kernel won't schedule anything from our app unless we yield + // so as long as we don't yield we won't concurrently run with + // other critical sections from our app. + // The kernel might schedule itself or other applications, but there + // is nothing we can do about that. + } + + unsafe fn release(_token: RawRestoreState) {} +} + +// Setup the heap and the global allocator. +unsafe fn setup_heap() { + use core::mem::MaybeUninit; + + const HEAP_SIZE: usize = 1024 * 6; + static mut HEAP_MEM: [MaybeUninit; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE]; + unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) } +} + +// Display +const W: i32 = 240; +const H: i32 = 240; + +struct Delay; + +impl embedded_hal::delay::DelayNs for Delay { + fn delay_ns(&mut self, ns: u32) { + Alarm::sleep_for(Milliseconds(ns / (1000 * 1000))).unwrap(); + } +} + +#[mcu_board_support::entry] +fn main() { + writeln!(Console::writer(), "st7789-slint: example\r").unwrap(); + + unsafe { + setup_heap(); + } + + // Configure platform for Slint + let window = slint::platform::software_renderer::MinimalSoftwareWindow::new( + slint::platform::software_renderer::RepaintBufferType::ReusedBuffer, + ); + + window.set_size(slint::PhysicalSize::new(240, 240)); + + slint::platform::set_platform(alloc::boxed::Box::new(SlintPlatform { + window: window.clone(), + })) + .unwrap(); + + MainWindow::new().unwrap().run().unwrap(); +} + +struct SlintPlatform { + window: alloc::rc::Rc, +} + +impl slint::platform::Platform for SlintPlatform { + fn create_window_adapter( + &self, + ) -> Result, slint::PlatformError> { + Ok(self.window.clone()) + } + + fn duration_since_start(&self) -> core::time::Duration { + core::time::Duration::from_millis(Alarm::get_milliseconds().unwrap()) + } + + fn debug_log(&self, arguments: core::fmt::Arguments) { + writeln!(Console::writer(), "{}\r", arguments).unwrap(); + } + + fn run_event_loop(&self) -> Result<(), slint::PlatformError> { + let mut gpio_dc = Gpio::get_pin(0).unwrap(); + let dc = gpio_dc.make_output().unwrap(); + + let mut gpio_reset = Gpio::get_pin(1).unwrap(); + let reset = gpio_reset.make_output().unwrap(); + + let di = SPIInterface::new(EmbeddedHalSpi, dc); + + let mut delay = Delay; + let display = Builder::new(ST7789, di) + .display_size(W as u16, H as u16) + .invert_colors(ColorInversion::Inverted) + .reset_pin(reset) + .init(&mut delay) + .unwrap(); + + let mut buffer_provider = DrawBuffer { + display, + buffer: &mut [slint::platform::software_renderer::Rgb565Pixel(100); 240], + }; + + writeln!(Console::writer(), "Setup platform, running loop\r").unwrap(); + + loop { + slint::platform::update_timers_and_animations(); + + self.window.draw_if_needed(|renderer| { + renderer.render_by_line(&mut buffer_provider); + }); + + if self.window.has_active_animations() { + continue; + } + + if let Some(duration) = slint::platform::duration_until_next_timer_update() { + Alarm::sleep_for(Milliseconds(duration.as_millis() as u32)).unwrap(); + } + } + } +} + +struct DrawBuffer<'a, Display> { + display: Display, + buffer: &'a mut [slint::platform::software_renderer::Rgb565Pixel], +} + +impl> + slint::platform::software_renderer::LineBufferProvider + for &mut DrawBuffer<'_, Display> +{ + type TargetPixel = slint::platform::software_renderer::Rgb565Pixel; + + fn process_line( + &mut self, + line: usize, + range: core::ops::Range, + render_fn: impl FnOnce(&mut [slint::platform::software_renderer::Rgb565Pixel]), + ) { + let buffer = &mut self.buffer[range.clone()]; + + render_fn(buffer); + + // 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, + buffer + .iter() + .map(|x| embedded_graphics::pixelcolor::raw::RawU16::new(x.0).into()), + ) + .unwrap(); + } +} diff --git a/demos/st7789-slint/ui/appwindow.slint b/demos/st7789-slint/ui/appwindow.slint new file mode 100644 index 00000000..936812f2 --- /dev/null +++ b/demos/st7789-slint/ui/appwindow.slint @@ -0,0 +1,9 @@ +import {Button, AboutSlint} from "std-widgets.slint"; + +export component MainWindow inherits Window { + width: 240px; + height: 240px; + VerticalLayout { + AboutSlint { } + } +}