Repository navigation
Display Drivers
TinyGPU keeps rendering (Surface/FrameBuffer/Sprite, in RGB565/RGB666/RGB888/monochrome memory) completely separate from getting the finished pixels onto real hardware. Display drivers are the second half: a small, bus-specific class that takes a rendered ISurface<RGB_T> and pushes it to a physical panel controller.
Every driver derives from the same abstract base, DisplayDriver<RGB_T>:
template <typename RGB_T = RGB565>
class DisplayDriver {
public:
virtual bool begin() = 0;
virtual void end() {}
virtual bool writeData(ISurface<RGB_T>& surface) = 0;
virtual bool writeData(ISurface<RGB_T>& surface, size_t x, size_t y) = 0;
virtual void setRotation(DisplayRotation rotation) {}
virtual size_t width() const = 0;
virtual size_t height() const = 0;
void writeColor(size_t width, size_t height, RGB_T color);
protected:
virtual bool setAddressWindow(size_t x, size_t y, size_t w, size_t h) = 0;
};Because every concrete driver implements this same interface, application code (and TinyGPU's own DeviceOutput wrapper, LCDBoard setups, and the LVGLDriver integration) can hold a DisplayDriver<RGB_T>&/DisplayDriver<RGB_T>* and work with any panel/bus combination without caring which one it actually is:
#include <TinyGPU.h>
#include <TinyGPU/Drivers/DisplayDriverSPI.h>
using namespace tinygpu;
ILI9341Driver<RGB565> displayDriver(SPI, /*cs=*/5, /*dc=*/2, /*rst=*/4);
DeviceOutput<RGB565> out(displayDriver);
void setup() {
SPI.begin();
out.begin();
}
void loop() {
FrameBufferRGB565 fb(240, 320, FontRGB565);
fb.begin();
fb.fillRect(0, 0, 240, 320, RGB565(255, 0, 0));
out.writeData(fb);
}Each bus family lives in its own header under src/TinyGPU/Drivers/ - include the one(s) you actually need rather than pulling in every bus's dependencies.
| File | Bus | Platform | Panel drivers included |
|---|---|---|---|
DisplayDriverSPI.h |
4-wire SPI | Any Arduino core (SPIClass) |
ST7735Driver, ST7789Driver, ILI9341Driver, ILI9342Driver, HX8357Driver, ST7796Driver
|
DisplayDriverQSPI.h |
Quad-SPI (1 clk, 1 cs, 4 data, no DC pin) | ESP32 (esp_lcd + spi_master) |
NV3041ADriver |
DisplayDriverParallel8.h |
8-bit parallel ("8080"), bit-banged | Any Arduino core |
ST7735Driver8080, ST7789Driver8080, ILI9341Driver8080, HX8357Driver8080, ST7796Driver8080
|
DisplayDriverParallel8ESP32.h |
8-bit parallel, hardware-accelerated (LCD_CAM/I80 or PARLIO) | ESP32 family (esp_lcd) |
ILI9341Driver8080ESP32, ST7789Driver8080ESP32
|
DisplayDriverParallel8RP2040.h |
8-bit parallel, PIO/DMA-accelerated | RP2040 (pico-sdk hardware/pio.h) |
ILI9341Driver8080RP2040, ST7789Driver8080RP2040
|
DisplayDriverDSI.h |
MIPI-DSI | ESP32-P4 only | ST7701Driver |
DisplayDriverSDL.h |
none - desktop window | Desktop (SDL2) |
DisplayDriverSDL (generic, no controller) |
All panel drivers are class templates parameterized on RGB_T (default RGB565); every bus's bulk writeData() currently assumes a 16bpp RGB_T (static_assert(sizeof(RGB_T) == 2, ...)), so use RGB565 unless a specific driver documents otherwise.
-
You have a real controller chip on SPI (ILI9341, ST7789, ...): use
DisplayDriverSPI.h. This is the default choice - works on every Arduino core, needs the fewest pins (CS/DC/RST + the SPI bus), and is what most breakout-board TFTs use. -
The panel is wired with 8 parallel data pins (D0-D7) instead of SPI: use one of the
DisplayDriverParallel8*files (see below for which one). -
The panel/board documents QSPI (1 clock + 1 CS + 4 data lines, no separate D/C pin) on an ESP32: use
DisplayDriverQSPI.h. -
The panel is MIPI-DSI (typically a P4 board driving a large panel at high resolution): use
DisplayDriverDSI.h. -
You're developing/testing on a desktop, no hardware attached: use
DisplayDriverSDL.h, paired withTouchDriverSDLfor input. -
Your exact panel isn't in the list: pick the file matching your bus, subclass the relevant base class (
DisplayDriverSPI/DisplayDriverParallel8/...), and supply your panel's own init register sequence via that base'swriteCommand()/writeDataN()helpers - every concrete driver in this library is a fairly small class built the same way, so an existing one is the best starting template.
The most portable option - built on the Arduino SPIClass API, so it works unmodified on AVR, SAMD, ESP32, RP2040, or any other core that provides one.
DisplayDriverSPI<RGB_T> is the shared base: it owns the SPI bus reference, CS/DC/RST pins, and X/Y offsets, and implements the common MIPI-DCS wire protocol (0x2A/0x2B column/row address set, 0x2C RAM write, plus writeCommand()/writeData8()/writeData16()/writeDataN() helpers for init sequences). Concrete panel classes only need to supply their chip's specific init register sequence and any per-chip quirks (rotation/MADCTL table, gamma, etc.).
#include <TinyGPU/Drivers/DisplayDriverSPI.h>
using namespace tinygpu;
ILI9341Driver<RGB565> driver(SPI, /*cs=*/5, /*dc=*/2, /*rst=*/4,
ILI9341Driver<RGB565>::Rotation::kLandscape);| Class | Controller | Notes |
|---|---|---|
ST7735Driver |
ST7735 | Small (typically 128x160) panels |
ST7789Driver |
ST7789 | Common 240x240/240x320 panels |
ILI9341Driver |
ILI9341 | Full power/gamma/VCOM init sequence (ported from Bodmer/TFT_eSPI); supports setRotation() (DisplayRotation) and setInvertColor() for clone panels whose color filter needs display inversion |
ILI9342Driver |
ILI9342 | Structurally identical to ILI9341Driver, with additional power/VCOM/gamma programming some ILI9341-labelled clone panels actually need |
HX8357Driver |
HX8357 | Minimal reset/sleep-out/pixel-format/display-on init |
ST7796Driver |
ST7796 | Minimal init + MADCTL |
ILI9341Driver/ILI9342Driver read a nativeWidth/nativeHeight (native/portrait orientation) at construction and report width()/height() swapped or not depending on the current DisplayRotation (see isLandscapeFamily() in DisplayDriver.h) - so layout code that reads width()/height() fresh (rather than caching its own copy) automatically follows a later setRotation() call.
For compact panel controllers that speak the increasingly common "4-wire quad-SPI" protocol - 1 clock, 1 chip-select, 4 data lines, no separate D/C pin, with every command wrapped in a fixed 32-bit frame. ESP32-only: built on ESP-IDF's esp_lcd_new_panel_io_spi() with quad_mode=1.
| Class | Controller | Notes |
|---|---|---|
NV3041ADriver |
New Vision NV3041A | 480x272 panels on Guition/Sunton JC4827W543-class ESP32-S3 boards; full vendor gate/source-timing/gamma init sequence, confirmed on real hardware; needs display-inversion on for correct colors on this IPS panel |
#include <TinyGPU/Drivers/DisplayDriverQSPI.h>
using namespace tinygpu;
NV3041ADriver<RGB565> driver(/*cs=*/45, /*sclk=*/47, /*d0=*/21, /*d1=*/48,
/*d2=*/40, /*d3=*/39);8-bit parallel (DisplayDriverParallel8.h, DisplayDriverParallel8ESP32.h, DisplayDriverParallel8RP2040.h)
Three implementations of the same wire protocol - 8 data lines (D0-D7) + WR/RS(DC)/CS(/RD) - trading portability against throughput:
| File | How it drives D0-D7 | Platform | When to use it |
|---|---|---|---|
DisplayDriverParallel8.h |
digitalWrite() per bit, pinMode/digitalWrite for WR/DC/CS |
Any Arduino core | Any board; simplest option, and the only one that works outside ESP32/RP2040. Throughput-limited (one digitalWrite() call per bit, per byte) |
DisplayDriverParallel8ESP32.h |
DMA through a real peripheral: LCD_CAM/I80 (esp_lcd_new_panel_io_i80) on ESP32/S2/S3, or PARLIO (esp_lcd_new_panel_io_parl) on newer targets without LCD_CAM (C6/H2/P4/...) |
ESP32 family | Much faster bulk transfers on ESP32; backend is picked automatically at compile time from the target's soc_caps.h
|
DisplayDriverParallel8RP2040.h |
One PIO state machine (assembled program, see below) + a DMA channel feeding its TX FIFO | RP2040 | Much faster bulk transfers on RP2040; requires D0-D7 to be 8 consecutive GPIOs (a PIO addressing constraint) and a separate GPIO for WR |
All three expose the same set of panel classes and constructor shape (d0..d7, wr, dc, cs, rst, ...), so switching between them for the same wiring is largely a find-and-replace of the class name:
| Panel | Bit-banged | ESP32 HW | RP2040 PIO |
|---|---|---|---|
| ILI9341 | ILI9341Driver8080 |
ILI9341Driver8080ESP32 |
ILI9341Driver8080RP2040 |
| ST7789 | ST7789Driver8080 |
ST7789Driver8080ESP32 |
ST7789Driver8080RP2040 |
| ST7735 | ST7735Driver8080 |
- | - |
| HX8357 | HX8357Driver8080 |
- | - |
| ST7796 | ST7796Driver8080 |
- | - |
// Portable, any board:
#include <TinyGPU/Drivers/DisplayDriverParallel8.h>
using namespace tinygpu;
ILI9341Driver8080<RGB565> driver(/*d0..d7=*/2, 3, 4, 5, 6, 7, 8, 9,
/*wr=*/10, /*dc=*/11, /*cs=*/12);
// ESP32, hardware-accelerated:
#include <TinyGPU/Drivers/DisplayDriverParallel8ESP32.h>
ILI9341Driver8080ESP32<RGB565> driver(2, 3, 4, 5, 6, 7, 8, 9,
/*wr=*/10, /*dc=*/11, /*cs=*/12);
// RP2040, PIO/DMA-accelerated - D0..D7 must be consecutive GPIOs:
#include <TinyGPU/Drivers/DisplayDriverParallel8RP2040.h>
ILI9341Driver8080RP2040<RGB565> driver(/*d0=*/2, /*wr=*/10, /*dc=*/11, /*cs=*/12);DisplayDriverParallel8RP2040.h embeds a 2-instruction PIO program (assembled with pico-sdk's pioasm, not hand-derived):
.program lcd8080_write
.side_set 1
.wrap_target
out pins, 8 side 0 ; drive the byte onto D0-D7, WR low
nop side 1 ; WR high - panel latches on this edge
.wrap
It runs at 2 PIO-clock cycles/byte; the state machine's clock divider (clockDiv, default 8.0) controls the resulting WR toggle rate - lower it for faster panels/wiring, raise it if data looks corrupted.
For panels driven over MIPI-DSI - ESP32-P4 only (esp_lcd_mipi_dsi.h only exists in that variant of arduino-esp32's bundled ESP-IDF; the header compiles to an empty no-op on every other target rather than hard-erroring). Unlike the bus-oriented drivers above, a DSI panel has no address-window/RAM-write concept: esp_lcd_new_panel_dpi() allocates a screen-sized, DMA-scanned framebuffer up front, and writeData() writes directly into a rect of it via esp_lcd_panel_draw_bitmap() - the continuously-running DPI bridge just scans out whatever is currently there. Only a single frame buffer is used, so a write landing mid-scan can tear (the same tradeoff the SPI/QSPI drivers already accept for a torn transfer landing mid-refresh).
| Class | Controller | Notes |
|---|---|---|
ST7701Driver |
Sitronix ST7701S | 480x800 portrait, 4.3" - as wired on the Guition JC4880P443C_I_W (ESP32-P4). Init sequence, DSI lane config (2 lanes @ 500Mbps) and video timing transcribed verbatim from a hardware-verified vendor BSP |
#include <TinyGPU/Drivers/DisplayDriverDSI.h>
using namespace tinygpu;
ST7701Driver<RGB565> driver(/*rst=*/-1, /*backlight=*/-1);DisplayDriverSDL<RGB_T> is a generic (not chip-specific) driver that renders a Surface into an SDL2 window - useful for developing and testing UI/rendering code without any hardware attached. writeData() also drains the SDL event queue and exits the process on the window's close button, since a sketch that never touches TouchDriverSDL (the library's other SDL-based piece, mapping the desktop mouse to a TouchDriver) would otherwise leave the window unresponsive to being closed.
#include <TinyGPU/Drivers/DisplayDriverSDL.h>
using namespace tinygpu;
DisplayDriverSDL<RGB565> driver(240, 320);Two independent, panel-wiring-dependent issues show up as "wrong colors," and are fixed in different places - don't confuse one for the other:
-
Red/blue swapped: the panel's subpixel order is BGR rather than RGB. This is handled per-driver via the controller's own MADCTL register (see e.g.
DisplayDriverSPI.h'sILI9341Driver::madctlForRotation(), which always sets the BGR bit) - not a setting you toggle yourself. -
Colors scrambled/inverted-looking across everything drawn (not just images): some panel/wiring combinations expect the opposite byte order from the one
RGB565stores pixels in by default.DisplayDriverSPI<RGB_T>::setSwapOutputBytes(bool)swaps each pixel's two bytes right before they go out over the wire - the one place all pixel data (fills/text/images alike) funnels through before hitting the bus, so it's the right fix for this symptom (as opposed to e.g.JPEGParser::setSwapBytes(), which only affects decoded images).
Every driver's source here was written by hand and reviewed; where this repository's own tooling could build-verify a driver (real Arduino-Emulator/desktop build for the portable/SPI drivers, arduino-cli cross-compiles for the ESP32 and RP2040 hardware-accelerated drivers), it has been - a real compile (and, where applicable, link) against the platform's actual toolchain, not just a syntax read-through. The one known exception is DisplayDriverParallel8ESP32.h's PARLIO backend (ESP32-C6/H2/P4/...): its source compiles cleanly, but at least one arduino-esp32 core version (3.3.10) is missing esp_lcd_new_panel_io_parl from its prebuilt libraries for those targets, so it fails to link there - see that file's class comment. None of this replaces testing against your own exact board/panel/wiring before shipping.