Firmware build check / build-check (push) Successful in 2m44s
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown() only exists on
ESP32-C6 (SOC_GPIO_SUPPORT_HP_PERIPH_PD_SLEEP_WAKEUP), so the ee02
(ESP32-S3) build failed with implicit-declaration errors in
{back,next,combo}_button.c once the epd13in3e driver's #error stopped
masking it.
Each button file now branches on that capability macro: the C6 path
(devkit/xiao) is untouched, and ESP32-S3 uses
esp_sleep_enable_ext1_wakeup_io() instead. The earlier ext1 attempt was
rejected on C6 hardware because its pull resistor didn't hold across
RTC_PERIPH power-down -- tracing the same path in ESP-IDF source shows
gpio_config()'s pull_up_en already delegates to rtc_gpio_pullup_en()
for RTC-capable pins on every non-original-ESP32 target, so the pull-up
should already survive the same power-down on S3. The _io() variant is
additive, so the three button files don't need cross-file mask
coordination. Also widens the button GPIO Kconfig range for
IDF_TARGET_ESP32S3 (0-21, matching its RTC-IO set) instead of the
C6-shaped 0-7.
Verified: ee02, devkit, and xiao all build clean end-to-end locally
(native ESP-IDF v6.0, no Docker in this sandbox). NOT verified: whether
this actually avoids the spurious-instant-wakeup bug on real EE02
hardware -- that failure mode was only ever confirmed empirically, not
root-caused in a way a compile can check. continue-on-error stays on
in CI's ee02 build step until that's confirmed.
275 lines
15 KiB
Markdown
275 lines
15 KiB
Markdown
# Hardware
|
|
|
|
## Parts
|
|
|
|
- An ESP32-C6 dev board (e.g. ESP32-C6-DevKitC-1). Needs 8MB flash --
|
|
see [`firmware/sdkconfig.defaults`](../firmware/sdkconfig.defaults) and
|
|
[`firmware/partitions.csv`](../firmware/partitions.csv) if yours differs.
|
|
- [Waveshare 7.3" E Ink Spectra 6 (E6) panel](https://www.waveshare.com/7.3inch-e-paper-hat-e.htm) --
|
|
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`](../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`](../firmware/README.md#skipping-to-the-next-photo).
|
|
- **Back photo (GPIO0)**: a normal press returns to the previously-shown
|
|
photo; see
|
|
[`firmware/README.md`](../firmware/README.md#going-back-to-the-previous-photo).
|
|
- **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`](../firmware/README.md#managing-the-queue-soft-resetting-and-factory-resetting).
|
|
|
|
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`](../firmware/README.md#building-for-the-seeed-xiao-esp32-c6-production-board),
|
|
which layers [`firmware/sdkconfig.xiao`](../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](#battery-optional-xiao-esp32-c6) 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`](../firmware/README.md#battery-xiao-esp32-c6)).
|
|
- 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`](../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, `ee02` builds end-to-end; unverified on real hardware)
|
|
|
|
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 now succeeds** (verified locally with a native,
|
|
non-Docker ESP-IDF v6.0 install -- see
|
|
`.claude/skills/build-firmware/SKILL.md`); `firmware/main/{back,next,combo}_button.c`
|
|
used to 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. Each of the
|
|
three button files now branches on that same capability macro: the
|
|
ESP32-C6 path (devkit/xiao) is untouched, and a new ESP32-S3 path uses
|
|
`esp_sleep_enable_ext1_wakeup_io()` (not the non-`_io()`
|
|
`esp_sleep_enable_ext1_wakeup()`, which resets any previously-registered
|
|
mask -- the `_io()` variant is additive, confirmed by reading
|
|
`esp_hw_support/sleep_modes.c`, so the three button files can each keep
|
|
registering their own GPIO independently, no combined-mask coordination
|
|
needed) plus `esp_sleep_get_ext1_wakeup_status()` for the wake-cause
|
|
check. The original C6 EXT1 attempt was rejected on hardware because
|
|
its pull resistor didn't hold across the RTC_PERIPH power-down (see
|
|
`firmware/main/next_button.c`'s `next_button_init()` comment) -- tracing
|
|
the same code path for ESP32-S3 shows `gpio_config()`'s `pull_up_en`
|
|
(already used by all three button files) delegates to
|
|
`rtc_gpio_pullup_en()` for RTC-capable pins on every non-original-ESP32
|
|
target (confirmed in `esp_driver_gpio/gpio.c`: `GPIO_RTCIO_ARE_INDEPENDENT`
|
|
is 1 for both C6 and S3, meaning the digital and RTC pull registers are
|
|
independent hardware and `gpio_config()` already sets the RTC one), so
|
|
the pull-up should already survive the same power-down on ESP32-S3
|
|
without any extra `rtc_gpio_*` calls. That reasoning is verified against
|
|
IDF source, **not against real EE02 hardware** -- a clean compile
|
|
confirms the code builds and links, not that it's actually
|
|
spurious-wakeup-free on a real board. CI's
|
|
`firmware-build-check.yml`/`firmware-release-build.yml`
|
|
`continue-on-error` on this board's step is intentionally still in place
|
|
until that hardware verification happens.
|
|
|
|
Confirmed so far:
|
|
|
|
- Panel: [Waveshare 13.3" e-Paper (E) Spectra 6](https://www.waveshare.com/13.3inch-e-paper-hat-plus-e.htm) --
|
|
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](https://www.seeedstudio.com/XIAO-ePaper-DIY-Kit-EE02-for-13-3-Spectratm-6-E-Ink.html) --
|
|
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](https://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_CLK` |
|
|
| MOSI | 9 | `EPD_PIN_MOSI` |
|
|
| CS (master half) | 44 | `EPD_PIN_CS_MASTER` |
|
|
| CS (slave half) | 41 | `EPD_PIN_CS_SLAVE` |
|
|
| DC | 10 | `EPD_PIN_DC` |
|
|
| RST | 38 | `EPD_PIN_RST` |
|
|
| BUSY | 4 | `EPD_PIN_BUSY` |
|
|
| Panel power-enable | 43 | `EPD_PIN_POWER_EN` |
|
|
|
|
User buttons are reportedly at GPIO 2/3/5, but which physical button
|
|
maps to which logical role (next/back/menu) still isn't confirmed. The
|
|
firmware's button Kconfig options (`FRAME_NEXT_BUTTON_GPIO` etc.,
|
|
`firmware/main/Kconfig.projbuild`) now range to GPIO -1 to 21 under
|
|
`IDF_TARGET_ESP32S3` (the ESP32-S3's own ext1-wakeup-capable RTC-IO
|
|
range) instead of the ESP32-C6-shaped -1 to 7, so GPIO 2/3/5 fit
|
|
regardless -- but `firmware/sdkconfig.ee02` still deliberately doesn't
|
|
override the defaults inherited from the C6 boards (GPIO 2/0/1) until
|
|
the role mapping above is confirmed.
|
|
|
|
SPI clock is reportedly reliable only up to 2MHz on this
|
|
panel/board per the community ESPHome integration (vs. epd7in3e's 4MHz
|
|
default) -- see `firmware/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.
|
|
|
|
Remaining unknowns before trusting this on real hardware: whether the
|
|
ESP32-S3 button-wakeup path above actually avoids a spurious-instant-wakeup
|
|
on a real board (not just compiles), the button-to-role mapping, 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`).
|