// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: MIT //! Board support for ESP32-S3-LCD-EV-Board with display and touch controller support. extern crate alloc; // --- Slint platform integration imports --- use slint::platform::software_renderer::Rgb565Pixel; // --- FT5x06 Touch Controller --- struct Ft5x06 { i2c: I2C, address: u8, } impl Ft5x06 where I2C: embedded_hal::i2c::I2c, { pub fn new(i2c: I2C, address: u8) -> Self { Self { i2c, address } } /// Reads the first touch point. Returns Some((x, y)) if touched, None otherwise. pub fn get_touch(&mut self) -> Result, I2C::Error> { // 1) read touch count from register 0x02 let mut buf = [0u8; 1]; self.i2c.write_read(self.address, &[0x02], &mut buf)?; let count = buf[0] & 0x0F; if count == 0 { return Ok(None); } // 2) read first touch coordinates from regs 0x03..0x06 let mut data = [0u8; 4]; self.i2c.write_read(self.address, &[0x03], &mut data)?; let x = (((data[0] & 0x0F) as u16) << 8) | data[1] as u16; let y = (((data[2] & 0x0F) as u16) << 8) | data[3] as u16; Ok(Some((x, y))) } } use alloc::boxed::Box; use esp_hal::dma::{DmaDescriptor, DmaTxBuf, CHUNK_SIZE}; use esp_hal::i2c; use esp_hal::peripherals::Peripherals; use slint::LogicalPosition; use slint::PhysicalPosition; use slint::PhysicalSize; use alloc::rc::Rc; use core::cell::RefCell; use esp_hal::clock::CpuClock::_240MHz; use esp_hal::delay::Delay; use esp_hal::i2c::master::{Error, I2c}; use esp_hal::lcd_cam::{ lcd::{ dpi::{Config as DpiConfig, Dpi, Format, FrameTiming}, ClockMode, Phase, Polarity, }, LcdCam, }; use esp_hal::time::Instant; use esp_hal::{ gpio::{Level, Output, OutputConfig}, time::Rate, Blocking, Config as HalConfig, }; use esp_println::logger::init_logger_from_env; use i_slint_core::input::PointerEventButton; use i_slint_core::platform::WindowEvent; use log::{error, info}; // === Display constants === const LCD_H_RES: u16 = 480; const LCD_V_RES: u16 = 480; const FRAME_BYTES: usize = (LCD_H_RES as usize * LCD_V_RES as usize) * 2; const NUM_DMA_DESC: usize = (FRAME_BYTES + CHUNK_SIZE - 1) / CHUNK_SIZE; // Place DMA descriptors in DMA-capable RAM #[link_section = ".dma"] static mut TX_DESCRIPTORS: [DmaDescriptor; NUM_DMA_DESC] = [DmaDescriptor::EMPTY; NUM_DMA_DESC]; #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { error!("Panic: {}", _info); loop {} } struct EspBackend { window: RefCell>>, peripherals: RefCell>, } impl Default for EspBackend { fn default() -> Self { EspBackend { window: RefCell::new(None), peripherals: RefCell::new(None) } } } /// Initialize the heap and set the Slint platform. pub fn init() { // Initialize peripherals first. let peripherals = esp_hal::init(HalConfig::default().with_cpu_clock(_240MHz)); init_logger_from_env(); info!("Peripherals initialized"); // Initialize the PSRAM allocator. esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram); // Create and install the Slint backend that owns the peripherals. slint::platform::set_platform(Box::new(EspBackend { window: RefCell::new(None), peripherals: RefCell::new(Some(peripherals)), })) .expect("Slint platform already initialized"); } 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> { // Reinitialize peripherals, PSRAM, and logger let peripherals = self.peripherals.borrow_mut().take().expect("Peripherals already taken"); // Setup I2C for the TCA9554 IO expander let i2c = I2c::new( peripherals.I2C0, i2c::master::Config::default().with_frequency(Rate::from_khz(400)), ) .unwrap() .with_sda(peripherals.GPIO47) .with_scl(peripherals.GPIO48); // Initialize the IO expander for controlling the display let mut expander = Tca9554::new(i2c); expander.write_output_reg(0b1111_0011).unwrap(); expander.write_direction_reg(0b1111_0001).unwrap(); let delay = Delay::new(); info!("Initializing display..."); // Set up the write_byte function for sending commands to the display let mut write_byte = |b: u8, is_cmd: bool| { const SCS_BIT: u8 = 0b0000_0010; const SCL_BIT: u8 = 0b0000_0100; const SDA_BIT: u8 = 0b0000_1000; let mut output = 0b1111_0001 & !SCS_BIT; expander.write_output_reg(output).unwrap(); for bit in core::iter::once(!is_cmd).chain((0..8).map(|i| (b >> i) & 0b1 != 0).rev()) { let prev = output; if bit { output |= SDA_BIT; } else { output &= !SDA_BIT; } if prev != output { expander.write_output_reg(output).unwrap(); } output &= !SCL_BIT; expander.write_output_reg(output).unwrap(); output |= SCL_BIT; expander.write_output_reg(output).unwrap(); } output &= !SCL_BIT; expander.write_output_reg(output).unwrap(); output &= !SDA_BIT; expander.write_output_reg(output).unwrap(); output |= SCS_BIT; expander.write_output_reg(output).unwrap(); }; // VSYNC must be high during initialization let mut vsync_pin = peripherals.GPIO3; let vsync_guard = Output::new(vsync_pin.reborrow(), Level::High, OutputConfig::default()); // Initialize the display by sending the initialization commands for &init in INIT_CMDS.iter() { match init { InitCmd::Cmd(cmd, args) => { write_byte(cmd, true); for &arg in args { write_byte(arg, false); } } InitCmd::Delay(ms) => { delay.delay_millis(ms as _); } } } drop(vsync_guard); // Set up DMA channel for LCD let tx_channel = peripherals.DMA_CH2; let lcd_cam = LcdCam::new(peripherals.LCD_CAM); // Configure the RGB display let config = DpiConfig::default() .with_clock_mode(ClockMode { polarity: Polarity::IdleLow, phase: Phase::ShiftLow }) .with_frequency(Rate::from_mhz(10)) .with_format(Format { enable_2byte_mode: true, ..Default::default() }) .with_timing(FrameTiming { horizontal_active_width: LCD_H_RES as usize, vertical_active_height: LCD_V_RES as usize, horizontal_total_width: 600, horizontal_blank_front_porch: 80, vertical_total_height: 600, vertical_blank_front_porch: 80, hsync_width: 10, vsync_width: 4, hsync_position: 10, }) .with_vsync_idle_level(Level::High) .with_hsync_idle_level(Level::High) .with_de_idle_level(Level::Low) .with_disable_black_region(false); let mut dpi = Dpi::new(lcd_cam.lcd, tx_channel, config) .unwrap() .with_vsync(vsync_pin.reborrow()) .with_hsync(peripherals.GPIO46) .with_de(peripherals.GPIO17) .with_pclk(peripherals.GPIO9) .with_data0(peripherals.GPIO10) .with_data1(peripherals.GPIO11) .with_data2(peripherals.GPIO12) .with_data3(peripherals.GPIO13) .with_data4(peripherals.GPIO14) .with_data5(peripherals.GPIO21) .with_data6(peripherals.GPIO8) .with_data7(peripherals.GPIO18) .with_data8(peripherals.GPIO45) .with_data9(peripherals.GPIO38) .with_data10(peripherals.GPIO39) .with_data11(peripherals.GPIO40) .with_data12(peripherals.GPIO41) .with_data13(peripherals.GPIO42) .with_data14(peripherals.GPIO2) .with_data15(peripherals.GPIO1); info!("Display initialized, entering main loop..."); const FRAME_PIXELS: usize = (LCD_H_RES as usize) * (LCD_V_RES as usize); const FRAME_BYTES: usize = FRAME_PIXELS * 2; // Allocate a PSRAM-backed DMA buffer for the frame let buf_box: Box<[u8; FRAME_BYTES]> = Box::new([0; FRAME_BYTES]); let psram_buf: &'static mut [u8] = Box::leak(buf_box); let mut dma_tx: DmaTxBuf = unsafe { let descriptors = &mut *core::ptr::addr_of_mut!(TX_DESCRIPTORS); DmaTxBuf::new(descriptors, psram_buf).unwrap() }; let mut pixel_box: Box<[Rgb565Pixel; FRAME_PIXELS]> = Box::new([Rgb565Pixel(0); FRAME_PIXELS]); let pixel_buf: &mut [Rgb565Pixel] = &mut *pixel_box; // Initialize pixel buffer and DMA buffer // The pixel buffer will be filled by Slint's renderer in the main loop let dst = dma_tx.as_mut_slice(); for (i, px) in pixel_buf.iter().enumerate() { let [lo, hi] = px.0.to_le_bytes(); dst[2 * i] = lo; dst[2 * i + 1] = hi; } // Initial flush of the screen buffer match dpi.send(false, dma_tx) { Ok(xfer) => { let (_res, dpi2, tx2) = xfer.wait(); dpi = dpi2; dma_tx = tx2; } Err((e, dpi2, tx2)) => { error!("Initial DMA send error: {:?}", e); dpi = dpi2; dma_tx = tx2; } } // Tell Slint the window dimensions match the DPI display resolution let size = PhysicalSize::new(LCD_H_RES.into(), LCD_V_RES.into()); self.window.borrow().as_ref().expect("Window adapter not created").set_size(size); // Initialize FT5x06 touch controller on I2C1 (example pins) // Reclaim the I2C bus from the expander for FT5x06 let i2c_bus = expander.into_i2c(); let mut touch = Ft5x06::new(i2c_bus, 0x38); let mut last_touch: Option = None; loop { // 1) Let Slint update its timers and animations slint::platform::update_timers_and_animations(); if let Some(window) = self.window.borrow().clone() { window.request_redraw(); } if let Some(window) = self.window.borrow().clone() { // Poll FT5x06 touch each frame since INT line is NC if let Ok(Some((x, y))) = touch.get_touch() { let pos = PhysicalPosition::new(x as i32, y as i32).to_logical(window.scale_factor()); if let Some(prev) = last_touch.replace(pos) { if prev != pos { window .try_dispatch_event(WindowEvent::PointerMoved { position: pos })?; } } else { window.try_dispatch_event(WindowEvent::PointerPressed { position: pos, button: PointerEventButton::Left, })?; } } else if let Some(pos) = last_touch.take() { window.try_dispatch_event(WindowEvent::PointerReleased { position: pos, button: PointerEventButton::Left, })?; window.try_dispatch_event(WindowEvent::PointerExited)?; } // 2) Render the UI into Slint's software renderer buffer window.draw_if_needed(|renderer| { let _dirty = renderer.render(pixel_buf, LCD_H_RES as usize); }); // 3) Pack pixels into DMA buffer { let dst = dma_tx.as_mut_slice(); for (i, px) in pixel_buf.iter().enumerate() { let [lo, hi] = px.0.to_le_bytes(); dst[2 * i] = lo; dst[2 * i + 1] = hi; } } // 3) One-shot DMA transfer of the full frame match dpi.send(false, dma_tx) { Ok(xfer) => { let (res, dpi2, tx2) = xfer.wait(); dpi = dpi2; dma_tx = tx2; if let Err(e) = res { error!("DMA error: {:?}", e); } } Err((e, dpi2, tx2)) => { error!("DMA send error: {:?}", e); dpi = dpi2; dma_tx = tx2; } } // 4) If there are active animations, continue immediately if window.has_active_animations() { continue; } } } } } // --- I2C expander (TCA9554) --- struct Tca9554 { i2c: I2c<'static, esp_hal::Blocking>, address: u8, } impl Tca9554 { pub fn new(i2c: I2c<'static, esp_hal::Blocking>) -> Self { Self { i2c, address: 0x20 } } pub fn write_direction_reg(&mut self, value: u8) -> Result<(), Error> { self.i2c.write(self.address, &[0x03, value]) } pub fn write_output_reg(&mut self, value: u8) -> Result<(), Error> { self.i2c.write(self.address, &[0x01, value]) } pub fn into_i2c(self) -> I2c<'static, Blocking> { self.i2c } } // Display initialization commands for the ESP32-S3-LCD-EV-Board #[derive(Copy, Clone, Debug)] enum InitCmd { Cmd(u8, &'static [u8]), Delay(u8), } const INIT_CMDS: &[InitCmd] = &[ InitCmd::Cmd(0xf0, &[0x55, 0xaa, 0x52, 0x08, 0x00]), InitCmd::Cmd(0xf6, &[0x5a, 0x87]), InitCmd::Cmd(0xc1, &[0x3f]), InitCmd::Cmd(0xc2, &[0x0e]), InitCmd::Cmd(0xc6, &[0xf8]), InitCmd::Cmd(0xc9, &[0x10]), InitCmd::Cmd(0xcd, &[0x25]), InitCmd::Cmd(0xf8, &[0x8a]), InitCmd::Cmd(0xac, &[0x45]), InitCmd::Cmd(0xa0, &[0xdd]), InitCmd::Cmd(0xa7, &[0x47]), InitCmd::Cmd(0xfa, &[0x00, 0x00, 0x00, 0x04]), InitCmd::Cmd(0x86, &[0x99, 0xa3, 0xa3, 0x51]), InitCmd::Cmd(0xa3, &[0xee]), InitCmd::Cmd(0xfd, &[0x3c, 0x3]), InitCmd::Cmd(0x71, &[0x48]), InitCmd::Cmd(0x72, &[0x48]), InitCmd::Cmd(0x73, &[0x00, 0x44]), InitCmd::Cmd(0x97, &[0xee]), InitCmd::Cmd(0x83, &[0x93]), InitCmd::Cmd(0x9a, &[0x72]), InitCmd::Cmd(0x9b, &[0x5a]), InitCmd::Cmd(0x82, &[0x2c, 0x2c]), InitCmd::Cmd(0xB1, &[0x10]), InitCmd::Cmd( 0x6d, &[ 0x00, 0x1f, 0x19, 0x1a, 0x10, 0x0e, 0x0c, 0x0a, 0x02, 0x07, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x08, 0x01, 0x09, 0x0b, 0x0d, 0x0f, 0x1a, 0x19, 0x1f, 0x00, ], ), InitCmd::Cmd( 0x64, &[ 0x38, 0x05, 0x01, 0xdb, 0x03, 0x03, 0x38, 0x04, 0x01, 0xdc, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a, ], ), InitCmd::Cmd( 0x65, &[ 0x38, 0x03, 0x01, 0xdd, 0x03, 0x03, 0x38, 0x02, 0x01, 0xde, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a, ], ), InitCmd::Cmd( 0x66, &[ 0x38, 0x01, 0x01, 0xdf, 0x03, 0x03, 0x38, 0x00, 0x01, 0xe0, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a, ], ), InitCmd::Cmd( 0x67, &[ 0x30, 0x01, 0x01, 0xe1, 0x03, 0x03, 0x30, 0x02, 0x01, 0xe2, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a, ], ), InitCmd::Cmd( 0x68, &[0x00, 0x08, 0x15, 0x08, 0x15, 0x7a, 0x7a, 0x08, 0x15, 0x08, 0x15, 0x7a, 0x7a], ), InitCmd::Cmd(0x60, &[0x38, 0x08, 0x7a, 0x7a, 0x38, 0x09, 0x7a, 0x7a]), InitCmd::Cmd(0x63, &[0x31, 0xe4, 0x7a, 0x7a, 0x31, 0xe5, 0x7a, 0x7a]), InitCmd::Cmd(0x69, &[0x04, 0x22, 0x14, 0x22, 0x14, 0x22, 0x08]), InitCmd::Cmd(0x6b, &[0x07]), InitCmd::Cmd(0x7a, &[0x08, 0x13]), InitCmd::Cmd(0x7b, &[0x08, 0x13]), InitCmd::Cmd( 0xd1, &[ 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, ], ), InitCmd::Cmd( 0xd2, &[ 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, ], ), InitCmd::Cmd( 0xd3, &[ 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, ], ), InitCmd::Cmd( 0xd4, &[ 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, ], ), InitCmd::Cmd( 0xd5, &[ 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, ], ), InitCmd::Cmd( 0xd6, &[ 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, ], ), InitCmd::Cmd(0x36, &[0x00]), InitCmd::Cmd(0x2A, &[0x00, 0x00, 0x01, 0xDF]), // 0 to 479 (0x1DF) // Set full row address range InitCmd::Cmd(0x2B, &[0x00, 0x00, 0x01, 0xDF]), // 0 to 479 (0x1DF) InitCmd::Cmd(0x3A, &[0x66]), InitCmd::Cmd(0x11, &[]), InitCmd::Delay(120), InitCmd::Cmd(0x29, &[]), InitCmd::Delay(20), ];