ESP32C3使用Slint渲染TFT屏幕

尽意
2025-05-10 / 0 评论 / 9 阅读 / 正在检测是否收录...
这个教程算是在st7789屏幕没有买之前就已经开始打算了,之前的使用slint驱动oled也算是为这个打下基础吧

创建新项目

可以参考 ESP32C3使用Slint渲染OLED屏幕 引入slint依赖,创建跟配置slint跟这篇文章一样,引入的依赖基本上大差不差,只不过这个项目还需要引入st7789屏幕的相关依赖。

然后在Cargo.toml中添加依赖


[dependencies]
# 基础依赖
critical-section = "1.2.0"
esp-hal          = { version = "1.0.0-beta.0", features = ["esp32c3","unstable"] }
esp-alloc = "0.7.0"
esp-println = { version = "0.13.1",features = ["esp32c3","log"] }
log = "0.4.27"
esp-backtrace = { version = "0.15.1" ,features = ["esp32c3","println","panic-handler"]}


# 图形相关
embedded-hal = "1.0.0"
embedded-graphics-core = "0.4.0"
embedded-graphics = "0.8.1"
embedded-graphics-framebuf = "0.5.0"
display-interface = "0.5.0"
display-interface-spi = "0.5.0"
mipidsi = "0.9.0"
embedded-hal-bus = "0.3.0"

# 异步
esp-hal-embassy  = { version = "0.7.0", features = ["esp32c3"] }
embassy-time     = { version = "0.4.0", features = ["generic-queue-8"] }
embassy-executor = { version = "0.7.0",features = ["task-arena-size-20480","executor-thread"] }
#embassy-futures = "0.1.1"
static_cell      = { version = "2.1.0", features = ["nightly"] }
tinygif = "0.0.4"



[dependencies.slint]
git = "https://github.com/slint-ui/slint"
rev = "29168bc89270798f6075a0a729c14a3f011ceb4f"  # 修复提交的哈希
default-features = false
features = [
    "compat-1-2",
    "unsafe-single-threaded",  # 启用单线程模式
    "libm",
    "renderer-software",
]

[build-dependencies]
slint-build = {git = "https://github.com/slint-ui/slint",rev = "29168bc89270798f6075a0a729c14a3f011ceb4f"}

创建ui文件

创建 ui/main.slint 文件,作为程序的入口文件

import { System,LocalTime,Route , Setting} from "./global.slint";
import { HomePage } from "./page/home_page.slint";
import { SettingPage } from "./page/setting_page.slint";

// 导出给rust代码调用
export { System,LocalTime,Setting }

export component MainView inherits Window {
    background: black;
    width: System.screen_width;
    height: System.screen_height;
    default-font-family: "黑体";


    // 简单实现路由
    if System.route==Route.HomePage: HomePage{}
    if System.route==Route.SettingPage: SettingPage{}

    // 显示帧率
    if Setting.fps == true:Rectangle {
        padding: 2px;
        x:0px;
        y:0px;
        width: fps.width;
        height: fps.height;
        background: black;
        fps:=Text {
            font-size: 20px;
            text: @tr("FPS:{}",System.fps);
            color: white;
        }
    }

    // 触摸切换页面,用于测试
    TouchArea {
        clicked => {
            if(System.route == Route.HomePage){
                System.route = Route.SettingPage;
            }else{
                System.route = Route.HomePage;
            }
        }
    }
}

创建 ui/global.slint 存储全局变量,跟rust代码进行交互



export enum Route {
    HomePage,
    SettingPage
}

export global System {

    // 屏幕宽高
    out property <length> screen_width:240px;
    out property <length> screen_height:240px;

    // 路由页面
    in-out property <Route> route:Route.HomePage;

    // 帧率
    in-out property <int> fps:0;


}

export global Setting {
    // 开启fps帧率显示
    in-out property <bool> fps:true;
}



export global LocalTime {
    in-out property <int> hour:12;
    in-out property <int> minute:12;
    in-out property <int> scond:30;

    in-out property <int> year:2025;
    in-out property <int> month:12;
    in-out property <int> day:12;

    in-out property <int> week:1;
    private property <[string]> weeks:["周日","周一","周二","周三","周四","周五","周六"];
    in-out property <string> week_cn:weeks[week];

}

创建ui/page/home_page.slint文件展示主页

import { System,LocalTime } from "../global.slint";

