# st77916
A `no_std` Rust driver for the **Sitronix ST77916** TFT-LCD display controller
(360x390, 262K color).
The ST77916 is found in round and square LCD modules driven over QSPI, SPI, or
parallel interfaces. It is used in boards such as the
**Waveshare ESP32-S3 Knob Touch LCD 1.8"** (360x360, QSPI).
## Features
- Generic over communication interface — implement `ControllerInterface` for
QSPI, 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 ST77916 command set from the datasheet (V1.0, Sitronix, 2022/08).
## 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]
st77916 = "0.1"
# Without embedded-graphics (raw driver only)
[dependencies]
st77916 = { version = "0.1", default-features = false }
```
## Usage
### Unbuffered (no `DrawTarget`)
The caller owns the pixel data and streams it directly.
```rust,ignore
use st77916::{St77916, DisplaySize, ColorMode};
const SIZE: DisplaySize = DisplaySize::new(360, 360);
let mut display = St77916::builder(qspi_interface, gpio_reset, SIZE)
.with_init_commands(&PANEL_INIT_SEQUENCE)
.build(ColorMode::Rgb565, &mut delay)?;
display.set_window(0, 0, 359, 359)?;
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 st77916::{St77916, 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(360, 360);
const FB_SIZE: usize = framebuffer_size(SIZE, ColorMode::Rgb565);
let mut display = St77916::builder(qspi_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. Draw operations target the back buffer;
`swap_buffers()` exchanges front and back, then `flush_front()` sends the
completed frame.
```rust,ignore
use st77916::{St77916, DisplaySize, ColorMode, Framebuffer, framebuffer_size};
use embedded_graphics::pixelcolor::Rgb565;
const SIZE: DisplaySize = DisplaySize::new(360, 360);
const FB_SIZE: usize = framebuffer_size(SIZE, ColorMode::Rgb565);
let mut display = St77916::builder(qspi_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..720].fill(0xFF); // Write to first row (360 pixels x 2 bytes)
display.mark_dirty(0, 0); // Mark row 0 as dirty
display.flush()?;
```
## 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()`).
## QSPI protocol
The ST77916 QSPI interface (datasheet Section 8.8.5) uses:
| `0x02` | PP | Single-line cmd + addr + data (register writes) |
| `0x32` | PP4O | Single-line cmd + addr, quad-line data (pixel writes) |
| `0x0B` | FAST READ | Single-line read with dummy byte |
Address format: `[0x00, command_byte, 0x00]` (24-bit).
## Datasheet
The ST77916 datasheet (V1.0, 264 pages) is hosted by Espressif:
https://dl.espressif.com/AE/esp-iot-solution/ST77916_SPEC_V1.0.pdf
## Acknowledgements
This crate is heavily inspired by
[theembeddedrustacean/sh8601-rs](https://github.com/theembeddedrustacean/sh8601-rs).
The trait-based architecture (`ControllerInterface`, `ResetInterface`), builder
pattern, framebuffer design, and `embedded-graphics` integration all follow the
patterns established in that crate.
## 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.