st7789v2 0.1.0

A Rust driver for the ST7789V2 TFT-LCD display controller
Documentation
# 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

| Feature              | Default | Description                                            |
| -------------------- | ------- | ------------------------------------------------------ |
| `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.