Vendored the panel's init/LUT/refresh register sequence from three
independent Waveshare reference drivers for this exact panel+controller
(RaspberryPi/c, ESP32, and the ESP32-S3-ePaper-13.3E6 ESP-IDF example),
which all agree byte-for-byte. The epd13in3e.c #error is gone; it
compiles clean and links (verified via /build-firmware ee02).
That vendor code also revealed the panel's SPI wire raster is a native
1200x1600 (portrait), not 1600x1200 as previously assumed -- rotated 90
degrees from the panel's landscape mount/marketing size. The old
assumption wasn't just a rotation bug: 1600x1200 and 1200x1600 don't
share a row stride, so packing at the wrong one would have shredded
images into a repeating diagonal garble on real hardware, not just
displayed them sideways. Fixed with a new PANEL_WIRE_TRANSPOSE in
image_pipeline.py, applied after the existing per-frame
ORIENTATION_TRANSPOSE, with a direction-agnostic regression test that
catches the stride bug specifically (a byte-count check alone can't,
since both orientations pack to the same total size).
A full ee02 build still fails, but no longer because of this driver --
main/{back,next,combo}_button.c call an ESP32-C6-only deep-sleep
GPIO-wakeup API with no ESP32-S3 fallback, a separate pre-existing gap
that was simply hidden behind the panel driver's old #error. See
docs/hardware.md for details; CI's continue-on-error on this board
stays in place until that's fixed too.
14 KiB
Hardware
Parts
- An ESP32-C6 dev board (e.g. ESP32-C6-DevKitC-1). Needs 8MB flash --
see
firmware/sdkconfig.defaultsandfirmware/partitions.csvif yours differs. - Waveshare 7.3" E Ink Spectra 6 (E6) panel -- 800x480, 6-color, SPI.
- A USB cable for flashing/power.
Wiring
The panel connects over SPI plus three control lines (data/command, reset,
busy). Defaults below match the reference build and are set in
firmware/components/epd7in3e/Kconfig --
override via idf.py menuconfig under E-Paper Display (epd7in3e)
Configuration if your wiring differs.
| Panel pin | ESP32-C6 GPIO | Kconfig option |
|---|---|---|
| CLK | 20 | EPD_PIN_CLK |
| DIN | 19 | EPD_PIN_MOSI |
| CS | 18 | EPD_PIN_CS |
| DC | 9 | EPD_PIN_DC |
| RST | 10 | EPD_PIN_RST |
| BUSY | 11 | EPD_PIN_BUSY |
| VCC | 3.3V | -- |
| GND | GND | -- |
Optionally, three buttons, all wired the same way -- momentary push button between the GPIO and GND, no external resistor needed (the firmware enables each pin's internal pull-up, so it idles high and reads low when pressed):
- Next photo (GPIO2): a normal press skips immediately to the next
photo; see
firmware/README.md. - Back photo (GPIO0): a normal press returns to the previously-shown
photo; see
firmware/README.md. - Menu / reset (GPIO1): one button, three actions by hold duration --
a quick press soft-resets the device (config kept); holding ~3s then
releasing overlays a "scan to manage" QR code on the current photo for
30 seconds; holding ~15s factory-resets it (clears WiFi/server config,
reprovisions); see
firmware/README.md.
All three pins were picked because they're within GPIO 0-7 -- the only
pins the ESP32-C6 can wake from deep sleep on -- aren't strapping pins,
and aren't already used by the panel wiring above. Also, not
incidentally, GPIO 0-7 is all the deep-sleep-wakeup-capable pins this
chip has (SOC_RTCIO_PIN_COUNT is 8) -- worth knowing if you're
targeting a compact board like the Seeed XIAO ESP32-C6, which only
breaks out 3 of them (GPIO0/1/2). These three buttons were deliberately
designed to fit exactly that budget: two dedicated pins for the
actions where instant, unambiguous response matters most (next, back),
and everything else folded onto the third pin via hold duration instead
of needing its own pin.
Wiring on the Seeed XIAO ESP32-C6
The XIAO only breaks out 11 GPIOs (0, 1, 2, 16, 17, 18, 19, 20, 21, 22,
23), so the dev-board defaults above don't fit -- DC/RST/BUSY (GPIO 9,
10, 11) aren't exposed on this board at all. Built with
build_for_board.sh xiao,
which layers firmware/sdkconfig.xiao on
top of the dev-board defaults, remapping DC/RST/BUSY onto three of the
remaining free pins:
| Panel pin | XIAO GPIO | XIAO silkscreen label | Kconfig option |
|---|---|---|---|
| CLK | 20 | D9 | EPD_PIN_CLK (unchanged) |
| DIN | 19 | D8 | EPD_PIN_MOSI (unchanged) |
| CS | 18 | D10 | EPD_PIN_CS (unchanged) |
| DC | 16 | D6 | EPD_PIN_DC |
| RST | 17 | D7 | EPD_PIN_RST |
| BUSY | 21 | D3 | EPD_PIN_BUSY |
| VCC | 3.3V | 3V3 | -- |
| GND | GND | GND | -- |
The XIAO's silkscreen labels its pins D0-D10, not raw GPIO numbers -- the table above gives both.
Buttons (unchanged from the table above -- the XIAO's only three ADC/deep-sleep-capable pins, GPIO 0/1/2, are exactly the ones already used): next=GPIO2/D2, back=GPIO0/D0, menu/reset=GPIO1/D1. That leaves GPIO22/D4 and GPIO23/D5 free, e.g. for the optional VBUS mains-sense divider described under Battery below.
sdkconfig.xiao also sets FRAME_XIAO_ANTENNA_INIT=y, which powers
the XIAO's onboard RF switch and selects its ceramic antenna at boot
(GPIO3/14, internal to the board -- not part of the wiring above).
Without it, WiFi doesn't reliably work on this board at all: the radio
comes up and logs look normal (e.g. the softAP starts and prints its
SSID/password) but the switch's control pins are left floating, so
nothing actually reaches the antenna -- the AP never becomes visible to
a scan, and a station connection would fail to associate the same way.
Seeed's own Arduino board package does this automatically; plain
ESP-IDF (what this firmware uses) doesn't, hence the explicit init.
A couple of things worth knowing if you pick different pins:
- Avoid the ESP32-C6's strapping pins (GPIO 4, 5, 8, 9, 15) and the USB-JTAG pins (12, 13) where possible -- strapping pins are sampled at reset to select boot mode. The reference wiring above already uses GPIO9 for DC, which is a strapping pin; it's only sampled during power-on/reset, so it's safe once the app is running, but if flashing/boot ever misbehaves on your board, check whether the panel is pulling that line low during reset.
- The panel's SPI interface is rated well above the firmware's default
4MHz clock (
EPD_SPI_CLOCK_HZ), but breadboard/dupont-wire connections are often unreliable much past a few MHz. Raise it once your physical wiring is confirmed solid.
Battery (optional, XIAO ESP32-C6)
For a battery-powered build on the Seeed XIAO ESP32-C6 (which has BAT+/BAT- charge pads on its underside and charges over USB-C):
- A 1S 3.7V LiPo with an integrated protection circuit (the board does no low-voltage cutoff of its own), soldered or JST-PH-pigtailed to the BAT pads. JST-PH polarity is not standardized -- verify with a multimeter before connecting.
- Battery level sense: two equal-value resistors in series from BAT+ to
GND (200k is the reference value, but any matched pair works -- it's
a ratio divider, so what matters is the two resistors matching each
other, not the absolute value; higher values just draw less constant
current from the battery, e.g. ~10uA for 200k+200k vs ~2uA for
1M+1M, at the cost of a slightly noisier ADC reading from the higher
source impedance -- not noticeable in practice up to around 1M given
how coarse the percent curve already is). Midpoint to GPIO0/A0
(shared with the back button -- deliberate, see
firmware/README.md). - Optional mains detection: 2x100k divider from the 5V pin to GND, midpoint to a spare GPIO (e.g. 22).
Both features are off by default in firmware
(FRAME_BATTERY_ADC_GPIO/FRAME_VBUS_SENSE_GPIO = -1) -- except
FRAME_BATTERY_ADC_GPIO, which firmware/sdkconfig.xiao
turns on (GPIO0) by default for XIAO builds, since that's the one
board this feature was designed for. Mains detection stays off by
default even there; set FRAME_VBUS_SENSE_GPIO yourself if you wire
that divider too.
Power
The device spends nearly all its time in deep sleep, waking briefly once an hour (configurable, see the server's web UI) to fetch and display a photo. A full-color refresh on this panel takes 15-30+ seconds and draws more current than deep sleep by a wide margin -- expect battery life (if not running from USB power) to be dominated by refresh frequency, not sleep current.
Board identifiers
Each board reports a name to the server (X-Frame-Board,
CONFIG_FRAME_BOARD_NAME) that's chip-qualified rather than the plain
devkit/xiao older firmware used -- devkit_esp32c6, xiao_esp32c6,
ee02 (see below). This changed once a second XIAO-based board (EE02,
an ESP32-S3) existed and "xiao" alone stopped disambiguating hardware.
The server keeps accepting the old bare names indefinitely, since
already-flashed devices can't be retroactively renamed.
13.3" Spectra 6 panel on Seeed's EE02 board (panel driver ported; board still doesn't build end-to-end)
A second panel size is supported server-side (the web UI shows a
read-only "Panel: 13.3" Spectra 6" once a frame's device reports
itself as ee02), and the panel driver itself is now real and
compiles clean -- firmware/components/epd13in3e's init/LUT/refresh
register sequence is a line-for-line port of Waveshare's own reference
drivers for this exact panel+controller, confirmed identically across
three independent vendor sources (Waveshare's RaspberryPi/c and ESP32
drivers for this panel, plus Waveshare's own ESP-IDF example for their
ESP32-S3-ePaper-13.3E6 driver board -- a different carrier than EE02,
but the same panel/controller, hence the same command bytes). See that
component's own top comment for details, and
server/app/image_pipeline.py's PANEL_WIRE_TRANSPOSE for a load-bearing
correction that came with it: the panel's SPI wire raster is a native
1200x1600 (portrait) raster, rotated 90 degrees from the panel's
1600x1200 landscape mount/marketing size -- getting that backwards
doesn't just rotate the image, it shreds it (1600x1200 and 1200x1600
don't share a row stride).
A full ee02 build still fails, but for an unrelated, pre-existing
reason now that the panel driver's own #error no longer stops the
build early: firmware/main/{back,next,combo}_button.c call
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(), an ESP32-C6-only
deep-sleep GPIO-wakeup API (gated by SOC_GPIO_SUPPORT_HP_PERIPH_PD_SLEEP_WAKEUP,
which ESP32-S3's soc_caps.h doesn't define) with no ESP32-S3 fallback
path. This was always broken for ee02, just masked behind the panel
driver's own compile failure -- see the note two paragraphs up about the
button GPIO range not yet being verified against the ESP32-S3, which was
already flagging this same gap before it had an actual compile error
attached to it. ESP32-S3 does support GPIO deep-sleep wakeup via
esp_sleep_enable_ext1_wakeup()/esp_sleep_get_ext1_wakeup_status()
(and so, notably, does ESP32-C6 -- SOC_PM_SUPPORT_EXT1_WAKEUP is set on
both chips), but EXT1 wakeup takes one combined GPIO mask/mode across all
three button files' independent calls rather than each button
registering its own -- porting to it is a real (if likely small)
cross-board refactor, not a mechanical swap, and hasn't been done. CI's
firmware-build-check.yml/firmware-release-build.yml continue-on-error
on this board's step stays in place until it is.
Confirmed so far:
-
Panel: Waveshare 13.3" e-Paper (E) Spectra 6 -- 1600x1200 mount size, 270.40x202.80mm, same 6-ink Spectra family as the 7.3" panel (and, now vendor-confirmed, the identical 4-bit nibble color codes). Full refresh ~19s. SPI wire raster is 1200x1600 (see above).
-
Board: Seeed's EE02 -- a XIAO ESP32-S3 Plus (16MB flash, 8MB PSRAM) socketed into a dedicated driver PCB, one reset + three user buttons, JST 2.0mm battery connector with built-in charging IC.
-
Wiring (source: github.com/rkaramandi/esphome-seeed-ee02, a community integration, not Seeed's own schematic -- treat as a starting point, confirm before relying on it; Waveshare's own ESP32-S3-ePaper-13.3E6 example uses different GPIO numbers, but that's for Waveshare's own driver board, a different carrier than EE02, so it doesn't apply here). Unlike epd7in3e's single chip-select, this panel is driven as two halves sharing one CLK/MOSI/DC/RST/BUSY bus with independent chip-selects -- now confirmed by the real driver code too (master = left half, slave = right half of each row).
Signal GPIO Kconfig option CLK 7 EPD_PIN_CLKMOSI 9 EPD_PIN_MOSICS (master half) 44 EPD_PIN_CS_MASTERCS (slave half) 41 EPD_PIN_CS_SLAVEDC 10 EPD_PIN_DCRST 38 EPD_PIN_RSTBUSY 4 EPD_PIN_BUSYPanel power-enable 43 EPD_PIN_POWER_ENUser buttons are reportedly at GPIO 2/3/5, but which physical button maps to which logical role (next/back/menu) isn't confirmed, and the firmware's button Kconfig options (
FRAME_NEXT_BUTTON_GPIOetc.,firmware/main/Kconfig.projbuild) still range-limit to GPIO 0-7 -- the ESP32-C6's deep-sleep-wakeup-capable set (see the build-blocker note above). SPI clock is reportedly reliable only up to 2MHz on this panel/board per the community ESPHome integration (vs. epd7in3e's 4MHz default) -- seefirmware/sdkconfig.ee02. Waveshare's own ESP32-S3-ePaper-13.3E6 example defaults to 10MHz, but that's a different carrier board, so it's a data point to try once real EE02 hardware exists, not a reason to bump the current conservative default blind.
Once the button-wakeup gap above is fixed, remaining unknowns before
trusting this on real hardware: the wiring table (community-sourced, not
official), and PANEL_WIRE_TRANSPOSE's rotation direction
(ROTATE_90 vs ROTATE_270 -- a physical-assembly fact no vendor driver
encodes, see that dict's own comment in image_pipeline.py).