This is an Arduino library for 360×360 round ST77916 QSPI panels on ESP32-S3. It is designed specifically to work with Adafruit_GFX, so any sketch written against a normal Adafruit display driver already knows most of this one's API.
The panel talks QSPI (4 data lines) rather than the usual 8/16-bit parallel or single-line SPI, and the driver is built around a framebuffer in PSRAM with dirty-rectangle tracking and queued DMA, so it stays fast enough for GIFs, MJPEG video and generative/sketch-style drawing rather than just static UI.
Please note this is a community driver, not an official Adafruit or Espressif product. It targets one panel family (ST77916, 360×360, QSPI) on one MCU family (ESP32-S3, for the PSRAM + QSPI peripheral).
- Drop-in Adafruit_GFX subclass —
drawPixel,fillRect, text, existing sketches, all work unmodified. - PSRAM framebuffer with dirty-rectangle tracking, so
display()only sends the pixels that changed. - Ping-pong DMA push with an optional second-core task (
present()) for tear-free double buffering. - Blitting: RGB565, 8-bit palettised, colour-keyed, integer-scaled, and a direct-to-panel path that bypasses the framebuffer for streaming video.
- Alpha blending and anti-aliased primitives (Wu lines, round-capped strokes, circles, arcs, scanline polygons).
P5.h, an optional p5.js-shaped drawing layer (transform stack,stroke/fill, Perlinnoise(),map/lerp/dist) for sketches that read like p5.js.ST77916_Sprite, a standalone offscreen RGB565 canvas for compositing HUDs and sprite frames.- 12 example sketches, from a benchmark and a bring-up-style geometry demo through GIF/MJPEG playback to generative pointillism and 3D-illusion typography.
- An off-target test suite (
test/hostcheck/run.sh) that builds and runs the driver's rotation, clipping, blitting and buffer-management logic on your dev machine, no hardware required.
Any ESP32-S3 board with a free QSPI bus (four data lines) and three spare
GPIOs for reset/chip-select/backlight will work — the wiring is not fixed to
one carrier board. Each example sketch has its pin numbers at the top as
named #defines:
// ---- wiring ------------------------------------------------------------
// Edit these to match your board. Any ESP32-S3 with a free QSPI bus (4 data
// lines) plus RST/CS/BL pins works; only the numbers change.
#define TFT_SCLK 17
#define TFT_IO0 18
#define TFT_IO1 21
#define TFT_IO2 14
#define TFT_IO3 13
#define TFT_RST 4
#define TFT_CS 7
#define TFT_BL 5Edit those eight numbers to match your board's silkscreen/schematic and everything else in the sketch stays the same.
Sketch → Include Library → Manage Libraries..., search for ST77916_QSPI, click Install. This also installs the required Adafruit GFX Library dependency automatically.
-
Download this repository as a ZIP (Code → Download ZIP on GitHub).
-
In the Arduino IDE: Sketch → Include Library → Add .ZIP Library..., and select the downloaded file.
Or unzip it directly into your sketchbook's
librariesfolder (~/Documents/Arduino/libraries/on macOS/Linux,Documents\Arduino\libraries\on Windows) — rename the extracted folder toST77916_QSPIif GitHub added a branch suffix. -
Restart the Arduino IDE.
- Adafruit GFX Library — install via Library Manager, this library builds on top of it.
- Board support: ESP32 by Espressif Systems, installed via Boards Manager, with an "ESP32S3 Dev Module" (or your board's specific entry) selected, PSRAM enabled.
- Optional, only for the matching examples: AnimatedGIF (GifPlayer), JPEGDEC (MjpegPlayer) — both by bitbank2, both in Library Manager.
#include <ST77916_QSPI.h>
#include <P5.h>
#define TFT_SCLK 17
#define TFT_IO0 18
#define TFT_IO1 21
#define TFT_IO2 14
#define TFT_IO3 13
#define TFT_RST 4
#define TFT_CS 7
#define TFT_BL 5
ST77916_QSPI_GFX tft(TFT_SCLK, TFT_IO0, TFT_IO1, TFT_IO2, TFT_IO3, TFT_RST, TFT_CS, TFT_BL);
P5 p5(tft);
void setup() {
ST77916_QSPI_GFX::Config cfg;
cfg.doubleBuffer = true;
cfg.pushTask = true;
tft.begin(cfg);
}
void loop() {
p5.background(ST77916_WHITE);
p5.stroke(ST77916_BLACK);
p5.strokeWeight(2);
p5.noFill();
p5.push();
p5.translate(180, 180);
p5.rotate(millis() * 0.0005f);
for (int i = 3; i < 9; i++) p5.polygon(0, 0, 20.0f * i, i);
p5.pop();
tft.present();
}Check begin()'s return value in real code — see tft.lastError() if it
returns false.
Open via File → Examples → ST77916_QSPI in the Arduino IDE.
| Example | What it shows |
|---|---|
Benchmark |
Timing for every push path; run this first on new hardware to pick chunkBytes/clockHz. |
Geometry |
p5-layer generative geometry, six movements, anti-aliased. |
bw_geometry |
The same idea via the raw ST77916_Draw primitives, no p5 layer. |
SpriteHUD |
Offscreen ST77916_Sprite HUD composited over an animated background with colour-key transparency. |
GifPlayer |
Animated GIF playback from an SD card via AnimatedGIF. |
MjpegPlayer |
Motion JPEG video playback from an SD card via JPEGDEC. |
RadialGrid |
Port of a Generative Gestaltung p5.js sketch (distance-driven radial grid). |
BhujodiMotifs |
Generative homage to Bhujodi extra-weft weaving patterns. |
PhotoPointillism, SunsetPointillism |
Pointillism renders of a photo, preprocessed offline into a point-cloud header (script included under image/). |
PointillismText |
Particle field that carves out static text as negative space. |
TypographicIllusion3D |
Extruded 3D-illusion wordmark built from a text mask. |
present() is the call to reach for; it does the right thing in every
configuration. Single buffered, it is a blocking push of the dirty region.
Double buffered with a push task (cfg.doubleBuffer = true; cfg.pushTask = true;), it flips buffers and transmits the frame you just drew on the other
core while you draw the next one.
The manual controls are still there if you want them: display(),
displayFull(), displayAsync(), waitDisplay(), swapBuffers().
A full frame is 360 × 360 × 2 = 259,200 bytes. At 70 MHz across four data lines that's 2.07 Mbit, so ~7.4 ms is the wire floor — nothing gets a full-panel refresh above roughly 135 fps on this bus, whatever the driver does.
Everything above that floor is CPU work: the framebuffer is kept in the
panel's own byte order so a push is a straight memcpy, chunks are queued so
copy and transfer overlap, and dirty-rectangle tracking means a small update
(say, a 120×120 region) only pushes about a ninth of a full frame.
Run examples/Benchmark and read the numbers off your own board before
tuning anything. Useful knobs, roughly in order of effect: doubleBuffer +
pushTask, chunkBytes (try 32768), and clockHz.
Both decoders can emit pixels in the panel's byte order, so playback costs little beyond the decode itself.
gif.begin(GIF_PALETTE_RGB565_BE);
// in GIFDraw:
tft.pushImage8(x, y, pDraw->iWidth, 1, pDraw->pPixels, pDraw->pPalette,
pDraw->ucHasTransparency ? pDraw->ucTransparent : -1,
ST77916_PIXELS_BE);jpeg.setPixelType(RGB565_BIG_ENDIAN);
// in JPEGDraw:
tft.pushImage(pDraw->x, pDraw->y, pDraw->iWidth, pDraw->iHeight,
pDraw->pPixels, ST77916_PIXELS_BE);pushRectDirect() streams straight to the panel without touching the
framebuffer, for when a decoder already produces full rows in display order
and there is no compositing to do.
test/hostcheck/run.sh
Builds the driver against stub Arduino/ESP-IDF headers and runs it on your
machine: rotation mapping in all four orientations, dirty-rectangle
arithmetic, the clip window, palettised/RGB565 blits in both byte orders,
edge clipping, blend saturation, and the double-buffered present path. It
also renders a test card to /tmp/frame.ppm, so antialiasing regressions
are visible without flashing anything. No hardware required; run it after
any change to src/ST77916_QSPI.cpp.
- Check
begin()'s return value andlastError()before assuming the panel is up. - If colours look swapped (red/blue reversed), set
Config::bgr = true. - If you see tearing or garbage on the wire, lower
clockHzbefore suspecting anything else in the driver. setRotation()is supported (handled in software, not via panel MADCTL), but on a round panel it mostly just costs a per-pixel branch — leave it at the default unless you need it.
MIT. See LICENSE.
Issues and pull requests are welcome at
github.com/Studio-Poetics/ST77916_QSPI.
Please run test/hostcheck/run.sh before opening a PR that touches
src/ST77916_QSPI.cpp.
Written by Studio Poetics.