Skip to content

Implementing TUI Interface on ST7789 Screen with Rust and Ratatui on ESP32-S3

kingzcheung
Published date:
Edit this post

If we use embedded-graphics directly to write complex UIs for the ST7789 screen, it can be a rather painful experience. The Rust version of LVGL doesn’t seem ready yet.

I accidentally discovered that Ratatui actually has an embedded version: Mousefood.

Ratatui is typically used to draw UIs in terminals, and can be understood as a text-based UI framework. This might seem unrelated to embedded screens. However, that’s completely wrong. MIPI high-definition screens may indeed need more advanced UI frameworks with better effects (like phone screens). But there are still a huge number of small screens with very low resolutions (like 128x128, 128x64), or even e-ink displays. These screens are exactly suited for text and simple graphics.

This is where Ratatui comes in handy.

Implementation

We’d better use no-std mode, so we need to disable default features:

[dependencies]
mousefood ={ version = "0.4.0", default-features = false, features = ["fonts"]}

Previously, when we initialized the screen using the mipidsi library, it was like this:

let mut display = Builder::new(ST7789, di)
        .reset_pin(rst)
        .color_order(mipidsi::options::ColorOrder::Rgb)
        .invert_colors(mipidsi::options::ColorInversion::Inverted)
        .init(&mut delay)
        .unwrap();

There’s a huge pitfall here. Mousefood must specify the display size, otherwise it will result in a blank screen with only the backlight on. And it won’t report any errors at all.

Therefore, we need to specify the display size during initialization:

    let mut display = Builder::new(ST7789, di)
        .reset_pin(rst)
        .display_size(240, 240) // <--------Must add this line!
        .color_order(mipidsi::options::ColorOrder::Rgb)
        .invert_colors(mipidsi::options::ColorInversion::Inverted)
        .init(&mut delay)
        .unwrap();

Configure the terminal instance:

    let config = EmbeddedBackendConfig {
        font_regular: MONO_10X20,
        // font_bold: Some(REGULAR_FONT),
        // font_italic:  Some(REGULAR_FONT),
        // color_theme:  theme,
        ..Default::default()
    };
    let backend = EmbeddedBackend::new(&mut display, config);
    let mut terminal = Terminal::new(backend).expect("failed to create terminal");

We can configure the font size through EmbeddedBackendConfig.

However, the mousefood library uses mono fonts from embedded-graphics, which don’t have Chinese characters by default.

So should we create our own mono format Chinese font? In the previous article: “Displaying Chinese Characters in Embedded Rust”, we tried using mono font format for Chinese characters, and the character width was extremely exaggerated.

However, the mousefood library doesn’t support BDF format fonts.

Theoretically, once you have the terminal, you can directly draw UI.

terminal.draw(draw).unwrap();
    loop {
        // terminal
        //     .draw(|f| {
        //         let area = f.area();
        //         f.render_widget(Block::bordered().title("Test"), area);
        //     })
        //     .unwrap();

        // println!("Here is loop.");
        delay.delay_millis(2000);
    }

fn draw(frame: &mut Frame) {
    let block = Block::bordered()
        .title("Mousefood")
        .style(Style::new().fg(ratatui::style::Color::Green));
    let paragraph = Paragraph::new("Hello from Mousefood!")
        .style(Style::new().fg(ratatui::style::Color::Red))
        .block(block);
    frame.render_widget(paragraph, frame.area());
}

The result is as follows:

img

Other Notes

The mousefood official example includes an ESP32 example. If you, like me, think that ESP32 should be similar to ESP32S3 and can be used with minor modifications, then you should note:

let spi = Spi::new(
        peripherals.SPI2,
        SpiConfig::default()
            .with_frequency(Rate::from_mhz(30))
            .with_mode(Mode::_3),// <-------must be in mode 0 for my esp32s3
    )
    .unwrap()
    .with_sck(peripherals.GPIO5)
    .with_mosi(peripherals.GPIO6);

It’s possible that .with_mode(Mode::_3) will cause the screen not to display. At least on my device, I need to set it to .with_mode(Mode::_0). This problem won’t panic or show any errors. It just simply doesn’t display. You need someone very familiar with screen hardware and SPI protocol to quickly find the cause.

