ESP32 - TFT LCD Touch Display SPI

Let's build a project that brings a full-color SPI TFT display to life on an ESP32. The ESP32 is a powerhouse dual-core microcontroller with Wi-Fi, Bluetooth, and plenty of GPIO - and its VSPI hardware SPI bus makes it simple to run a high-speed TFT display at rates the panel is capable of.

What we will build:

This project covers both touch and non-touch SPI TFT LCD displays. It works with 1.3, 1.54, 2.2, 2.4, 2.8, 3.2, and 3.5 inch panels driven by ILI9341, ILI9488, or ST7789 controller chips.

ESP32 TFT SPI Display

Hardware Used In This Tutorial

1×ESP-WROOM-32 Dev Module
1×Alternatively, ESP-WROOM-32 Dev Module - Wide
1×Alternatively, ESP32 Uno-form board
1×Alternatively, ESP32 S3 Uno-form board
1×USB Cable Type-A to Type-C (for USB-A PC)
1×USB Cable Type-C to Type-C (for USB-C PC)
1×SPI TFT display module
1×Jumper Wires
1×Recommended: Screw Terminal Expansion Board for ESP32
1×Recommended: Breakout Expansion Board for ESP32
1×Recommended: Power Splitter for ESP32

Or you can buy the following kits:

1×DIYables ESP32 Starter Kit (ESP32 included)
1×DIYables Sensor Kit (18 sensors/displays)
Disclosure: Some of the links in this section are Amazon affiliate links, meaning we may earn a commission at no additional cost to you if you make a purchase through them. Additionally, some links direct you to products from our own brand, DIYables .

What Is the SPI TFT Display?

An SPI TFT module pairs a color LCD with a driver IC that accepts drawing commands over a 4-wire SPI bus. The library supports three common chips:

  • ILI9341 - 16-bit RGB565 color, up to 40 MHz SPI.
  • ILI9488 - 18-bit RGB666 color over SPI, up to 24 MHz.
  • ST7789 - 16-bit RGB565 color, up to 40 MHz SPI.

Recommendation: If you have not yet purchased a display, we recommend the ST7789 driver. It is widely available, runs at full 40 MHz SPI speed, and is the most straightforward choice for new projects.

Drawing is handled through the Adafruit GFX API, which includes shapes, text, custom fonts, and bitmap output.

Note: The ESP32 uses 3.3V logic. Most SPI TFT modules operate at 3.3V. Verify your module's voltage spec before wiring.

Pinout

Most SPI TFT LCD displays have the following pins:

Display pins:

Pin Function
VCC Power supply
GND Ground
CS Chip Select — pulled low to select the display on the SPI bus
DC / RS Data / Command select — high for pixel data, low for commands
RST Hardware reset — optional; tie to 3.3V if unused
MOSI / SDI / SDA SPI data in (MCU → display)
SCK / CLK SPI clock
MISO / SDO SPI data out (display → MCU) — optional for display-only use
LED / BL / BLK Backlight power — connect to 3.3V or a PWM pin for dimming

SD card pins (if your application needs to access the SD card):

Pin Function
SD_CS / TF_CS SD card Chip Select
MOSI / SDI MOSI — data from MCU to SD card
SCK / CLK SCK — SPI clock
MISO / SDO MISO — data from SD card to MCU

For TFT displays that support touch, there are additional touch pins (if your application uses the touch function and the display supports it):

Pin Function
T_CS Touch controller Chip Select
T_CLK SCK — SPI clock
T_DIN MOSI — data from MCU to touch controller
T_DO MISO — data from touch controller to MCU
T_IRQ Touch interrupt — optional; signals when the screen is being touched

Note: Some non-touch display modules also expose T_CS, T_CLK, T_DIN, T_DO, and T_IRQ pins. These are non-functional on those boards — the touch controller IC is not populated. They appear because the PCB reuses the same layout as the touch-enabled version to reduce manufacturing variants.

TFT SPI Display Pinout

Wiring Diagram

Without Touch

Connect MOSI to GPIO23, SCK to GPIO18, MISO to GPIO19 on the ESP32. CS, DC, and RST can be any available GPIO — GPIO5, GPIO2, GPIO4 are used in the examples.

Display:

TFT Pin ESP32 Pin Description
VCC 3.3V Power supply (3.3V only)
GND GND Ground
CS GPIO5 Chip Select
DC / RS GPIO2 Data / Command select
RST GPIO4 Reset (optional)
MOSI / SDI GPIO23 Hardware SPI MOSI (VSPI)
SCK GPIO18 Hardware SPI clock (VSPI)
MISO / SDO GPIO19 Hardware SPI MISO (VSPI, optional)
LED / BL 3.3V Backlight power

SD card (if your application needs to access the SD card):

SD Pin ESP32 Pin Description
SD_CS / TF_CS any free GPIO SD card Chip Select
MOSI / SDI GPIO23 Shared with display MOSI (GPIO23)
SCK / CLK GPIO18 Shared with display SCK (GPIO18)
MISO / SDO GPIO19 Shared with display MISO (GPIO19)
ESP32 TFT SPI Display wiring diagram without touch

This image is created using Fritzing. Click to enlarge image

Note: The diagram above shows the correct wiring. In practice, connecting two wires into the same ESP32 pin header hole is not easy. A convenient solution is to use a screw terminal block breakout board for ESP32 — two wires can be secured into the same screw terminal, or one wire in the screw and one in the adjacent header pin.

ESP32 TFT SPI Display wiring diagram without touch using screw terminal block

This image is created using Fritzing. Click to enlarge image

With Touch

Connect the XPT2046 touch controller to the ESP32 VSPI bus, sharing GPIO23, GPIO18, and GPIO19 with the display.

Display:

TFT Pin ESP32 Pin Description
VCC 3.3V Power supply (3.3V only)
GND GND Ground
CS GPIO5 Chip Select
DC / RS GPIO2 Data / Command select
RST GPIO4 Reset (optional)
MOSI / SDI GPIO23 Hardware SPI MOSI (VSPI)
SCK GPIO18 Hardware SPI clock (VSPI)
MISO / SDO GPIO19 Hardware SPI MISO (VSPI, optional)
LED / BL 3.3V Backlight power

Touch controller (if your application uses the touch function and the display supports it):

Touch Pin ESP32 Pin Description
T_CS any free GPIO Touch Chip Select
T_IRQ any free GPIO Touch interrupt (optional)
T_DIN GPIO23 Shared with display MOSI (GPIO23)
T_CLK GPIO18 Shared with display SCK (GPIO18)
T_DO GPIO19 Shared with display MISO (GPIO19)
ESP32 TFT SPI Display wiring diagram with touch

This image is created using Fritzing. Click to enlarge image

Note: The diagram above shows the correct wiring. In practice, connecting two wires into the same ESP32 pin header hole is not easy. A convenient solution is to use a screw terminal block breakout board for ESP32 — two wires can be secured into the same screw terminal, or one wire in the screw and one in the adjacent header pin.

ESP32 TFT SPI Display wiring diagram with touch using screw terminal block

This image is created using Fritzing. Click to enlarge image

If your MCU has two or more hardware SPI interfaces, you can assign each peripheral (display, SD card, touch controller) to its own dedicated SPI bus. If your MCU has only one hardware SPI interface, all three peripherals share the same three data lines (MOSI, SCK, MISO) — on the ESP32 these are GPIO23, GPIO18, and GPIO19. Each peripheral has its own CS pin, so only one is active at a time. The DIYables_TFT_SPI library manages both the display and the XPT2046 touch controller through a single API — no separate SPI library is needed for the touch side.

If you're unfamiliar with how to supply power to the ESP32 and other components, you can find guidance in the following tutorial: The best way to Power ESP32 and sensors/displays.

Library Installation

  1. Connect the ESP32 board to your computer through its USB-C port.
  2. Open Arduino IDE. Select your ESP32 board variant from the board menu and pick the correct COM port.
  3. Open the Libraries panel.
  4. Search for "DIYables_TFT_SPI". Locate the DIYables entry.
  5. Click Install and accept all dependency installations.
  • Search for DIYables TFT SPI created by DIYables.io and click the Install button.
