How to Build the Tellaprompter with an ESP32 and ILI9341 Touchscreen

A compact touchscreen teleprompter you can mount above your screen for cleaner video recording

ESP32CNC & MakingIntermediate1.5 hours2 components

Updated

How to Build the Tellaprompter with an ESP32 and ILI9341 Touchscreen
For illustrative purposes only
On this page

What you'll build

Build a compact touchscreen teleprompter for recording videos without looking away from your camera. An ESP32 drives the display, scrolls your script up the screen, and uses a physical push button to pause or resume while you record.

The touchscreen is part of the control surface too. Tap the left half of the screen to slow the scroll down, tap the right half to speed it up, and tap near the top to restart the script from the beginning. This guide is based on Louise de Sadeleer and Sam Beek's Tellaprompter build video, with a starter project that gives you the working electronics and firmware setup.

What you are building

The starter firmware has four jobs:

  1. initialise the ILI9341 TFT display over SPI,
  2. read the XPT2046 touchscreen on the same SPI bus,
  3. render wrapped script lines in a smooth upward scroll,
  4. let you pause, resume, restart, slow down, and speed up without reflashing.

The sketch stores the script in a const char* script[] array. That keeps the first version simple and repeatable: edit the script text, deploy again, then use the button and touch controls while recording.

Upload and calibrate

Open the Tellaprompter starter in Schematik and deploy it to the ESP32. Schematik installs the listed libraries for the compile:

  • Adafruit GFX Library
  • Adafruit ILI9341
  • XPT2046_Touchscreen

After flashing, the screen should clear to black and start drawing the script. The button on GPIO21 pauses and resumes the scroll. If the text moves but touch feels backwards or offset, adjust the four calibration constants near the top of the sketch:

#define TOUCH_X_MIN 300
#define TOUCH_X_MAX 3800
#define TOUCH_Y_MIN 300
#define TOUCH_Y_MAX 3800

Controls

  • Push button on GPIO21: pause or resume scrolling.
  • Touch the left half of the screen: slow the scroll down.
  • Touch the right half of the screen: speed the scroll up.
  • Touch the top strip: restart from the first script line.

The scroll speed is clamped between the minimum and maximum values in the sketch, so repeated taps will not push it into unusable values.

Edit the script

The starter keeps the script in the firmware:

const char* script[] = {
  "Welcome to the",
  "Teleprompter!",
  "",
  "This text will",
  "scroll up the",
  "screen."
};

Keep each line short. The starter is tuned for a 240 × 320 portrait display with TEXT_SIZE set to 2, so around 20 characters per line is a safe limit.

Troubleshooting

  • The display stays white or black. Recheck VCC, GND, TFT_CS, TFT_DC, TFT_RST, and SCK/MOSI. Most ILI9341 problems are one swapped signal wire.
  • The backlight is off. Check TFT_BL on GPIO16. Some modules also let you tie backlight directly to 3V3 if you do not need software control.
  • Touch does nothing. Recheck MISO, TOUCH_CS, TOUCH_IRQ, and shared ground. The display can work even when the touch controller is wired wrong.
  • Touch directions feel wrong. Tune the TOUCH_X_MIN, TOUCH_X_MAX, TOUCH_Y_MIN, and TOUCH_Y_MAX constants.
  • The button does not pause. Confirm one side of the button goes to GPIO21 and the other side goes to GND. The sketch uses the ESP32's internal pull-up.
  • Text wraps badly. Shorten the strings in the script[] array or reduce TEXT_SIZE.

Going further

Once the basic build works, move it off the breadboard into a simple printed holder or monitor clip. Keep the electronics unchanged first. The useful core is already there: readable script text, speed control, restart, and a real pause button you can hit without looking down.

Wiring diagram

Loading diagram…
Interactive wiring diagram

Components needed

ComponentTypeQtyBuy
ILI9341 TFT Touchscreendisplay1
Push Buttonother1

Assembly

1

Prepare (everything unpowered)

Unplug USB or battery. Clear the bench and keep the Pins tab open — every connection below follows that table. Board: ESP32 DevKit v1.

2

Wire ILI9341 TFT Touchscreen

Wire ILI9341 TFT Touchscreen (ili9341-tft-touchscreen_0) like this: ILI9341 TFT Touchscreen · VCC → 3V3 [power] ILI9341 TFT Touchscreen · GND → GND [ground] ILI9341 TFT Touchscreen · MOSI → GPIO23 [spi] ILI9341 TFT Touchscreen · MISO → GPIO19 [spi] ILI9341 TFT Touchscreen · SCK → GPIO18 [spi] ILI9341 TFT Touchscreen · TFT_CS → GPIO4 [digital] ILI9341 TFT Touchscreen · TFT_DC → GPIO27 [digital] ILI9341 TFT Touchscreen · TFT_RST → GPIO32 [digital] ILI9341 TFT Touchscreen · TOUCH_CS → GPIO13 [digital] ILI9341 TFT Touchscreen · TOUCH_IRQ → GPIO14 [digital] ILI9341 TFT Touchscreen · TFT_BL → GPIO16 [digital] Give each jumper a light tug so loose Dupont connectors show up now, not after flashing.

