# st7789v2
A `no_std` Rust driver for the **Sitronix ST7789V2** TFT-LCD display controller
(240x320, 262K color).
The ST7789V2 is a widely used single-chip controller for small TFT panels,
commonly driven over SPI. It is found in boards such as the
**Waveshare ESP32-C6 1.69" LCD** (240x240, SPI) and many other hobbyist and
commercial display modules.
## Features
- Generic over communication interface — implement `ControllerInterface` for
SPI, parallel, or any other bus.
- Generic over reset pin — GPIO, I2C expander, or any other mechanism via
`ResetInterface`.
- **Three buffering modes** selected at compile-time via a generic parameter
(requires the `embedded-graphics` feature):
- **Unbuffered DrawTarget** — zero RAM, each draw is a hardware transaction.
- **Single-buffered** — internal framebuffer with `DrawTarget` support and
automatic dirty band tracking (only changed rows are flushed).
- **Double-buffered** — two framebuffers for overlapping render and flush on
hardware with separate CPU/DMA memory buses.
- Builder pattern with `with_init_commands()` for vendor-specific panel init
sequences.
- Static or heap-allocated framebuffer.
- RGB565, RGB888, and Gray8 color formats.
- Full ST7789V2 command set from the datasheet (V1.0, Sitronix, 2016/11),
including Command Table 2 (manufacturer registers).
## Cargo features
| `embedded-graphics` | **yes** | `DrawTarget` support, buffering modes, color trait |
Without the feature the crate provides the core driver (commands, `set_window`,
`send_pixels`, sleep, MADCTL, brightness) with zero dependency on
`embedded-graphics-core`.
```toml
# Default — includes embedded-graphics integration
[dependencies]
st7789v2 = "0.1"
# Without embedded-graphics (raw driver only)
[dependencies]
st7789v2 = { version = "0.1", default-features = false }
```
## Usage
### Unbuffered (no `DrawTarget`)
The caller owns the pixel data and streams it directly.
```rust,ignore
use st7789v2::{St7789v2, DisplaySize, ColorMode};
const SIZE: DisplaySize = DisplaySize::new(240, 240);
let mut display = St7789v2::builder(spi_interface, gpio_reset, SIZE)
.with_init_commands(&PANEL_INIT_SEQUENCE)
.build(ColorMode::Rgb565, &mut delay)?;
display.set_window(0, 0, 239, 239)?;
display.send_pixels(&my_pixel_data)?;
```
### Single-buffered (recommended)
Internal framebuffer with `embedded-graphics` `DrawTarget`. Draw operations
write to RAM; `flush()` sends only the changed rows to the display.
```rust,ignore
use st7789v2::{St7789v2, DisplaySize, ColorMode, Framebuffer, framebuffer_size};
use embedded_graphics::pixelcolor::Rgb565;
use embedded_graphics::prelude::*;
use embedded_graphics::primitives::{Circle, PrimitiveStyle};
const SIZE: DisplaySize = DisplaySize::new(240, 240);
const FB_SIZE: usize = framebuffer_size(SIZE, ColorMode::Rgb565);
let mut display = St7789v2::builder(spi_interface, gpio_reset, SIZE)
.with_init_commands(&PANEL_INIT_SEQUENCE)
.buffered::<Rgb565>(Framebuffer::heap::<FB_SIZE>())
.build(ColorMode::Rgb565, &mut delay)?;
Circle::new(Point::new(100, 100), 50)
.into_styled(PrimitiveStyle::with_fill(Rgb565::RED))
.draw(&mut display)?;
// Flush only the dirty rows to the display
display.flush()?;
// Or force a full refresh (e.g. after wake from sleep)
display.full_flush()?;
```
### Double-buffered
Two framebuffers allow overlapping render and flush on hardware with separate
CPU and DMA memory buses.
```rust,ignore
use st7789v2::{St7789v2, DisplaySize, ColorMode, Framebuffer, framebuffer_size};
use embedded_graphics::pixelcolor::Rgb565;
const SIZE: DisplaySize = DisplaySize::new(240, 240);
const FB_SIZE: usize = framebuffer_size(SIZE, ColorMode::Rgb565);
let mut display = St7789v2::builder(spi_interface, gpio_reset, SIZE)
.with_init_commands(&PANEL_INIT_SEQUENCE)
.double_buffered::<Rgb565>(
Framebuffer::heap::<FB_SIZE>(),
Framebuffer::heap::<FB_SIZE>(),
)
.build(ColorMode::Rgb565, &mut delay)?;
loop {
render_scene(&mut display);
display.swap_buffers();
display.flush_front()?;
}
```
### Direct framebuffer access
For advanced use cases, the framebuffer can be accessed directly. After writing
pixels manually, call `mark_dirty()` so the next `flush()` includes the
modified rows.
```rust,ignore
let fb = display.framebuffer_mut();
fb[0..480].fill(0xFF); // Write to first row (240 pixels x 2 bytes)
display.mark_dirty(0, 0); // Mark row 0 as dirty
display.flush()?;
```
### Implementing `ControllerInterface` for SPI
The ST7789V2 uses a standard SPI interface with a DC (data/command) pin. Here
is a typical implementation pattern:
```rust,ignore
use st7789v2::ControllerInterface;
struct MySpiInterface<SPI, DC, CS> {
spi: SPI,
dc: DC,
cs: CS,
ramwr_sent: bool,
}
impl<SPI, DC, CS> ControllerInterface for MySpiInterface<SPI, DC, CS>
where
SPI: embedded_hal::spi::SpiBus<u8>,
DC: embedded_hal::digital::OutputPin,
CS: embedded_hal::digital::OutputPin,
{
type Error = core::convert::Infallible; // or your error type
fn send_command(&mut self, cmd: u8) -> Result<(), Self::Error> {
self.ramwr_sent = false;
self.dc.set_low().ok();
self.cs.set_low().ok();
self.spi.write(&[cmd]).ok();
self.cs.set_high().ok();
Ok(())
}
fn send_command_with_data(&mut self, cmd: u8, data: &[u8]) -> Result<(), Self::Error> {
self.ramwr_sent = false;
self.dc.set_low().ok();
self.cs.set_low().ok();
self.spi.write(&[cmd]).ok();
self.dc.set_high().ok();
self.spi.write(data).ok();
self.cs.set_high().ok();
Ok(())
}
fn send_pixels(&mut self, pixels: &[u8]) -> Result<(), Self::Error> {
if !self.ramwr_sent {
self.dc.set_low().ok();
self.cs.set_low().ok();
self.spi.write(&[0x2C]).ok(); // RAMWR
self.dc.set_high().ok();
self.ramwr_sent = true;
} else {
self.dc.set_high().ok();
self.cs.set_low().ok();
}
self.spi.write(pixels).ok();
self.cs.set_high().ok();
Ok(())
}
}
```
## Dirty band tracking
The single-buffered mode automatically tracks which rows have been modified by
`DrawTarget` operations (`draw_iter`, `fill_solid`, `fill_contiguous`, `clear`).
When `flush()` is called:
- If nothing changed since the last flush, it returns immediately (no SPI traffic).
- Otherwise, only the contiguous band of modified rows is sent — a single
`set_window` + `send_pixels` call with no allocation.
This is transparent to all callers — the `DrawTarget` API is unchanged. For UIs
where only a small region updates each frame (e.g. a clock or status bar), this
can reduce flush time from ~12ms (full screen) to <1ms.
Use `full_flush()` to bypass dirty tracking when needed (e.g. after wake from
sleep or direct framebuffer writes without `mark_dirty()`).
## SPI protocol
The ST7789V2 uses standard 4-line SPI with a DC (data/command) pin:
- **DC low** = command byte
- **DC high** = data / pixel bytes
- Commands: `CASET` (0x2A), `RASET` (0x2B), `RAMWR` (0x2C)
- Pixel format set via `COLMOD` (0x3A): `0x55` = RGB565, `0x66` = RGB666
## Datasheet
ST7789V2 Datasheet V1.0, Sitronix Technology Corporation, 2016/11 (319 pages).
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
http://www.apache.org/licenses/LICENSE-2.0)
- MIT License ([LICENSE-MIT](LICENSE-MIT) or
http://opensource.org/licenses/MIT)
at your option.