// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: MIT #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { esp_println::println!("Panic: {:?}", info); loop {} } use alloc::boxed::Box; use alloc::rc::Rc; use core::cell::RefCell; use embedded_graphics_core::draw_target::DrawTarget; use embedded_graphics_core::geometry::OriginDimensions; use embedded_graphics_core::pixelcolor::RgbColor; 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::peripherals::Peripherals; use esp_hal::time::Instant; use esp_hal::{ delay::Delay, gpio::{Level, Output, OutputConfig}, i2c::master::I2c, spi::master::{Config as SpiConfig, Spi}, spi::Mode as SpiMode, time::Rate, }; use esp_println::logger::init_logger_from_env; use log::{error, info}; use mipidsi::options::{ColorInversion, ColorOrder}; // Touch support imports use embedded_hal_bus::i2c::RefCellDevice; use ft3x68_rs::{Ft3x68Driver, ResetInterface}; use slint::platform::{PointerEventButton, WindowEvent}; use slint::PhysicalPosition; use static_cell::StaticCell; // FT6336U I2C address (compatible with FT3x68 driver) const FT6336U_DEVICE_ADDRESS: u8 = 0x38; // AW9523 I2C address const AW9523_I2C_ADDRESS: u8 = 0x58; /// Touch reset implementation via AW9523 GPIO expander using direct I2C commands /// Based on the AW9523 datasheet and M5Stack CoreS3 schematics pub struct TouchResetDriverAW9523 { i2c: I2C, } impl TouchResetDriverAW9523 { pub fn new(i2c: I2C) -> Self { TouchResetDriverAW9523 { i2c } } } impl ResetInterface for TouchResetDriverAW9523 where I2C: embedded_hal::i2c::I2c, { type Error = I2C::Error; fn reset(&mut self) -> Result<(), Self::Error> { let delay = Delay::new(); // AW9523 register addresses: // 0x02: Port 0 Configuration (0=output, 1=input) // 0x03: Port 1 Configuration (0=output, 1=input) // 0x04: Port 0 Output (pin values for outputs) // 0x05: Port 1 Output (pin values for outputs) // Configure P0_0 (touch reset) as output (bit 0 = 0) // Keep other pins as they are - read current config first let mut config_p0 = [0u8; 1]; self.i2c.write_read(AW9523_I2C_ADDRESS, &[0x02], &mut config_p0)?; let new_config_p0 = config_p0[0] & !0x01; // Clear bit 0 to make P0_0 output self.i2c.write(AW9523_I2C_ADDRESS, &[0x02, new_config_p0])?; // Pull reset (P0_0) low let mut output_p0 = [0u8; 1]; self.i2c.write_read(AW9523_I2C_ADDRESS, &[0x04], &mut output_p0)?; let new_output_low = output_p0[0] & !0x01; // Clear bit 0 to pull P0_0 low self.i2c.write(AW9523_I2C_ADDRESS, &[0x04, new_output_low])?; delay.delay_millis(10); // Pull reset (P0_0) high let new_output_high = output_p0[0] | 0x01; // Set bit 0 to pull P0_0 high self.i2c.write(AW9523_I2C_ADDRESS, &[0x04, new_output_high])?; delay.delay_millis(300); Ok(()) } } struct EspBackend { window: RefCell>>, peripherals: RefCell>, } impl slint::platform::Platform for EspBackend { fn create_window_adapter( &self, ) -> Result, 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> { self.run_event_loop() } } 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"); } /// Initialize the AXP2101 power management unit for M5Stack CoreS3 using shared I2C /// This implements the exact same sequence as the working custom implementation /// Based on https://github.com/tuupola/axp192 /// and https://github.com/m5stack/M5CoreS3/blob/main/src/AXP2101.cpp fn init_axp2101_power(mut i2c_device: I2C) -> Result<(), ()> where I2C: embedded_hal::i2c::I2c, { const AXP2101_ADDRESS: u8 = 0x34; info!("Initializing AXP2101 power management with M5Stack CoreS3 sequence..."); // This sequence matches exactly the working custom implementation: // 1. CHG_LED register (0x69) <- 0x35 (0b00110101) // 2. ALDO_ENABLE register (0x90) <- 0xBF // 3. ALDO4 register (0x95) <- 0x1C (0b00011100) // Step 1: Configure charge LED (register 0x69 = 105 decimal) if i2c_device.write(AXP2101_ADDRESS, &[0x69, 0x35]).is_err() { error!("Failed to write to CHG_LED register (0x69)"); return Err(()); } info!("AXP2101: CHG_LED configured (0x69 <- 0x35)"); // Step 2: Enable ALDO outputs (register 0x90 = 144 decimal) if i2c_device.write(AXP2101_ADDRESS, &[0x90, 0xBF]).is_err() { error!("Failed to write to ALDO_ENABLE register (0x90)"); return Err(()); } info!("AXP2101: ALDO outputs enabled (0x90 <- 0xBF)"); // Step 3: Configure ALDO4 voltage (register 0x95 = 149 decimal) if i2c_device.write(AXP2101_ADDRESS, &[0x95, 0x1C]).is_err() { error!("Failed to write to ALDO4 register (0x95)"); return Err(()); } info!("AXP2101: ALDO4 voltage configured (0x95 <- 0x1C)"); info!("AXP2101 power management initialized successfully with M5Stack CoreS3 sequence"); Ok(()) } /// Initialize the AW9523 GPIO expander for M5Stack CoreS3 using shared I2C /// This implements the exact same sequence as the working custom implementation /// Based on: https://github.com/m5stack/M5CoreS3/blob/main/src/AXP2101.cpp fn init_aw9523_gpio_expander(mut i2c_device: I2C) -> Result<(), ()> where I2C: embedded_hal::i2c::I2c, { info!("Initializing AW9523 GPIO expander with M5Stack CoreS3 sequence..."); // Step 1: Configure Port 0 Configuration (register 0x02) <- 0b00000101 (0x05) if i2c_device.write(AW9523_I2C_ADDRESS, &[0x02, 0b00000101]).is_err() { error!("Failed to write to AW9523 Port 0 Configuration register (0x02)"); return Err(()); } info!("AW9523: Port 0 Configuration set (0x02 <- 0x05)"); // Step 2: Configure Port 1 Configuration (register 0x03) <- 0b00000011 (0x03) if i2c_device.write(AW9523_I2C_ADDRESS, &[0x03, 0b00000011]).is_err() { error!("Failed to write to AW9523 Port 1 Configuration register (0x03)"); return Err(()); } info!("AW9523: Port 1 Configuration set (0x03 <- 0x03)"); // Step 3: Configure Port 0 Output (register 0x04) <- 0b00011000 (0x18) if i2c_device.write(AW9523_I2C_ADDRESS, &[0x04, 0b00011000]).is_err() { error!("Failed to write to AW9523 Port 0 Output register (0x04)"); return Err(()); } info!("AW9523: Port 0 Output set (0x04 <- 0x18)"); // Step 4: Configure Port 1 Output (register 0x05) <- 0b00001100 (0x0C) if i2c_device.write(AW9523_I2C_ADDRESS, &[0x05, 0b00001100]).is_err() { error!("Failed to write to AW9523 Port 1 Output register (0x05)"); return Err(()); } info!("AW9523: Port 1 Output set (0x05 <- 0x0C)"); // Step 5: Configure register 0x11 <- 0b00010000 (0x10) if i2c_device.write(AW9523_I2C_ADDRESS, &[0x11, 0b00010000]).is_err() { error!("Failed to write to AW9523 register (0x11)"); return Err(()); } info!("AW9523: Register 0x11 configured (0x11 <- 0x10)"); // Step 6: Configure register 0x13 <- 0b11111111 (0xFF) if i2c_device.write(AW9523_I2C_ADDRESS, &[0x13, 0b11111111]).is_err() { error!("Failed to write to AW9523 register (0x13)"); return Err(()); } info!("AW9523: Register 0x13 configured (0x13 <- 0xFF)"); info!("AW9523 GPIO expander initialized successfully with M5Stack CoreS3 sequence"); Ok(()) } impl EspBackend { 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 I2C bus for all I2C devices (AXP2101, AW9523, touch controller) --- let power_i2c = I2c::new( peripherals.I2C0, esp_hal::i2c::master::Config::default().with_frequency(Rate::from_khz(400)), ) .unwrap() .with_sda(peripherals.GPIO12) // AXP2101 SDA .with_scl(peripherals.GPIO11); // AXP2101 SCL // --- Use StaticCell to create a shared I2C bus for all I2C devices --- static I2C_BUS: StaticCell>> = StaticCell::new(); let i2c_bus = I2C_BUS.init(RefCell::new(power_i2c)); // --- Begin AXP2101 Power Management Initialization --- // Initialize power management using shared I2C bus - critical for M5Stack CoreS3 match init_axp2101_power(RefCellDevice::new(i2c_bus)) { Ok(_) => { info!("Power management initialized successfully"); } Err(_) => { error!("Failed to initialize AXP2101 power management"); // Return error since power management is critical return Err(slint::PlatformError::Other("AXP2101 initialization failed".into())); } }; // Small delay to let power rails stabilize delay.delay_ms(100); // --- Begin AW9523 GPIO Expander Initialization --- // Initialize AW9523 GPIO expander using M5Stack CoreS3 specific sequence match init_aw9523_gpio_expander(RefCellDevice::new(i2c_bus)) { Ok(_) => { info!("AW9523 GPIO expander initialized successfully"); } Err(_) => { error!("Failed to initialize AW9523 GPIO expander"); // Return error since GPIO expander is needed for touch return Err(slint::PlatformError::Other("AW9523 initialization failed".into())); } }; // --- End AW9523 Initialization --- // --- Begin SPI and Display Initialization --- let spi = Spi::::new( peripherals.SPI2, SpiConfig::default().with_frequency(Rate::from_mhz(40)).with_mode(SpiMode::_0), ) .unwrap() .with_sck(peripherals.GPIO36) // SPI Clock .with_mosi(peripherals.GPIO37); // SPI MOSI // Display control pins let dc = Output::new(peripherals.GPIO35, Level::Low, OutputConfig::default()); // D/C pin let cs = Output::new(peripherals.GPIO3, Level::High, OutputConfig::default()); // CS pin let reset = Output::new(peripherals.GPIO15, Level::High, OutputConfig::default()); // Reset pin // Wrap SPI into a bus. let spi_delay = Delay::new(); let spi_device = ExclusiveDevice::new(spi, cs, spi_delay).unwrap(); // Create buffer for display interface let mut buffer = [0u8; 512]; let di = mipidsi::interface::SpiInterface::new(spi_device, dc, &mut buffer); // Add small delay before display initialization delay.delay_ms(10); // Initialize the display with settings let mut display = mipidsi::Builder::new(mipidsi::models::ILI9342CRgb565, di) .reset_pin(reset) .display_size(320, 240) .color_order(ColorOrder::Bgr) .invert_colors(ColorInversion::Inverted) .init(&mut delay) .unwrap(); // Clear display to test it's working use embedded_graphics::pixelcolor::Rgb565; display .clear(Rgb565::BLUE) .map_err(|_| slint::PlatformError::Other("Display clear failed".into()))?; info!("Display initialized and cleared to blue"); // Set up the backlight pin (controlled via AXP2101, but we can use GPIO for basic control) let mut backlight = Output::new(peripherals.GPIO16, Level::Low, OutputConfig::default()); backlight.set_high(); // Enable backlight // Update the Slint window size from the display (320x240 for M5Stack CoreS3) 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 --- // --- Begin Touch Initialization --- info!("Initializing FT6336U touch controller..."); // Create touch reset driver using shared I2C bus let touch_reset = TouchResetDriverAW9523::new(RefCellDevice::new(i2c_bus)); // Initialize FT6336U touch driver using shared I2C bus let mut touch_driver = Ft3x68Driver::new( RefCellDevice::new(i2c_bus), FT6336U_DEVICE_ADDRESS, touch_reset, delay, ); match touch_driver.initialize() { Ok(_) => info!("FT6336U touch controller initialized successfully"), Err(e) => { error!("Touch initialization failed: {:?}", e); // Continue without touch } } // --- End Touch Initialization --- // 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 touch input using FT3x68Driver match touch_driver.touch1() { Ok(touch_state) => { match touch_state { ft3x68_rs::TouchState::Pressed(touch_point) => { info!("Touch detected: x={}, y={}", touch_point.x, touch_point.y); // Convert touch coordinates to logical position let pos = PhysicalPosition::new( touch_point.x as i32, touch_point.y as i32, ) .to_logical(window.scale_factor()); if let Some(prev_pos) = last_touch.replace(pos) { // If position changed, send a PointerMoved event if prev_pos != pos { let _ = window.try_dispatch_event(WindowEvent::PointerMoved { position: pos, }); } } else { // No previous touch, send a PointerPressed event let _ = window.try_dispatch_event(WindowEvent::PointerPressed { position: pos, button: PointerEventButton::Left, }); } } ft3x68_rs::TouchState::Released => { // Touch was released, send PointerReleased if we had a previous touch if let Some(pos) = last_touch.take() { let _ = window.try_dispatch_event(WindowEvent::PointerReleased { position: pos, button: PointerEventButton::Left, }); let _ = window.try_dispatch_event(WindowEvent::PointerExited); } } } } Err(_) => { // Touch error - ignore and continue } } // 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, RST: OutputPin, > slint::platform::software_renderer::LineBufferProvider for &mut DrawBuffer<'_, mipidsi::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); // 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(); } }