源码:https://github.com/xiaguangbo/slint_esp32_tokio
cpu 是 esp32c2,屏幕是 ili9341,触摸是 xpt2046,使用 spi 半双工
不使用DMA(esp-rs还没支持),SPI 40M,240*320全屏刷新为1.5秒,虽然比不了 lvgl,但类lvgl的slint是目前rust跨全平台唯一的选择
这是一个游戏,翻到两个一样的就成功,slint官网有入门示例,就是这个,然后把 .slint 和 控制逻辑拿过来直接用。就是slint平台需要稍微移植下,字体会自动打包
Cargo.toml
[package]
name = "esp32c2"
version = "0.1.0"
authors = ["xxx"]
edition = "2021"
resolver = "2"
rust-version = "1.71"[profile.release]
opt-level = "s"[profile.dev]
debug = true # Symbols are nice and they don't increase the size on Flash
opt-level = "z"[features]
default = ["std", "embassy", "esp-idf-svc/native"]pio = ["esp-idf-svc/pio"]
std = ["alloc", "esp-idf-svc/binstart", "esp-idf-svc/std"]
alloc = ["esp-idf-svc/alloc"]
nightly = ["esp-idf-svc/nightly"]
experimental = ["esp-idf-svc/experimental"]
embassy = ["esp-idf-svc/embassy-sync","esp-idf-svc/critical-section","esp-idf-svc/embassy-time-driver",
][dependencies]
log = { version = "*", default-features = false }
esp-idf-svc = { version = "*", default-features = false }tokio = { version = "*", features = ["rt", "time", "sync"] }
num-traits = "*"
chrono = "*"
rand = "*"
slint = { version = "*", default-features = false, features = ["compat-1-2","renderer-software","unsafe-single-threaded",
] }[build-dependencies]
embuild = "*"
slint-build = "*"
appwindow.slint
struct TileData {image: image,image_visible: bool,solved: bool,
}component MemoryTile inherits Rectangle {in property <bool> open_curtain;in property <bool> solved;in property <image> icon;callback clicked;height: 50px;width: 50px;border-radius: self.width / 2;background: solved ? #34CE57 : #3960D5;clip: true;animate background { duration: 800ms; }Image {source: icon;width: parent.width;height: parent.height;}// Left curtainRectangle {background: #193076;x: 0px;width: open_curtain ? 0px : (parent.width / 2);height: parent.height;clip: true;animate width {duration: 250ms;easing: ease-in;}Image {width: root.width - 25px;height: root.height - 25px;x: 13px;y: 13px;source: @image-url("../icons/tile_logo.png");}}// Right curtainRectangle {background: #193076;x: open_curtain ? parent.width : (parent.width / 2);width: open_curtain ? 0px : (parent.width / 2);height: parent.height;clip: true;animate width {duration: 250ms;easing: ease-in;}animate x {duration: 250ms;easing: ease-in;}Image {width: root.width - 25px;height: root.height - 25px;x: parent.width - self.width - 13px;y: 13px;source: @image-url("../icons/tile_logo.png");}}TouchArea {clicked => {// Delegate to the user of this elementroot.clicked();}width: 100%;height: 100%;}
}export component AppWindow inherits Window {width: 240px;height: 320px;callback check_if_pair_solved();// Addedin property <bool> disable_tiles;// Addedin-out property <[TileData]> memory_tiles: [{ image: @image-url("../icons/at.png") },{ image: @image-url("../icons/balance-scale.png") },{ image: @image-url("../icons/bicycle.png") },{ image: @image-url("../icons/bus.png") },{ image: @image-url("../icons/cloud.png") },{ image: @image-url("../icons/cogs.png") },{ image: @image-url("../icons/motorcycle.png") },{ image: @image-url("../icons/video.png") },];for tile[i] in memory_tiles: MemoryTile {x: mod(i, 4) * (root.width / 4);y: floor(i / 4) * (root.width / 4);width: 50px;height: 50px;icon: tile.image;open_curtain: tile.image_visible || tile.solved; // 任何一个满足都打开帘子// propagate the solved status from the model to the tilesolved: tile.solved;clicked => {// old: tile.image_visible = !tile.image_visible;// new:// 可不可以点击if (!root.disable_tiles) {tile.image_visible = !tile.image_visible;root.check_if_pair_solved();}}}
}
ui
use std::{borrow::Borrow, cell::RefCell, rc::Rc};use slint::platform::{software_renderer::*, PointerEventButton, WindowAdapter, WindowEvent};
use slint::Model;
use tokio::time;use esp_idf_svc::hal::{gpio::*, peripheral::*, spi::*};use crate::component::{ili9341, xpt2046};slint::include_modules!();pub async fn work<SPI, CS, CS2, DC>(spi1: SPI, spi2: SPI, cs1: CS, cs2: CS2, dc: DC)
whereSPI: Borrow<SpiDriver<'static>> + 'static,CS: Peripheral<P = CS> + OutputPin,CS2: Peripheral<P = CS2> + OutputPin,DC: Peripheral<P = DC> + OutputPin,
{let mut ili9341 = ili9341::ILI9341::new(spi1, cs1, dc);let xpt2046 = xpt2046::XPT2046::new(spi2, cs2);ili9341.open();let buffer_provider = DrawBuffer {display: ili9341,buffer: vec![Rgb565Pixel::default(); ili9341::ILI9341_WIDTH as usize].leak(),};slint::platform::set_platform(Box::new(SlintBackend {window: Default::default(),now: std::time::Instant::now().into(),buffer_provider: buffer_provider.into(),touch: xpt2046.into(),last_touch: None.into(),})).unwrap();let main_window = AppWindow::new().unwrap();// Fetch the tiles from the modellet mut tiles: Vec<TileData> = main_window.get_memory_tiles().iter().collect();// Duplicate them to ensure that we have pairstiles.extend(tiles.clone());// Randomly mix the tilesuse rand::seq::SliceRandom;let mut rng = rand::thread_rng();tiles.shuffle(&mut rng);// Assign the shuffled Vec to the model propertylet tiles_model = std::rc::Rc::new(slint::VecModel::from(tiles));main_window.set_memory_tiles(tiles_model.clone().into());let main_window_weak = main_window.as_weak();// 点击的回调函数main_window.on_check_if_pair_solved(move || {// 如果元素的(image_visible && !solved)为真,则得到他// 就是被打开看的且没有被标记的对象let mut flipped_tiles = tiles_model.iter().enumerate().filter(|(_, tile)| tile.image_visible && !tile.solved);// 当检查出有两个这样的元素就进入判断if let (Some((t1_idx, mut t1)), Some((t2_idx, mut t2))) =(flipped_tiles.next(), flipped_tiles.next()){let is_pair_solved = t1 == t2; // 比较两个元素的值是不是一样的,包括图片的 rgba 和元素属性,也就是 TileData 的所有成员// 一样if is_pair_solved {t1.solved = true; // 彻底打开帘子tiles_model.set_row_data(t1_idx, t1);t2.solved = true;tiles_model.set_row_data(t2_idx, t2);}// 不一样else {let main_window = main_window_weak.unwrap();main_window.set_disable_tiles(true); // 防止继续点击let tiles_model = tiles_model.clone();// 延时 1sslint::Timer::single_shot(std::time::Duration::from_secs(1), move || {main_window.set_disable_tiles(false); // 可继续点击t1.image_visible = false; // 关闭帘子tiles_model.set_row_data(t1_idx, t1);t2.image_visible = false;tiles_model.set_row_data(t2_idx, t2);});}}});loop {slint::run_event_loop().unwrap();time::sleep(time::Duration::from_millis(20)).await;}
}pub struct SlintBackend<'a, SPI, DC>
whereSPI: Borrow<SpiDriver<'a>> + 'a,DC: Peripheral<P = DC> + OutputPin,
{window: RefCell<Option<Rc<MinimalSoftwareWindow>>>,now: RefCell<std::time::Instant>,buffer_provider: RefCell<DrawBuffer<'a, SPI, DC>>,touch: RefCell<xpt2046::XPT2046<'a, SPI>>,last_touch: RefCell<Option<slint::LogicalPosition>>,
}impl<'a, SPI, DC> slint::platform::Platform for SlintBackend<'a, SPI, DC>
whereSPI: Borrow<SpiDriver<'a>> + 'a,DC: Peripheral<P = DC> + OutputPin,
{fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, slint::PlatformError> {let window = MinimalSoftwareWindow::new(RepaintBufferType::ReusedBuffer);self.window.replace(Some(window.clone()));self.window.borrow().as_ref().unwrap().set_size(slint::PhysicalSize::new(ili9341::ILI9341_WIDTH as u32,ili9341::ILI9341_HEIGHT as u32,));Ok(window)}fn duration_since_start(&self) -> std::time::Duration {self.now.borrow().elapsed()}fn run_event_loop(&self) -> Result<(), slint::PlatformError> {let while_now = std::time::Instant::now();let mut touch_now = std::time::Instant::now();let mut touch_ed = false;// 连续绘制达到 100ms 就跳过while while_now.elapsed().as_millis() < 100 {slint::platform::update_timers_and_animations();if let Some(window) = self.window.borrow().clone() {if !touch_ed {touch_ed = !touch_ed;touch_now = std::time::Instant::now();if let Some(event) = match self.touch.borrow_mut().read() {Some(v) => {let position = slint::PhysicalPosition::new((v.x * ili9341::ILI9341_WIDTH as f32) as i32,(v.y * ili9341::ILI9341_HEIGHT as f32) as i32,).to_logical(window.scale_factor());Some(match self.last_touch.borrow_mut().replace(position) {Some(_) => WindowEvent::PointerMoved { position },_ => WindowEvent::PointerPressed {position,button: PointerEventButton::Left,},})}_ => self.last_touch.borrow_mut().take().map(|position| {WindowEvent::PointerReleased {position,button: PointerEventButton::Left,}}),} {let is_pointer_release_event =matches!(event, WindowEvent::PointerReleased { .. });window.dispatch_event(event);if is_pointer_release_event {window.dispatch_event(WindowEvent::PointerExited);}}} else {if touch_now.elapsed().as_millis() >= 20 {// 每隔一段时间才能再次读取触摸,避免频繁处理touch_ed = !touch_ed;}}window.draw_if_needed(|renderer| {renderer.render_by_line(&mut *self.buffer_provider.borrow_mut());});if !window.has_active_animations() {// 如果没有需要绘制的东西就跳出,否则就继续绘制break;}}}Ok(())}
}struct DrawBuffer<'a, SPI, DC>
whereSPI: Borrow<SpiDriver<'a>> + 'a,DC: Peripheral<P = DC> + OutputPin,
{display: ili9341::ILI9341<'a, SPI, DC>,buffer: &'a mut [Rgb565Pixel],
}impl<'a, SPI, DC> LineBufferProvider for &mut DrawBuffer<'a, SPI, DC>
whereSPI: Borrow<SpiDriver<'a>> + 'a,DC: Peripheral<P = DC> + OutputPin,
{type TargetPixel = Rgb565Pixel;fn process_line(&mut self,line: usize,range: std::ops::Range<usize>,render_fn: impl FnOnce(&mut [Rgb565Pixel]),) {let buffer = &mut self.buffer[range.clone()];render_fn(buffer);self.display.write_pixel_slint(range.start as u16,line as u16,range.end as u16,line as u16,&buffer,);}
}
work
use std::{rc, thread};use tokio::{runtime, task, time};use esp_idf_svc::hal::{gpio, peripherals, spi};use crate::module::*;pub fn work() {thread::Builder::new().stack_size(8 * 1024).spawn(|| {task::LocalSet::new().block_on(&runtime::Builder::new_current_thread().enable_all().build().unwrap(),async {let peripherals = peripherals::Peripherals::take().unwrap();let spi = spi::SpiDriver::new::<spi::SPI2>(peripherals.spi2,peripherals.pins.gpio0,peripherals.pins.gpio1,Option::<gpio::AnyIOPin>::None,&spi::SpiDriverConfig::new(),).unwrap();let spi = rc::Rc::new(spi);let spi_1 = spi.clone();let spi_2 = spi.clone();task::spawn_local(async move {ui::work(spi_1,spi_2,peripherals.pins.gpio3,peripherals.pins.gpio4,peripherals.pins.gpio2,).await;});loop {time::sleep(time::Duration::MAX).await;}},);}).unwrap();
}
build
fn main() {embuild::espidf::sysenv::output();slint_build::compile_with_config("ui/appwindow.slint",slint_build::CompilerConfiguration::new().embed_resources(slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer),).unwrap();
}
ili9341
use std::borrow::*;use esp_idf_svc::hal::{delay::Delay, gpio::*, peripheral::*, prelude::*, spi::*};use slint::platform::software_renderer::Rgb565Pixel;pub const ILI9341_WIDTH: u16 = 240;
pub const ILI9341_HEIGHT: u16 = 320;/*
# 初始化
第一个字节是命令
等待5ms36, 48 左上右下竖屏、c8 右下左上竖屏、e8 左下右上横屏、28右上左下横屏
3a, 55 像素格式 565
11 退出睡眠
29 开显示、28 关显示。不会更改内存内容# 设置区域
可设置一点、一行或一个方块,方块区域会自动换行2a x坐标
16bit xs
16bit xe2b y坐标
16bit ys
16bit ye2c
16bit 565 颜色数据
*/pub struct ILI9341<'a, SPI, DC>
whereSPI: Borrow<SpiDriver<'a>> + 'a,DC: Peripheral<P = DC> + OutputPin,
{spi: SpiDeviceDriver<'a, SPI>,dc: PinDriver<'a, DC, Output>,line_buf: Vec<u8>,
}impl<'a, SPI, DC> ILI9341<'a, SPI, DC>
whereSPI: Borrow<SpiDriver<'a>> + 'a,DC: Peripheral<P = DC> + OutputPin,
{pub fn new<CS>(spi: SPI, cs: CS, dc: DC) -> SelfwhereCS: Peripheral<P = CS> + OutputPin,{let config = config::Config::default().baudrate(40.MHz().into()).duplex(config::Duplex::Half3Wire);Self {spi: SpiDeviceDriver::new(spi, Some(cs), &config).unwrap(),dc: PinDriver::output(dc).unwrap(),line_buf: vec![0u8; (ILI9341_WIDTH * 2) as usize],}}pub fn open(&mut self) {let delay = Delay::new_default();for _ in 0..2 {delay.delay_ms(20);self.write_cmd_data_u8(0x36, Some(&[0x48]));self.write_cmd_data_u8(0x3a, Some(&[0x55]));self.write_cmd_data_u8(0x11, None);self.write_cmd_data_u8(0x29, None);}}pub fn write_pixel_slint(&mut self,x: u16,y: u16,x_end: u16,y_end: u16,pixel: &[Rgb565Pixel],) {self.write_draw_range(x, y, x_end, y_end);self.write_cmd_data_slint(0x2c, pixel);}fn write_cmd_data_u8(&mut self, cmd: u8, data: Option<&[u8]>) {self.dc.set_low().unwrap();self.spi.write(&[cmd]).unwrap();if let Some(v) = data {self.dc.set_high().unwrap();self.spi.write(v).unwrap();}}fn write_draw_range(&mut self, x: u16, y: u16, x_end: u16, y_end: u16) {let mut x_buf = [0u8; 4];let mut y_buf = [0u8; 4];x_buf[0..=1].copy_from_slice(&x.to_be_bytes());x_buf[2..=3].copy_from_slice(&x_end.to_be_bytes());y_buf[0..=1].copy_from_slice(&y.to_be_bytes());y_buf[2..=3].copy_from_slice(&y_end.to_be_bytes());self.write_cmd_data_u8(0x2a, Some(&x_buf));self.write_cmd_data_u8(0x2b, Some(&y_buf));}fn write_cmd_data_slint(&mut self, cmd: u8, data: &[Rgb565Pixel]) {let mut i = 0;data.iter().for_each(|v| {self.line_buf[i..=i + 1].copy_from_slice(v.0.to_be_bytes().as_ref());i += 2;});self.dc.set_low().unwrap();self.spi.write(&[cmd]).unwrap();self.dc.set_high().unwrap();self.spi.write(self.line_buf[0..i].as_ref()).unwrap();}
}
xpt2046
use std::borrow::*;use esp_idf_svc::hal::{gpio::*, peripheral::*, prelude::*, spi::*};/*
d0 读 x 轴
90 读 y 轴
*/pub struct XPT2046Touch {pub x: f32,pub y: f32,
}pub struct XPT2046<'a, SPI>
whereSPI: Borrow<SpiDriver<'a>> + 'a,
{spi: SpiDeviceDriver<'a, SPI>,
}impl<'a, SPI> XPT2046<'a, SPI>
whereSPI: Borrow<SpiDriver<'a>> + 'a,
{pub fn new<CS>(spi: SPI, cs: CS) -> SelfwhereCS: Peripheral<P = CS> + OutputPin,{let config = config::Config::default().baudrate(2.MHz().into()).duplex(config::Duplex::Half3Wire);Self {spi: SpiDeviceDriver::new(spi, Some(cs), &config).unwrap(),}}pub fn read(&mut self) -> Option<XPT2046Touch> {let mut x_u16 = [0u16; 3];let mut y_u16 = [0u16; 3];for i in 0..x_u16.len() {let mut x = [0u8; 2];let mut y = [0u8; 2];self.spi.transaction(&mut [Operation::Write(&[0xd0]), Operation::Read(&mut x)]).unwrap();self.spi.transaction(&mut [Operation::Write(&[0x90]), Operation::Read(&mut y)]).unwrap();x_u16[i] = u16::from_be_bytes(x) << 1 >> 4;y_u16[i] = u16::from_be_bytes(y) << 1 >> 4;}x_u16.sort();y_u16.sort();// 实测最大最小值let x = x_u16[1].max(336).min(3847);let y = y_u16[1].max(184).min(3584);let x = (x - 336) as f32 / (3847 - 336) as f32;let y = (y - 184) as f32 / (3584 - 184) as f32;// 判断有没有触摸if x == 0 as f32 && y == 1 as f32 {None} else {Some(XPT2046Touch { x, y })}}
}