This example demonstrates how to control three RGB LEDs (red, green, blue) via touch buttons on a 2.8‑inch TFT LCD touch screen with an ESP32. Each button toggles its corresponding LED through three states: OFF → ON → FLICKER → OFF … A slider at the bottom simultaneously adjusts the brightness of all LEDs currently in the ON state, enabling color mixing. The improved code features a clean layout and non‑blocking flicker to avoid lag.
RGB_LED_TOUCH.ino.#define RED_PIN 22
#define GREEN_PIN 16
#define BLUE_PIN 17
#define LED_ON 0
#define LED_OFF 1
#define RTP_DOUT 39
#define RTP_DIN 32
#define RTP_SCK 25
#define RTP_CS 33
#define RTP_IRQ 36
my_touch.setCal() (default shown):my_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
OFF.ON, LED turns on steadily.FLI, LED flickers 6 times (0.5‑second intervals), then stays on.OFF, LED turns off.ON will change brightness simultaneously (value displayed above the slider, range 0–255).Screen (landscape 320×240) is divided into four areas from top to bottom:
| Area | Y Range | Description |
|---|---|---|
| Instruction text | y=15, 32 | Two white lines explaining button and slider functions |
| Three colour buttons | Centre y=95 (top ~77, bottom ~112) | Three coloured buttons, horizontally aligned |
| Brightness value | y=120 | Displays current brightness (0–255) |
| Slider | y=165–184 | Horizontal slider with knob, endpoints show 0 and 255 |
⚠️ Key points:
TFT_RED, TFT_GREEN, TFT_BLUE; outline and text are white.0x8430), filled portion is cyan (0x07FF), knob is magenta (TFT_MAGENTA).while(!digitalRead(RTP_IRQ)); – if the pin behaves unexpectedly, it may hang; a timeout can be added (see Modifications).setCal() if alignment is off.LED_ON and LED_OFF.RTP_IRQ to wait for touch release, ensuring single‑press events.This example is based on the ESP32‑WROOM‑32E with an ILI9341 TFT LCD and resistive touch. It uses the TFT_eSPI and TFT_Touch libraries, and LEDC (PWM) for brightness control.
The top comment provides the full wiring table:
// CS DC/RS RESET SDI/MOSI SCK SDO/MISO BL RTP_DOUT RTP_DIN RTP_SCK RTP_CS RTP_IRQ RED GREEN BLUE VCC GND
//ESP32-WROOM-32E: 15 2 ESP32-EN 13 14 12 21 39 32 25 33 36 22 16 17 5V GND
| Function | Pin Name | ESP32 GPIO | Description |
|---|---|---|---|
| TFT LCD | CS | 15 | Chip select |
| TFT LCD | DC/RS | 2 | Data/Command |
| TFT LCD | RESET | ESP32-EN | Reset (shared) |
| TFT LCD | SDI/MOSI | 13 | SPI data |
| TFT LCD | SCK | 14 | SPI clock |
| TFT LCD | SDO/MISO | 12 | SPI data return |
| TFT LCD | BL | 21 | Backlight |
| Touch | RTP_DOUT | 39 | SPI data out |
| Touch | RTP_DIN | 32 | SPI data in |
| Touch | RTP_SCK | 25 | SPI clock |
| Touch | RTP_CS | 33 | Chip select |
| Touch | RTP_IRQ | 36 | Interrupt (release detection) |
| RGB LED | RED | 22 | Red LED (active‑low) |
| RGB LED | GREEN | 16 | Green LED |
| RGB LED | BLUE | 17 | Blue LED |
| Power | VCC | 5V | Supply |
| Power | GND | GND | Ground |
#define RED_PIN 22
#define GREEN_PIN 16
#define BLUE_PIN 17
#define LED_ON 0
#define LED_OFF 1
RED_PIN, GREEN_PIN, BLUE_PIN – GPIO pins for each LED.LED_ON = 0 (active‑low, common‑anode).LED_OFF = 1.#define RTP_DOUT 39
#define RTP_DIN 32
#define RTP_SCK 25
#define RTP_CS 33
#define RTP_IRQ 36
my_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
#define PWM_FREQ 2000
#define PWM_RES 8
#define STATE_OFF 0
#define STATE_ON 1
#define STATE_FLI 2
uint8_t led_state[NUM_KEYS] = {STATE_OFF, STATE_OFF, STATE_OFF};
PWM_FREQ – 2000 Hz.PWM_RES – 8‑bit resolution (0–255).STATE_OFF, STATE_ON, STATE_FLI – state constants.led_state array holds current state for each LED.struct FlickData {
bool active;
uint8_t led_num;
unsigned long start_time;
uint8_t phase;
uint8_t count;
};
FlickData flick = {false, 0, 0, 0, 0};
active – whether flicker is ongoing.led_num – which LED (0/1/2) is flickering.start_time – millis() at start of current phase.phase – 0 = off, 1 = on.count – completed on‑off cycles.int getPin(uint8_t led_num)int getPin(uint8_t led_num) {
switch(led_num) {
case 0: return RED_PIN;
case 1: return GREEN_PIN;
case 2: return BLUE_PIN;
}
return -1;
}
void light_led(uint8_t led_num, bool turn_on)void light_led(uint8_t led_num, bool turn_on) {
int pin = getPin(led_num);
if (pin < 0) return;
ledcDetach(pin);
digitalWrite(pin, turn_on ? LED_ON : LED_OFF);
}
ledcDetach(pin) detaches any PWM to avoid conflict.void start_flick(uint8_t led_num)void start_flick(uint8_t led_num) {
if (flick.active) {
int old_pin = getPin(flick.led_num);
ledcDetach(old_pin);
digitalWrite(old_pin, LED_OFF);
}
int pin = getPin(led_num);
ledcDetach(pin);
digitalWrite(pin, LED_OFF);
flick.active = true;
flick.led_num = led_num;
flick.start_time = millis();
flick.phase = 0;
flick.count = 0;
}
void stop_flick()void stop_flick() {
if (flick.active) {
int pin = getPin(flick.led_num);
ledcDetach(pin);
digitalWrite(pin, LED_OFF);
flick.active = false;
}
}
void update_flick()void update_flick() {
if (!flick.active) return;
unsigned long now = millis();
int pin = getPin(flick.led_num);
if (pin < 0) { flick.active = false; return; }
if (now - flick.start_time >= 500) {
flick.start_time = now;
if (flick.phase == 0) {
digitalWrite(pin, LED_ON);
flick.phase = 1;
} else {
digitalWrite(pin, LED_OFF);
flick.phase = 0;
flick.count++;
if (flick.count >= 6) {
flick.active = false;
digitalWrite(pin, LED_ON);
}
}
}
}
loop() to update flicker state without blocking.void setup() {
tft.begin();
tft.setRotation(1);
my_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
tft.fillScreen(TFT_BLACK);
pinMode(RED_PIN, OUTPUT);
pinMode(GREEN_PIN, OUTPUT);
pinMode(BLUE_PIN, OUTPUT);
digitalWrite(RED_PIN, LED_OFF);
digitalWrite(GREEN_PIN, LED_OFF);
digitalWrite(BLUE_PIN, LED_OFF);
pinMode(RTP_IRQ, INPUT);
tft.setCursor(5, 15);
tft.setTextFont(2);
tft.setTextSize(1);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.println("Press button: OFF -> ON -> FLI -> OFF");
tft.setCursor(5, 32);
tft.println("Slider controls all ON LEDs (mixed color)");
tft.setTextColor(0xFFE0);
tft.setTextSize(2);
tft.setCursor(tft.width()/2 - 150, 195);
tft.println("0");
tft.setCursor(tft.width()/2 + 110, 195);
tft.println("255");
tft.fillRoundRect(tft.width()/2 - 138, 165, 276, 21, 10, 0x8430);
tft.fillCircle(tft.width()/2 - 128, 175, 10, TFT_MAGENTA);
tft.setTextColor(0xFFE0);
tft.setTextSize(1);
tft.drawNumber(0, tft.width()/2 - 15, 120, 1);
tft.setFreeFont(&FreeSansBold9pt7b);
for (int i = 0; i < NUM_KEYS; i++) {
key_b[i].initButton(&tft,
KEY_X + i * (KEY_W + KEY_SPACING_X),
KEY_Y,
KEY_W, KEY_H,
TFT_WHITE,
k_color[i],
TFT_WHITE,
"",
KEY_TEXTSIZE);
key_b[i].setLabelDatum(13 - (KEY_W/2), 2, ML_DATUM);
key_b[i].drawButton(false, "OFF");
}
}
tft.begin() – initialises ILI9341.tft.setRotation(1) – landscape (320×240).my_touch.setCal(...) – sets calibration.tft.fillScreen(TFT_BLACK) – black background.pinMode(...) – sets LED pins as outputs, initially off.pinMode(RTP_IRQ, INPUT) – touch interrupt pin.tft.setCursor(5,15) and tft.println(...) – draw two instruction lines at y=15 and y=32.tft.setCursor(...) – draw “0” and “255” at y=195.tft.fillRoundRect(...) – draw slider background (brown, y=165, height 21).tft.fillCircle(...) – initial knob (magenta, far left).tft.drawNumber(0, ...) – initial brightness value at y=120.tft.setFreeFont(&FreeSansBold9pt7b) – set button font.for loop – creates three buttons, initial label "OFF".loop() consists of three parts: button detection, button state handling, and slider processing.
void loop() {
update_flick();
uint16_t t_x = 0, t_y = 0;
uint8_t i;
bool pressed = my_touch.Pressed();
for (i = 0; i < NUM_KEYS; i++) {
if (pressed) {
t_x = my_touch.X();
t_y = my_touch.Y();
if (key_b[i].contains(t_x, t_y)) {
key_b[i].press(true);
while (!digitalRead(RTP_IRQ));
}
} else {
key_b[i].press(false);
}
}
...
}
update_flick() – non‑blocking flicker update.my_touch.Pressed() – true if touch detected.my_touch.X() / Y() – get touch coordinates.key_b[i].contains(t_x, t_y) – check if touch inside button.key_b[i].press(true) – mark button as pressed.while (!digitalRead(RTP_IRQ)); – wait for release.for (i = 0; i < NUM_KEYS; i++) {
if (key_b[i].justPressed()) {
const char* label;
switch (led_state[i]) {
case STATE_OFF: label = "OFF"; break;
case STATE_ON: label = "ON"; break;
case STATE_FLI: label = "FLI"; break;
}
key_b[i].drawButton(true, label);
}
if (key_b[i].justReleased()) {
if (flick.active) stop_flick();
uint8_t new_state = (led_state[i] + 1) % 3;
led_state[i] = new_state;
const char* label;
switch (new_state) {
case STATE_OFF:
label = "OFF";
light_led(i, false);
break;
case STATE_ON:
label = "ON";
light_led(i, true);
break;
case STATE_FLI:
label = "FLI";
start_flick(i);
break;
}
key_b[i].drawButton(false, label);
}
}
key_b[i].justPressed() – true once when button pressed.drawButton(true, label) – draw inverted (pressed) state.key_b[i].justReleased() – true once when released.stop_flick() – cancel any ongoing flicker.new_state = (led_state[i] + 1) % 3 – cycle 0→1→2→0.light_led or start_flick and update label.drawButton(false, label) – normal (released) state.if (pressed) {
t_x = my_touch.X();
t_y = my_touch.Y();
if ((t_x >= tft.width()/2 - 128) && (t_x < tft.width()/2 + 128) &&
t_y > 164 && t_y < 184) {
int offset = t_x - (tft.width()/2 - 128);
if (offset < 0) offset = 0;
if (offset > 255) offset = 255;
int brightness = 255 - offset;
tft.setTextColor(0xFFE0, TFT_BLACK);
tft.fillRoundRect(t_x - 10, 165, tft.width()/2 + 138 - t_x + 10, 21, 10, 0x8430);
tft.fillRoundRect(tft.width()/2 - 138, 165, t_x - tft.width()/2 + 148, 21, 10, 0x07FF);
tft.fillCircle(t_x, 175, 10, TFT_MAGENTA);
tft.setTextPadding(50);
tft.setTextSize(1);
tft.drawNumber(offset, tft.width()/2 - 15, 120, 1);
for (int idx = 0; idx < NUM_KEYS; idx++) {
if (led_state[idx] == STATE_ON) {
int pin, ch;
switch (idx) {
case 0: pin = RED_PIN; ch = 0; break;
case 1: pin = GREEN_PIN; ch = 1; break;
case 2: pin = BLUE_PIN; ch = 2; break;
default: continue;
}
ledcAttachChannel(pin, PWM_FREQ, PWM_RES, ch);
ledcWrite(pin, brightness);
}
}
}
}
offset = t_x - (tft.width()/2 - 128) – map to 0~255.brightness = 255 - offset – invert for common‑anode.STATE_ON, attach PWM and write brightness.You can tailor the code to your needs with these adjustments:
my_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
Modify in update_flick():
if (now - flick.start_time >= 500) // change interval (ms)
if (flick.count >= 6) // change number of cycles
Change macros:
#define PWM_FREQ 2000
#define PWM_RES 8
Adjust macros:
#define KEY_X 75
#define KEY_Y 95
#define KEY_W 65
#define KEY_H 35
#define KEY_SPACING_X 20
Modify slider to only control the last pressed LED, or add a selection mechanism; this version is designed for mixed control.
Modify k_color[] array:
uint16_t k_color[NUM_KEYS] = {TFT_RED, TFT_GREEN, TFT_BLUE};
Replace while(!digitalRead(RTP_IRQ)); with:
unsigned long start = millis();
while (!digitalRead(RTP_IRQ) && millis() - start < 1000) { }
Add Serial.begin(115200); in setup(), and print state changes or slider values.
No touch response or offset – check pin definitions and calibration; verify RTP_IRQ wiring; try commenting the wait loop and using a simple delay(100).
LED not lighting or always on – verify pin numbers and common‑anode wiring; if common‑cathode, swap LED_ON and LED_OFF.
Flicker causes lag – this version uses non‑blocking; if still laggy, check for other blocking code.
Slider has no effect – ensure at least one LED is in ON state; confirm touch point falls within slider area (y 165–184); verify PWM attachment and write.
Compile error: FreeSansBold9pt7b not defined – enable the font in TFT_eSPI's User_Setup.h or use a different font (e.g., &FreeSans9pt7b).
Screen garbled or blank – check LCD SPI wiring and power; confirm TFT_eSPI library is configured for ILI9341 with correct pins.
Button press hangs – likely due to while(!digitalRead(RTP_IRQ));; add a timeout (see Modification #7).