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
Updated

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:
- initialise the ILI9341 TFT display over SPI,
- read the XPT2046 touchscreen on the same SPI bus,
- render wrapped script lines in a smooth upward scroll,
- 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 LibraryAdafruit ILI9341XPT2046_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, andTOUCH_Y_MAXconstants. - 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 reduceTEXT_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
Components needed
| Component | Type | Qty | Buy |
|---|---|---|---|
| ILI9341 TFT Touchscreen | display | 1 | |
| Push Button | other | 1 |
Assembly
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.
- Use different wire colors for VCC, GND, and signals if you have them
- Do not power the board until wiring matches the pin map
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.
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.
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.
- Skim the Code tab before uploading
- Schematik passes the listed libraries into the compile step for you
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.
- If the board resets in a loop, recheck GND and 3.3V/5V levels
Pin assignments
| Pin | Connection | Type |
|---|---|---|
| 3V3 | ili9341-tft-touchscreen_0 VCC | POWER |
| GND | ili9341-tft-touchscreen_0 GND | GROUND |
| GPIO 23 | ili9341-tft-touchscreen_0 MOSI | SPI |
| GPIO 19 | ili9341-tft-touchscreen_0 MISO | SPI |
| GPIO 18 | ili9341-tft-touchscreen_0 SCK | SPI |
| GPIO 4 | ili9341-tft-touchscreen_0 TFT_CS | DIGITAL |
| GPIO 27 | ili9341-tft-touchscreen_0 TFT_DC | DIGITAL |
| GPIO 32 | ili9341-tft-touchscreen_0 TFT_RST | DIGITAL |
| GPIO 13 | ili9341-tft-touchscreen_0 TOUCH_CS | DIGITAL |
| GPIO 14 | ili9341-tft-touchscreen_0 TOUCH_IRQ | DIGITAL |
| GPIO 16 | ili9341-tft-touchscreen_0 TFT_BL | DIGITAL |
| GND | button_1 GND | GROUND |
| GPIO 21 | button_1 SIGNAL | DIGITAL |
Code
#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.ioReady to build this?
Open this project in Schematik to get the full wiring diagram, pin assignments, and deployable code for the Tellaprompter.
Open in Schematik →