Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Marcussacapuces91/doc-TFT_eSPI/llms.txt

Use this file to discover all available pages before exploring further.

TFT_eSPI is engineered from the ground up for speed, and Direct Memory Access (DMA) is its most powerful performance tool. DMA lets the processor hand off a block of pixel data to the SPI peripheral and immediately return to application code — the pixel transfer continues in the background without occupying the CPU. Combined with double-buffering, this enables the CPU to compose the next frame while the previous one is still being sent to the display, eliminating the CPU idle time that would otherwise be wasted waiting for SPI transfers to complete.

DMA Support Matrix

DMA availability depends on both the processor family and the interface type in use. Parallel interfaces on ESP32 do not support DMA through this library.
ProcessorSPI DMA8-bit Parallel DMA16-bit Parallel DMA
RP2040✅ Yes✅ Yes✅ Yes
ESP32✅ Yes❌ No❌ N/A
ESP32-S3✅ Yes❌ No❌ N/A
ESP32-C3 / S2❌ No❌ N/A❌ N/A
STM32Fxxx✅ Yes❌ No❌ N/A
ESP8266❌ No❌ N/A❌ N/A
Other / Generic❌ No❌ N/A❌ N/A
Some ILI9xxx display controllers (ILI9481, ILI9486, ILI9488) do not support DMA even over SPI due to their 18-bit pixel format requirement.

DMA API

Initializing and Releasing DMA

bool initDMA(bool ctrl_cs = false);
void deInitDMA(void);
Call initDMA() once in setup() before any DMA transfer. When ctrl_cs is true, the library manages the chip-select pin automatically during DMA transfers (recommended for most setups). Returns true on success.
void setup() {
  tft.init();
  tft.initDMA(true);    // Enable DMA with automatic CS control
}
Call deInitDMA() to release DMA resources (rarely needed in normal use).

Checking and Waiting for DMA Completion

bool dmaBusy(void);
void dmaWait(void);
dmaBusy() returns true while a DMA transfer is in progress. dmaWait() blocks until the current transfer completes. Always call dmaWait() (or check dmaBusy()) before modifying the buffer that was handed to a DMA transfer — the DMA controller is still reading from it until the transfer finishes.
// Wait for any previous DMA transfer to finish before reusing the buffer
tft.dmaWait();
The DMA_Enabled public boolean reflects whether DMA has been successfully initialised:
if (tft.DMA_Enabled) {
  Serial.println("DMA is active");
}

Transferring Pixel Data via DMA

void pushImageDMA(int32_t x, int32_t y, int32_t w, int32_t h,
                  uint16_t *data, uint16_t *buffer = nullptr);

void pushPixelsDMA(uint16_t *image, uint32_t len);
  • pushImageDMA(x, y, w, h, data, buffer) — transfers a rectangular pixel array to the display at position (x, y). The optional buffer argument enables double-buffering: while data is transferred by DMA, your code can write the next frame into buffer. On the next call, swap data and buffer.
  • pushPixelsDMA(image, len) — streams len raw 16-bit pixels to the current CGRAM window without repositioning the address window first. Use after setAddrWindow().

Double-Buffering Pattern

The key to maximum frame rates is overlapping CPU rendering with DMA transfer. Allocate two buffers of equal size and alternate between them each frame:
#include <TFT_eSPI.h>

TFT_eSPI    tft = TFT_eSPI();
TFT_eSprite sprA = TFT_eSprite(&tft);
TFT_eSprite sprB = TFT_eSprite(&tft);

#define W 240
#define H 135

void setup() {
  tft.init();
  tft.initDMA(true);
  tft.fillScreen(TFT_BLACK);

  sprA.setColorDepth(16);
  sprB.setColorDepth(16);
  sprA.createSprite(W, H);
  sprB.createSprite(W, H);
}

void loop() {
  static TFT_eSprite *drawSpr  = &sprA;  // Sprite being drawn into
  static TFT_eSprite *sendSpr  = &sprB;  // Sprite being sent via DMA

  // --- Render into the draw buffer ---
  drawSpr->fillSprite(TFT_BLACK);
  drawSpr->fillCircle(random(W), random(H), 20, random(0x10000));

  // --- Wait for previous DMA transfer to finish ---
  tft.dmaWait();

  // --- Start DMA transfer of the send buffer ---
  tft.pushImageDMA(0, 0, W, H, (uint16_t*)sendSpr->getPointer());

  // --- Swap buffers: next loop draws into the old send buffer ---
  TFT_eSprite *tmp = drawSpr;
  drawSpr = sendSpr;
  sendSpr = tmp;
}
For the double-buffer swap to work correctly, both sprite buffers must be the same size and colour depth. Allocate them both with setColorDepth(16) before createSprite() and do not call deleteSprite() between frames.

