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:

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:
-
CPOL (Clock Polarity): Clock level when idle
- CPOL = 0: Clock is low when idle
- CPOL = 1: Clock is high when idle
-
CPHA (Clock Phase): When data is sampled
- CPHA = 0: Sample on the first edge of the clock
- CPHA = 1: Sample on the second edge of the clock
Detailed Explanation of Four Modes
| Mode | CPOL | CPHA | Idle Clock | Data Sampling | Common Use |
|---|---|---|---|---|---|
| Mode 0 | 0 | 0 | Low level | Rising edge | Most common, default for many devices |
| Mode 1 | 0 | 1 | Low level | Falling edge | Some specific devices |
| Mode 2 | 1 | 0 | High level | Falling edge | Some specific devices |
| Mode 3 | 1 | 1 | High level | Rising edge | Also 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());
}