3

Wire Push Button

Wire Push Button (button_1) like this: Push Button · GND → GND [ground] Push Button · SIGNAL → GPIO21 [digital] Give each jumper a light tug so loose Dupont connectors show up now, not after flashing.

4

Flash firmware with Schematik Deploy

Connect the board by USB. In Schematik, open Deploy and click Deploy to compile and flash the sketch to your board. Use Chrome or Edge for Web Serial. When the browser asks, choose your board's serial port.

5

Power on and verify

After upload, the board resets and starts running the sketch. Open the serial monitor or watch the LEDs and sensors for the behavior the code describes — this is the fun first proof that the build is alive. If anything gets hot or smells wrong, unplug USB immediately.

Pin assignments

PinConnectionType
3V3ili9341-tft-touchscreen_0 VCCPOWER
GNDili9341-tft-touchscreen_0 GNDGROUND
GPIO 23ili9341-tft-touchscreen_0 MOSISPI
GPIO 19ili9341-tft-touchscreen_0 MISOSPI
GPIO 18ili9341-tft-touchscreen_0 SCKSPI
GPIO 4ili9341-tft-touchscreen_0 TFT_CSDIGITAL
GPIO 27ili9341-tft-touchscreen_0 TFT_DCDIGITAL
GPIO 32ili9341-tft-touchscreen_0 TFT_RSTDIGITAL
GPIO 13ili9341-tft-touchscreen_0 TOUCH_CSDIGITAL
GPIO 14ili9341-tft-touchscreen_0 TOUCH_IRQDIGITAL
GPIO 16ili9341-tft-touchscreen_0 TFT_BLDIGITAL
GNDbutton_1 GNDGROUND
GPIO 21button_1 SIGNALDIGITAL

Code

Arduino C++
#include <Arduino.h>
// Teleprompter for ESP32 + ILI9341 TFT + XPT2046 Touch + Push Button
// Button (GPIO21): pause/resume scrolling
// Touch screen: tap left half = slower, tap right half = faster

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <XPT2046_Touchscreen.h>

// Pin definitions
#define TFT_CS   4
#define TFT_DC   27
#define TFT_RST  32
#define TFT_BL   16
#define TOUCH_CS 13
#define TOUCH_IRQ 14
#define BUTTON_PIN 21

// Display dimensions
#define SCREEN_W 240
#define SCREEN_H 320

// Text settings
#define TEXT_SIZE    2
#define CHAR_H       16   // 8px * TEXT_SIZE
#define LINE_SPACING 2
#define ROW_H        (CHAR_H + LINE_SPACING)
#define CHARS_PER_LINE 20 // SCREEN_W / (6 * TEXT_SIZE)

// Touch calibration (adjust if needed)
#define TOUCH_X_MIN 300
#define TOUCH_X_MAX 3800
#define TOUCH_Y_MIN 300
#define TOUCH_Y_MAX 3800

Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
XPT2046_Touchscreen touch(TOUCH_CS, TOUCH_IRQ);

// --- Script text ---
// Each entry is a line of text (up to CHARS_PER_LINE chars)
const char* script[] = {
  "Welcome to the",
  "Teleprompter!",
  "",
  "This text will",
  "scroll up the",
  "screen.",
  "",
  "Use the button",
  "to pause and",
  "resume.",
  "",
  "Tap left side",
  "to slow down.",
  "",
  "Tap right side",
  "to speed up.",
  "",
  "Add your own",
  "script text by",
  "editing the",
  "script array.",
  "",
  "Good luck with",
  "your recording!",
  "",
  "-- END --",
  "",
  "",
  "",
  "",
};

const int SCRIPT_LINES = sizeof(script) / sizeof(script[0]);
const int VISIBLE_ROWS = SCREEN_H / ROW_H;

// Scrolling state
// scrollPos is in sub-pixel units (pixels * 16) for smooth scrolling
int32_t scrollPos = 0;          // pixels scrolled (top of visible area)
int32_t scrollSpeed = 1;        // pixels per update (1..8)
bool paused = false;

// Button debounce
bool lastBtnState = HIGH;
unsigned long lastBtnTime = 0;
#define DEBOUNCE_MS 50

// Touch debounce
unsigned long lastTouchTime = 0;
#define TOUCH_COOLDOWN_MS 400

// Rendering: track last drawn scroll position to avoid full redraws every frame
int32_t lastDrawnScroll = -9999;

// Total pixel height of script
int totalHeight() {
  return SCRIPT_LINES * ROW_H;
}