Newbiely | Arduino IDE 2.3.8
──
File
Edit
Sketch
Tools
Help
ESP32 Dev Module
Library Manager
Type:
All
Topic:
All
DIYables TFT SPI by DIYables.io
Supports ILI9341 (240x320, 16-bit RGB565), ILI9488 (320x480, 18-bit RGB666), and ST7789 (240x320, 16-bit RGB565) TFT displays over SPI interface. Extends Adafruit GFX for full graphics support. Works with any Arduino-compatible board that has SPI. More info
1.0.0
INSTALL
Newbiely.ino
···
1 void setup() {
Output
Serial Monitor
Ln 1, Col 1
ESP32 Dev Module on COM15
1

Project Foundation

The base sketch for every ESP32 TFT project with this library:

#include <DIYables_TFT_SPI.h> #define TFT_CS_PIN 5 #define TFT_DC_PIN 2 #define TFT_RST_PIN 4 // Uncomment the line that matches your driver chip: DIYables_ILI9341_SPI TFT_display(240, 320, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); // DIYables_ILI9488_SPI TFT_display(320, 480, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); // DIYables_ST7789_SPI TFT_display(240, 320, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); void setup() { TFT_display.begin(); TFT_display.setRotation(1); } void loop() { // drawing code here }

Let's Build - Draw Shapes

The DrawShapes example draws the full range of Adafruit GFX primitives: circles, triangles, rectangles, rounded rectangles, and lines.

/* * This ESP32 code is created by esp32io.com * * This ESP32 code is released in the public domain * * For more detail (instruction and wiring diagram), visit https://esp32io.com/tutorials/esp32-tft-lcd-touch-display-spi */ /* Created by DIYables This example code is in the public domain Product page: https://diyables.io */ // ============================================= // Single include brings in the base class plus all driver classes. // ============================================= #include <DIYables_TFT_SPI.h> // ============================================= // Wiring (ESP32) // ============================================= // TFT pins (always required) // TFT module ESP32 // ------------ --------------------------------- // VCC -> 3.3V (NOT 5V!) // GND -> GND // CS -> GPIO5 (TFT_CS_PIN) // RESET -> GPIO4 (TFT_RST_PIN) // DC / RS -> GPIO2 (TFT_DC_PIN) // SDI / MOSI -> GPIO23 (hardware SPI MOSI / VSPI) // SCK -> GPIO18 (hardware SPI SCK / VSPI) // SDO / MISO -> GPIO19 (only needed when reading from display / VSPI) // LED -> 3.3V (or any GPIO via initBacklight) // ============================================= // ============================================= // SPI pin definitions (adjust for your board) // ============================================= #define TFT_CS_PIN 5 #define TFT_DC_PIN 2 #define TFT_RST_PIN 4 // Panel resolution in native (portrait) orientation - change to match your module #define TFT_WIDTH 240 #define TFT_HEIGHT 320 // MOSI and SCK use default hardware SPI pins // ============================================= // Create display object (uncomment matching driver) // ============================================= // DIYables_ILI9341_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); // DIYables_ILI9488_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); DIYables_ST7789_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); #define BLACK DIYables_TFT_SPI::colorRGB(0, 0, 0) #define BLUE DIYables_TFT_SPI::colorRGB(0, 0, 255) #define RED DIYables_TFT_SPI::colorRGB(255, 0, 0) #define GREEN DIYables_TFT_SPI::colorRGB(0, 255, 0) #define ORANGE DIYables_TFT_SPI::colorRGB(255, 165, 0) #define PINK DIYables_TFT_SPI::colorRGB(255, 192, 203) #define VIOLET DIYables_TFT_SPI::colorRGB(148, 0, 211) #define TURQUOISE DIYables_TFT_SPI::colorRGB(64, 224, 208) #define WHITE DIYables_TFT_SPI::colorRGB(255, 255, 255) // Helper to draw a filled diamond void fillDiamond(int cx, int cy, int h, int v, uint16_t color) { int x0 = cx, y0 = cy - v; int x1 = cx + h, y1 = cy; int x2 = cx, y2 = cy + v; int x3 = cx - h, y3 = cy; TFT_display.fillTriangle(x0, y0, x1, y1, x2, y2, color); TFT_display.fillTriangle(x0, y0, x2, y2, x3, y3, color); } void setup() { TFT_display.begin(); TFT_display.setRotation(1); // Landscape } void loop() { TFT_display.fillScreen(WHITE); uint16_t w = TFT_display.width(); uint16_t h = TFT_display.height(); // Scale positions relative to screen size with better spacing int col1 = w / 8; int col2 = w * 3 / 8; int col3 = w * 5 / 8; int col4 = w * 7 / 8; int row1 = h / 4; int row2 = h / 2; int row3 = h * 3 / 4; // Outlined circle TFT_display.drawCircle(col1, row1, 30, RED); // Filled circle TFT_display.fillCircle(col2, row1, 30, RED); // Outlined triangle TFT_display.drawTriangle(col3 - 30, row1 + 25, col3 + 30, row1 + 25, col3, row1 - 25, BLUE); // Filled triangle TFT_display.fillTriangle(col4 - 30, row1 + 25, col4 + 30, row1 + 25, col4, row1 - 25, GREEN); // Outlined rectangle TFT_display.drawRect(col1 - 35, row2 - 20, 70, 40, ORANGE); // Filled rectangle TFT_display.fillRect(col2 - 35, row2 - 20, 70, 40, TURQUOISE); // Outlined round rectangle TFT_display.drawRoundRect(col3 - 35, row2 - 20, 70, 40, 10, VIOLET); // Filled round rectangle TFT_display.fillRoundRect(col4 - 35, row2 - 20, 70, 40, 10, PINK); // Outlined diamond (centered between col1 and col2) int diamond1_x = (col1 + col2) / 2; TFT_display.drawLine(diamond1_x, row3 - 30, diamond1_x + 25, row3, GREEN); TFT_display.drawLine(diamond1_x + 25, row3, diamond1_x, row3 + 30, GREEN); TFT_display.drawLine(diamond1_x, row3 + 30, diamond1_x - 25, row3, GREEN); TFT_display.drawLine(diamond1_x - 25, row3, diamond1_x, row3 - 30, GREEN); // Filled diamond (centered between col3 and col4) int diamond2_x = (col3 + col4) / 2; fillDiamond(diamond2_x, row3, 25, 30, BLUE); delay(10000); }

Upload and Test

  • Wire the TFT module to the ESP32 using the table above.
  • Plug in the USB-C cable.
  • In Arduino IDE, select the board and port, paste the code, and press Upload.
  • After uploading, the display fills with a loop of colored shapes.

Drawing API Reference

Method Purpose Usage
begin() Initialize the display. TFT_display.begin();
setRotation(r) Set screen orientation 0-3. TFT_display.setRotation(1);
fillScreen(color) Fill entire screen with one color. TFT_display.fillScreen(BLACK);
colorRGB(r,g,b) Build a 16-bit color value. colorRGB(255,128,0)
fillCircle(x,y,r,color) Solid filled circle. TFT_display.fillCircle(120,160,60,RED);
fillRect(x,y,w,h,color) Solid rectangle. TFT_display.fillRect(0,0,100,50,BLUE);
drawLine(x0,y0,x1,y1,color) Straight line between two points. TFT_display.drawLine(0,0,320,240,WHITE);

Let's Build - Show Text and Number

The ShowTextAndNumber example uses the Adafruit GFX text engine to print strings and numeric values at adjustable sizes and colors.

/* * This ESP32 code is created by esp32io.com * * This ESP32 code is released in the public domain * * For more detail (instruction and wiring diagram), visit https://esp32io.com/tutorials/esp32-tft-lcd-touch-display-spi */ /* Created by DIYables This example code is in the public domain Product page: https://diyables.io */ // ============================================= // Single include brings in the base class plus all driver classes. // ============================================= #include <DIYables_TFT_SPI.h> // ============================================= // Wiring (ESP32) // ============================================= // TFT pins (always required) // TFT module ESP32 // ------------ --------------------------------- // VCC -> 3.3V (NOT 5V!) // GND -> GND // CS -> GPIO5 (TFT_CS_PIN) // RESET -> GPIO4 (TFT_RST_PIN) // DC / RS -> GPIO2 (TFT_DC_PIN) // SDI / MOSI -> GPIO23 (hardware SPI MOSI / VSPI) // SCK -> GPIO18 (hardware SPI SCK / VSPI) // SDO / MISO -> GPIO19 (only needed when reading from display / VSPI) // LED -> 3.3V (or any GPIO via initBacklight) // ============================================= // ============================================= // SPI pin definitions (adjust for your board) // ============================================= #define TFT_CS_PIN 5 #define TFT_DC_PIN 2 #define TFT_RST_PIN 4 // Panel resolution in native (portrait) orientation - change to match your module #define TFT_WIDTH 240 #define TFT_HEIGHT 320 // ============================================= // Create display object (uncomment matching driver) // ============================================= // DIYables_ILI9341_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); // DIYables_ILI9488_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); DIYables_ST7789_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); #define MAGENTA DIYables_TFT_SPI::colorRGB(255, 0, 255) #define WHITE DIYables_TFT_SPI::colorRGB(255, 255, 255) void setup() { Serial.begin(9600); Serial.println(F("TFT SPI Display - Show text and numbers")); TFT_display.begin(); TFT_display.setRotation(1); // Landscape TFT_display.fillScreen(WHITE); // Set text color and size TFT_display.setTextColor(MAGENTA); TFT_display.setTextSize(3); // Sample values float temperature = 23.5; float humidity = 78.6; // Display temperature TFT_display.setCursor(20, 20); TFT_display.print("Temperature: "); TFT_display.print(temperature, 1); TFT_display.print(char(247)); TFT_display.println("C"); // Display humidity TFT_display.setCursor(20, 60); TFT_display.print("Humidity: "); TFT_display.print(humidity, 1); TFT_display.print("%"); } void loop() { }

Upload and Test

  • Wire and upload as above.
  • The display shows multiple lines of text and numbers in different colors and sizes.

Drawing API Reference

Method Purpose Usage
setTextColor(color) Sets the text foreground color. TFT_display.setTextColor(WHITE);
setTextSize(size) Scales text. Size 1 = 6×8 px, size 2 = 12×16 px. TFT_display.setTextSize(2);
setCursor(x, y) Places the text cursor at pixel (x, y). TFT_display.setCursor(10, 20);
print(value) Prints a string or number at the cursor. TFT_display.print("ESP32!");
println(value) Prints and advances cursor to the next line. TFT_display.println(42);

Let's Build - Draw Image

Let's build an image viewer. The DrawImage example loads a full-color RGB565 bitmap from the ESP32's program flash and renders it on the display. The pixel data is declared in bitmap.h as a const uint16_t array with PROGMEM. On the ESP32, PROGMEM data is mapped from flash into the virtual address space, so the image draws without consuming any heap RAM.

Copy bitmap.h into the sketch folder before compiling.

/* * This ESP32 code is created by esp32io.com * * This ESP32 code is released in the public domain * * For more detail (instruction and wiring diagram), visit https://esp32io.com/tutorials/esp32-tft-lcd-touch-display-spi */ /* Created by DIYables This example code is in the public domain Product page: https://diyables.io */ // ============================================= // Single include brings in the base class plus all driver classes. // ============================================= #include <DIYables_TFT_SPI.h> #include "bitmap.h" // ============================================= // Wiring (ESP32) // ============================================= // TFT pins (always required) // TFT module ESP32 // ------------ --------------------------------- // VCC -> 3.3V (NOT 5V!) // GND -> GND // CS -> GPIO5 (TFT_CS_PIN) // RESET -> GPIO4 (TFT_RST_PIN) // DC / RS -> GPIO2 (TFT_DC_PIN) // SDI / MOSI -> GPIO23 (hardware SPI MOSI / VSPI) // SCK -> GPIO18 (hardware SPI SCK / VSPI) // SDO / MISO -> GPIO19 (only needed when reading from display / VSPI) // LED -> 3.3V (or any GPIO via initBacklight) // ============================================= // ============================================= // SPI pin definitions (adjust for your board) // ============================================= #define TFT_CS_PIN 5 #define TFT_DC_PIN 2 #define TFT_RST_PIN 4 // Panel resolution in native (portrait) orientation - change to match your module #define TFT_WIDTH 240 #define TFT_HEIGHT 320 // ============================================= // Create display object (uncomment matching driver) // ============================================= // DIYables_ILI9341_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); // DIYables_ILI9488_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); DIYables_ST7789_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); #define WHITE DIYables_TFT_SPI::colorRGB(255, 255, 255) int img_width = 120; int img_height = 53; void setup() { Serial.begin(9600); Serial.println(F("TFT SPI Display - Draw Image")); TFT_display.begin(); uint16_t SCREEN_WIDTH = TFT_display.width(); uint16_t SCREEN_HEIGHT = TFT_display.height(); int x = (SCREEN_WIDTH - img_width) / 2; int y = (SCREEN_HEIGHT - img_height) / 2; TFT_display.fillScreen(WHITE); TFT_display.drawRGBBitmap(x, y, myBitmap, img_width, img_height); } void loop() { delay(2000); TFT_display.invertDisplay(true); delay(2000); TFT_display.invertDisplay(false); }

Upload and Test

  • Place bitmap.h in the same folder as the sketch.
  • Wire the TFT module to the ESP32 as shown above (MOSI=GPIO23, SCK=GPIO18, MISO=GPIO19, CS=GPIO5, DC=GPIO2, RST=GPIO4). Use 3.3V for VCC.
  • Plug in the USB-C cable.
  • In Arduino IDE, select the board and port, press Upload.
  • The display renders the bitmap image from flash.

Drawing API Reference

Method Purpose Usage
drawRGBBitmap(x,y,bitmap,w,h) Draws an RGB565 PROGMEM bitmap with its top-left corner at (x, y). TFT_display.drawRGBBitmap(0, 0, myImage, 240, 320);
fillScreen(color) Clears the display before drawing the image. TFT_display.fillScreen(BLACK);

Let's Build - Draw Image SD Card

Let's build an SD card image viewer. The DrawImageSDcard example reads a raw RGB565 binary image from a micro SD card and streams the pixel data to the display in chunks. The ESP32 handles both the SD card and the display on the same VSPI bus, switching CS lines to arbitrate between them.

Wire the SD module to GPIO23 (MOSI), GPIO18 (SCK), GPIO19 (MISO). Define a separate CS pin for the SD module as SD_CS_PIN in the sketch.

/* * This ESP32 code is created by esp32io.com * * This ESP32 code is released in the public domain * * For more detail (instruction and wiring diagram), visit https://esp32io.com/tutorials/esp32-tft-lcd-touch-display-spi */ /* Created by DIYables This example code is in the public domain Product page: https://diyables.io */ // ============================================= // Single include brings in the base class plus all driver classes. // ============================================= #include <DIYables_TFT_SPI.h> #include <SD.h> // ============================================= // Wiring (ESP32) // ============================================= // TFT + SD module pins // TFT + SD module ESP32 // ----------------------- --------------------------------- // VCC -> 3.3V (NOT 5V!) // GND -> GND // TFT CS -> GPIO5 (TFT_CS_PIN) // TFT RESET -> GPIO4 (TFT_RST_PIN) // TFT DC / RS -> GPIO2 (TFT_DC_PIN) // SD CS -> GPIO14 (SD_CS) // SDI / MOSI (shared) -> GPIO23 (hardware SPI MOSI / VSPI) // SDO / MISO (shared) -> GPIO19 (hardware SPI MISO / VSPI) // SCK (shared) -> GPIO18 (hardware SPI SCK / VSPI) // LED -> 3.3V (or any GPIO via initBacklight) // ============================================= // ============================================= // SPI pin definitions (adjust for your board) // ============================================= #define TFT_CS_PIN 5 #define TFT_DC_PIN 2 #define TFT_RST_PIN 4 // Panel resolution in native (portrait) orientation - change to match your module #define TFT_WIDTH 240 #define TFT_HEIGHT 320 #define SD_CS 14 // SD card chip select (must differ from TFT_CS_PIN) // ============================================= // Create display object (uncomment matching driver) // ============================================= // DIYables_ILI9341_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); // DIYables_ILI9488_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); DIYables_ST7789_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); #define WHITE DIYables_TFT_SPI::colorRGB(255, 255, 255) #define BUFFPIXEL 20 File bmpFile; uint16_t SCREEN_WIDTH; uint16_t SCREEN_HEIGHT; // Helper functions to read BMP file header uint16_t read16(File &f) { uint16_t result; result = f.read(); result |= (f.read() << 8); return result; } uint32_t read32(File &f) { uint32_t result; result = f.read(); result |= ((uint32_t)f.read() << 8); result |= ((uint32_t)f.read() << 16); result |= ((uint32_t)f.read() << 24); return result; } int32_t readS32(File &f) { int32_t result; result = f.read(); result |= ((uint32_t)f.read() << 8); result |= ((uint32_t)f.read() << 16); result |= ((uint32_t)f.read() << 24); return result; } bool getBMPDimensions(const char *filename, uint32_t &w, uint32_t &h) { File f = SD.open(filename); if (!f) return false; if (read16(f) != 0x4D42) { f.close(); return false; } read32(f); // file size read32(f); // reserved read32(f); // image offset read32(f); // DIB header size w = read32(f); int32_t sh = readS32(f); h = (sh < 0) ? -sh : sh; f.close(); return true; } void drawBMP(const char *filename, int x, int y) { bmpFile = SD.open(filename); if (!bmpFile) { Serial.println("File not found"); return; } if (read16(bmpFile) != 0x4D42) { Serial.println("Not a BMP file"); bmpFile.close(); return; } uint32_t fileSize = read32(bmpFile); read32(bmpFile); // Reserved uint32_t imageOffset = read32(bmpFile); uint32_t dibHeaderSize = read32(bmpFile); uint32_t bmpWidth = read32(bmpFile); int32_t bmpHeight = readS32(bmpFile); bool topDown = false; if (bmpHeight < 0) { bmpHeight = -bmpHeight; topDown = true; } if (read16(bmpFile) != 1) { Serial.println("Invalid BMP file"); bmpFile.close(); return; } uint16_t depth = read16(bmpFile); if (depth != 24) { Serial.println("Only 24-bit BMP is supported"); bmpFile.close(); return; } if (read32(bmpFile) != 0) { Serial.println("Unsupported BMP compression"); bmpFile.close(); return; } bmpFile.seek(imageOffset); uint8_t sdbuffer[3 * BUFFPIXEL]; uint16_t color; uint32_t rowSize = (bmpWidth * 3 + 3) & ~3; if (x >= SCREEN_WIDTH || y >= SCREEN_HEIGHT) return; uint32_t maxRow = min((uint32_t)bmpHeight, (uint32_t)(SCREEN_HEIGHT - y)); uint32_t maxCol = min(bmpWidth, (uint32_t)(SCREEN_WIDTH - x)); for (uint32_t row = 0; row < maxRow; row++) { int32_t rowPos = topDown ? row : bmpHeight - 1 - row; uint32_t filePosition = imageOffset + rowPos * rowSize; bmpFile.seek(filePosition); for (uint32_t col = 0; col < maxCol; col += BUFFPIXEL) { uint32_t pixelsToRead = min((uint32_t)BUFFPIXEL, maxCol - col); bmpFile.read(sdbuffer, 3 * pixelsToRead); for (uint32_t i = 0; i < pixelsToRead; i++) { uint8_t b = sdbuffer[i * 3]; uint8_t g = sdbuffer[i * 3 + 1]; uint8_t r = sdbuffer[i * 3 + 2]; color = DIYables_TFT_SPI::colorRGB(r, g, b); if ((x + col + i) < SCREEN_WIDTH && (y + row) < SCREEN_HEIGHT) { TFT_display.drawPixel(x + col + i, y + row, color); } } } } bmpFile.close(); Serial.println("BMP drawn"); } void setup() { Serial.begin(9600); if (!SD.begin(SD_CS)) { Serial.println("SD card initialization failed!"); return; } Serial.println("SD card initialized."); TFT_display.begin(); TFT_display.setRotation(1); // Landscape SCREEN_WIDTH = TFT_display.width(); SCREEN_HEIGHT = TFT_display.height(); TFT_display.fillScreen(WHITE); uint32_t imgWidth, imgHeight; if (getBMPDimensions("diyables.bmp", imgWidth, imgHeight)) { int x = (SCREEN_WIDTH - imgWidth) / 2; int y = (SCREEN_HEIGHT - imgHeight) / 2; drawBMP("diyables.bmp", x, y); } else { Serial.println("Failed to get BMP dimensions"); } } void loop() { }

Upload and Test

  • Wire the SD module to the ESP32 VSPI bus. Share GPIO23/18/19 with the display. Connect SD CS to the pin defined as SD_CS_PIN.
  • Copy a raw RGB565 binary image file to the root of the SD card. Dimensions must match your panel.
  • Plug in the USB-C cable.
  • In Arduino IDE, select board and port, press Upload.
  • The display renders the image streamed from the SD card.

Drawing API Reference

Method Purpose Usage
startWrite() Opens a direct SPI write session and asserts the display CS. TFT_display.startWrite();
setAddrWindow(x0,y0,x1,y1) Defines the rectangular pixel write region on the panel. TFT_display.setAddrWindow(0, 0, 239, 319);
pushColors(buf, len) Sends a buffer of RGB565 pixel values to the display. TFT_display.pushColors(buf, 512);
endWrite() Closes the SPI session and releases the display CS. TFT_display.endWrite();

Let's Build - Use External Font

Let's build a custom text display. The UseExternalFont example replaces the default 5×7 pixel font with a sharper Adafruit GFX-compatible outline font. One call to setFont() is all it takes to upgrade the typography. Calling setFont(NULL) restores the original built-in font at any time.

/* * This ESP32 code is created by esp32io.com * * This ESP32 code is released in the public domain * * For more detail (instruction and wiring diagram), visit https://esp32io.com/tutorials/esp32-tft-lcd-touch-display-spi */ /* Created by DIYables This example code is in the public domain Product page: https://diyables.io */ // ============================================= // Single include brings in the base class plus all driver classes. // ============================================= #include <DIYables_TFT_SPI.h> #include <Fonts/FreeSansBold12pt7b.h> // ============================================= // Wiring (ESP32) // ============================================= // TFT pins (always required) // TFT module ESP32 // ------------ --------------------------------- // VCC -> 3.3V (NOT 5V!) // GND -> GND // CS -> GPIO5 (TFT_CS_PIN) // RESET -> GPIO4 (TFT_RST_PIN) // DC / RS -> GPIO2 (TFT_DC_PIN) // SDI / MOSI -> GPIO23 (hardware SPI MOSI / VSPI) // SCK -> GPIO18 (hardware SPI SCK / VSPI) // SDO / MISO -> GPIO19 (only needed when reading from display / VSPI) // LED -> 3.3V (or any GPIO via initBacklight) // ============================================= // ============================================= // SPI pin definitions (adjust for your board) // ============================================= #define TFT_CS_PIN 5 #define TFT_DC_PIN 2 #define TFT_RST_PIN 4 // Panel resolution in native (portrait) orientation - change to match your module #define TFT_WIDTH 240 #define TFT_HEIGHT 320 // ============================================= // Create display object (uncomment matching driver) // ============================================= // DIYables_ILI9341_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); // DIYables_ILI9488_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); DIYables_ST7789_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); #define MAGENTA DIYables_TFT_SPI::colorRGB(255, 0, 255) #define WHITE DIYables_TFT_SPI::colorRGB(255, 255, 255) void setup() { Serial.begin(9600); Serial.println(F("TFT SPI Display - Use external font")); TFT_display.begin(); TFT_display.setFont(&FreeSansBold12pt7b); TFT_display.setRotation(1); // Landscape TFT_display.fillScreen(WHITE); TFT_display.setTextColor(MAGENTA); TFT_display.setTextSize(1); float temperature = 23.5; float humidity = 78.6; TFT_display.setCursor(20, 30); TFT_display.print("Temperature: "); TFT_display.print(temperature, 1); TFT_display.print(char(247)); TFT_display.println("C"); TFT_display.setCursor(20, 70); TFT_display.print("Humidity: "); TFT_display.print(humidity, 1); TFT_display.print("%"); } void loop() { }

Upload and Test

  • Wire the TFT module to the ESP32 as shown above.
  • Plug in the USB-C cable.
  • In Arduino IDE, select board and port, press Upload.
  • The display renders text in the custom font.

Drawing API Reference

Method Purpose Usage
setFont(&FontName) Activates a custom GFX-compatible font. Pass NULL to restore the built-in 5×7 font. TFT_display.setFont(&FreeSans12pt7b);
setCursor(x, y) Positions the text cursor at the given pixel coordinate. TFT_display.setCursor(10, 40);
setTextColor(color) Sets the foreground color for subsequent text. TFT_display.setTextColor(WHITE);
print(text) Prints a string at the cursor using the active font. TFT_display.print("ESP32 Project");

Let's Build - Touch Get Point

Let's build a touch coordinate reader. The TouchGetPoint example initializes the XPT2046 on the ESP32's VSPI bus and prints raw ADC values to the Serial Monitor whenever the screen is pressed. This is the first step toward a touch-aware project: understanding the raw value range your panel produces.

Wiring (all signals at 3.3V):

Touch Pin ESP32 Pin Description
T_CLK GPIO18 SCK — shared with display
T_DIN GPIO23 MOSI — shared with display
T_DO GPIO19 MISO — shared with display
T_CS GPIO15 Touch Chip Select
T_IRQ GPIO27 Touch interrupt (optional)
/* * This ESP32 code is created by esp32io.com * * This ESP32 code is released in the public domain * * For more detail (instruction and wiring diagram), visit https://esp32io.com/tutorials/esp32-tft-lcd-touch-display-spi */ /* Touch Get Point Example ----------------------- This example demonstrates how to read and display touch coordinates using a DIYables SPI TFT display with a 4-wire resistive touch panel. When you touch the screen, the sketch prints the mapped (screen) X and Y coordinates to the Serial Monitor and draws a red dot at the touched location. NOTE: Run the TouchCalibration example first and paste the calibration values into setTouchCalibration() below if the touch coordinates are inaccurate. Created by DIYables This example code is in the public domain Product page: https://diyables.io */ // ============================================= // Single include brings in the base class plus all driver classes. // ============================================= #include <DIYables_TFT_SPI.h> // ============================================= // Wiring (ESP32) // ============================================= // TFT pins (always required) // TFT module ESP32 // ------------ --------------------------------- // VCC -> 3.3V (NOT 5V!) // GND -> GND // CS -> GPIO5 (TFT_CS_PIN) // RESET -> GPIO4 (TFT_RST_PIN) // DC / RS -> GPIO2 (TFT_DC_PIN) // SDI / MOSI -> GPIO23 (hardware SPI MOSI / VSPI) // SCK -> GPIO18 (hardware SPI SCK / VSPI) // SDO / MISO -> GPIO19 (only needed when reading from display / VSPI) // LED -> 3.3V (or any GPIO via initBacklight) // // XPT2046 / ADS7843 SPI touch controller // (modules with pins: T_CS, T_CLK, T_DIN, T_DO, T_IRQ) // Touch pin ESP32 // ------------ --------------------------------- // T_CS -> GPIO15 (TOUCH_CS_PIN) // T_IRQ -> GPIO27 (TOUCH_IRQ_PIN, optional - use -1 to skip) // T_CLK -> GPIO18 (shared with display SCK / VSPI) // T_DIN -> GPIO23 (shared with display MOSI / VSPI) // T_DO -> GPIO19 (shared with display MISO / VSPI) // ============================================= // ============================================= // SPI pin definitions (adjust for your board) // ============================================= #define TFT_CS_PIN 5 #define TFT_DC_PIN 2 #define TFT_RST_PIN 4 // Panel resolution in native (portrait) orientation - change to match your module #define TFT_WIDTH 240 #define TFT_HEIGHT 320 // MOSI and SCK use default hardware SPI pins // ============================================= // Touch pin definitions (XPT2046 SPI touch controller) // ============================================= #define TOUCH_CS_PIN 15 // T_CS (any GPIO) #define TOUCH_IRQ_PIN 27 // T_IRQ (any GPIO, or -1 if not connected) // ============================================= // ============================================= // Calibration values. // Run the TouchCalibration example and update these if touch is inaccurate. // Typical raw ranges: // - XPT2046 : ~200..3900 (default below) // - 4-wire resistive : ~100..900 // ============================================= #define TOUCH_LEFT_X 300 #define TOUCH_RIGHT_X 3700 #define TOUCH_TOP_Y 300 #define TOUCH_BOT_Y 3700 // ============================================= // Create display object (uncomment matching driver) // ============================================= // DIYables_ILI9341_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); // DIYables_ILI9488_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); DIYables_ST7789_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); #define RED DIYables_TFT_SPI::colorRGB(255, 0, 0) #define WHITE DIYables_TFT_SPI::colorRGB(255, 255, 255) void setup() { Serial.begin(9600); TFT_display.begin(); TFT_display.setRotation(0); TFT_display.fillScreen(WHITE); TFT_display.initTouchSPI(TOUCH_CS_PIN, TOUCH_IRQ_PIN); TFT_display.setTouchCalibration(TOUCH_LEFT_X, TOUCH_RIGHT_X, TOUCH_TOP_Y, TOUCH_BOT_Y); Serial.println("Touch the screen to see coordinates."); } void loop() { int x, y; if (TFT_display.getTouch(x, y)) { Serial.print("Touch at: "); Serial.print(x); Serial.print(", "); Serial.println(y); TFT_display.fillCircle(x, y, 4, RED); delay(200); } }

Upload and Test

  • Wire the XPT2046 to the ESP32 VSPI bus, sharing GPIO23/18/19 with the display. T_CS→GPIO15, T_IRQ→GPIO27.
  • Plug in the USB-C cable.
  • In Arduino IDE, select board and port, press Upload.
  • Open the Serial Monitor at 9600 baud. Touch the display to see raw X, Y, and Z values printed.

Drawing API Reference

Method Purpose Usage
initTouchSPI(cs, irq) Initializes the XPT2046 on the shared SPI bus. Pass -1 for irq if the interrupt pin is not connected. TFT_display.initTouchSPI(14, 27);
readTouchRaw(x, y, z) Returns raw ADC values from the controller with no calibration applied. Returns true when pressed. TFT_display.readTouchRaw(x, y, z);

Let's Build - Touch Draw

Let's build a finger-painting app. The TouchDraw example combines the XPT2046 touch controller with the display's drawing API. Each touch position maps to a calibrated pixel coordinate and a small filled circle is drawn there, creating a stroke as the finger moves.

/* * This ESP32 code is created by esp32io.com * * This ESP32 code is released in the public domain * * For more detail (instruction and wiring diagram), visit https://esp32io.com/tutorials/esp32-tft-lcd-touch-display-spi */ /* Touch Draw Lines Example ------------------------- Draws lines on the screen following the pen. - Touch and drag on the screen to draw. - Lift the pen to stop drawing. - Touch again to start a new line from the last point. NOTE: Run the TouchCalibration example and update setTouchCalibration() below if the touch coordinates are inaccurate. Created by DIYables This example code is in the public domain Product page: https://diyables.io */ // ============================================= // Single include brings in the base class plus all driver classes. // ============================================= #include <DIYables_TFT_SPI.h> // ============================================= // Wiring (ESP32) // ============================================= // TFT pins (always required) // TFT module ESP32 // ------------ --------------------------------- // VCC -> 3.3V (NOT 5V!) // GND -> GND // CS -> GPIO5 (TFT_CS_PIN) // RESET -> GPIO4 (TFT_RST_PIN) // DC / RS -> GPIO2 (TFT_DC_PIN) // SDI / MOSI -> GPIO23 (hardware SPI MOSI / VSPI) // SCK -> GPIO18 (hardware SPI SCK / VSPI) // SDO / MISO -> GPIO19 (only needed when reading from display / VSPI) // LED -> 3.3V (or any GPIO via initBacklight) // // XPT2046 / ADS7843 SPI touch controller // (modules with pins: T_CS, T_CLK, T_DIN, T_DO, T_IRQ) // Touch pin ESP32 // ------------ --------------------------------- // T_CS -> GPIO15 (TOUCH_CS_PIN) // T_IRQ -> GPIO27 (TOUCH_IRQ_PIN, optional - use -1 to skip) // T_CLK -> GPIO18 (shared with display SCK / VSPI) // T_DIN -> GPIO23 (shared with display MOSI / VSPI) // T_DO -> GPIO19 (shared with display MISO / VSPI) // ============================================= // ============================================= // SPI pin definitions (adjust for your board) // ============================================= #define TFT_CS_PIN 5 #define TFT_DC_PIN 2 #define TFT_RST_PIN 4 // Panel resolution in native (portrait) orientation - change to match your module #define TFT_WIDTH 240 #define TFT_HEIGHT 320 // ============================================= // Touch pin definitions (XPT2046 SPI touch controller) // ============================================= #define TOUCH_CS_PIN 15 // T_CS (any GPIO) #define TOUCH_IRQ_PIN 27 // T_IRQ (any GPIO, or -1 if not connected) // ============================================= // ============================================= // Calibration values. // Run the TouchCalibration example and update these if touch is inaccurate. // Typical raw ranges: // - XPT2046 : ~200..3900 (default below) // - 4-wire resistive : ~100..900 // ============================================= #define TOUCH_LEFT_X 300 #define TOUCH_RIGHT_X 3700 #define TOUCH_TOP_Y 300 #define TOUCH_BOT_Y 3700 // ============================================= // Create display object (uncomment matching driver) // ============================================= // DIYables_ILI9341_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); // DIYables_ILI9488_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); DIYables_ST7789_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); #define RED DIYables_TFT_SPI::colorRGB(255, 0, 0) #define WHITE DIYables_TFT_SPI::colorRGB(255, 255, 255) #define PEN_RADIUS 3 void setup() { TFT_display.begin(); TFT_display.setRotation(0); TFT_display.fillScreen(WHITE); TFT_display.initTouchSPI(TOUCH_CS_PIN, TOUCH_IRQ_PIN); TFT_display.setTouchCalibration(TOUCH_LEFT_X, TOUCH_RIGHT_X, TOUCH_TOP_Y, TOUCH_BOT_Y); } void loop() { int x, y; if (TFT_display.getTouch(x, y)) { TFT_display.fillCircle(x, y, PEN_RADIUS, RED); } }

Upload and Test

  • Wire the XPT2046 to the ESP32 as described in the Touch Get Point section above.
  • Plug in the USB-C cable.
  • In Arduino IDE, select board and port, press Upload.
  • Drag a finger across the display to paint on screen.

Drawing API Reference

Method Purpose Usage
initTouchSPI(cs, irq) Initializes the XPT2046 on the shared VSPI bus. TFT_display.initTouchSPI(14, 27);
setTouchCalibration(minX,maxX,minY,maxY) Maps raw ADC values to screen pixel coordinates. Get the four values from the TouchCalibration example. TFT_display.setTouchCalibration(200, 3800, 300, 3700);
getTouch(x, y) Returns calibrated touch coordinates in screen pixels. Returns true while the screen is pressed. if (TFT_display.getTouch(x, y)) { ... }
fillCircle(x, y, r, color) Draws a small dot at the touch position to build up the painting. TFT_display.fillCircle(x, y, 3, RED);

Let's Build - Touch Button

Let's build a touch button panel. The TouchButton example draws rectangular buttons as colored regions on the display, then polls for touch coordinates on each loop iteration. When a touch point lands inside a button's bounds, the button highlights and the action fires. This is the core pattern for any ESP32 touch UI.

T_IRQ is optional. Pass -1 as the irq argument to use polling mode without the interrupt pin.

/* * This ESP32 code is created by esp32io.com * * This ESP32 code is released in the public domain * * For more detail (instruction and wiring diagram), visit https://esp32io.com/tutorials/esp32-tft-lcd-touch-display-spi */ /* Touch Button Press/Release Example ------------------------------------ This example shows how to detect press and release events on a rectangular button using a DIYables SPI TFT display with a 4-wire resistive touch panel. When you touch inside the button, it changes colour and shows "PRESSED". When you release, it returns to its original state. NOTE: Run the TouchCalibration example and update setTouchCalibration() below if the touch coordinates are inaccurate. Created by DIYables This example code is in the public domain Product page: https://diyables.io */ // ============================================= // Single include brings in the base class plus all driver classes. // ============================================= #include <DIYables_TFT_SPI.h> // ============================================= // Wiring (ESP32) // ============================================= // TFT pins (always required) // TFT module ESP32 // ------------ --------------------------------- // VCC -> 3.3V (NOT 5V!) // GND -> GND // CS -> GPIO5 (TFT_CS_PIN) // RESET -> GPIO4 (TFT_RST_PIN) // DC / RS -> GPIO2 (TFT_DC_PIN) // SDI / MOSI -> GPIO23 (hardware SPI MOSI / VSPI) // SCK -> GPIO18 (hardware SPI SCK / VSPI) // SDO / MISO -> GPIO19 (only needed when reading from display / VSPI) // LED -> 3.3V (or any GPIO via initBacklight) // // XPT2046 / ADS7843 SPI touch controller // (modules with pins: T_CS, T_CLK, T_DIN, T_DO, T_IRQ) // Touch pin ESP32 // ------------ --------------------------------- // T_CS -> GPIO15 (TOUCH_CS_PIN) // T_IRQ -> -1 (TOUCH_IRQ_PIN, optional - use -1 to skip) // T_CLK -> GPIO18 (shared with display SCK / VSPI) // T_DIN -> GPIO23 (shared with display MOSI / VSPI) // T_DO -> GPIO19 (shared with display MISO / VSPI) // ============================================= // ============================================= // SPI pin definitions (adjust for your board) // ============================================= #define TFT_CS_PIN 5 #define TFT_DC_PIN 2 #define TFT_RST_PIN 4 // Panel resolution in native (portrait) orientation - change to match your module #define TFT_WIDTH 240 #define TFT_HEIGHT 320 // ============================================= // Touch pin definitions (XPT2046 SPI touch controller) // ============================================= #define TOUCH_CS_PIN 15 // T_CS (any GPIO) #define TOUCH_IRQ_PIN -1 // T_IRQ (any GPIO, or -1 if not connected) // ============================================= // ============================================= // Calibration values. // Run the TouchCalibration example and update these if touch is inaccurate. // Typical raw ranges: // - XPT2046 : ~200..3900 (default below) // - 4-wire resistive : ~100..900 // ============================================= #define TOUCH_LEFT_X 300 #define TOUCH_RIGHT_X 3700 #define TOUCH_TOP_Y 300 #define TOUCH_BOT_Y 3700 // ============================================= // Create display object (uncomment matching driver) // ============================================= // DIYables_ILI9341_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); // DIYables_ILI9488_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); DIYables_ST7789_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); #define BLACK DIYables_TFT_SPI::colorRGB( 0, 0, 0) #define WHITE DIYables_TFT_SPI::colorRGB(255, 255, 255) #define GRAY DIYables_TFT_SPI::colorRGB(128, 128, 128) #define RED DIYables_TFT_SPI::colorRGB(255, 0, 0) #define BUTTON_X 30 #define BUTTON_Y 100 #define BUTTON_W 180 #define BUTTON_H 60 #define DEBOUNCE_DELAY 50 bool lastPressed = false; bool stablePressed = false; unsigned long lastDebounceTime = 0; void drawButton(bool pressed) { uint16_t bg = pressed ? GRAY : RED; TFT_display.fillRect(BUTTON_X, BUTTON_Y, BUTTON_W, BUTTON_H, bg); TFT_display.drawRect(BUTTON_X, BUTTON_Y, BUTTON_W, BUTTON_H, BLACK); TFT_display.setTextColor(WHITE, bg); TFT_display.setTextSize(3); TFT_display.setCursor(BUTTON_X + 10, BUTTON_Y + 16); TFT_display.print(pressed ? "PRESSED" : " PRESS "); } void setup() { Serial.begin(9600); TFT_display.begin(); TFT_display.setRotation(0); TFT_display.fillScreen(WHITE); TFT_display.initTouchSPI(TOUCH_CS_PIN, TOUCH_IRQ_PIN); TFT_display.setTouchCalibration(TOUCH_LEFT_X, TOUCH_RIGHT_X, TOUCH_TOP_Y, TOUCH_BOT_Y); drawButton(false); } void loop() { int x, y; bool pressed = false; if (TFT_display.getTouch(x, y)) { if (x >= BUTTON_X && x < (BUTTON_X + BUTTON_W) && y >= BUTTON_Y && y < (BUTTON_Y + BUTTON_H)) { pressed = true; } } if (pressed != lastPressed) { lastDebounceTime = millis(); } lastPressed = pressed; if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) { if (pressed != stablePressed) { stablePressed = pressed; drawButton(stablePressed); Serial.println(stablePressed ? "Button PRESSED" : "Button RELEASED"); } } }

Upload and Test

  • Connect T_CS to GPIO15. T_IRQ can be left unconnected.
  • Plug in the USB-C cable.
  • In Arduino IDE, select board and port, press Upload.
  • Tap each button on the display and confirm it highlights and triggers its action.

Drawing API Reference

Method Purpose Usage
initTouchSPI(cs, irq) Initializes the XPT2046. Pass -1 for irq to disable the interrupt pin. TFT_display.initTouchSPI(14, -1);
setTouchCalibration(minX,maxX,minY,maxY) Applies calibration so getTouch() returns accurate pixel coordinates. TFT_display.setTouchCalibration(200, 3800, 300, 3700);
getTouch(x, y) Gets the current calibrated touch position. Returns true while pressed. if (TFT_display.getTouch(x, y)) { ... }
fillRect(x, y, w, h, color) Draws a button as a solid filled rectangle. TFT_display.fillRect(10, 10, 120, 60, BLUE);

Let's Build - Touch Calibration

Let's build a calibration tool. The TouchCalibration example guides you through measuring the raw ADC extremes of your XPT2046 panel. Touch each corner of the display as prompted and record the printed minimum and maximum X and Y values. Those four numbers are the constants for setTouchCalibration() in all other touch projects.

/* * This ESP32 code is created by esp32io.com * * This ESP32 code is released in the public domain * * For more detail (instruction and wiring diagram), visit https://esp32io.com/tutorials/esp32-tft-lcd-touch-display-spi */ /* Touch Screen Calibration Example --------------------------------- This example measures the raw touch coordinates at all four screen corners and prints ready-to-use calibration values to the Serial Monitor. It uses readTouchRaw() directly â€" it does NOT rely on getTouch() or any existing calibration values, so it works even when touch is completely broken. INSTRUCTIONS: 1. Upload this sketch to your board. 2. Open the Serial Monitor (Ctrl+Shift+M) and set baud rate to 9600. 3. The screen shows a blinking red dot in each corner, numbered 1â€"4: 1 = Top-left 2 = Top-right 3 = Bottom-right 4 = Bottom-left 4. Press and HOLD firmly on the blinking dot. Keep holding until the Serial Monitor prints "Captured!" for that corner. 5. Release, then wait for the next dot to appear and repeat. 6. After all 4 corners, the Serial Monitor prints the calibration values and a ready-to-use setTouchCalibration() call. Copy it into your sketch. NOTE: While waiting, the Serial Monitor continuously prints the live raw Z/X/Y readings so you can confirm that touch is being detected. Created by DIYables This example code is in the public domain Product page: https://diyables.io */ // ============================================= // Single include brings in the base class plus all driver classes. // ============================================= #include <DIYables_TFT_SPI.h> // ============================================= // Wiring (ESP32) // ============================================= // TFT pins (always required) // TFT module ESP32 // ------------ --------------------------------- // VCC -> 3.3V (NOT 5V!) // GND -> GND // CS -> GPIO5 (TFT_CS_PIN) // RESET -> GPIO4 (TFT_RST_PIN) // DC / RS -> GPIO2 (TFT_DC_PIN) // SDI / MOSI -> GPIO23 (hardware SPI MOSI / VSPI) // SCK -> GPIO18 (hardware SPI SCK / VSPI) // SDO / MISO -> GPIO19 (only needed when reading from display / VSPI) // LED -> 3.3V (or any GPIO via initBacklight) // // XPT2046 / ADS7843 SPI touch controller // (modules with pins: T_CS, T_CLK, T_DIN, T_DO, T_IRQ) // Touch pin ESP32 // ------------ --------------------------------- // T_CS -> GPIO15 (TOUCH_CS_PIN) // T_IRQ -> GPIO27 (TOUCH_IRQ_PIN, optional - use -1 to skip) // T_CLK -> GPIO18 (shared with display SCK / VSPI) // T_DIN -> GPIO23 (shared with display MOSI / VSPI) // T_DO -> GPIO19 (shared with display MISO / VSPI) // ============================================= // ============================================= // SPI pin definitions (adjust for your board) // ============================================= #define TFT_CS_PIN 5 #define TFT_DC_PIN 2 #define TFT_RST_PIN 4 // Panel resolution in native (portrait) orientation - change to match your module #define TFT_WIDTH 240 #define TFT_HEIGHT 320 // ============================================= // Touch pin definitions (XPT2046 SPI touch controller) // ============================================= #define TOUCH_CS_PIN 15 // T_CS (any GPIO) #define TOUCH_IRQ_PIN 27 // T_IRQ (any GPIO, or -1 if not connected) // ============================================= // ============================================= // Create display object (uncomment matching driver) // ============================================= // DIYables_ILI9341_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); // DIYables_ILI9488_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); DIYables_ST7789_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); // Minimum pressure to count as a valid touch. #define TOUCH_Z_MIN 10 // How many consecutive valid samples required before a corner is accepted. #define SAMPLES_NEEDED 10 // Delay between samples (ms). #define SAMPLE_DELAY_MS 30 #define DOT_RADIUS 12 #define BLACK DIYables_TFT_SPI::colorRGB( 0, 0, 0) #define WHITE DIYables_TFT_SPI::colorRGB(255, 255, 255) #define RED DIYables_TFT_SPI::colorRGB(255, 0, 0) // Corner pixel positions â€" filled in setup() once display size is known. // Order: 0=top-left, 1=top-right, 2=bottom-right, 3=bottom-left int cx[4], cy[4]; // Captured averaged raw values per corner. int cap_x[4], cap_y[4]; // ----------------------------------------------------------------------- void drawDot(int corner, bool on) { uint16_t color = on ? RED : WHITE; TFT_display.fillCircle(cx[corner], cy[corner], DOT_RADIUS, color); TFT_display.setTextSize(2); TFT_display.setTextColor(BLACK, color); TFT_display.setCursor(cx[corner] - 6, cy[corner] - 8); TFT_display.print(corner + 1); } void captureCorner(int corner) { const char* names[] = { "Top-left", "Top-right", "Bottom-right", "Bottom-left" }; Serial.println(); Serial.print("Corner "); Serial.print(corner + 1); Serial.print(" ("); Serial.print(names[corner]); Serial.println(")"); Serial.println(" Press and HOLD firmly on the blinking dot."); Serial.println(" Keep holding until you see 'Captured!'"); unsigned long lastBlink = 0; unsigned long lastPrint = 0; bool dotOn = false; int goodSamples = 0; long sumX = 0, sumY = 0; while (true) { // Blink the dot if (millis() - lastBlink > 400) { lastBlink = millis(); dotOn = !dotOn; drawDot(corner, dotOn); } int raw_x, raw_y, z; TFT_display.readTouchRaw(raw_x, raw_y, z); // Print live readings every 500 ms if (millis() - lastPrint > 500) { lastPrint = millis(); Serial.print(" Z="); Serial.print(z); Serial.print(" X="); Serial.print(raw_x); Serial.print(" Y="); Serial.println(raw_y); } if (z >= TOUCH_Z_MIN) { sumX += raw_x; sumY += raw_y; goodSamples++; if (goodSamples >= SAMPLES_NEEDED) { cap_x[corner] = sumX / goodSamples; cap_y[corner] = sumY / goodSamples; Serial.print(" Captured! raw_x="); Serial.print(cap_x[corner]); Serial.print(" raw_y="); Serial.println(cap_y[corner]); drawDot(corner, false); delay(500); return; } } else { goodSamples = 0; sumX = 0; sumY = 0; } delay(SAMPLE_DELAY_MS); } } // ----------------------------------------------------------------------- void setup() { Serial.begin(9600); TFT_display.begin(); TFT_display.setRotation(0); // Always calibrate in rotation 0 TFT_display.fillScreen(WHITE); TFT_display.initTouchSPI(TOUCH_CS_PIN, TOUCH_IRQ_PIN); int w = TFT_display.width(); int h = TFT_display.height(); int m = DOT_RADIUS + 4; cx[0] = m; cy[0] = m; cx[1] = w - m; cy[1] = m; cx[2] = w - m; cy[2] = h - m; cx[3] = m; cy[3] = h - m; Serial.println("=== Touch Calibration ==="); for (int i = 0; i < 4; i++) { captureCorner(i); } // Derive calibration values from the four corners int min_x = (cap_x[0] + cap_x[3]) / 2; // left edge int max_x = (cap_x[1] + cap_x[2]) / 2; // right edge int min_y = (cap_y[0] + cap_y[1]) / 2; // top edge int max_y = (cap_y[2] + cap_y[3]) / 2; // bottom edge Serial.println(); Serial.println("=== Calibration Results ==="); Serial.print(" Left X (min_x): "); Serial.println(min_x); Serial.print(" Right X (max_x): "); Serial.println(max_x); Serial.print(" Top Y (min_y): "); Serial.println(min_y); Serial.print(" Bot Y (max_y): "); Serial.println(max_y); Serial.println(); Serial.println("Copy this line into your sketch:"); Serial.print(" TFT_display.setTouchCalibration("); Serial.print(min_x); Serial.print(", "); Serial.print(max_x); Serial.print(", "); Serial.print(min_y); Serial.print(", "); Serial.print(max_y); Serial.println(");"); TFT_display.fillScreen(WHITE); TFT_display.setTextColor(BLACK); TFT_display.setTextSize(2); TFT_display.setCursor(10, 10); TFT_display.println("Done! Check"); TFT_display.setCursor(10, 35); TFT_display.println("Serial Monitor"); } void loop() {}

Upload and Test

  • Wire the XPT2046 to the ESP32 as described in the Touch Get Point section.
  • Plug in the USB-C cable.
  • In Arduino IDE, select board and port, press Upload.
  • Open the Serial Monitor at 9600 baud. Follow the on-screen prompts and touch each corner.
  • Record the four printed values and use them in setTouchCalibration() in your other touch projects.

Drawing API Reference

Method Purpose Usage
initTouchSPI(cs, irq) Initializes the XPT2046 touch controller. TFT_display.initTouchSPI(14, 27);
readTouchRaw(x, y, z) Reads raw ADC values to determine the calibration range. TFT_display.readTouchRaw(x, y, z);
setTouchCalibration(minX,maxX,minY,maxY) Stores calibration constants so getTouch() maps raw values to pixel coordinates correctly. TFT_display.setTouchCalibration(200, 3800, 300, 3700);

Let's Build - Custom SPI

Let's build on a custom SPI bus. The ESP32 has two hardware SPI controllers: VSPI and HSPI. The CustomSPI example shows how to create an SPIClass instance for the HSPI bus and pass it to the display constructor. This frees the VSPI bus for other peripherals such as SD cards or sensor modules that share SPI.

/* * This ESP32 code is created by esp32io.com * * This ESP32 code is released in the public domain * * For more detail (instruction and wiring diagram), visit https://esp32io.com/tutorials/esp32-tft-lcd-touch-display-spi */ /* Created by DIYables This example code is in the public domain Product page: https://diyables.io This example demonstrates how to use a custom (non-default) SPI bus with the DIYables TFT SPI library. This is useful on boards that have multiple SPI interfaces, such as: - ESP32: HSPI / VSPI - Arduino Giga / Portenta: SPI1 - Raspberry Pi Pico: SPI1 */ // ============================================= // Single include brings in the base class plus all driver classes. // ============================================= #include <DIYables_TFT_SPI.h> // ============================================= // Wiring (ESP32 - VSPI by default) // ============================================= // TFT pins using VSPI (default SPI on ESP32) // TFT module ESP32 VSPI // ------------ --------------------------------- // VCC -> 3.3V (NOT 5V!) // GND -> GND // CS -> GPIO5 (TFT_CS_PIN) // RESET -> GPIO4 (TFT_RST_PIN) // DC / RS -> GPIO2 (TFT_DC_PIN) // SDI / MOSI -> GPIO23 (VSPI MOSI) // SCK -> GPIO18 (VSPI SCK) // SDO / MISO -> GPIO19 (VSPI MISO) // LED -> 3.3V (or any GPIO via initBacklight) // // To use HSPI instead, uncomment the HSPI section below and wire: // MOSI -> GPIO13, SCK -> GPIO14, MISO -> GPIO12 // ============================================= // ============================================= // SPI pin definitions (adjust for your board) // ============================================= #define TFT_CS_PIN 5 #define TFT_DC_PIN 2 #define TFT_RST_PIN 4 // Panel resolution in native (portrait) orientation - change to match your module #define TFT_WIDTH 240 #define TFT_HEIGHT 320 // ============================================= // Select alternate SPI bus (uncomment for your board) // ============================================= // --- ESP32: use HSPI --- // SPIClass hspi(HSPI); // #define MY_SPI &hspi // --- ESP32: use VSPI (default) --- #define MY_SPI &SPI // --- Arduino Giga / Portenta / RP2040: use SPI1 --- // #define MY_SPI &SPI1 // ============================================= // Create display object with custom SPI bus // (uncomment matching driver) // ============================================= DIYables_ILI9341_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN, MY_SPI); // DIYables_ILI9488_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN, MY_SPI); // DIYables_ST7789_SPI TFT_display(TFT_WIDTH, TFT_HEIGHT, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN, MY_SPI); #define BLACK DIYables_TFT_SPI::colorRGB(0, 0, 0) #define WHITE DIYables_TFT_SPI::colorRGB(255, 255, 255) #define RED DIYables_TFT_SPI::colorRGB(255, 0, 0) #define GREEN DIYables_TFT_SPI::colorRGB(0, 255, 0) #define BLUE DIYables_TFT_SPI::colorRGB(0, 0, 255) void setup() { Serial.begin(9600); // For ESP32 HSPI: optionally remap pins before begin() // hspi.begin(14, 12, 13, -1); // SCK, MISO, MOSI, SS TFT_display.begin(); TFT_display.setRotation(1); // Landscape TFT_display.fillScreen(BLACK); uint16_t w = TFT_display.width(); uint16_t h = TFT_display.height(); // Draw a simple test pattern TFT_display.fillRect(0, 0, w / 3, h, RED); TFT_display.fillRect(w / 3, 0, w / 3, h, GREEN); TFT_display.fillRect(w * 2 / 3, 0, w / 3, h, BLUE); TFT_display.setTextColor(WHITE); TFT_display.setTextSize(2); TFT_display.setCursor(10, h / 2 - 10); TFT_display.print("Custom SPI bus OK"); } void loop() { // Nothing to do }

Upload and Test

  • Wire the TFT display to the ESP32 HSPI pins or the custom SPI bus you define in the sketch.
  • Plug in the USB-C cable.
  • In Arduino IDE, select board and port, press Upload.
  • The display starts on the selected SPI bus and shows a color-bar pattern to confirm success.

Drawing API Reference

Method Purpose Usage
SPIClass myBus(HSPI) Create an SPIClass instance bound to the HSPI controller (GPIO14/12/13/15). SPIClass HSPI_bus(HSPI);
DIYables_ILI9341_SPI(w,h,cs,dc,rst,spi) Constructor accepting a pointer to any SPIClass. Defaults to &SPI (VSPI) when omitted. DIYables_ILI9341_SPI tft(240, 320, 5, 2, 4, &HSPI_bus);
begin() Initializes the display on the configured SPI bus. TFT_display.begin();

Troubleshoot

Issue Cause Fix
Blank screen Wrong SPI pins or wrong power Use VSPI pins GPIO23/18/19; check VCC is 3.3V
Garbled image Wrong driver active Uncomment only the matching constructor
Image is offset Constructor width/height mismatch Match values to your actual panel dimensions
No touch response Missing calibration data Run TouchCalibration example and copy the printed values
Port not visible USB driver missing Install CP210x or CH340 USB driver for your board

Platform Support

The library is built on Arduino's standard SPI API and supports all Arduino-compatible ESP32 variants (architectures=*).

※ OUR MESSAGES