What is this code for?

It defines four operating modes for SPI devices (0, 1, 2, 3). When your device communicates with other chips (like sensors, displays, SD cards, etc.) via SPI protocol, both parties must use the same mode to work properly.

Two Key Parameters of SPI Mode

The two parameters mentioned in the code comments determine the timing of communication:

Detailed Explanation of Four Modes

ModeCPOLCPHAIdle ClockData SamplingCommon Use
Mode 000Low levelRising edgeMost common, default for many devices
Mode 101Low levelFalling edgeSome specific devices
Mode 210High levelFalling edgeSome specific devices
Mode 311High levelRising edgeAlso quite common

Complete Code

The complete implementation code is as follows:

#![no_std]
#![no_main]
#![deny(
    clippy::mem_forget,
    reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
    holding buffers for the duration of a data transfer."
)]
#![deny(clippy::large_stack_frames)]

use alloc::boxed::Box;
use defmt::println;
use defmt_rtt as _;
use embedded_graphics::pixelcolor::Rgb888;
use embedded_graphics::prelude::{DrawTarget, Point, RgbColor, Size};
use embedded_graphics::primitives::Rectangle;
use embedded_hal_bus::spi::ExclusiveDevice;
use esp_alloc as _;
use esp_hal::main;
use esp_hal::{clock::CpuClock, delay::Delay, gpio, spi::master::Config, time::Rate};
use tui::regular_font::REGULAR_FONT;
use mipidsi::{Builder, interface::SpiInterface, models::ST7789};
use mousefood::fonts::MONO_10X20;
use mousefood::prelude::*;
use ratatui::{
    Frame, Terminal,
    layout::Rect,
    style::Style,
    widgets::{Block, Paragraph},
};



#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

extern crate alloc;

esp_bootloader_esp_idf::esp_app_desc!();

#[allow(
    clippy::large_stack_frames,
    reason = "it's not unusual to allocate larger buffers etc. in main"
)]
#[main]
fn main() -> ! {
    let mut delay = Delay::new();
    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let peripherals = esp_hal::init(config);

    esp_alloc::heap_allocator!(size: 64 * 1024);

    // LCD init
    let dc = gpio::Output::new(peripherals.GPIO15, gpio::Level::Low, Default::default());
    let mut rst = gpio::Output::new(peripherals.GPIO7, gpio::Level::Low, Default::default());
    rst.set_high();
    let cs = gpio::Output::new(peripherals.GPIO16, gpio::Level::High, Default::default());
    let spi = esp_hal::spi::master::Spi::new(
        peripherals.SPI2,
        Config::default().with_frequency(Rate::from_mhz(30)),
    )
    .unwrap()
    .with_sck(peripherals.GPIO5)
    .with_mosi(peripherals.GPIO6);

    let spi_device = ExclusiveDevice::new_no_delay(spi, cs).unwrap();
    let buffer = Box::leak(Box::new([0_u8; 512]));

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

    let config = EmbeddedBackendConfig {
        font_regular: MONO_10X20,
        // font_bold: Some(REGULAR_FONT),
        // font_italic:  Some(REGULAR_FONT),
        // color_theme:  theme,
        ..Default::default()
    };

    let backend = EmbeddedBackend::new(&mut display, config);

    let mut terminal = Terminal::new(backend).expect("failed to create terminal");

    terminal.draw(draw).unwrap();
    loop {
        // terminal
        //     .draw(|f| {
        //         let area = f.area();
        //         f.render_widget(Block::bordered().title("Test"), area);
        //     })
        //     .unwrap();

        // println!("Here is loop.");
        delay.delay_millis(2000);
    }
}

fn draw(frame: &mut Frame) {
    let block = Block::bordered()
        .title("Mousefood")
        .style(Style::new().fg(ratatui::style::Color::Green));
    let paragraph = Paragraph::new("Hello from Mousefood!")
        .style(Style::new().fg(ratatui::style::Color::Red))
        .block(block);
    frame.render_widget(paragraph, frame.area());
}
Previous
ESP32-S3中使用Rust进行ADC模数转换:读取XY轴摇杆传感器教程
Next
使用 Rust 和 Ratatui 在 ESP32-S3 上实现 ST7789 屏幕的 TUI 界面