# st77916
A `no_std` Rust driver for the **Sitronix ST77916** TFT-LCD display controller
(360×390, 262K color) with `embedded-graphics` support.
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"** (360×360, 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:
- **Unbuffered** (default) — caller owns pixel data, streams via `send_pixels()`.
- **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.
- `embedded-graphics` `DrawTarget` implementation with optimized `fill_solid`,
`fill_contiguous`, and `clear`.
- RGB565, RGB888, and Gray8 color formats.
- Full ST77916 command set from the datasheet (V1.0, Sitronix, 2022/08).
## Usage
### Unbuffered (default)
The caller owns the pixel data and streams it directly. No `DrawTarget` support
unless `.unbuffered::<COLOR>()` is called on the builder.
```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;
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)?;
// Draw with embedded-graphics
use embedded_graphics::prelude::*;
use embedded_graphics::primitives::{Circle, PrimitiveStyle};
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)?;
// Typical frame loop:
loop {
// Draw into back buffer via DrawTarget...
render_scene(&mut display);
// Swap: back becomes front
display.swap_buffers();
// Flush front buffer (can overlap with next frame's render
// if the interface supports non-blocking DMA)
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 × 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).
This is the same framing used by the SH8601 and other similar controllers,
so a QSPI implementation written for one will work with the other.
## 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. Many thanks to the author for the excellent
reference implementation.
## 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.