Close

CYD Bring-Up: SPI, Backlight, and Other Lies

A project log for ESP32 Personal Library Console

ESP32-based personal library console for tracking reads, organizing TBR, and book discovery.

julia-mJulia M 03/03/2026 at 04:180 Comments

Goal: make the ESP32-2432S028 (aka Cheap Yellow Display) draw pixels and respond to touch without lying to me.

Spoiler: it draws pixels. Eventually.


SPI situation (why are there two?)

Display and touch are not on the same SPI bus. Display = HSPI. Touch = VSPI. Nowhere obvious in the listing. No schematic in the box. TFT_eSPI assumes one bus. That was a dead end.

Switched to LovyanGFX. It lets you assign SPI hosts directly. That solved 80% of the pain.

// display on HSPI
cfg.spi_host = HSPI_HOST;

// touch on VSPI
SPI.begin(25, 39, 32, 33);
ts.begin();

After that: at least I had a black screen instead of nothing.


Black screen, but technically alive

Serial looked fine. init() returned. Still black. Backlight is on GPIO21. Defaults LOW. You must pull it HIGH before display.init().

pinMode(21, OUTPUT);
digitalWrite(21, HIGH);
display.init();

Once that went high: pixels. Very wrong pixels, but pixels.


Diagonal tearing / half-screen offset

Image split diagonally like a broken framebuffer. Turned out to be panel vs memory dimensions mismatch. ILI9341 RAM is 240x320. Board is mounted 320x240. LovyanGFX calculates offsets from those values. If they don’t match physical layout → chaos.

cfg.panel_width  = 320;
cfg.memory_width = 320;
cfg.panel_height = 240;
cfg.memory_height= 240;

display.setRotation(4);

That fixed the split. Rotation 4 = rotation 0 + mirror. Feels wrong. Works.


RGB backwards

White text rendered blue. Red looked cyan. ILI9341 variant swaps color order.

cfg.rgb_order = true;

One boolean. 20 minutes of confusion.


Touch mapping (XPT2046 fun)

Touch panel mounted portrait inside landscape enclosure. Raw axes swapped and inverted. So p.x is basically screen Y (inverted). p.y is screen X.

TS_Point p = ts.getPoint();

int16_t sx = map(p.y, 300, 3800, 0, 320);
int16_t sy = map(p.x, 3900, 200, 0, 240);

sx = constrain(sx, 0, 319);
sy = constrain(sy, 0, 239);

After calibration it behaves. Mostly.


End of Day 1 state

SD card shares VSPI with touch. CS juggling was annoying. Dropped SD. Using LittleFS in flash instead. ~2.5MB available with custom partition table.

app0,    app,  factory,  0x10000,  0x180000
spiffs,  data, spiffs,   0x190000, 0x270000

Background frames: 320x240 RGB565 → 153,600 bytes each. About 16 fit comfortably.


Tooling

Wrote a small Python converter to batch PNG → raw .bin + .theme skeletons. No runtime decoding. Just stream to display.

python tools/convert_image.py img/ data/
pio run -e cyd --target uploadfs

Next

Hardware is stable enough to build UI on top. Finally feels like a platform instead of a demo sketch.

Discussions