// Draw the teleprompter view for a given scroll offset (pixels from top of script)
void drawView(int32_t scrollPx) {
  tft.fillScreen(ILI9341_BLACK);

  // First line index that is (partially) visible
  int firstLine = scrollPx / ROW_H;
  int yOffset = -(scrollPx % ROW_H); // negative offset for partial top line

  for (int i = 0; i < VISIBLE_ROWS + 2; i++) {
    int lineIdx = firstLine + i;
    int y = yOffset + i * ROW_H;
    if (y >= SCREEN_H) break;
    if (lineIdx < 0 || lineIdx >= SCRIPT_LINES) continue;

    // Center text horizontally
    const char* line = script[lineIdx];
    int len = strlen(line);
    int xPos = (SCREEN_W - len * 6 * TEXT_SIZE) / 2;
    if (xPos < 0) xPos = 0;

    tft.setTextSize(TEXT_SIZE);
    tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
    tft.setCursor(xPos, y);
    tft.print(line);
  }

  // Draw speed indicator bar at bottom
  tft.fillRect(0, SCREEN_H - 4, (scrollSpeed * SCREEN_W) / 8, 4, ILI9341_GREEN);

  // Draw pause indicator
  if (paused) {
    tft.setTextSize(1);
    tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
    tft.setCursor(2, 2);
    tft.print("PAUSED");
  }
}

// Draw only the speed bar and pause indicator (overlay update)
void drawOverlay() {
  // Clear overlay area
  tft.fillRect(0, 0, 60, 10, ILI9341_BLACK);
  tft.fillRect(0, SCREEN_H - 4, SCREEN_W, 4, ILI9341_BLACK);

  // Speed bar
  tft.fillRect(0, SCREEN_H - 4, (scrollSpeed * SCREEN_W) / 8, 4, ILI9341_GREEN);

  // Pause text
  if (paused) {
    tft.setTextSize(1);
    tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
    tft.setCursor(2, 2);
    tft.print("PAUSED");
  }
}

void setup() {
  Serial.begin(115200);

  // Backlight on
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);

  // Button
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  // TFT init
  tft.begin();
  tft.setRotation(2); // Portrait, connector at top

  // Touch init
  touch.begin();
  touch.setRotation(2);

  // Splash screen
  tft.fillScreen(ILI9341_BLACK);
  tft.setTextSize(2);
  tft.setTextColor(ILI9341_CYAN);
  tft.setCursor(30, 130);
  tft.print("TELEPROMPTER");
  tft.setTextSize(1);
  tft.setTextColor(ILI9341_WHITE);
  tft.setCursor(40, 160);
  tft.print("Press button to start");
  delay(2000);

  // Start paused, position at top with a small margin
  scrollPos = 0;
  paused = true;
  drawView(scrollPos);
  drawOverlay();
}

void loop() {
  unsigned long now = millis();

  // --- Button handling ---
  bool btnState = digitalRead(BUTTON_PIN);
  if (btnState == LOW && lastBtnState == HIGH && (now - lastBtnTime) > DEBOUNCE_MS) {
    paused = !paused;
    lastBtnTime = now;
    drawOverlay();
  }
  lastBtnState = btnState;

  // --- Touch handling ---
  if ((now - lastTouchTime) > TOUCH_COOLDOWN_MS && touch.tirqTouched() && touch.touched()) {
    TS_Point p = touch.getPoint();
    // Map raw touch to screen coordinates
    int tx = map(p.x, TOUCH_X_MIN, TOUCH_X_MAX, 0, SCREEN_W);
    // tx < SCREEN_W/2 = left = slower, tx >= SCREEN_W/2 = right = faster
    if (tx < SCREEN_W / 2) {
      scrollSpeed--;
      if (scrollSpeed < 1) scrollSpeed = 1;
    } else {
      scrollSpeed++;
      if (scrollSpeed > 8) scrollSpeed = 8;
    }
    lastTouchTime = now;
    drawOverlay();
  }

  // --- Scrolling ---
  static unsigned long lastScrollTime = 0;
  // Scroll every 30ms (adjust for smooth motion)
  if (!paused && (now - lastScrollTime) >= 30) {
    lastScrollTime = now;
    scrollPos += scrollSpeed;

    // Clamp: stop at end of script (last line visible)
    int maxScroll = totalHeight() - SCREEN_H;
    if (maxScroll < 0) maxScroll = 0;
    if (scrollPos > maxScroll) {
      scrollPos = maxScroll;
      paused = true; // auto-pause at end
    }

    // Redraw view (full redraw each scroll step)
    drawView(scrollPos);
    drawOverlay();
  }
}

// Run this and build other cool things at schematik.io
Libraries: Adafruit GFX Library, Adafruit ILI9341, XPT2046_Touchscreen