Writing / Build Log
Five hours, eleven sketches, one blank screen
A 128x64 ST7920 worked on one set of ESP32 pins and stayed blank on another. The multimeter said everything was fine. Here is the bisection that followed, including the sketch that turned the ESP32 into its own logic analyzer.
The panel worked. That was the problem.
The same ST7920 128x64 module, driven by the same U8g2 demo code, lit up perfectly on GPIO 25/32/33. Soldered to GPIO 5/19/23 on the v7 board, it showed a static faint line and nothing else. Power was there. Backlight was there. Contrast was there. PSB was tied low for serial mode. Continuity from every ESP32 pad to every LCD pad checked out on the meter.
Two different ESP32 boards failed identically, which ruled out one dead board and made it worse — a reproducible fault with no visible cause.
What follows is the actual sequence, reconstructed from file timestamps. It is a lab notebook, not a tutorial.
The night
21:19 — `ESP32_Bare_GPIO_Test`. Start at the bottom. Can the chip toggle a pin at all.
22:00 — `ESP32_Onboard_Blink_Test`. Is the board alive and flashing correctly. Both passed, which is the boring outcome you want first.
Then a three-hour gap in the timestamps. I do not know what happened in it.
01:04 — `ESP32_LCD_v7_Pins_Test`. Reproduce the failure cleanly, in isolation, with nothing else in the sketch.
01:32 — `ESP32_Pin_Identify_Test`. The first real hypothesis, and a good one:
Cheap ESP32 DevKit boards from different vendors sometimes print pin labels (D5/D19/D23/...) that don't map 1:1 to the GPIO number on that specific board revision.
The sketch blinks one pin at a time at 1Hz for six seconds while printing which pin is active, so you can hold a probe on the physical wire and confirm the silkscreen is not lying. Nine minutes to write, and it eliminates an entire category of wasted night.
01:41 — `ESP32_LCD_OldPins_Test`. Confirm the panel still works on 25/32/33. Never skip the control.
01:43 — `ESP32_LCD_SwapTest`. Two minutes after the last one, so this was already queued up. The board is soldered down and pins cannot be moved, so test the most common hand-soldering mistake in software instead: SCLK and MOSI landed on swapped pads. Identical to the previous sketch except 19 is treated as clock and 5 as data. The note in the header is the part worth keeping:
If THIS version lights up the screen, the physical E/R-W traces are crossed on the board — compensate by keeping this swapped definition permanently in the real firmware instead of touching solder.
That is the right instinct. A software workaround for a hardware mistake is not a hack when the alternative is reflowing a working board at 2am.
01:45 — `ESP32_LCD_SWSPI_Test`. Remove the hardware SPI peripheral entirely — U8G2_ST7920_128X64_F_SW_SPI bit-bangs with plain digitalWrite(), no GPIO matrix, no SPI peripheral. Same pins, same panel, different transport. The reasoning names a specific suspect:
A multimeter only proves voltage is present — it can't catch a corrupted bit pattern from ESP32 hardware-SPI + GPIO-matrix routing quirks, which are a known rough edge on GPIO5 specifically (it's a default VSPI/strapping pin).
And it pre-commits to what each outcome means, including the unglamorous one: if it is still blank, power-cycle the whole rig rather than resetting the ESP32, because the ST7920 needs a clean power-on reset and a soldered board that has been reflashed repeatedly without a full power-off can leave the controller wedged.
02:17 — the self-scope
This is the sketch worth stealing.
Every check had passed. Two boards failed the same way. Proven-good code on a proven-good panel produced a faint static line. At that point the question stops being "is the voltage right" and becomes "are these lines actually toggling the way the code intends, right now, during a real draw" — which a multimeter's slow DC sampling cannot answer.
No logic analyzer on the bench. So: use the ESP32 to watch itself.
Three jumper taps onto the existing LCD lines, landing on GPIO 34/35/39 — input-only ADC1 pins, unused everywhere else in the project, so they can be tapped without unsoldering anything. Then interrupt handlers count real edges and track the time between them while the same checkerboard pattern is drawn:
struct EdgeStats {
volatile uint32_t count = 0;
volatile uint32_t lastUs = 0;
volatile uint32_t minDeltaUs = UINT32_MAX;
volatile uint32_t maxDeltaUs = 0;
};
void IRAM_ATTR onEdge(volatile EdgeStats* s) {
uint32_t now = micros();
uint32_t delta = now - s->lastUs;
s->lastUs = now;
s->count++;
if (s->count > 1) { // skip the first bogus delta since boot
if (delta < s->minDeltaUs) s->minDeltaUs = delta;
if (delta > s->maxDeltaUs) s->maxDeltaUs = delta;
}
}Once a second it prints edge counts and min/max microseconds per line. The interpretation was decided before running it, which is the whole point of a diagnostic:
- Thousands of edges per frame on all three lines → the ESP32 is sending everything correctly and the fault is downstream, at the panel.
- Healthy SCLK but near-nothing on MOSI or CS → that specific line has an electrical fault a static multimeter check cannot see.
- A legitimate 500kHz clock should show a tight min/max around 1µs; large outliers mean glitching.
Roughly 1KB of framebuffer per frame gives you a concrete expected edge count to check against, rather than a vibe.
What the night was actually about
Reading it back, the through-line is strapping pins. GPIO5 is a default VSPI pin and a strapping pin. Elsewhere in the same firmware, two more of the same class got fixed:
MODEM_TX_PINmoved off GPIO12 (MTDI), which sets flash voltage at reset and idles HIGH as a UART TX line — confirmed in the field to cause boot loops.BACKLIGHT_PINmoved off GPIO15 (MTDO) to GPIO21, a plain non-strapping GPIO, for the same class of reason.
On an ESP32 the pin map is not a free choice. A pin that is electrically fine can still be wrong because of what the bootloader samples at reset.
The second lesson is cheaper: a multimeter proves a voltage exists. It cannot prove a bit pattern is correct. When every static check passes and the thing still does not work, you need something that observes behaviour over time — and you can build that out of the microcontroller already on the bench.
How it ended, honestly
It works. The firmware that ships today drives the panel over hardware SPI on GPIO 5/19/23 — the same pins that stayed blank all night — and I cannot tell you which single test fixed it.
That is not false modesty. Reading the artifacts back, the fix arrived alongside a pin reshuffle that moved the modem UART off the SPI pins, and two separate strapping-pin corrections landed in the same window. Several things changed before the screen lit up, and I did not isolate the one that mattered.
The honest lesson is the one I keep relearning: bisect until it works, then keep bisecting until you know why. I stopped at the first half. A build log that ends "and then it worked" is a build log with a hole in it, and the hole is mine.
What the night was still worth:
- The self-scope sketch exists now, and it is reusable on any ESP32 project where a bus looks dead and there is no analyser on the bench.
- The strapping-pin failure mode is now something I check first rather than discover at 2am.
- Every hypothesis got its own file, so the reasoning survives even though the conclusion did not.
If you are debugging the same panel: start with the pin map, not the wiring. On an ESP32 a pin that is electrically perfect can still be the wrong pin.