export component HomePage inherits Rectangle{
    width: System.screen_width;
    height: System.screen_height;
    // 背景图片
    bg:=Image {
        source: @image-url("../../assets/bg_ndmz.png");
    }

    animate x {
         easing: ease-in-out;
         duration: 300ms;
    }


    VerticalLayout {
        spacing: 4px;
        alignment: start;

        week:=HorizontalLayout{
            padding-top: 20px;
            alignment: center;
            spacing: 5px;
            Text {
                text: LocalTime.month;
                color: white;
                font-size: 20px;
            }
            Text {
                text: LocalTime.week_cn;
                color: white;
                font-size: 20px;
            }

        }

        time:=HorizontalLayout {
                padding: 2px;
                spacing: 5px;
                alignment: center;
                Rectangle {
//                    background: blue;
                    Text {
                        text: LocalTime.hour;
                        font-size: 60px;
                        color: white;
                        font-weight: 500;
                    }
                }
                Text {
                    width: 20px;
                    text: ":";
                    font-size: 60px;
                    color: white;
                    font-weight: 500;
                }
                Rectangle {
//                    background: pink;
                    Text {
                        text: LocalTime.minute;
                        font-size: 60px;
                        color: white;
                        font-weight: 500;
                    }
                }

        }
    }

    // 这里的定时器是简单模拟时间,实际项目应该从rust代码传入时间
    Timer {
        running: true;
        interval: 1s;
        triggered => {
            LocalTime.minute+=1;
        }
    }

}

创建ui/page/setting_page.slint展示设置页面,这里简单做了给跑马灯的效果

import { System } from "../global.slint";

export component SettingPage inherits Rectangle{
    width: System.screen_width;
    height: System.screen_height;
    background: white;

    property <length> pos_x:0px;
    text:=Text {
        x:pos_x;
        text: "{}这里是设置页面aaaadfsadfsdf";
        font-size: 40px;
        font-weight: 999;
    }

    Timer {
        running: true;
        interval: 50ms;
        triggered => {

            if(pos_x < System.screen_width - text.width){
                pos_x = System.screen_width;
            }else{
                pos_x -= 1px;
            }
        }
    }

}

编写main.rs代码

#![no_std]
#![no_main]
extern crate alloc;

use alloc::boxed::Box;
use alloc::format;
use alloc::rc::Rc;
use alloc::string::ToString;
use alloc::vec::Vec;
use core::cell::RefCell;
use core::fmt::Debug;
use core::sync::atomic::{AtomicBool, Ordering};
use critical_section::Mutex;
use embassy_executor::Spawner;
use embassy_time::Timer;
use embedded_graphics::mono_font::ascii::FONT_10X20;
use embedded_graphics::mono_font::iso_8859_7::FONT_5X8;
use embedded_graphics::mono_font::MonoTextStyle;
use embedded_graphics::prelude::Primitive;
use embedded_graphics::primitives::PrimitiveStyle;
use embedded_graphics::text::Text;
use embedded_graphics_core::Drawable;
use embedded_graphics_core::pixelcolor::{Bgr565, BinaryColor, Rgb565};
use embedded_graphics_core::prelude::{DrawTarget, ImageDrawable, Point, RgbColor, Size};
use embedded_graphics_core::primitives::Rectangle;
use embedded_hal::delay::DelayNs;
use embedded_hal_bus::spi::{ExclusiveDevice, NoDelay};
use esp_hal::clock::{CpuClock, RadioClockController};
use esp_hal::delay::Delay;
use esp_hal::{handler, main, ram, spi, Blocking};
use esp_hal::gpio::{Event, Input, InputConfig, Io, Level, Output, OutputConfig, Pull};
use esp_hal::i2c::master::I2c;
use esp_hal::peripherals::Peripherals;
use esp_hal::rtc_cntl::Rtc;
use esp_hal::spi::master::Config;
use esp_hal::spi::master::Spi;
use esp_hal::spi::Mode;
use esp_hal::time::{Duration, Instant, Rate};
use esp_hal::timer::systimer::SystemTimer;
use esp_hal::timer::timg::TimerGroup;
use esp_println::logger::{init_logger, init_logger_from_env};
use esp_println::println;
use log::{error, info, warn, LevelFilter};
use mipidsi::{Builder, Display};
use mipidsi::interface::SpiInterface;
use mipidsi::models::ST7789;
use mipidsi::options::{ColorInversion, Orientation};
use slint::platform::{Platform, WindowAdapter};
use slint::platform::software_renderer::{LineBufferProvider, MinimalSoftwareWindow, Rgb565Pixel};
use slint::{invoke_from_event_loop, ModelRc, PhysicalSize, PlatformError, Rgb8Pixel, Rgba8Pixel, SharedPixelBuffer, TimerMode, Weak};
slint::include_modules!();

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
    error!("panic:{}",info);
    loop {}
}

