st77916 0.1.0

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

| Opcode | Mode      | Description                                           |
| ------ | --------- | ----------------------------------------------------- |
| `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.