Repository: slint-ui/slint
Pull request #8112: ESP32-S3-BOX-3 support - update to esp-hal 1.0.0
Author: login: georgik
Created: 2025-04-11T12:51:37Z | Merged: 2025-04-15T09:32:27Z

The example/mcu-board-support was updated with ESP32-S3-BOX-3 support and newer esp-hal 1.0.0.

The board also contains GT911 which has RST connected to shared pin, so it requires either handling of address configuration using RST and INT or fallback mechanism to check two addresses for GT911 on I2C. (This part could be refined in the future).

Timer was implmented using now and storing timer0 in the struct.

I welcome discussion about this changes, so that we can refine it to the way how Slint framework is built.

The working example can be tested from repo: https://github.com/georgik/esp32-slint-demo

Simply:
```
cargo r -r
```


Diff:
diff --git a/.cargo/config.toml b/.cargo/config.toml
index 4c90f1c87a8..76255d855fa 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -6,9 +6,8 @@ xtask = "run --package xtask --"
 
 [target.xtensa-esp32s3-none-elf]
 rustflags = [
-  # Recommended by the esp-backtrace crate used for panic handler
   "-C",
-  "force-frame-pointers",
+  "link-arg=-nostartfiles",
   # Without this flag, we get miscompilation of floating point operations that cause the clipping region to be totally wrong
   "-C",
   "target-feature=-fp",
diff --git a/api/cpp/Cargo.toml b/api/cpp/Cargo.toml
index 88e3989a746..e57374bb34d 100644
--- a/api/cpp/Cargo.toml
+++ b/api/cpp/Cargo.toml
@@ -58,8 +58,8 @@ i-slint-core = { workspace = true, features = ["ffi"] }
 slint-interpreter = { workspace = true, features = ["ffi", "compat-1-2"], optional = true }
 raw-window-handle = { version = "0.6", optional = true }
 
-esp-backtrace = { version = "0.14.0", features = ["panic-handler", "println"], optional = true }
-esp-println = { version = "0.12.0", default-features = false, features = ["uart"], optional = true }
+esp-backtrace = { version = "0.15.1", features = ["panic-handler", "println"], optional = true }
+esp-println = { version = "0.13.1", default-features = false, features = ["log", "auto"], optional = true }
 unicode-segmentation = { workspace = true }
 
 [build-dependencies]
diff --git a/demos/energy-monitor/src/main.rs b/demos/energy-monitor/src/main.rs
index 366f0c1e18c..830b481d787 100644
--- a/demos/energy-monitor/src/main.rs
+++ b/demos/energy-monitor/src/main.rs
@@ -3,6 +3,10 @@
 #![no_std]
 #![cfg_attr(all(feature = "mcu-board-support", not(feature = "simulator")), no_main)]
 
+#[cfg(feature = "mcu-board-support")]
+#[allow(unused_imports)]
+use mcu_board_support::prelude::*;
+
 #[cfg(not(feature = "mcu-board-support"))]
 pub fn main() {
     energy_monitor::main();
diff --git a/demos/printerdemo_mcu/main.rs b/demos/printerdemo_mcu/main.rs
index 93a8d6e0f6e..deb61a23939 100644
--- a/demos/printerdemo_mcu/main.rs
+++ b/demos/printerdemo_mcu/main.rs
@@ -11,6 +11,9 @@ use alloc::vec::Vec;
 use alloc::rc::Rc;
 use slint::Model;
 
+#[allow(unused_imports)]
+use mcu_board_support::prelude::*;
+
 slint::include_modules!();
 
 struct PrinterQueueData {
diff --git a/docs/astro/src/content/docs/guide/platforms/embedded.mdx b/docs/astro/src/content/docs/guide/platforms/embedded.mdx
index e10632956e7..7a769536c4e 100644
--- a/docs/astro/src/content/docs/guide/platforms/embedded.mdx
+++ b/docs/astro/src/content/docs/guide/platforms/embedded.mdx
@@ -99,6 +99,9 @@ The main.rs will look something like this
 #![cfg_attr(not(feature = "simulator"), no_main)]
 slint::include_modules!();
 
+#[allow(unused_imports)]
+use mcu_board_support::prelude::*;
+
 #[mcu_board_support::entry]
 fn main() -> ! {
     mcu_board_support::init();
diff --git a/examples/carousel/rust/main.rs b/examples/carousel/rust/main.rs
index d196248e70a..8b018ba6aa9 100644
--- a/examples/carousel/rust/main.rs
+++ b/examples/carousel/rust/main.rs
@@ -7,6 +7,10 @@
 #[cfg(feature = "mcu-board-support")]
 extern crate alloc;
 
+#[cfg(feature = "mcu-board-support")]
+#[allow(unused_imports)]
+use mcu_board_support::prelude::*;
+
 #[cfg(target_arch = "wasm32")]
 use wasm_bindgen::prelude::*;
 
diff --git a/examples/mcu-board-support/Cargo.toml b/examples/mcu-board-support/Cargo.toml
index 594f9b13a99..71b283bc86d 100644
--- a/examples/mcu-board-support/Cargo.toml
+++ b/examples/mcu-board-support/Cargo.toml
@@ -70,7 +70,21 @@ stm32h735g = [
   "slint/libm",
   "getrandom",
 ]
-# esp32-s3-box = ["slint/unsafe-single-threaded", "esp-hal/esp32s3", "embedded-hal", "embedded-hal-bus", "esp-alloc", "esp-println/esp32s3", "esp-backtrace/esp32s3", "dep:mipidsi", "embedded-graphics-core", "slint/libm", "tt21100"]
+esp32-s3-box-3 = [
+  "slint/unsafe-single-threaded",
+  "esp-hal/esp32s3",
+  "esp-hal/unstable",             # required for PSRAM support
+  "esp-hal/psram",
+  "embedded-hal",
+  "embedded-hal-bus",
+  "esp-alloc",
+  "esp-println/esp32s3",
+  "esp-backtrace/esp32s3",
+  "dep:mipidsi",
+  "embedded-graphics-core",
+  "slint/libm",
+  "gt911",
+]
 stm32u5g9j-dk2 = [
   "embassy-stm32/stm32u5g9zj",
   "embassy-stm32/time-driver-any",
@@ -116,6 +130,8 @@ pin-weak = { version = "1", default-features = false }
 rgb = "0.8.27"
 cfg-if = "1"
 
+log = "0.4.27"
+
 embedded-alloc = { version = "0.6", optional = true }
 cortex-m-rt = { version = "0.7", optional = true }
 cortex-m = { version = "0.7.2", optional = true }
@@ -135,11 +151,10 @@ embedded-display-controller = { version = "0.2.0", optional = true }
 ft5336 = { version = "0.2", optional = true }
 
 # Commented out until ported to newer version that matches embassy-time needed by stm32u5
-# esp-hal = { version = "0.22", optional = true }
-esp-alloc = { version = "0.5", optional = true }
-esp-println = { version = "0.12.0", optional = true }
-esp-backtrace = { version = "0.14.0", optional = true, features = ["panic-handler", "println"] }
-tt21100 = { version = "0.1", optional = true }
+esp-hal = { version = "1.0.0-beta.0", optional = true }
+esp-alloc = { version = "0.7", optional = true }
+esp-println = { version = "0.13.1", features = ["log"], optional = true }
+esp-backtrace = { version = "0.15.1", optional = true, features = ["panic-handler", "println"] }
 
 mipidsi = { version = "0.9.0", optional = true }
 embedded-graphics-core = { version = "0.4", optional = true }
diff --git a/examples/mcu-board-support/README.md b/examples/mcu-board-support/README.md
index e9c144e5382..13189548581 100644
--- a/examples/mcu-board-support/README.md
+++ b/examples/mcu-board-support/README.md
@@ -17,6 +17,9 @@ The main.rs will look something like this
 #![cfg_attr(not(feature = "simulator"), no_main)]
 slint::include_modules!();
 
+#[allow(unused_imports)]
+use mcu_board_support::prelude::*;
+
 #[mcu_board_support::entry]
 fn main() -> ! {
     mcu_board_support::init();
@@ -205,13 +208,10 @@ CARGO_PROFILE_RELEASE_OPT_LEVEL=s CARGO_TARGET_THUMBV8M_MAIN_NONE_EABIHF_RUNNER=
 When flashing, with `esplash`, you will be prompted to select a USB port. If this port is always the same, then you can also pass it as a parameter on the command line to avoid the prompt. For example if
 `/dev/ttyUSB1` is the device file for your port, the command line changes to `espflash --monitor /dev/ttyUSB1 path/to/binary/to/flash_and_monitor`.
 
-<!--
 #### ESP32-S3-Box
 
 To compile and run the demo:
 
 ```sh
-CARGO_PROFILE_RELEASE_OPT_LEVEL=s cargo +esp run -p printerdemo_mcu --target xtensa-esp32s3-none-elf --no-default-features --features=mcu-board-support/esp32-s3-box --release --config examples/mcu-board-support/esp32_s3_box/cargo-config.toml
+CARGO_PROFILE_RELEASE_OPT_LEVEL=s cargo +esp run -p printerdemo_mcu --target xtensa-esp32s3-none-elf --no-default-features --features=mcu-board-support/esp32-s3-box-3 --release --config examples/mcu-board-support/esp32_s3_box_3/cargo-config.toml
 ```
-
--->
diff --git a/examples/mcu-board-support/esp32_s3_box.rs b/examples/mcu-board-support/esp32_s3_box.rs
deleted file mode 100644
index 82aac303819..00000000000
--- a/examples/mcu-board-support/esp32_s3_box.rs
+++ /dev/null
@@ -1,219 +0,0 @@
-// Copyright © SixtyFPS GmbH <info@slint.dev>
-// SPDX-License-Identifier: MIT
-
-use alloc::boxed::Box;
-use alloc::rc::Rc;
-use core::cell::RefCell;
-use core::mem::MaybeUninit;
-use embedded_graphics_core::geometry::OriginDimensions;
-use embedded_hal::digital::OutputPin;
-use esp_alloc as _;
-use esp_backtrace as _;
-use esp_hal::delay::Delay;
-pub use esp_hal::entry;
-use esp_hal::gpio::{Input, Level, Output, Pull};
-use esp_hal::i2c::master as i2c;
-use esp_hal::prelude::*;
-use esp_hal::rtc_cntl::Rtc;
-use esp_hal::spi::master as spi;
-use esp_hal::timer::{systimer::SystemTimer, timg::TimerGroup};
-use mipidsi::{options::Orientation, Display};
-use slint::platform::WindowEvent;
-
-pub fn init() {
-    const HEAP_SIZE: usize = 250 * 1024;
-    static mut HEAP: MaybeUninit<[u8; HEAP_SIZE]> = MaybeUninit::uninit();
-    unsafe {
-        esp_alloc::HEAP.add_region(esp_alloc::HeapRegion::new(
-            HEAP.as_mut_ptr() as *mut u8,
-            HEAP_SIZE,
-            esp_alloc::MemoryCapability::Internal.into(),
-        ));
-    }
-    slint::platform::set_platform(Box::new(EspBackend::default()))
-        .expect("backend already initialized");
-}
-
-#[derive(Default)]
-struct EspBackend {
-    window: RefCell<Option<Rc<slint::platform::software_renderer::MinimalSoftwareWindow>>>,
-}
-
-impl slint::platform::Platform for EspBackend {
-    fn create_window_adapter(
-        &self,
-    ) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
-        let window = slint::platform::software_renderer::MinimalSoftwareWindow::new(
-            slint::platform::software_renderer::RepaintBufferType::ReusedBuffer,
-        );
-        self.window.replace(Some(window.clone()));
-        Ok(window)
-    }
-
-    fn duration_since_start(&self) -> core::time::Duration {
-        core::time::Duration::from_millis(
-            SystemTimer::now() / (SystemTimer::ticks_per_second() / 1000),
-        )
-    }
-
-    fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
-        let peripherals = esp_hal::init(esp_hal::Config::default());
-
-        let mut rtc = Rtc::new(peripherals.LPWR);
-        rtc.rwdt.disable();
-        let mut timer_group0 = TimerGroup::new(peripherals.TIMG0);
-        timer_group0.wdt.disable();
-        let mut timer_group1 = TimerGroup::new(peripherals.TIMG1);
-        timer_group1.wdt.disable();
-
-        let mut delay = Delay::new();
-
-        let i2c = i2c::I2c::new(
-            peripherals.I2C0,
-            i2c::Config { frequency: 400u32.kHz(), ..i2c::Config::default() },
-        )
-        .with_sda(peripherals.GPIO8)
-        .with_scl(peripherals.GPIO18);
-
-        let mut touch = tt21100::TT21100::new(i2c, Input::new(peripherals.GPIO3, Pull::Up))
-            .expect("Initialize the touch device");
-
-        let spi = spi::Spi::new_with_config(
-            peripherals.SPI2,
-            spi::Config { frequency: 60u32.MHz(), ..spi::Config::default() },
-        )
-        .with_sck(peripherals.GPIO7)
-        .with_mosi(peripherals.GPIO6);
-
-        let dc = Output::new(peripherals.GPIO4, Level::Low);
-        let cs = Output::new(peripherals.GPIO5, Level::Low);
-        let rst = Output::new(peripherals.GPIO48, Level::Low);
-
-        let spi = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi, cs).unwrap();
-        let mut buffer = [0u8; 512];
-        let di = mipidsi::interface::SpiInterface::new(spi, dc, &mut buffer);
-        let display = mipidsi::Builder::new(mipidsi::models::ILI9342CRgb565, di)
-            .reset_pin(rst)
-            .orientation(Orientation::new().rotate(mipidsi::options::Rotation::Deg180))
-            .color_order(mipidsi::options::ColorOrder::Bgr)
-            .init(&mut delay)
-            .unwrap();
-
-        let mut backlight = Output::new(peripherals.GPIO45, Level::High);
-        backlight.set_high();
-
-        let size = display.size();
-        let size = slint::PhysicalSize::new(size.width, size.height);
-
-        self.window.borrow().as_ref().unwrap().set_size(size);
-
-        let mut buffer_provider = DrawBuffer {
-            display,
-            buffer: &mut [slint::platform::software_renderer::Rgb565Pixel(0); 320],
-        };
-
-        let mut last_touch = None;
-
-        loop {
-            slint::platform::update_timers_and_animations();
-
-            if let Some(window) = self.window.borrow().clone() {
-                let mut event_count = 0;
-                // The hardware keeps a queue of events. We should ideally process all event from the queue before rendering
-                // or we will get outdated event in the next frames. But move events are constantly added to the queue
-                // so we would block the whole interface, so add an arbitrary threshold
-                while event_count < 15 && touch.data_available().unwrap() {
-                    event_count += 1;
-                    match touch.event() {
-                        // Ignore error because we sometimes get an error at the beginning
-                        Err(_) => (),
-                        Ok(tt21100::Event::Button(..)) => (),
-                        Ok(tt21100::Event::Touch { report: _, touches }) => {
-                            let button = slint::platform::PointerEventButton::Left;
-                            if let Some(event) = touches
-                                .0
-                                .map(|record| {
-                                    let position = slint::PhysicalPosition::new(
-                                        ((319. - record.x as f32) * size.width as f32 / 319.) as _,
-                                        (record.y as f32 * size.height as f32 / 239.) 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.try_dispatch_event(event)?;
-
-                                // removes hover state on widgets
-                                if is_pointer_release_event {
-                                    window.try_dispatch_event(WindowEvent::PointerExited)?;
-                                }
-                            }
-                        }
-                    }
-                }
-
-                window.draw_if_needed(|renderer| {
-                    renderer.render_by_line(&mut buffer_provider);
-                });
-                if window.has_active_animations() {
-                    continue;
-                }
-            }
-            // TODO
-        }
-    }
-
-    fn debug_log(&self, arguments: core::fmt::Arguments) {
-        esp_println::println!("{}", arguments);
-    }
-}
-
-struct DrawBuffer<'a, Display> {
-    display: Display,
-    buffer: &'a mut [slint::platform::software_renderer::Rgb565Pixel],
-}
-
-impl<
-        DI: mipidsi::interface::Interface<Word = u8>,
-        RST: OutputPin<Error = core::convert::Infallible>,
-    > slint::platform::software_renderer::LineBufferProvider
-    for &mut DrawBuffer<'_, Display<DI, mipidsi::models::ILI9342CRgb565, RST>>
-{
-    type TargetPixel = slint::platform::software_renderer::Rgb565Pixel;
-
-    fn process_line(
-        &mut self,
-        line: usize,
-        range: core::ops::Range<usize>,
-        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_core::pixelcolor::raw::RawU16::new(x.0).into()),
-            )
-            .unwrap();
-    }
-}
diff --git a/examples/mcu-board-support/esp32_s3_box_3.rs b/examples/mcu-board-support/esp32_s3_box_3.rs
new file mode 100644
index 00000000000..3412a3aa4e2
--- /dev/null
+++ b/examples/mcu-board-support/esp32_s3_box_3.rs
@@ -0,0 +1,304 @@
+// Copyright © SixtyFPS GmbH <info@slint.dev>
+// SPDX-License-Identifier: MIT
+
+use alloc::boxed::Box;
+use alloc::rc::Rc;
+use core::cell::RefCell;
+use embedded_graphics_core::geometry::OriginDimensions;
+use embedded_hal::delay::DelayNs;
+use embedded_hal::digital::OutputPin;
+use embedded_hal_bus::spi::ExclusiveDevice;
+use esp_alloc as _;
+use esp_backtrace as _;
+use esp_hal::clock::CpuClock;
+use esp_hal::gpio::DriveMode;
+use esp_hal::peripherals::Peripherals;
+use esp_hal::time::Instant;
+use esp_hal::{
+    delay::Delay,
+    gpio::{Level, Output, OutputConfig},
+    i2c::master::I2c,
+    // init,
+    spi::master::{Config as SpiConfig, Spi},
+    spi::Mode as SpiMode,
+    time::Rate,
+};
+use esp_println::logger::init_logger_from_env;
+use gt911::Gt911Blocking;
+use log::{error, info, warn};
+use mipidsi::options::{ColorOrder, Orientation, Rotation};
+use slint::platform::PointerEventButton;
+use slint::platform::WindowEvent;
+
+struct EspBackend {
+    window: RefCell<Option<Rc<slint::platform::software_renderer::MinimalSoftwareWindow>>>,
+    peripherals: RefCell<Option<Peripherals>>,
+}
+
+impl Default for EspBackend {
+    fn default() -> Self {
+        EspBackend { window: RefCell::new(None), peripherals: RefCell::new(None) }
+    }
+}
+
+/// Initializes the heap and sets the Slint platform.
+pub fn init() {
+    // Initialize peripherals first.
+    let peripherals = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::_240MHz));
+    init_logger_from_env();
+    info!("Peripherals initialized");
+
+    // Initialize the PSRAM allocator.
+    esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram);
+
+    // Create an EspBackend that now owns the peripherals.
+    slint::platform::set_platform(Box::new(EspBackend {
+        peripherals: RefCell::new(Some(peripherals)),
+        window: RefCell::new(None),
+    }))
+    .expect("backend already initialized");
+}
+
+impl slint::platform::Platform for EspBackend {
+    fn create_window_adapter(
+        &self,
+    ) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
+        let window = slint::platform::software_renderer::MinimalSoftwareWindow::new(
+            slint::platform::software_renderer::RepaintBufferType::ReusedBuffer,
+        );
+        self.window.replace(Some(window.clone()));
+        Ok(window)
+    }
+
+    fn duration_since_start(&self) -> core::time::Duration {
+        core::time::Duration::from_millis(Instant::now().duration_since_epoch().as_millis())
+    }
+
+    fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
+        // Take and configure peripherals.
+        let peripherals = self.peripherals.borrow_mut().take().expect("Peripherals already taken");
+        let mut delay = Delay::new();
+
+        // The following sequence is necessary to properly initialize touch on ESP32-S3-BOX-3
+        // Based on issue from ESP-IDF: https://github.com/espressif/esp-bsp/issues/302#issuecomment-1971559689
+        // Related code: https://github.com/espressif/esp-bsp/blob/30f0111a97b8fbe2efb7e58366fcf4d26b380f23/components/lcd_touch/esp_lcd_touch_gt911/esp_lcd_touch_gt911.c#L101-L133
+        // --- Begin GT911 I²C Address Selection Sequence ---
+        // Define constants for the two possible addresses.
+        const ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS: u8 = 0x14;
+        const ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP: u8 = 0x5D;
+
+        // Our desired address.
+        const DESIRED_ADDR: u8 = 0x14;
+        // For desired address 0x14, assume the configuration’s reset level is false (i.e. 0 means active).
+        let reset_level = Level::Low;
+
+        // Configure the INT pin (GPIO3) as output; starting high because of internal pull-up.
+        let mut int_pin = Output::new(peripherals.GPIO3, Level::High, OutputConfig::default());
+        // Force INT low to prepare for address selection.
+        int_pin.set_low();
+        delay.delay_ms(10);
+        int_pin.set_low();
+        delay.delay_ms(1);
+
+        // Configure the shared RESET pin (GPIO48) as output in open–drain mode.
+        let mut rst = Output::new(
+            peripherals.GPIO48,
+            Level::Low, // start in active state
+            OutputConfig::default().with_drive_mode(DriveMode::OpenDrain),
+        );
+
+        // Set RESET to the reset-active level (here, false).
+        rst.set_level(reset_level);
+        // (Ensure INT remains low.)
+        int_pin.set_low();
+        delay.delay_ms(10);
+
+        // Now, select the I²C address:
+        // For GT911 address 0x14, the desired INT level is low; otherwise, for backup (0x5D) it would be high.
+        let gpio_level = if DESIRED_ADDR == ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS {
+            Level::Low
+        } else if DESIRED_ADDR == ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP {
+            Level::High
+        } else {
+            Level::Low
+        };
+        int_pin.set_level(gpio_level);
+        delay.delay_ms(1);
+
+        // Toggle the RESET pin:
+        // Release RESET by setting it to the opposite of the reset level.
+        rst.set_level(!reset_level);
+        delay.delay_ms(10);
+        delay.delay_ms(50);
+        // --- End GT911 I²C Address Selection Sequence ---
+
+        // --- Begin SPI and Display Initialization ---
+        let spi = Spi::<esp_hal::Blocking>::new(
+            peripherals.SPI2,
+            SpiConfig::default().with_frequency(Rate::from_mhz(40)).with_mode(SpiMode::_0),
+        )
+        .unwrap()
+        .with_sck(peripherals.GPIO7)
+        .with_mosi(peripherals.GPIO6);
+
+        // Display control pins.
+        let dc = Output::new(peripherals.GPIO4, Level::Low, OutputConfig::default());
+        let cs = Output::new(peripherals.GPIO5, Level::Low, OutputConfig::default());
+
+        // Wrap SPI into a bus.
+        let spi_delay = Delay::new();
+        let spi_device = ExclusiveDevice::new(spi, cs, spi_delay).unwrap();
+        let mut buffer = [0u8; 512];
+        let di = mipidsi::interface::SpiInterface::new(spi_device, dc, &mut buffer);
+
+        // Initialize the display.
+        let display = mipidsi::Builder::new(mipidsi::models::ILI9486Rgb565, di)
+            .reset_pin(rst)
+            .orientation(Orientation::new().rotate(Rotation::Deg180))
+            .color_order(ColorOrder::Bgr)
+            .init(&mut delay)
+            .unwrap();
+
+        // Set up the backlight pin.
+        let mut backlight = Output::new(peripherals.GPIO47, Level::Low, OutputConfig::default());
+        backlight.set_high();
+
+        // Update the Slint window size from the display.
+        let size = display.size();
+        let size = slint::PhysicalSize::new(size.width, size.height);
+        self.window.borrow().as_ref().unwrap().set_size(size);
+
+        // --- End Display Initialization ---
+
+        let mut i2c = I2c::new(
+            peripherals.I2C0,
+            esp_hal::i2c::master::Config::default().with_frequency(Rate::from_khz(400)),
+        )
+        .unwrap()
+        .with_sda(peripherals.GPIO8)
+        .with_scl(peripherals.GPIO18);
+
+        // Initialize the touch driver.
+        let mut touch = Gt911Blocking::new(ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS);
+        match touch.init(&mut i2c) {
+            Ok(_) => info!("Touch initialized"),
+            Err(e) => {
+                warn!("Touch initialization failed: {:?}", e);
+                let touch_fallback = Gt911Blocking::new(ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP);
+                match touch_fallback.init(&mut i2c) {
+                    Ok(_) => {
+                        info!("Touch initialized with backup address");
+                        touch = touch_fallback;
+                    }
+                    Err(e) => error!("Touch initialization failed with backup address: {:?}", e),
+                }
+            }
+        }
+
+        // Prepare a draw buffer for the Slint software renderer.
+        let mut buffer_provider = DrawBuffer {
+            display,
+            buffer: &mut [slint::platform::software_renderer::Rgb565Pixel(0); 320],
+        };
+
+        // Variable to track the last touch position.
+        let mut last_touch = None;
+
+        // Main event loop.
+        loop {
+            slint::platform::update_timers_and_animations();
+
+            if let Some(window) = self.window.borrow().clone() {
+                // Poll the GT911 for touch data.
+                match touch.get_touch(&mut i2c) {
+                    // Active touch detected: Some(point) means a press or move.
+                    Ok(Some(point)) => {
+                        // Convert GT911 raw coordinates (assumed in pixels) into a PhysicalPosition.
+                        let pos = slint::PhysicalPosition::new(point.x as i32, point.y as i32)
+                            .to_logical(window.scale_factor());
+
+                        let event = if let Some(previous_pos) = last_touch.replace(pos) {
+                            // If the position changed, send a PointerMoved event.
+                            if previous_pos != pos {
+                                WindowEvent::PointerMoved { position: pos }
+                            } else {
+                                // If the position is unchanged, skip event generation.
+                                continue;
+                            }
+                        } else {
+                            // No previous touch recorded, generate a PointerPressed event.
+                            WindowEvent::PointerPressed {
+                                position: pos,
+                                button: PointerEventButton::Left,
+                            }
+                        };
+
+                        // Dispatch the event to Slint.
+                        window.try_dispatch_event(event)?;
+                    }
+                    // No active touch: if a previous touch existed, dispatch pointer release.
+                    Ok(None) => {
+                        if let Some(pos) = last_touch.take() {
+                            window.try_dispatch_event(WindowEvent::PointerReleased {
+                                position: pos,
+                                button: PointerEventButton::Left,
+                            })?;
+                            window.try_dispatch_event(WindowEvent::PointerExited)?;
+                        }
+                    }
+                    // On errors, you can log them if desired.
+                    Err(_) => {
+                        // Optionally log or ignore errors.
+                    }
+                }
+
+                // Render the window if needed.
+                window.draw_if_needed(|renderer| {
+                    renderer.render_by_line(&mut buffer_provider);
+                });
+
+                if window.has_active_animations() {
+                    continue;
+                }
+            }
+        }
+    }
+}
+
+/// Provides a draw buffer for the MinimalSoftwareWindow renderer.
+struct DrawBuffer<'a, Display> {
+    display: Display,
+    buffer: &'a mut [slint::platform::software_renderer::Rgb565Pixel],
+}
+
+impl<
+        DI: mipidsi::interface::Interface<Word = u8>,
+        RST: OutputPin<Error = core::convert::Infallible>,
+    > slint::platform::software_renderer::LineBufferProvider
+    for &mut DrawBuffer<'_, mipidsi::Display<DI, mipidsi::models::ILI9486Rgb565, RST>>
+{
+    type TargetPixel = slint::platform::software_renderer::Rgb565Pixel;
+
+    fn process_line(
+        &mut self,
+        line: usize,
+        range: core::ops::Range<usize>,
+        render_fn: impl FnOnce(&mut [slint::platform::software_renderer::Rgb565Pixel]),
+    ) {
+        let buffer = &mut self.buffer[range.clone()];
+        render_fn(buffer);
+
+        // Update the display with the rendered line.
+        self.display
+            .set_pixels(
+                range.start as u16,
+                line as u16,
+                range.end as u16,
+                line as u16,
+                buffer
+                    .iter()
+                    .map(|x| embedded_graphics_core::pixelcolor::raw::RawU16::new(x.0).into()),
+            )
+            .unwrap();
+    }
+}
diff --git a/examples/mcu-board-support/esp32_s3_box/cargo-config.toml b/examples/mcu-board-support/esp32_s3_box_3/cargo-config.toml
similarity index 59%
rename from examples/mcu-board-support/esp32_s3_box/cargo-config.toml
rename to examples/mcu-board-support/esp32_s3_box_3/cargo-config.toml
index 15b65d46aae..41d9aee4c0d 100644
--- a/examples/mcu-board-support/esp32_s3_box/cargo-config.toml
+++ b/examples/mcu-board-support/esp32_s3_box_3/cargo-config.toml
@@ -1,18 +1,13 @@
 # Copyright © SixtyFPS GmbH <info@slint.dev>
 # SPDX-License-Identifier: MIT
 
