Writing · Field guide
Driving Spectra 6 from an ESP32-S3
If you’re a firmware engineer opening a Waveshare 7.3″ box for the first time, this is the guide we wish existed. Pin map, dual-SPI vs QSPI, power budget, waveform LUTs, dithering, OTA — with the specific gotchas that cost us weeks in the field.
Published 2026-07-30. Based on the stack we ship in 8frames, a Spectra 6 consumer product. Assumes ESP32-S3-WROOM-1 (16 MB flash, 8 MB PSRAM) and a Good Display GDEP073E01 (7.3″, 800×480) or GDEP133C02 (13.3″, 1600×1200) module.
The happy path, in one paragraph
Wire the panel in single-SPI mode for the 7.3″ (GDEP073E01) and dual-SPI for the 13.3″ (GDEP133C02). Send the initialization sequence from Good Display’s reference. Push a full 800×480×4-bit framebuffer (192 kB) or the 13.3″ equivalent (1.9 MB — plan for it), issue the display-update command, wait for BUSY to go idle (~20–30 s), then immediately put the panel into deep-sleep before you cut power. That last step is what keeps Spectra 6 panels alive past 100k refreshes.
Pin map — the one we ship
The ESP32-S3-WROOM-1 has enough IO to give the panel its own dedicated SPI bus without stealing pins the rest of the product wants. Ours:
| Signal | ESP32-S3 pin | Notes |
|---|---|---|
| SCK | GPIO 12 | SPI clock, up to 20 MHz |
| MOSI | GPIO 11 | Panel is write-only, no MISO |
| CS | GPIO 10 | Manual CS, hold low for full frame |
| DC | GPIO 8 | 0=command, 1=data |
| RST | GPIO 7 | Toggle low >10 ms on power-up |
| BUSY | GPIO 6 | Input, high=busy, watch with GPIO ISR |
| PWR_EN | GPIO 5 | Cuts the panel’s DC-DC after sleep cmd |
Reserve GPIO 15–18 for QSPI PSRAM on the WROOM-1 module. Do not reuse them. Avoid GPIO 26–32 (SPI flash on some variants).
Dual-SPI and QSPI: when they matter
The 7.3″ GDEP073E01 clocks in fine on single-SPI at 20 MHz — a full frame pushes in around 80 ms plus the 20-second waveform, so the SPI time is noise. The 13.3″ GDEP133C02 is a different story: 1.9 MB per frame at 20 MHz single-SPI is over 800 ms, which chews into your deep-sleep-per-hour battery budget on a device that refreshes every few minutes. Dual-SPI halves it. On the 31.5″ GDEP315C01, QSPI isn’t optional — the frame is 5.5 MB.
Practical rule: Single-SPI ≤ 7.3″. Dual-SPI for 10″ and 13.3″. QSPI for 25″ and up.
Power budget — the number that decides your battery
Spectra 6 draws ~50 mA average during a refresh, for 20–30 s. Call it 400 mAs (0.11 mAh) per refresh. That’s dominated by the panel’s own DC-DC, not the ESP32-S3. Between refreshes, the ESP32-S3 in deep-sleep with RTC retention draws ~10 µA if you cut power to the panel properly with a PMOS or load switch.
Napkin math for a 3000 mAh Li-ion cell, with Wi-Fi wake budgeted at 3 mAh per refresh (a few seconds of DTIM listen + push):
| Refresh cadence | Runtime on 3 Ah |
|---|---|
| Every 5 min | ~1 month |
| Every hour | ~1 year |
| Every 6 hours | ~4 years |
| Once per day | ~8 years (battery self-discharge wins first) |
“Once a day” devices are essentially battery-limited by self-discharge and calendar aging, not by the display. That’s the sweet spot for shelf-edge and industrial e-labels.
Waveform LUTs and lot drift
Spectra 6 ships with a stock waveform lookup table from E Ink / Good Display. It works. What it doesn’t do is stay optimal across panel lots — the reds pull orange and the yellows go greenish over a few thousand refreshes if you run stock waveforms across a batch. In production we tune LUTs per lot using a color chart on the first-off unit, then bake the tuned LUT into the firmware image for that lot. It’s a couple of hours of work per lot and buys you consistent color across your rollout.
Ghosting
Spectra 6 ghosts less than Gallery 3 but more than BWRY. Running an occasional “clear” refresh (all-white, then all-black, then the new frame) every ~50 refreshes resets accumulated ghost. Users perceive the clear cycle as a slightly longer refresh, not a defect.
Dithering that doesn’t look muddy
Six pigments is enough for illustrated art and brand marks without dithering, and enough for photography with dithering that respects the pigment set. We use error-diffusion (Floyd-Steinberg or Atkinson) constrained to the six pigments, with two production tricks:
- Pre-warp saturation. Boost input saturation by ~15% before dithering. Reflective panels always look less saturated than an emissive preview; the boost compensates.
- Dither in the browser. We do the dithering client-side in JavaScript before upload. The browser preview is what you see on the panel, minus a few percent. Zero server load; users learn the pigment palette’s limits by trying.
OTA and factory reset
Two partitions, A/B image switching, signed manifests. Nothing exotic. The specific decisions that matter for e-paper products:
- Update on refresh wake, not on schedule.The device is already awake with Wi-Fi up for the content fetch. Piggyback the version check on the same wake to keep battery impact near zero.
- Never brick on a bad image. Boot into the previous slot after two failed boots on the new slot. Field devices you can’t easily reflash need this to be right.
- Factory-reset draws a QR code on the panel.The QR carries the device serial and a one-time re-pairing URL. Recover from a lost Wi-Fi credential in seconds without a phone-side app.
What we open-sourced, what we didn’t
The dithering pipeline and the Wi-Fi provisioning flow live in our GitHub org as reference implementations under MIT. The tuned Spectra 6 waveform LUTs, the deep-sleep power management, and the OTA fleet code are our production stack — we license those to customers as source, non-exclusively, as part of an engagement.