If you are looking into ESP32 CYD projects, the absolute best starting point is the ESP32-2432S028R (the canonical 2.8-inch resistive touch "Cheap Yellow Display"). It pairs an ESP32-WROOM-32 with an ILI9341 TFT and XPT2046 touch controller on a single PCB for about $12 to $15 in 2026. It is the undisputed king of budget embedded UI builds, provided you know how to navigate its quirks with SPI bus sharing and touch calibration.
This guide cuts through the generic tutorials. We will build a functional MQTT Smart Desk Dashboard, map the exact pins to avoid bus collisions, and debug the specific upload errors that plague this board.
Decision Tree: Which CYD Variant Should You Buy?
The "CYD" name has spawned several clones. Before ordering, use this decision path to pick the right hardware for your build.
| Criteria | ESP32-2432S028R (2.8" Resistive) | JC3636W535 (3.5" IPS Capacitive) | ESP32-3248S035 (3.5" Resistive) |
|---|---|---|---|
| Price (2026) | $12 - $15 | $22 - $26 | $16 - $19 |
| Touch Type | Resistive (XPT2046) | Capacitive (GT911) | Resistive (XPT2046) |
| Resolution | 320x240 | 480x320 | 480x320 |
| Best For | Buttons, dashboards, basic UI | Multi-touch, high-res graphics | Larger single-touch buttons |
| Community Support | Massive (TFT_eSPI native) | Growing (Requires LVGL/Arduino_GFX) | Moderate |
Hardware Spec Sheet & Pin Mapping
The biggest trap in ESP32 CYD projects is SPI bus collision. The TFT and the Touch controller are wired to separate SPI buses on the ESP32-2432S028R. If you try to initialize them on the same default VSPI bus, your ESP32 will hard-fault or the touch will read garbage.
CYD ESP32-2432S028R Pin Mapping
| Component | Function | GPIO Pin | SPI Bus |
|---|---|---|---|
| TFT (ILI9341) | SCK / MISO / MOSI | 14 / 12 / 13 | VSPI (Default) |
| CS / DC / RST / BL | 15 / 2 / -1 / 21 | - | |
| Touch (XPT2046) | SCK / MISO / MOSI | 25 / 39 / 32 | HSPI (Must specify!) |
| CS / IRQ | 33 / 36 | - | |
| RGB LED | Red / Green / Blue | 4 / 16 / 17 | - (Active LOW) |
Step-by-Step: Building the MQTT Dashboard
We are building a desk dashboard that subscribes to an MQTT topic to display room temperature, and features a touch button to toggle a smart relay.
- Install Libraries: In Arduino IDE, install
TFT_eSPIby Bodmer,XPT2046_Touchscreenby Paul Stoffregen,WiFi, andPubSubClient. - Configure TFT_eSPI: This is where 90% of builds fail. You must edit the
User_Setup.hfile inside the TFT_eSPI library folder. Comment out all default displays and uncomment/add these exact lines:#define ILI9341_DRIVER #define TFT_WIDTH 240 #define TFT_HEIGHT 320 #define TFT_MISO 12 #define TFT_MOSI 13 #define TFT_SCLK 14 #define TFT_CS 15 #define TFT_DC 2 #define TFT_RST -1 #define TFT_BL 21 #define TOUCH_CS -1 // We handle touch separately to avoid bus clash #define SPI_FREQUENCY 40000000 - Wire the Power: The CYD has a USB Micro-B port. Power it via a known-good 5V/2A data cable. Do not rely on your PC's 500mA USB port if you are driving the backlight at 100% and transmitting WiFi simultaneously; the brownout detector will reset the ESP32.
- Upload the Code: Flash the code provided below.
- Verify: The screen should boot with a yellow "Connecting..." banner. Once on WiFi, it will draw the UI. Press the touch button; the serial monitor will log the MQTT publish event.
Complete Compilable Code with Error Handling
This code targets the ESP32-2432S028R (2.8" Resistive). It explicitly instantiates the HSPI bus for the touch controller to prevent SPI collisions and includes WiFi/MQTT reconnect logic.
#include
#include
#include
#include
#include
// --- Network & MQTT Config ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASS";
const char* mqtt_server = "192.168.1.100";
const int mqtt_port = 1883;
// --- Touch Pin Definitions (HSPI) ---
#define TOUCH_CS 33
#define TOUCH_IRQ 36
#define TOUCH_SCK 25
#define TOUCH_MISO 39
#define TOUCH_MOSI 32
// UI Layout
#define BTN_X 60
#define BTN_Y 180
#define BTN_W 120
#define BTN_H 60
TFT_eSPI tft = TFT_eSPI();
// Explicitly use HSPI for Touch to avoid VSPI collision with TFT
SPIClass touchscreenSPI = SPIClass(HSPI);
XPT2046_Touchscreen touchscreen(TOUCH_CS, TOUCH_IRQ);
WiFiClient espClient;
PubSubClient client(espClient);
bool relayState = false;
void setup_wifi() {
delay(10);
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
tft.drawString("Connecting to WiFi...", 10, 10, 2);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
tft.fillScreen(TFT_BLACK);
}
void reconnect_mqtt() {
while (!client.connected()) {
String clientId = "CYD-" + String(random(0xffff), HEX);
if (client.connect(clientId.c_str())) {
client.subscribe("home/office/temp");
} else {
delay(5000); // Wait 5s before retrying
}
}
}
void callback(char* topic, byte* payload, unsigned int length) {
// Handle incoming MQTT temp data (simplified)
String tempStr = "";
for (int i = 0; i < length; i++) tempStr += (char)payload[i];
tft.fillRect(10, 50, 220, 40, TFT_BLACK);
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.drawString("Temp: " + tempStr + "C", 10, 50, 4);
}
void drawUI() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.drawString("Smart Desk Hub", 10, 10, 4);
drawButton();
}
void drawButton() {
uint16_t color = relayState ? TFT_GREEN : TFT_DARKGREY;
tft.fillRoundRect(BTN_X, BTN_Y, BTN_W, BTN_H, 8, color);
tft.setTextColor(TFT_WHITE, color);
tft.drawCentreString(relayState ? "RELAY ON" : "RELAY OFF", BTN_X + BTN_W/2, BTN_Y + 20, 4);
}
void setup() {
Serial.begin(115200);
tft.init();
tft.setRotation(1); // Landscape
tft.fillScreen(TFT_BLACK);
// Initialize Touch on HSPI
touchscreenSPI.begin(TOUCH_SCK, TOUCH_MISO, TOUCH_MOSI, TOUCH_CS);
touchscreen.begin(touchscreenSPI);
touchscreen.setRotation(1);
setup_wifi();
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback);
drawUI();
}
void loop() {
if (!client.connected()) reconnect_mqtt();
client.loop();
if (touchscreen.tirqTouched() && touchscreen.touched()) {
TS_Point p = touchscreen.getPoint();
// Basic calibration mapping for 320x240 landscape
int x = map(p.x, 200, 3700, 0, 320);
int y = map(p.y, 240, 3800, 0, 240);
// Constrain to screen bounds
x = constrain(x, 0, 320);
y = constrain(y, 0, 240);
if (x >= BTN_X && x <= BTN_X + BTN_W && y >= BTN_Y && y <= BTN_Y + BTN_H) {
relayState = !relayState;
drawButton();
if (client.connected()) {
client.publish("home/office/relay", relayState ? "ON" : "OFF");
}
delay(300); // Software debounce
}
}
}
Debugging: Upload Timeouts and Touch Drift
When working with the CYD, you will inevitably hit hardware-specific errors. Here is how to diagnose them.
Error: "Failed to connect to ESP32: Timed out waiting for packet header"
Exact Error String: A fatal error occurred: Failed to connect to ESP32: Timed out waiting for packet header
Ranked Causes & Fixes:
- GPIO 0 / Boot Pin Interference (Most Likely): On the CYD, the touch controller's CS pin (GPIO 33) and other traces can sometimes pull the boot strapping pins out of the required state during auto-reset. Fix: Press and hold the physical BOOT button on the back of the CYD while clicking "Upload" in the IDE. Release it once the IDE says "Connecting...".
- Charge-Only USB Cable: The Micro-B port on these boards is frequently paired with cheap cables that lack data lines. Fix: Swap to a verified data-capable cable.
- Missing CH340 Driver: The CYD uses a CH340 USB-to-UART chip, not the CP2102 found on premium DevKits. Fix: Download and install the latest CH340 drivers from the manufacturer (WCH) for your OS.
The First 3 Things to Check When the Screen is White/Blank
If the code compiles and uploads, but the screen remains glaring white:
- Check User_Setup.h: Did you actually save the file and recompile? TFT_eSPI will silently fall back to a default 1.4" ST7735 if your defines are commented out, resulting in garbage or white screens.
- Check Backlight Pin (GPIO 21): The backlight is active HIGH. If you initialized the TFT but didn't set pin 21 HIGH (TFT_eSPI usually handles this if defined, but custom setups might miss it), the screen is on but dark. Shine a flashlight at it to verify.
- Check Rotation: The ILI9341 on the CYD is physically mounted in portrait but wired for landscape. If
tft.setRotation(1)is missing, your UI is drawing off-screen.
Scaling: Extend or Simplify the Build
Once you have the baseline dashboard running, you need to decide how to scale the project.
- To Simplify (Kiosk Mode): Strip out the MQTT and WiFi code entirely. Use the CYD as an offline timer or Pomodoro clock. This allows you to put the ESP32 into deep sleep between touch interrupts, running the board for weeks on a 2000mAh LiPo battery wired to the 5V and GND header pins.
- To Extend (LVGL Integration): If you need animated dials, sliding panels, or anti-aliased fonts, abandon raw TFT_eSPI drawing commands. Migrate to LVGL (Light and Versatile Graphics Library). You will need to use the Arduino_GFX library instead of TFT_eSPI for the LVGL display driver, which requires updating your
lv_conf.hbuffer sizes to at least 1/10th of the screen (approx. 15KB of RAM) to prevent tearing.
For deep hardware specifications, always refer to the official ESP32-WROOM-32 datasheet to verify GPIO strapping requirements before adding external sensors to the CYD's breakout headers. If you encounter persistent Arduino core panics, consult the Espressif Arduino Troubleshooting Guide for watchdog timer resets.
analogRead(34) and use ledcWrite() to PWM the backlight pin. Do not try to use standard analogWrite() on the ESP32; it will fail silently.