+[env]
+ESP_LOG = "INFO"
+ESP_HAL_CONFIG_PSRAM_MODE = "octal"
+
 [target.xtensa-esp32s3-none-elf]
 runner = "espflash flash --monitor"
-rustflags = [
-  "-C",
-  "force-frame-pointers",
-  "-C",
-  "link-arg=-nostartfiles",
-  "-C",
-  "link-arg=-Wl,-Tlinkall.x",
-  "-C",
-  "target-feature=-fp",
-]
+rustflags = ["-C", "link-arg=-Wl,-Tlinkall.x"]
 
 [build]
 target = "xtensa-esp32s3-none-elf"
diff --git a/examples/mcu-board-support/lib.rs b/examples/mcu-board-support/lib.rs
index 79836d0958a..b61ac2fb666 100644
--- a/examples/mcu-board-support/lib.rs
+++ b/examples/mcu-board-support/lib.rs
@@ -29,17 +29,19 @@ mod stm32u5g9j_dk2;
 #[cfg(feature = "stm32u5g9j-dk2")]
 pub use stm32u5g9j_dk2::*;
 
-// #[cfg(feature = "esp32-s3-box")]
-// mod esp32_s3_box;
-// #[cfg(feature = "esp32-s3-box")]
-// pub use esp32_s3_box::*;
+#[cfg(feature = "esp32-s3-box-3")]
+mod esp32_s3_box_3;
+#[cfg(feature = "esp32-s3-box-3")]
+pub use esp32_s3_box_3::*;
+#[cfg(feature = "esp32-s3-box-3")]
+pub use esp_hal::main as entry;
 
 #[cfg(not(any(
     feature = "pico-st7789",
     feature = "pico2-st7789",
     feature = "stm32h735g",
     feature = "stm32u5g9j-dk2",
