Community project
Touchscreen Demo App
This project demonstrates a touchscreen interface running on an ESP32 microcontroller with a 320x240 ILI9341 display and XPT2046 touch controller. The guide covers wiring the display and touch panel to the ESP32, uploading the firmware, and validating touch responsiveness with a four-corner calibration check.
Builders will receive a complete parts list, wiring diagram, and step-by-step assembly instructions. The firmware uses the LovyanGFX library to render graphics and handle touch input, providing a foundation for interactive applications. After assembly, the touch check screen confirms proper connectivity before moving on to more complex interactions.
Wiring diagram
Assemble it in 3 steps
1. Set the display board down safely
Place the L0496 smart display face-up on a dry, non-metal surface. Its screen and touch layer are already built in, so do not add any jumper wires for this game.
- Keep the clear touch surface clean so taps register reliably.
- Do not place the powered board on loose metal objects; they can touch the pins underneath and damage the board.
2. Connect it by USB
Use a data-capable USB cable to connect the L0496 board to your computer. The USB cable supplies power and lets Schematik place the game on the board.
- If nothing happens when you later deploy, try a different USB cable because some cables provide power only.
- Do not force the USB plug; forcing it can damage the connector.
3. Complete the four-corner touch check
After the game starts, touch the orange TAP square in the highlighted upper-left, upper-right, lower-left, and lower-right corners in that order. The game uses those four touches to match the quiz buttons to your screen automatically.
- Touch near the middle of each orange square and lift your finger before moving to the next one.
- Do not skip a highlighted square or touch a different area first; the game uses this short check to decide which touch direction needs correcting.
Deploy the firmware
#include <Arduino.h>
#include <LovyanGFX.hpp>
enum AppScreen { TOUCH_CHECK, GRADE_PICKER, QUIZ };
// Forward declarations
void centered(const String &text, int x, int y, uint16_t foreground, uint16_t background, uint8_t size);
void button(int x, int y, int w, int h, uint16_t colour, const String &label, uint8_t size);
int randomDifferent(int forbidden, int spread);
void makeQuestion();
void drawGradePicker();
void drawQuiz();
void showResult(bool correct);
void startQuiz(int selectedGrade);
void drawTouchCheck();
void handleTouchCheck(uint16_t x, uint16_t y);
void handleTap(uint16_t x, uint16_t y);
constexpr int SCREEN_W = 320;
constexpr int SCREEN_H = 240;
constexpr int TFT_MISO_PIN = 12;
constexpr int TFT_MOSI_PIN = 13;
constexpr int TFT_SCLK_PIN = 14;
constexpr int TFT_CS_PIN = 15;
constexpr int TFT_DC_PIN = 2;
constexpr int TFT_BACKLIGHT_PIN = 21;
constexpr int TOUCH_SCLK_PIN = 25;
constexpr int TOUCH_MOSI_PIN = 32;
constexpr int TOUCH_MISO_PIN = 39;
constexpr int TOUCH_CS_PIN = 33;
constexpr int TOUCH_IRQ_PIN = 36;
class L0496Display : public lgfx::LGFX_Device {
lgfx::Panel_ILI9341 panel;
lgfx::Bus_SPI displayBus;
lgfx::Light_PWM backlight;
lgfx::Touch_XPT2046 touch;
public:
L0496Display() {
auto busConfig = displayBus.config();
busConfig.spi_host = HSPI_HOST;
busConfig.spi_mode = 0;
busConfig.freq_write = 40000000;
busConfig.freq_read = 16000000;
busConfig.spi_3wire = false;
busConfig.use_lock = true;
busConfig.dma_channel = 1;
busConfig.pin_sclk = TFT_SCLK_PIN;
busConfig.pin_mosi = TFT_MOSI_PIN;
busConfig.pin_miso = TFT_MISO_PIN;
busConfig.pin_dc = TFT_DC_PIN;
displayBus.config(busConfig);
panel.setBus(&displayBus);
auto panelConfig = panel.config();
panelConfig.pin_cs = TFT_CS_PIN;
panelConfig.pin_rst = -1;
panelConfig.panel_width = 240;
panelConfig.panel_height = 320;
panelConfig.offset_x = 0;
panelConfig.offset_y = 0;
panelConfig.readable = true;
panelConfig.invert = true;
panelConfig.rgb_order = false;
panel.config(panelConfig);
auto lightConfig = backlight.config();
lightConfig.pin_bl = TFT_BACKLIGHT_PIN;
lightConfig.invert = false;
lightConfig.freq = 44100;
lightConfig.pwm_channel = 7;
backlight.config(lightConfig);
panel.setLight(&backlight);
auto touchConfig = touch.config();
// Landscape calibration: reverse left/right only; keep top/bottom normal.
touchConfig.x_min = 3800;
touchConfig.x_max = 300;
touchConfig.y_min = 300;
touchConfig.y_max = 3800;
// Poll the touch controller directly. Some L0496 revisions do not present
// the XPT2046 interrupt line reliably, even though touch SPI is available.
touchConfig.pin_int = -1;
touchConfig.bus_shared = false;
touchConfig.offset_rotation = 0;
touchConfig.spi_host = VSPI_HOST;
touchConfig.freq = 1000000;
touchConfig.pin_sclk = TOUCH_SCLK_PIN;
touchConfig.pin_mosi = TOUCH_MOSI_PIN;
touchConfig.pin_miso = TOUCH_MISO_PIN;
touchConfig.pin_cs = TOUCH_CS_PIN;
touch.config(touchConfig);
panel.setTouch(&touch);
setPanel(&panel);
}
};
L0496Display display;
AppScreen appScreen = GRADE_PICKER;
int grade = 1;
int score = 0;
int questionNumber = 0;
int answer = 0;
int choices[4];
String questionText;
bool wasTouched = false;
bool showingResult = false;
unsigned long resultStartedAt = 0;
// The four-corner check learns whether this particular panel is mirrored.
uint16_t cornerX[4] = {0, 0, 0, 0};
uint16_t cornerY[4] = {0, 0, 0, 0};
uint8_t cornerStep = 0;
bool touchFlipX = false;
bool touchFlipY = false;
constexpr uint16_t NAVY = 0x10A4;
constexpr uint16_t BLUE = 0x249F;
constexpr uint16_t PURPLE = 0x79B8;
constexpr uint16_t GREEN = 0x0640;
constexpr uint16_t ORANGE = 0xFD20;
constexpr uint16_t RED = 0xD904;
constexpr uint16_t WHITE = 0xFFFF;
constexpr uint16_t PALE = 0xD6BF;
void centered(const String &text, int x, int y, uint16_t foreground, uint16_t background, uint8_t size = 1) {
display.setTextSize(size);
display.setTextColor(foreground, background);
display.setTextDatum(middle_center);
display.drawString(text, x, y);
}
void button(int x, int y, int w, int h, uint16_t colour, const String &label, uint8_t size = 1) {
display.fillRoundRect(x, y, w, h, 9, colour);
centered(label, x + w / 2, y + h / 2, WHITE, colour, size);
}
int randomDifferent(int forbidden, int spread) {
int value;
do {
value = answer + random(-spread, spread + 1);
if (value < 0) value = random(0, spread * 2 + 1);
} while (value == forbidden || value == answer);
return value;
}
void makeQuestion() {
int a = 0;
int b = 0;
int kind = 0;
int limit = 10;
if (grade == 1) {
a = random(0, 11); b = random(0, 11 - a); answer = a + b;
questionText = String(a) + " + " + String(b) + " = ?";
} else if (grade == 2) {
limit = 20; kind = random(0, 2);
a = random(0, limit + 1); b = random(0, limit + 1);
if (kind == 0 && a + b <= limit) { answer = a + b; questionText = String(a) + " + " + String(b) + " = ?"; }
else { if (b > a) { int t = a; a = b; b = t; } answer = a - b; questionText = String(a) + " - " + String(b) + " = ?"; }
} else if (grade == 3) {
limit = 100; kind = random(0, 2); a = random(0, limit + 1); b = random(0, limit + 1);
if (kind == 0 && a + b <= limit) { answer = a + b; questionText = String(a) + " + " + String(b) + " = ?"; }
else { if (b > a) { int t = a; a = b; b = t; } answer = a - b; questionText = String(a) + " - " + String(b) + " = ?"; }
} else {
kind = random(0, grade == 6 ? 3 : 2);
if (kind == 0) {
a = random(2, grade == 4 ? 11 : 13); b = random(2, grade == 4 ? 11 : 21);
answer = a * b; questionText = String(a) + " x " + String(b) + " = ?";
} else if (kind == 1) {
b = random(2, grade == 4 ? 11 : 13); answer = random(2, grade == 4 ? 11 : 21); a = b * answer;
questionText = String(a) + " / " + String(b) + " = ?";
} else {
a = random(100, 501); b = random(10, 200); answer = a + b;
questionText = String(a) + " + " + String(b) + " = ?";
}
}
int correctSlot = random(0, 4);
for (int i = 0; i < 4; ++i) choices[i] = -1;
choices[correctSlot] = answer;
int spread = max(5, answer / 3 + 2);
for (int i = 0; i < 4; ++i) {
if (i == correctSlot) continue;
int candidate;
bool duplicate;
do {
candidate = randomDifferent(-9999, spread);
duplicate = false;
for (int j = 0; j < 4; ++j) if (choices[j] == candidate) duplicate = true;
} while (duplicate);
choices[i] = candidate;
}
++questionNumber;
}
void drawTouchCheck() {
static const char *cornerNames[] = {"upper-left", "upper-right", "lower-left", "lower-right"};
display.fillScreen(NAVY);
centered("Touch check", 160, 26, WHITE, NAVY, 2);
centered("Touch the highlighted corner", 160, 52, PALE, NAVY);
centered(String("Step ") + String(cornerStep + 1) + " of 4", 160, 76, WHITE, NAVY);
const int markerSize = 48;
const int margin = 12;
int markerX = (cornerStep == 1 || cornerStep == 3) ? SCREEN_W - margin - markerSize : margin;
int markerY = (cornerStep >= 2) ? SCREEN_H - margin - markerSize : 92;
display.fillRoundRect(markerX, markerY, markerSize, markerSize, 10, ORANGE);
centered("TAP", markerX + markerSize / 2, markerY + markerSize / 2, WHITE, ORANGE);
centered(cornerNames[cornerStep], 160, 218, PALE, NAVY);
}
void drawGradePicker() {
display.fillScreen(NAVY);
centered("Maths Quest", 160, 27, WHITE, NAVY, 2);
centered("Choose your school grade", 160, 52, PALE, NAVY);
for (int i = 0; i < 6; ++i) {
int column = i % 2;
int row = i / 2;
button(24 + column * 148, 76 + row * 48, 124, 38, column == 0 ? BLUE : PURPLE, String("Grade ") + String(i + 1));
}
centered("Answer questions to earn points!", 160, 228, PALE, NAVY);
}
void drawQuiz() {
display.fillScreen(NAVY);
display.fillRect(0, 0, SCREEN_W, 34, BLUE);
centered(String("Grade ") + String(grade), 52, 17, WHITE, BLUE);
centered(String("Score: ") + String(score), 160, 17, WHITE, BLUE);
button(258, 5, 54, 24, RED, "Home");
centered(questionText, 160, 73, WHITE, NAVY, 2);
for (int i = 0; i < 4; ++i) {
int x = 20 + (i % 2) * 145;
int y = 112 + (i / 2) * 58;
button(x, y, 135, 46, i % 2 == 0 ? GREEN : ORANGE, String(choices[i]), 2);
}
centered("Tap the correct answer", 160, 222, PALE, NAVY);
}
void showResult(bool correct) {
display.fillRoundRect(48, 83, 224, 60, 10, correct ? GREEN : RED);
centered(correct ? "Great job! +1 point" : String("Not quite. It is ") + String(answer), 160, 113, WHITE, correct ? GREEN : RED, 1);
showingResult = true;
resultStartedAt = millis();
}
void startQuiz(int selectedGrade) {
grade = selectedGrade;
score = 0;
questionNumber = 0;
appScreen = QUIZ;
makeQuestion();
drawQuiz();
}
void handleTouchCheck(uint16_t x, uint16_t y) {
cornerX[cornerStep] = x;
cornerY[cornerStep] = y;
++cornerStep;
if (cornerStep < 4) {
drawTouchCheck();
return;
}
// Compare physical left/right and top/bottom taps. If their reported order is
// backwards, reverse only that coordinate before the quiz uses it.
touchFlipX = cornerX[0] > cornerX[1];
touchFlipY = cornerY[0] > cornerY[2];
Serial.printf("Touch check complete: flipX=%d flipY=%d\n", touchFlipX, touchFlipY);
appScreen = GRADE_PICKER;
drawGradePicker();
}
void handleTap(uint16_t x, uint16_t y) {
if (appScreen == TOUCH_CHECK) {
handleTouchCheck(x, y);
return;
}
if (touchFlipX) x = SCREEN_W - 1 - x;
if (touchFlipY) y = SCREEN_H - 1 - y;
if (appScreen == GRADE_PICKER) {
int row = -1;
if (y >= 76 && y < 114) row = 0;
else if (y >= 124 && y < 162) row = 1;
else if (y >= 172 && y < 210) row = 2;
if (row >= 0) {
if (x >= 24 && x < 148) startQuiz(row * 2 + 1);
else if (x >= 172 && x < 296) startQuiz(row * 2 + 2);
}
return;
}
if (showingResult) return;
if (y < 34 && x >= 258) { appScreen = GRADE_PICKER; drawGradePicker(); return; }
// Only score a tap that falls inside one of the four visible answer buttons.
// This leaves the small spaces between buttons inactive instead of selecting a neighbour.
for (int i = 0; i < 4; ++i) {
const int buttonX = 20 + (i % 2) * 145;
const int buttonY = 112 + (i / 2) * 58;
const bool insideButton = x >= buttonX && x < buttonX + 135 &&
y >= buttonY && y < buttonY + 46;
if (insideButton) {
const bool correct = choices[i] == answer;
if (correct) ++score;
showResult(correct);
return;
}
}
}
void setup() {
Serial.begin(115200);
delay(100);
display.init();
display.setRotation(1);
display.setBrightness(255);
randomSeed(esp_random());
Serial.println("Maths Quest started");
appScreen = TOUCH_CHECK;
drawTouchCheck();
}
void loop() {
uint16_t x, y;
bool touched = display.getTouch(&x, &y);
if (touched && !wasTouched) {
handleTap(x, y);
}
wasTouched = touched;
if (showingResult && millis() - resultStartedAt >= 900) {
showingResult = false;
makeQuestion();
drawQuiz();
}
delay(20);
}Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.