#[esp_hal_embassy::main]
async fn main(spawner: Spawner) {
    // generator version: 0.3.1
    // init_logger_from_env();
    init_logger(LevelFilter::Debug);
    esp_alloc::heap_allocator!(size: 100 * 1024);
    info!("分配内存完成");

    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let peripherals =esp_hal::init(config);

    // 配置异步
    let timer0 = SystemTimer::new(peripherals.SYSTIMER);
    esp_hal_embassy::init(timer0.alarm0);


    let mut delay = Delay::new();

    let mut rst = Output::new(peripherals.GPIO3,Level::Low,OutputConfig::default());
    rst.set_high();

    let dc = Output::new(peripherals.GPIO4,Level::Low,OutputConfig::default());
    let mut backlight = Output::new(peripherals.GPIO5,Level::Low,OutputConfig::default());

    let sclk = peripherals.GPIO6;
    let sda = peripherals.GPIO7;
    let sdi = peripherals.GPIO8;
    let cs = peripherals.GPIO10;


    let spi = Spi::new(peripherals.SPI2,
                       Config::default()
                           .with_mode(Mode::_3)
                           .with_frequency(Rate::from_mhz(60))
    ).unwrap()
        .with_sck(sclk)
        .with_miso(sdi)
        .with_mosi(sda);


    let cs_output = Output::new(cs,Level::High,OutputConfig::default());
    let spi_device =  ExclusiveDevice::new_no_delay(spi,cs_output).unwrap();

    let di = SpiInterface::new(spi_device,dc,unsafe{ &mut BUFFER });
    let display = Builder::new(ST7789, di)
        .display_size(240,240)
        .invert_colors(ColorInversion::Inverted)
        .color_order(mipidsi::options::ColorOrder::Rgb) 
        .orientation(Orientation::new())
        .reset_pin(rst)
        .init(&mut delay).unwrap();

    println!("display 初始化完成");
    backlight.set_high();

    info!("开启异步任务");
    spawner.spawn(ui_run(display)).ok();

    // for inspiration have a look at the examples at https://github.com/esp-rs/esp-hal/tree/esp-hal-v1.0.0-beta.0/examples/src/bin
}
static mut BUFFER: [u8; 512] = [0_u8; 512];


type DISPLAY = Display<SpiInterface<'static, ExclusiveDevice<Spi<'static,Blocking>,Output<'static>,NoDelay>,Output<'static>>,ST7789,Output<'static>>;

#[embassy_executor::task]
async fn ui_run(mut display: DISPLAY){
    // 创建平台实例
    let window = MinimalSoftwareWindow::new(
        slint::platform::software_renderer::RepaintBufferType::ReusedBuffer,
    );
    window.set_size(PhysicalSize::new(240,240));
    let platform = EspPlatform::new(window.clone());
    slint::platform::set_platform(Box::new(platform)).unwrap();

    // 创建ui
    let ui = MainView::new().unwrap();
    ui.window().set_size(PhysicalSize::new(240,240));

    // 初始化显示
    display.clear(Rgb565::BLACK).unwrap();
    info!("清理屏幕");

    // 创建行缓冲区
    let display_width = 240; // 根据实际显示宽度设置
    let mut the_frame_buffer = [Rgb565Pixel(0); 240 ];

    let mut time = Instant::now();;
    let mut last_frame_time = time.duration_since_epoch();
    let mut fps = 0;
    let mut frame_count = 0;
    loop {
        frame_count+=1;

        // 更新UI状态
        slint::platform::update_timers_and_animations();
        // 渲染UI
        window.draw_if_needed(|renderer| {

            renderer.render_by_line(FrameBuffer{ frame_buffer: &mut the_frame_buffer, stride: display_width,display:&mut display });

        });

        // 计算FPS
        let now = time.duration_since_epoch();
        if now.as_millis()-last_frame_time.as_millis() >= 1000{
            last_frame_time = now;
            fps = frame_count;
            frame_count = 0;
            ui.global::<System>().set_fps(fps);
        }


        // 根据动画状态决定是否延迟
        if !window.has_active_animations() {
            if let Some(duration) = slint::platform::duration_until_next_timer_update(){
                Timer::after_millis(duration.as_millis() as u64).await;
                continue;
            }
        }
        Timer::after_millis(10).await;
    }
}


// 平台实现结构体
struct EspPlatform{
    window: Rc<MinimalSoftwareWindow>,
}

impl EspPlatform {
    fn new(window:Rc<MinimalSoftwareWindow>) -> Self {
        Self {
            window
        }
    }
}


/// 实现 Platform trait
impl Platform for EspPlatform {
    fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError> {
        info!("成功创建了window");
        Ok(self.window.clone())
    }

    fn duration_since_start(&self) -> core::time::Duration {
        // 使用定时器获取时间(需要实际硬件计时器实现)
        let time = Instant::now().duration_since_epoch().as_millis();
        // info!("time :{}",time);
        core::time::Duration::from_millis(
            time
        )
    }
}


struct FrameBuffer<'a>{ frame_buffer: &'a mut [Rgb565Pixel], stride: usize,display:&'a mut DISPLAY }
impl<'a> LineBufferProvider for FrameBuffer<'a> {
    type TargetPixel = Rgb565Pixel;
    fn process_line(
        &mut self,
        line: usize,
        range: core::ops::Range<usize>,
        render_fn: impl FnOnce(&mut [Self::TargetPixel]),
    ) {

        let buf = &mut self.frame_buffer[range.clone()];
        render_fn(buf);

        self.display.set_pixels(
            range.start as u16,
            line as _,
            range.end as u16,
            line as u16,
            buf.iter().map(|x|{
                embedded_graphics_core::pixelcolor::raw::RawU16::new(x.0).into()
            })
        ).unwrap()


    }
}


效果展示

浏览首页页面
mai6b3e6.png
设置页面
mai6bjlw.png
最终的效果展示
mai6ace3.png

0

评论 (0)

取消