SPI Transaction Wrappers

startWrite / endWrite

void startWrite(void);
void writeColor(uint16_t color, uint32_t len);
void endWrite(void);
Every individual drawing call (drawLine(), fillRect(), etc.) internally calls startWrite() and endWrite() to assert the chip-select and begin/end the SPI transaction. When you call multiple drawing functions in sequence, this repeated CS toggling adds measurable overhead. Wrap a batch of drawing calls between a single startWrite() / endWrite() pair to assert CS once and hold the SPI bus for the entire sequence:
Wrapping multiple drawing calls inside a startWrite() / endWrite() block is one of the easiest ways to improve rendering speed. The CS pin is asserted only once for the entire batch rather than once per call.
tft.startWrite();
  tft.fillRect(0,   0,  80, 80, TFT_RED);
  tft.fillRect(80,  0,  80, 80, TFT_GREEN);
  tft.fillRect(160, 0,  80, 80, TFT_BLUE);
  tft.drawString("RGB", 10, 30, 4);
tft.endWrite();
writeColor(color, len) writes len pixels of the same colour to the current CGRAM window directly, without any bounds checking overhead. It must be called between startWrite() and endWrite().

Bulk Pixel Transfer

Two additional low-level methods bypass the higher-level drawing machinery entirely:
void pushBlock(uint16_t color, uint32_t len);
void pushPixels(const void *data, uint32_t len);
  • pushBlock(color, len) — fills the current CGRAM window with len pixels of a single colour. Faster than calling writeColor in a loop because it uses optimised SPI burst code.
  • pushPixels(data, len) — streams arbitrary pixel data from a buffer. Byte swap behaviour is controlled by setSwapBytes().
Both require an active CGRAM window (call setAddrWindow() first) and must be called inside a startWrite() / endWrite() block.
uint16_t image[320 * 240];  // Pre-filled pixel buffer
// … populate image[] …

tft.startWrite();
tft.setAddrWindow(0, 0, 320, 240);
tft.pushPixels(image, 320 * 240);
tft.endWrite();

SPI Frequency Configuration

SPI clock speed is the single biggest factor in raw throughput. Set SPI_FREQUENCY in User_Setup.h to the highest stable rate your wiring supports. The read frequency is set separately because most displays require a slower clock for reading back data:
#define SPI_FREQUENCY       40000000   // 40 MHz write (ESP32 effective max ~26 MHz)
#define SPI_READ_FREQUENCY  20000000   // 20 MHz for readPixel() etc.
Practical maximums vary by platform and PCB quality:
  • ESP32 — SPI clock is derived from the 80 MHz APB clock; 40 MHz is the highest setting, but electrical effects often limit the reliable rate to ~26–27 MHz.
  • RP2040 — The PIO-based SPI can run higher; 62.5 MHz has been demonstrated on short PCB traces.
  • STM32 — Depends on the specific variant and APB2 prescaler.

Library Attribute Control

setAttribute() and getAttribute() configure boolean library capabilities at runtime. Three IDs are defined:
void    setAttribute(uint8_t id, uint8_t a);
uint8_t getAttribute(uint8_t id);

#define CP437_SWITCH  1   // CP437 font character error correction
#define UTF8_SWITCH   2   // UTF-8 decoding in print/write stream
#define PSRAM_ENABLE  3   // Allow PSRAM allocation for Sprites (ESP32)
IDConstantEffect when true
1CP437_SWITCHApplies CP437 glyph correction for GLCD font
2UTF8_SWITCHDecodes multi-byte UTF-8 sequences in print()
3PSRAM_ENABLESprite createSprite() may allocate from PSRAM
tft.setAttribute(UTF8_SWITCH, true);    // Enable UTF-8 decoding
tft.setAttribute(PSRAM_ENABLE, true);   // Enable PSRAM for sprites

ESP32 Transfer Buffer Checking

On ESP32, the DMA-backed SPI peripheral uses a set of internal transfer buffers. The spiBusyCheck member controls how many of those buffers are checked for completion before a new transfer begins. In practice this is managed automatically by dmaWait(), but it is exposed for advanced tuning via the SPI_BUSY_CHECK compile-time define in User_Setup.h.

Build docs developers (and LLMs) love