源码: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 curtain
Rectangle {
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 curtain
Rectangle {
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 element
root.clicked();
}
width: 100%;
height: 100%;
}
}
export component AppWindow inherits Window {
width: 240px;
height: 320px;
callback check_if_pair_solved();
// Added
in property <bool> disable_tiles;
// Added
in-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 tile
solved: 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)
where
SPI: 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 model
let mut tiles: Vec<TileData> = main_window.get_memory_tiles().iter().collect();
// Duplicate them to ensure that we have pairs
tiles.extend(tiles.clone());
// Randomly mix the tiles
use rand::seq::SliceRandom;
let mut rng = rand::thread_rng();
tiles.shuffle(&mut rng);
// Assign the shuffled Vec to the model property
let 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();
// 延时 1s
slint::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>
where
SPI: 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>
where
SPI: 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>
where
SPI: 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>
where
SPI: 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;
/*
# 初始化
第一个字节是命令
等待5ms
36, 48 左上右下竖屏、c8 右下左上竖屏、e8 左下右上横屏、28右上左下横屏
3a, 55 像素格式 565
11 退出睡眠
29 开显示、28 关显示。不会更改内存内容
# 设置区域
可设置一点、一行或一个方块,方块区域会自动换行
2a x坐标
16bit xs
16bit xe
2b y坐标
16bit ys
16bit ye
2c
16bit 565 颜色数据
*/
pub struct ILI9341<'a, SPI, DC>
where
SPI: 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>
where
SPI: Borrow<SpiDriver<'a>> + 'a,
DC: Peripheral<P = DC> + OutputPin,
{
pub fn new<CS>(spi: SPI, cs: CS, dc: DC) -> Self
where
CS: 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>
where
SPI: Borrow<SpiDriver<'a>> + 'a,
{
spi: SpiDeviceDriver<'a, SPI>,
}
impl<'a, SPI> XPT2046<'a, SPI>
where
SPI: Borrow<SpiDriver<'a>> + 'a,
{
pub fn new<CS>(spi: SPI, cs: CS) -> Self
where
CS: 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 })
}
}
}