-//    feature = "esp32-s3-box"
+    feature = "esp32-s3-box-3"
 )))]
 pub use i_slint_core_macros::identity as entry;
 
@@ -48,9 +50,14 @@ pub use i_slint_core_macros::identity as entry;
     feature = "pico2-st7789",
     feature = "stm32h735g",
     feature = "stm32u5g9j-dk2",
-//    feature = "esp32-s3-box"
+    feature = "esp32-s3-box-3"
 )))]
 pub fn init() {}
 
 #[cfg(feature = "stm32u5g9j-dk2")]
 mod embassy;
+
+pub mod prelude {
+    #[cfg(feature = "esp32-s3-box-3")]
+    pub use esp_hal;
+}


Reviews:
user: login: WilstonOreo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T13:29:09Z
user: login: tronical
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T13:41:53Z
user: login: MabezDev
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-14T11:47:22Z
user: login: georgik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T13:53:29Z
user: login: tronical
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-15T09:28:17Z

Review comments:
diff_hunk: @@ -0,0 +1,354 @@
+// Copyright © SixtyFPS GmbH <info@slint.dev>
+// SPDX-License-Identifier: MIT
+
+use alloc::boxed::Box;
+use alloc::rc::Rc;
+use core::cell::RefCell;
+use embedded_graphics_core::geometry::OriginDimensions;
+use embedded_hal::delay::DelayNs;
+use embedded_hal::digital::OutputPin;
+use embedded_hal_bus::spi::ExclusiveDevice;
+use esp_alloc as _;
+use esp_backtrace as _;
+use esp_hal::clock::CpuClock;
+use esp_hal::gpio::DriveMode;
+use esp_hal::peripherals::Peripherals;
+use esp_hal::time::Instant;
+use esp_hal::timer::timg::TimerGroup;
+use esp_hal::timer::Timer;
+use esp_hal::{
+    delay::Delay,
+    gpio::{Level, Output, OutputConfig},
+    i2c::master::I2c,
+    // init,
+    spi::master::{Config as SpiConfig, Spi},
+    spi::Mode as SpiMode,
+    time::Rate,
+};
+use esp_println::logger::init_logger_from_env;
+use gt911::Gt911Blocking;
+use log::{error, info};
+use mipidsi::options::{ColorOrder, Orientation, Rotation};
+use slint::platform::PointerEventButton;
+use slint::platform::WindowEvent;
+
+struct TimerState {
+    start_instant: Instant,
+    timer0: Option<esp_hal::timer::timg::Timer>,
+}
+
+// Provide a default implementation for TimerState.
+// We assume that Instant::EPOCH is a good default.
+impl Default for TimerState {
+    fn default() -> Self {
+        TimerState { start_instant: Instant::EPOCH, timer0: None }
+    }
+}
+
+struct EspBackend {
+    window: RefCell<Option<Rc<slint::platform::software_renderer::MinimalSoftwareWindow>>>,
+    peripherals: RefCell<Option<Peripherals>>,
+    timer_state: RefCell<TimerState>,
+}
+
+impl Default for EspBackend {
+    fn default() -> Self {
+        EspBackend {
+            window: RefCell::new(None),
+            peripherals: RefCell::new(None),
+            timer_state: RefCell::new(TimerState::default()),
+        }
+    }
+}
+
+/// Initializes the heap and sets the Slint platform.
+pub fn init() {
+    // Initialize peripherals first.
+    let peripherals = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::_240MHz));
+    init_logger_from_env();
+    info!("Peripherals initialized");
+
+    // Initialize the PSRAM allocator.
+    esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram);
+
+    // Create an EspBackend that now owns the peripherals.
+    slint::platform::set_platform(Box::new(EspBackend {
+        peripherals: RefCell::new(Some(peripherals)),
+        window: RefCell::new(None),
+        timer_state: RefCell::new(TimerState::default()),
+    }))
+    .expect("backend already initialized");
+}
+
+impl slint::platform::Platform for EspBackend {
+    fn create_window_adapter(
+        &self,
+    ) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
+        let window = slint::platform::software_renderer::MinimalSoftwareWindow::new(
+            slint::platform::software_renderer::RepaintBufferType::ReusedBuffer,
+        );
+        self.window.replace(Some(window.clone()));
+        Ok(window)
+    }
+
+    fn duration_since_start(&self) -> core::time::Duration {
+        let ts = self.timer_state.borrow();
+        if let Some(ref t0) = ts.timer0 {
+            let now_ticks = t0.now();
+            let hal_elapsed = now_ticks - ts.start_instant;
+            core::time::Duration::from_micros(hal_elapsed.as_micros())
+        } else {
+            core::time::Duration::from_nanos(0)
+        }
+    }
+
+    fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
+        // Take and configure peripherals.
+        let peripherals = self.peripherals.borrow_mut().take().expect("Peripherals already taken");
+        let mut delay = Delay::new();
+
+        // Initialize timer0 from TIMG0.
+        let timer_group0 = TimerGroup::new(peripherals.TIMG0);
+        let timer0 = timer_group0.timer0;
+
+        // Start the timer.
+        timer0.start();
+
+        // Read the current tick value.
+        let start = timer0.now();
+        info!("Timer initialized: start_ticks = {}", start);
+
+        // Update timer_state (one borrow_mut call).
+        {
+            let mut ts = self.timer_state.borrow_mut();
+            ts.start_instant = start;
+            ts.timer0 = Some(timer0);
+        }
+
+        // The following sequence is necessary to properly initialize touch on ESP32-S3-BOX-3
+        // Based on issue from ESP-IDF: https://github.com/espressif/esp-bsp/issues/302#issuecomment-1971559689
+        // Related code: https://github.com/espressif/esp-bsp/blob/30f0111a97b8fbe2efb7e58366fcf4d26b380f23/components/lcd_touch/esp_lcd_touch_gt911/esp_lcd_touch_gt911.c#L101-L133
+        // --- Begin GT911 I²C Address Selection Sequence ---
+        // Define constants for the two possible addresses.
+        const ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS: u8 = 0x14;
+        const ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP: u8 = 0x5D;
+
+        // Our desired address.
+        const DESIRED_ADDR: u8 = 0x14;
+        // For desired address 0x14, assume the configuration’s reset level is false (i.e. 0 means active).
+        let reset_level = Level::Low;
+
+        // Configure the INT pin (GPIO3) as output; starting high because of internal pull-up.
+        let mut int_pin = Output::new(peripherals.GPIO3, Level::High, OutputConfig::default());
+        // Force INT low to prepare for address selection.
+        int_pin.set_low();
+        delay.delay_ms(10);
+        int_pin.set_low();
+        delay.delay_ms(1);
+
+        // Configure the shared RESET pin (GPIO48) as output in open–drain mode.
+        let mut rst = Output::new(
+            peripherals.GPIO48,
+            Level::Low, // start in active state
+            OutputConfig::default().with_drive_mode(DriveMode::OpenDrain),
+        );
+
+        // Set RESET to the reset-active level (here, false).
+        rst.set_level(reset_level);
+        // (Ensure INT remains low.)
+        int_pin.set_low();
+        delay.delay_ms(10);
+
+        // Now, select the I²C address:
+        // For GT911 address 0x14, the desired INT level is low; otherwise, for backup (0x5D) it would be high.
+        let gpio_level = if DESIRED_ADDR == ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS {
+            Level::Low
+        } else if DESIRED_ADDR == ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP {
+            Level::High
+        } else {
+            Level::Low
+        };
+        int_pin.set_level(gpio_level);
+        delay.delay_ms(1);
+
+        // Toggle the RESET pin:
+        // Release RESET by setting it to the opposite of the reset level.
+        rst.set_level(!reset_level);
+        delay.delay_ms(10);
+        delay.delay_ms(50);
+        // --- End GT911 I²C Address Selection Sequence ---
+
+        // --- Begin SPI and Display Initialization ---
+        let spi = Spi::<esp_hal::Blocking>::new(
+            peripherals.SPI2,
+            SpiConfig::default().with_frequency(Rate::from_mhz(40)).with_mode(SpiMode::_0),
+        )
+        .unwrap()
+        .with_sck(peripherals.GPIO7)
+        .with_mosi(peripherals.GPIO6);
+
+        // Display control pins.
+        let dc = Output::new(peripherals.GPIO4, Level::Low, OutputConfig::default());
+        let cs = Output::new(peripherals.GPIO5, Level::Low, OutputConfig::default());
+
+        // Wrap SPI into a bus.
+        let spi_delay = Delay::new();
+        let spi_device = ExclusiveDevice::new(spi, cs, spi_delay).unwrap();
+        let mut buffer = [0u8; 512];
+        let di = mipidsi::interface::SpiInterface::new(spi_device, dc, &mut buffer);
+
+        // Initialize the display.
+        let display = mipidsi::Builder::new(mipidsi::models::ILI9486Rgb565, di)
+            .reset_pin(rst)
+            .orientation(Orientation::new().rotate(Rotation::Deg180))
+            .color_order(ColorOrder::Bgr)
+            .init(&mut delay)
+            .unwrap();
+
+        // Set up the backlight pin.
+        let mut backlight = Output::new(peripherals.GPIO47, Level::Low, OutputConfig::default());
+        backlight.set_high();
+
+        // Update the Slint window size from the display.
+        let size = display.size();
+        let size = slint::PhysicalSize::new(size.width, size.height);
+        self.window.borrow().as_ref().unwrap().set_size(size);
+
+        // --- End Display Initialization ---
+
+        let mut i2c = I2c::new(
+            peripherals.I2C0,
+            esp_hal::i2c::master::Config::default().with_frequency(Rate::from_khz(400)),
+        )
+        .unwrap()
+        .with_sda(peripherals.GPIO8)
+        .with_scl(peripherals.GPIO18);
+
+        // Initialize the touch driver.
+        let mut touch = Gt911Blocking::new(ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS);
+        match touch.init(&mut i2c) {
+            Ok(_) => info!("Touch initialized"),
+            Err(e) => {
+                info!("Touch initialization failed: {:?}", e);
path: examples/mcu-board-support/esp32_s3_box_3.rs
user: login: WilstonOreo
body: `warning!()` might be better here
created_at: 2025-04-11T13:18:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+// Copyright © SixtyFPS GmbH <info@slint.dev>
+// SPDX-License-Identifier: MIT
+
+use alloc::boxed::Box;
+use alloc::rc::Rc;
+use core::cell::RefCell;
+use embedded_graphics_core::geometry::OriginDimensions;
+use embedded_hal::delay::DelayNs;
+use embedded_hal::digital::OutputPin;
+use embedded_hal_bus::spi::ExclusiveDevice;
+use esp_alloc as _;
+use esp_backtrace as _;
+use esp_hal::clock::CpuClock;
+use esp_hal::gpio::DriveMode;
+use esp_hal::peripherals::Peripherals;
+use esp_hal::time::Instant;
+use esp_hal::timer::timg::TimerGroup;
+use esp_hal::timer::Timer;
+use esp_hal::{
+    delay::Delay,
+    gpio::{Level, Output, OutputConfig},
+    i2c::master::I2c,
+    // init,
+    spi::master::{Config as SpiConfig, Spi},
+    spi::Mode as SpiMode,
+    time::Rate,
+};
+use esp_println::logger::init_logger_from_env;
+use gt911::Gt911Blocking;
+use log::{error, info};
+use mipidsi::options::{ColorOrder, Orientation, Rotation};
+use slint::platform::PointerEventButton;
+use slint::platform::WindowEvent;
+
+struct TimerState {
+    start_instant: Instant,
+    timer0: Option<esp_hal::timer::timg::Timer>,
+}
+
+// Provide a default implementation for TimerState.
+// We assume that Instant::EPOCH is a good default.
+impl Default for TimerState {
+    fn default() -> Self {
+        TimerState { start_instant: Instant::EPOCH, timer0: None }
+    }
+}
+
+struct EspBackend {
+    window: RefCell<Option<Rc<slint::platform::software_renderer::MinimalSoftwareWindow>>>,
+    peripherals: RefCell<Option<Peripherals>>,
+    timer_state: RefCell<TimerState>,
+}
+
+impl Default for EspBackend {
+    fn default() -> Self {
+        EspBackend {
+            window: RefCell::new(None),
+            peripherals: RefCell::new(None),
+            timer_state: RefCell::new(TimerState::default()),
+        }
+    }
+}
+
+/// Initializes the heap and sets the Slint platform.
+pub fn init() {
+    // Initialize peripherals first.
+    let peripherals = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::_240MHz));
+    init_logger_from_env();
+    info!("Peripherals initialized");
+
+    // Initialize the PSRAM allocator.
+    esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram);
+
+    // Create an EspBackend that now owns the peripherals.
+    slint::platform::set_platform(Box::new(EspBackend {
+        peripherals: RefCell::new(Some(peripherals)),
+        window: RefCell::new(None),
+        timer_state: RefCell::new(TimerState::default()),
+    }))
+    .expect("backend already initialized");
+}
+
+impl slint::platform::Platform for EspBackend {
+    fn create_window_adapter(
+        &self,
+    ) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
+        let window = slint::platform::software_renderer::MinimalSoftwareWindow::new(
+            slint::platform::software_renderer::RepaintBufferType::ReusedBuffer,
+        );
+        self.window.replace(Some(window.clone()));
+        Ok(window)
+    }
+
+    fn duration_since_start(&self) -> core::time::Duration {
+        let ts = self.timer_state.borrow();
+        if let Some(ref t0) = ts.timer0 {
+            let now_ticks = t0.now();
+            let hal_elapsed = now_ticks - ts.start_instant;
+            core::time::Duration::from_micros(hal_elapsed.as_micros())
+        } else {
+            core::time::Duration::from_nanos(0)
+        }
+    }
+
+    fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
+        // Take and configure peripherals.
+        let peripherals = self.peripherals.borrow_mut().take().expect("Peripherals already taken");
+        let mut delay = Delay::new();
+
+        // Initialize timer0 from TIMG0.
+        let timer_group0 = TimerGroup::new(peripherals.TIMG0);
+        let timer0 = timer_group0.timer0;
+
+        // Start the timer.
+        timer0.start();
+
+        // Read the current tick value.
+        let start = timer0.now();
+        info!("Timer initialized: start_ticks = {}", start);
+
+        // Update timer_state (one borrow_mut call).
+        {
+            let mut ts = self.timer_state.borrow_mut();
+            ts.start_instant = start;
+            ts.timer0 = Some(timer0);
+        }
+
+        // The following sequence is necessary to properly initialize touch on ESP32-S3-BOX-3
+        // Based on issue from ESP-IDF: https://github.com/espressif/esp-bsp/issues/302#issuecomment-1971559689
+        // Related code: https://github.com/espressif/esp-bsp/blob/30f0111a97b8fbe2efb7e58366fcf4d26b380f23/components/lcd_touch/esp_lcd_touch_gt911/esp_lcd_touch_gt911.c#L101-L133
+        // --- Begin GT911 I²C Address Selection Sequence ---
+        // Define constants for the two possible addresses.
+        const ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS: u8 = 0x14;
+        const ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP: u8 = 0x5D;
+
+        // Our desired address.
+        const DESIRED_ADDR: u8 = 0x14;
+        // For desired address 0x14, assume the configuration’s reset level is false (i.e. 0 means active).
+        let reset_level = Level::Low;
+
+        // Configure the INT pin (GPIO3) as output; starting high because of internal pull-up.
+        let mut int_pin = Output::new(peripherals.GPIO3, Level::High, OutputConfig::default());
+        // Force INT low to prepare for address selection.
+        int_pin.set_low();
+        delay.delay_ms(10);
+        int_pin.set_low();
+        delay.delay_ms(1);
+
+        // Configure the shared RESET pin (GPIO48) as output in open–drain mode.
+        let mut rst = Output::new(
+            peripherals.GPIO48,
+            Level::Low, // start in active state
+            OutputConfig::default().with_drive_mode(DriveMode::OpenDrain),
+        );
+
+        // Set RESET to the reset-active level (here, false).
+        rst.set_level(reset_level);
+        // (Ensure INT remains low.)
+        int_pin.set_low();
+        delay.delay_ms(10);
+
+        // Now, select the I²C address:
+        // For GT911 address 0x14, the desired INT level is low; otherwise, for backup (0x5D) it would be high.
+        let gpio_level = if DESIRED_ADDR == ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS {
+            Level::Low
+        } else if DESIRED_ADDR == ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP {
+            Level::High
+        } else {
+            Level::Low
+        };
+        int_pin.set_level(gpio_level);
+        delay.delay_ms(1);
+
+        // Toggle the RESET pin:
+        // Release RESET by setting it to the opposite of the reset level.
+        rst.set_level(!reset_level);
+        delay.delay_ms(10);
+        delay.delay_ms(50);
+        // --- End GT911 I²C Address Selection Sequence ---
+
+        // --- Begin SPI and Display Initialization ---
+        let spi = Spi::<esp_hal::Blocking>::new(
+            peripherals.SPI2,
+            SpiConfig::default().with_frequency(Rate::from_mhz(40)).with_mode(SpiMode::_0),
+        )
+        .unwrap()
+        .with_sck(peripherals.GPIO7)
+        .with_mosi(peripherals.GPIO6);
+
+        // Display control pins.
+        let dc = Output::new(peripherals.GPIO4, Level::Low, OutputConfig::default());
+        let cs = Output::new(peripherals.GPIO5, Level::Low, OutputConfig::default());
+
+        // Wrap SPI into a bus.
+        let spi_delay = Delay::new();
+        let spi_device = ExclusiveDevice::new(spi, cs, spi_delay).unwrap();
+        let mut buffer = [0u8; 512];
+        let di = mipidsi::interface::SpiInterface::new(spi_device, dc, &mut buffer);
+
+        // Initialize the display.
+        let display = mipidsi::Builder::new(mipidsi::models::ILI9486Rgb565, di)
+            .reset_pin(rst)
+            .orientation(Orientation::new().rotate(Rotation::Deg180))
+            .color_order(ColorOrder::Bgr)
+            .init(&mut delay)
+            .unwrap();
+
+        // Set up the backlight pin.
+        let mut backlight = Output::new(peripherals.GPIO47, Level::Low, OutputConfig::default());
+        backlight.set_high();
+
+        // Update the Slint window size from the display.
+        let size = display.size();
+        let size = slint::PhysicalSize::new(size.width, size.height);
+        self.window.borrow().as_ref().unwrap().set_size(size);
+
+        // --- End Display Initialization ---
+
+        let mut i2c = I2c::new(
+            peripherals.I2C0,
+            esp_hal::i2c::master::Config::default().with_frequency(Rate::from_khz(400)),
+        )
+        .unwrap()
+        .with_sda(peripherals.GPIO8)
+        .with_scl(peripherals.GPIO18);
+
+        // Initialize the touch driver.
+        let mut touch = Gt911Blocking::new(ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS);
+        match touch.init(&mut i2c) {
+            Ok(_) => info!("Touch initialized"),
+            Err(e) => {
+                info!("Touch initialization failed: {:?}", e);
+                let touch_fallback = Gt911Blocking::new(ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP);
+                match touch_fallback.init(&mut i2c) {
+                    Ok(_) => {
+                        info!("Touch initialized with backup address");
+                        touch = touch_fallback;
+                    }
+                    Err(e) => error!("Touch initialization failed with backup address: {:?}", e),
+                }
+            }
+        }
+
+        // Prepare a draw buffer for the Slint software renderer.
+        let mut buffer_provider = DrawBuffer {
+            display,
+            buffer: &mut [slint::platform::software_renderer::Rgb565Pixel(0); 320],
+        };
+
+        // Variable to track the last touch position.
+        let mut last_touch = None;
+
+        // Main event loop.
+        loop {
+            slint::platform::update_timers_and_animations();
+
+            if let Some(window) = self.window.borrow().clone() {
+                // Poll the GT911 for touch data.
+                match touch.get_touch(&mut i2c) {
+                    // Active touch detected: Some(point) means a press or move.
+                    Ok(Some(point)) => {
+                        // Convert GT911 raw coordinates (assumed in pixels) into a PhysicalPosition.
+                        let pos = slint::PhysicalPosition::new(point.x as i32, point.y as i32)
+                            .to_logical(window.scale_factor());
+
+                        let event = if let Some(previous_pos) = last_touch.replace(pos) {
+                            // If the position changed, send a PointerMoved event.
+                            if previous_pos != pos {
+                                WindowEvent::PointerMoved { position: pos }
+                            } else {
+                                // If the position is unchanged, skip event generation.
+                                continue;
+                            }
+                        } else {
+                            // No previous touch recorded, generate a PointerPressed event.
+                            WindowEvent::PointerPressed {
+                                position: pos,
+                                button: PointerEventButton::Left,
+                            }
+                        };
+
+                        // Dispatch the event to Slint.
+                        window.try_dispatch_event(event)?;
+                    }
+                    // No active touch: if a previous touch existed, dispatch pointer release.
+                    Ok(None) => {
+                        if let Some(pos) = last_touch.take() {
+                            window.try_dispatch_event(WindowEvent::PointerReleased {
+                                position: pos,
+                                button: PointerEventButton::Left,
+                            })?;
+                            window.try_dispatch_event(WindowEvent::PointerExited)?;
+                        }
+                    }
+                    // On errors, you can log them if desired.
+                    Err(_) => {
+                        // Optionally log or ignore errors.
+                    }
+                }
+
+                // Render the window if needed.
+                window.draw_if_needed(|renderer| {
+                    renderer.render_by_line(&mut buffer_provider);
+                });
+
+                if window.has_active_animations() {
+                    continue;
+                }
+            }
+        }
+    }
+
+    fn debug_log(&self, arguments: core::fmt::Arguments) {
+        esp_println::println!("{}", arguments);
+    }
+}
+
+/// Provides a draw buffer for the MinimalSoftwareWindow renderer.
+struct DrawBuffer<'a, Display> {
+    display: Display,
+    buffer: &'a mut [slint::platform::software_renderer::Rgb565Pixel],
+}
+
+impl<
+        DI: mipidsi::interface::Interface<Word = u8>,
+        RST: OutputPin<Error = core::convert::Infallible>,
+    > slint::platform::software_renderer::LineBufferProvider
+    for &mut DrawBuffer<'_, mipidsi::Display<DI, mipidsi::models::ILI9486Rgb565, RST>>
+{
+    type TargetPixel = slint::platform::software_renderer::Rgb565Pixel;
+
+    fn process_line(
+        &mut self,
+        line: usize,
+        range: core::ops::Range<usize>,
+        render_fn: impl FnOnce(&mut [slint::platform::software_renderer::Rgb565Pixel]),
+    ) {
+        let buffer = &mut self.buffer[range.clone()];
+        render_fn(buffer);
+
+        // Update the display with the rendered line.
+        self.display
+            .set_pixels(
+                range.start as u16,
+                line as u16,
+                range.end as u16,
+                line as u16,
+                buffer
+                    .iter()
+                    .map(|x| embedded_graphics_core::pixelcolor::raw::RawU16::new(x.0).into()),
path: examples/mcu-board-support/esp32_s3_box_3.rs
user: login: WilstonOreo
body: Total pro would be to implement the [`TargetPixel` trait](https://docs.slint.dev/latest/docs/rust/slint/platform/software_renderer/trait.TargetPixel) for `embedded_graphics_core::pixelcolor::raw::RawU16`. Might improve rendering performance
created_at: 2025-04-11T13:25:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+// Copyright © SixtyFPS GmbH <info@slint.dev>
+// SPDX-License-Identifier: MIT
+
+use alloc::boxed::Box;
+use alloc::rc::Rc;
+use core::cell::RefCell;
+use embedded_graphics_core::geometry::OriginDimensions;
+use embedded_hal::delay::DelayNs;
+use embedded_hal::digital::OutputPin;
+use embedded_hal_bus::spi::ExclusiveDevice;
+use esp_alloc as _;
+use esp_backtrace as _;
+use esp_hal::clock::CpuClock;
+use esp_hal::gpio::DriveMode;
+use esp_hal::peripherals::Peripherals;
+use esp_hal::time::Instant;
+use esp_hal::timer::timg::TimerGroup;
+use esp_hal::timer::Timer;
+use esp_hal::{
+    delay::Delay,
+    gpio::{Level, Output, OutputConfig},
+    i2c::master::I2c,
+    // init,
+    spi::master::{Config as SpiConfig, Spi},
+    spi::Mode as SpiMode,
+    time::Rate,
+};
+use esp_println::logger::init_logger_from_env;
+use gt911::Gt911Blocking;
+use log::{error, info};
+use mipidsi::options::{ColorOrder, Orientation, Rotation};
+use slint::platform::PointerEventButton;
+use slint::platform::WindowEvent;
+
+struct TimerState {
+    start_instant: Instant,
+    timer0: Option<esp_hal::timer::timg::Timer>,
+}
+
+// Provide a default implementation for TimerState.
+// We assume that Instant::EPOCH is a good default.
+impl Default for TimerState {
+    fn default() -> Self {
+        TimerState { start_instant: Instant::EPOCH, timer0: None }
+    }
+}
+
+struct EspBackend {
+    window: RefCell<Option<Rc<slint::platform::software_renderer::MinimalSoftwareWindow>>>,
+    peripherals: RefCell<Option<Peripherals>>,
+    timer_state: RefCell<TimerState>,
+}
+
+impl Default for EspBackend {
+    fn default() -> Self {
+        EspBackend {
+            window: RefCell::new(None),
+            peripherals: RefCell::new(None),
+            timer_state: RefCell::new(TimerState::default()),
+        }
+    }
+}
+
+/// Initializes the heap and sets the Slint platform.
+pub fn init() {
+    // Initialize peripherals first.
+    let peripherals = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::_240MHz));
+    init_logger_from_env();
+    info!("Peripherals initialized");
+
+    // Initialize the PSRAM allocator.
+    esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram);
+
+    // Create an EspBackend that now owns the peripherals.
+    slint::platform::set_platform(Box::new(EspBackend {
+        peripherals: RefCell::new(Some(peripherals)),
+        window: RefCell::new(None),
+        timer_state: RefCell::new(TimerState::default()),
+    }))
+    .expect("backend already initialized");
+}
+
+impl slint::platform::Platform for EspBackend {
+    fn create_window_adapter(
+        &self,
+    ) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
+        let window = slint::platform::software_renderer::MinimalSoftwareWindow::new(
+            slint::platform::software_renderer::RepaintBufferType::ReusedBuffer,
+        );
+        self.window.replace(Some(window.clone()));
+        Ok(window)
+    }
+
+    fn duration_since_start(&self) -> core::time::Duration {
+        let ts = self.timer_state.borrow();
+        if let Some(ref t0) = ts.timer0 {
+            let now_ticks = t0.now();
+            let hal_elapsed = now_ticks - ts.start_instant;
+            core::time::Duration::from_micros(hal_elapsed.as_micros())
+        } else {
+            core::time::Duration::from_nanos(0)
+        }
+    }
+
+    fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
+        // Take and configure peripherals.
+        let peripherals = self.peripherals.borrow_mut().take().expect("Peripherals already taken");
+        let mut delay = Delay::new();
+
+        // Initialize timer0 from TIMG0.
+        let timer_group0 = TimerGroup::new(peripherals.TIMG0);
+        let timer0 = timer_group0.timer0;
+
+        // Start the timer.
+        timer0.start();
+
+        // Read the current tick value.
+        let start = timer0.now();
+        info!("Timer initialized: start_ticks = {}", start);
+
+        // Update timer_state (one borrow_mut call).
+        {
+            let mut ts = self.timer_state.borrow_mut();
+            ts.start_instant = start;
+            ts.timer0 = Some(timer0);
+        }
+
+        // The following sequence is necessary to properly initialize touch on ESP32-S3-BOX-3
+        // Based on issue from ESP-IDF: https://github.com/espressif/esp-bsp/issues/302#issuecomment-1971559689
+        // Related code: https://github.com/espressif/esp-bsp/blob/30f0111a97b8fbe2efb7e58366fcf4d26b380f23/components/lcd_touch/esp_lcd_touch_gt911/esp_lcd_touch_gt911.c#L101-L133
+        // --- Begin GT911 I²C Address Selection Sequence ---
+        // Define constants for the two possible addresses.
+        const ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS: u8 = 0x14;
+        const ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP: u8 = 0x5D;
+
+        // Our desired address.
+        const DESIRED_ADDR: u8 = 0x14;
+        // For desired address 0x14, assume the configuration’s reset level is false (i.e. 0 means active).
+        let reset_level = Level::Low;
+
+        // Configure the INT pin (GPIO3) as output; starting high because of internal pull-up.
+        let mut int_pin = Output::new(peripherals.GPIO3, Level::High, OutputConfig::default());
+        // Force INT low to prepare for address selection.
+        int_pin.set_low();
+        delay.delay_ms(10);
+        int_pin.set_low();
+        delay.delay_ms(1);
+
+        // Configure the shared RESET pin (GPIO48) as output in open–drain mode.
+        let mut rst = Output::new(
+            peripherals.GPIO48,
+            Level::Low, // start in active state
+            OutputConfig::default().with_drive_mode(DriveMode::OpenDrain),
+        );
+
+        // Set RESET to the reset-active level (here, false).
+        rst.set_level(reset_level);
+        // (Ensure INT remains low.)
+        int_pin.set_low();
+        delay.delay_ms(10);
+
+        // Now, select the I²C address:
+        // For GT911 address 0x14, the desired INT level is low; otherwise, for backup (0x5D) it would be high.
+        let gpio_level = if DESIRED_ADDR == ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS {
+            Level::Low
+        } else if DESIRED_ADDR == ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP {
+            Level::High
+        } else {
+            Level::Low
+        };
+        int_pin.set_level(gpio_level);
+        delay.delay_ms(1);
+
+        // Toggle the RESET pin:
+        // Release RESET by setting it to the opposite of the reset level.
+        rst.set_level(!reset_level);
+        delay.delay_ms(10);
+        delay.delay_ms(50);
+        // --- End GT911 I²C Address Selection Sequence ---
+
+        // --- Begin SPI and Display Initialization ---
+        let spi = Spi::<esp_hal::Blocking>::new(
+            peripherals.SPI2,
+            SpiConfig::default().with_frequency(Rate::from_mhz(40)).with_mode(SpiMode::_0),
+        )
+        .unwrap()
+        .with_sck(peripherals.GPIO7)
+        .with_mosi(peripherals.GPIO6);
+
+        // Display control pins.
+        let dc = Output::new(peripherals.GPIO4, Level::Low, OutputConfig::default());
+        let cs = Output::new(peripherals.GPIO5, Level::Low, OutputConfig::default());
+
+        // Wrap SPI into a bus.
+        let spi_delay = Delay::new();
+        let spi_device = ExclusiveDevice::new(spi, cs, spi_delay).unwrap();
+        let mut buffer = [0u8; 512];
+        let di = mipidsi::interface::SpiInterface::new(spi_device, dc, &mut buffer);
+
+        // Initialize the display.
+        let display = mipidsi::Builder::new(mipidsi::models::ILI9486Rgb565, di)
+            .reset_pin(rst)
+            .orientation(Orientation::new().rotate(Rotation::Deg180))
+            .color_order(ColorOrder::Bgr)
+            .init(&mut delay)
+            .unwrap();
+
+        // Set up the backlight pin.
+        let mut backlight = Output::new(peripherals.GPIO47, Level::Low, OutputConfig::default());
+        backlight.set_high();
+
+        // Update the Slint window size from the display.
+        let size = display.size();
+        let size = slint::PhysicalSize::new(size.width, size.height);
+        self.window.borrow().as_ref().unwrap().set_size(size);
+
+        // --- End Display Initialization ---
+
+        let mut i2c = I2c::new(
+            peripherals.I2C0,
+            esp_hal::i2c::master::Config::default().with_frequency(Rate::from_khz(400)),
+        )
+        .unwrap()
+        .with_sda(peripherals.GPIO8)
+        .with_scl(peripherals.GPIO18);
+
+        // Initialize the touch driver.
+        let mut touch = Gt911Blocking::new(ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS);
+        match touch.init(&mut i2c) {
+            Ok(_) => info!("Touch initialized"),
+            Err(e) => {
+                info!("Touch initialization failed: {:?}", e);
+                let touch_fallback = Gt911Blocking::new(ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP);
+                match touch_fallback.init(&mut i2c) {
+                    Ok(_) => {
+                        info!("Touch initialized with backup address");
+                        touch = touch_fallback;
+                    }
+                    Err(e) => error!("Touch initialization failed with backup address: {:?}", e),
+                }
+            }
+        }
+
+        // Prepare a draw buffer for the Slint software renderer.
+        let mut buffer_provider = DrawBuffer {
+            display,
+            buffer: &mut [slint::platform::software_renderer::Rgb565Pixel(0); 320],
+        };
+
+        // Variable to track the last touch position.
+        let mut last_touch = None;
+
+        // Main event loop.
+        loop {
+            slint::platform::update_timers_and_animations();
+
+            if let Some(window) = self.window.borrow().clone() {
+                // Poll the GT911 for touch data.
+                match touch.get_touch(&mut i2c) {
+                    // Active touch detected: Some(point) means a press or move.
+                    Ok(Some(point)) => {
+                        // Convert GT911 raw coordinates (assumed in pixels) into a PhysicalPosition.
+                        let pos = slint::PhysicalPosition::new(point.x as i32, point.y as i32)
+                            .to_logical(window.scale_factor());
+
+                        let event = if let Some(previous_pos) = last_touch.replace(pos) {
+                            // If the position changed, send a PointerMoved event.
+                            if previous_pos != pos {
+                                WindowEvent::PointerMoved { position: pos }
+                            } else {
+                                // If the position is unchanged, skip event generation.
+                                continue;
+                            }
+                        } else {
+                            // No previous touch recorded, generate a PointerPressed event.
+                            WindowEvent::PointerPressed {
+                                position: pos,
+                                button: PointerEventButton::Left,
+                            }
+                        };
+
+                        // Dispatch the event to Slint.
+                        window.try_dispatch_event(event)?;
+                    }
+                    // No active touch: if a previous touch existed, dispatch pointer release.
+                    Ok(None) => {
+                        if let Some(pos) = last_touch.take() {
+                            window.try_dispatch_event(WindowEvent::PointerReleased {
+                                position: pos,
+                                button: PointerEventButton::Left,
+                            })?;
+                            window.try_dispatch_event(WindowEvent::PointerExited)?;
+                        }
+                    }
+                    // On errors, you can log them if desired.
+                    Err(_) => {
+                        // Optionally log or ignore errors.
+                    }
+                }
+
+                // Render the window if needed.
+                window.draw_if_needed(|renderer| {
+                    renderer.render_by_line(&mut buffer_provider);
+                });
+
+                if window.has_active_animations() {
+                    continue;
+                }
+            }
+        }
+    }
+
+    fn debug_log(&self, arguments: core::fmt::Arguments) {
path: examples/mcu-board-support/esp32_s3_box_3.rs
user: login: WilstonOreo
body: This function can be removed
created_at: 2025-04-11T13:26:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,6 +132,11 @@ pin-weak = { version = "1", default-features = false }
 rgb = "0.8.27"
 cfg-if = "1"
 
+
+esp-hal = { version = "1.0.0-beta.0", features = ["esp32s3", "unstable"], optional = true }
+log = "0.4.27"
+embedded-graphics-framebuf = "0.5.0"
path: examples/mcu-board-support/Cargo.toml
user: login: tronical
body: This looks like it's not needed?
created_at: 2025-04-11T13:40:26Z
author_association: MEMBER
diff_hunk: @@ -119,6 +132,11 @@ pin-weak = { version = "1", default-features = false }
 rgb = "0.8.27"
 cfg-if = "1"
 
+
+esp-hal = { version = "1.0.0-beta.0", features = ["esp32s3", "unstable"], optional = true }
+log = "0.4.27"
path: examples/mcu-board-support/Cargo.toml
user: login: tronical
body: This should be an optional dependency and enabled for `esp32-s3-box-3`.
created_at: 2025-04-11T13:41:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,354 @@
+// Copyright © SixtyFPS GmbH <info@slint.dev>
+// SPDX-License-Identifier: MIT
+
+use alloc::boxed::Box;
+use alloc::rc::Rc;
+use core::cell::RefCell;
+use embedded_graphics_core::geometry::OriginDimensions;
+use embedded_hal::delay::DelayNs;
+use embedded_hal::digital::OutputPin;
+use embedded_hal_bus::spi::ExclusiveDevice;
+use esp_alloc as _;
+use esp_backtrace as _;
+use esp_hal::clock::CpuClock;
+use esp_hal::gpio::DriveMode;
+use esp_hal::peripherals::Peripherals;
+use esp_hal::time::Instant;
+use esp_hal::timer::timg::TimerGroup;
+use esp_hal::timer::Timer;
+use esp_hal::{
+    delay::Delay,
+    gpio::{Level, Output, OutputConfig},
+    i2c::master::I2c,
+    // init,
+    spi::master::{Config as SpiConfig, Spi},
+    spi::Mode as SpiMode,
+    time::Rate,
+};
+use esp_println::logger::init_logger_from_env;
+use gt911::Gt911Blocking;
+use log::{error, info};
+use mipidsi::options::{ColorOrder, Orientation, Rotation};
+use slint::platform::PointerEventButton;
+use slint::platform::WindowEvent;
+
+struct TimerState {
+    start_instant: Instant,
+    timer0: Option<esp_hal::timer::timg::Timer>,
+}
+
+// Provide a default implementation for TimerState.
+// We assume that Instant::EPOCH is a good default.
+impl Default for TimerState {
+    fn default() -> Self {
+        TimerState { start_instant: Instant::EPOCH, timer0: None }
+    }
+}
+
+struct EspBackend {
+    window: RefCell<Option<Rc<slint::platform::software_renderer::MinimalSoftwareWindow>>>,
+    peripherals: RefCell<Option<Peripherals>>,
+    timer_state: RefCell<TimerState>,
+}
+
+impl Default for EspBackend {
+    fn default() -> Self {
+        EspBackend {
+            window: RefCell::new(None),
+            peripherals: RefCell::new(None),
+            timer_state: RefCell::new(TimerState::default()),
+        }
+    }
+}
+
+/// Initializes the heap and sets the Slint platform.
+pub fn init() {
+    // Initialize peripherals first.
+    let peripherals = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::_240MHz));
+    init_logger_from_env();
+    info!("Peripherals initialized");
+
+    // Initialize the PSRAM allocator.
+    esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram);
+
+    // Create an EspBackend that now owns the peripherals.
+    slint::platform::set_platform(Box::new(EspBackend {
+        peripherals: RefCell::new(Some(peripherals)),
+        window: RefCell::new(None),
+        timer_state: RefCell::new(TimerState::default()),
+    }))
+    .expect("backend already initialized");
+}
+
+impl slint::platform::Platform for EspBackend {
+    fn create_window_adapter(
+        &self,
+    ) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
+        let window = slint::platform::software_renderer::MinimalSoftwareWindow::new(
+            slint::platform::software_renderer::RepaintBufferType::ReusedBuffer,
+        );
+        self.window.replace(Some(window.clone()));
+        Ok(window)
+    }
+
+    fn duration_since_start(&self) -> core::time::Duration {
+        let ts = self.timer_state.borrow();
path: examples/mcu-board-support/esp32_s3_box_3.rs
user: login: MabezDev
body: You can remove all the code associated with this here, with the timing primitives from https://docs.espressif.com/projects/rust/esp-hal/1.0.0-beta.0/esp32s3/esp_hal/time/index.html

I think `core::time::Duration::from_secs(Instant::now().duration_since_epoch().as_secs())` should do it.
created_at: 2025-04-14T11:46:48Z
author_association: NONE
diff_hunk: @@ -0,0 +1,354 @@
+// Copyright © SixtyFPS GmbH <info@slint.dev>
+// SPDX-License-Identifier: MIT
+
+use alloc::boxed::Box;
+use alloc::rc::Rc;
+use core::cell::RefCell;
+use embedded_graphics_core::geometry::OriginDimensions;
+use embedded_hal::delay::DelayNs;
+use embedded_hal::digital::OutputPin;
+use embedded_hal_bus::spi::ExclusiveDevice;
+use esp_alloc as _;
+use esp_backtrace as _;
+use esp_hal::clock::CpuClock;
+use esp_hal::gpio::DriveMode;
+use esp_hal::peripherals::Peripherals;
+use esp_hal::time::Instant;
+use esp_hal::timer::timg::TimerGroup;
+use esp_hal::timer::Timer;
+use esp_hal::{
+    delay::Delay,
+    gpio::{Level, Output, OutputConfig},
+    i2c::master::I2c,
+    // init,
+    spi::master::{Config as SpiConfig, Spi},
+    spi::Mode as SpiMode,
+    time::Rate,
+};
+use esp_println::logger::init_logger_from_env;
+use gt911::Gt911Blocking;
+use log::{error, info};
+use mipidsi::options::{ColorOrder, Orientation, Rotation};
+use slint::platform::PointerEventButton;
+use slint::platform::WindowEvent;
+
+struct TimerState {
+    start_instant: Instant,
+    timer0: Option<esp_hal::timer::timg::Timer>,
path: examples/mcu-board-support/esp32_s3_box_3.rs
user: login: MabezDev
body: No need to waste a timer here, see below
created_at: 2025-04-14T11:47:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,354 @@
+// Copyright © SixtyFPS GmbH <info@slint.dev>
+// SPDX-License-Identifier: MIT
+
+use alloc::boxed::Box;
+use alloc::rc::Rc;
+use core::cell::RefCell;
+use embedded_graphics_core::geometry::OriginDimensions;
+use embedded_hal::delay::DelayNs;
+use embedded_hal::digital::OutputPin;
+use embedded_hal_bus::spi::ExclusiveDevice;
+use esp_alloc as _;
+use esp_backtrace as _;
+use esp_hal::clock::CpuClock;
+use esp_hal::gpio::DriveMode;
+use esp_hal::peripherals::Peripherals;
+use esp_hal::time::Instant;
+use esp_hal::timer::timg::TimerGroup;
+use esp_hal::timer::Timer;
+use esp_hal::{
+    delay::Delay,
+    gpio::{Level, Output, OutputConfig},
+    i2c::master::I2c,
+    // init,
+    spi::master::{Config as SpiConfig, Spi},
+    spi::Mode as SpiMode,
+    time::Rate,
+};
+use esp_println::logger::init_logger_from_env;
+use gt911::Gt911Blocking;
+use log::{error, info};
+use mipidsi::options::{ColorOrder, Orientation, Rotation};
+use slint::platform::PointerEventButton;
+use slint::platform::WindowEvent;
+
+struct TimerState {
+    start_instant: Instant,
+    timer0: Option<esp_hal::timer::timg::Timer>,
+}
+
+// Provide a default implementation for TimerState.
+// We assume that Instant::EPOCH is a good default.
+impl Default for TimerState {
+    fn default() -> Self {
+        TimerState { start_instant: Instant::EPOCH, timer0: None }
+    }
+}
+
+struct EspBackend {
+    window: RefCell<Option<Rc<slint::platform::software_renderer::MinimalSoftwareWindow>>>,
+    peripherals: RefCell<Option<Peripherals>>,
+    timer_state: RefCell<TimerState>,
+}
+
+impl Default for EspBackend {
+    fn default() -> Self {
+        EspBackend {
+            window: RefCell::new(None),
+            peripherals: RefCell::new(None),
+            timer_state: RefCell::new(TimerState::default()),
+        }
+    }
+}
+
+/// Initializes the heap and sets the Slint platform.
+pub fn init() {
+    // Initialize peripherals first.
+    let peripherals = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::_240MHz));
+    init_logger_from_env();
+    info!("Peripherals initialized");
+
+    // Initialize the PSRAM allocator.
+    esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram);
+
+    // Create an EspBackend that now owns the peripherals.
+    slint::platform::set_platform(Box::new(EspBackend {
+        peripherals: RefCell::new(Some(peripherals)),
+        window: RefCell::new(None),
+        timer_state: RefCell::new(TimerState::default()),
+    }))
+    .expect("backend already initialized");
+}
+
+impl slint::platform::Platform for EspBackend {
+    fn create_window_adapter(
+        &self,
+    ) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
+        let window = slint::platform::software_renderer::MinimalSoftwareWindow::new(
+            slint::platform::software_renderer::RepaintBufferType::ReusedBuffer,
+        );
+        self.window.replace(Some(window.clone()));
+        Ok(window)
+    }
+
+    fn duration_since_start(&self) -> core::time::Duration {
+        let ts = self.timer_state.borrow();
path: examples/mcu-board-support/esp32_s3_box_3.rs
user: login: georgik
body: Great. It works, just it was necessary to use as_millis, otherwise animations were too slow.
created_at: 2025-04-14T13:53:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: CLAassistant
created_at: 2025-04-11T12:51:43Z
body: [![CLA assistant check](https://cla-assistant.io/pull/badge/signed)](https://cla-assistant.io/slint-ui/slint?pullRequest=8112) <br/>All committers have signed the CLA.
author_association: NONE
user: login: georgik
created_at: 2025-04-14T13:33:02Z
body: @MabezDev The demo application is using the [following code](https://github.com/slint-ui/slint/blob/master/demos/printerdemo_mcu/main.rs#L35)
```
#[mcu_board_support::entry]
fn main() -> ! {
```

This was working in previous hal, but in esp-hal 1.0.0 is was exposed as `#[main]` which is proc macro. This will result in dragging `esp_hal` dependency to the application and it's necessary to add a dependency on esp-hal directly to application, which was not the case before. [Here's the `Cargo.toml`](https://github.com/slint-ui/slint/pull/8112/files#diff-fa53c87e42dcb237c2a89a23a0bbf6e4be065d9617df5d20d1903de6590d814cR29)

Any idea how to resolve the situation in case of proc macro?

author_association: CONTRIBUTOR
user: login: MabezDev
created_at: 2025-04-14T13:36:26Z
body: Why not just rexport `main` as `entry` in your `mcu_board_support` crate? Is it because it's a proc macro that it's not possible?
author_association: NONE
user: login: georgik
created_at: 2025-04-14T13:39:53Z
body: @MabezDev That's the current solution. The only problem is that it's necessary to have esp-hal as a dependency on application level. Otherwise resolution of `esp_hal::main fails`:
```
error[E0433]: failed to resolve: use of undeclared crate or module `esp_hal`
  --> demos/printerdemo_mcu/main.rs:36:1
   |
36 | #[mcu_board_support::entry]
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `esp_hal`
```


author_association: CONTRIBUTOR
user: login: MabezDev
created_at: 2025-04-14T13:45:07Z
body: > @MabezDev That's the current solution. The only problem is that it's necessary to have esp-hal as a dependency on application level. Otherwise resolution of `esp_hal::main fails`:
> 
> ```
> error[E0433]: failed to resolve: use of undeclared crate or module `esp_hal`
>   --> demos/printerdemo_mcu/main.rs:36:1
>    |
> 36 | #[mcu_board_support::entry]
>    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `esp_hal`
> ```

I see, could you file an issue against esp-rs/esp-hal please? I think this is something we want to support.
author_association: NONE