Port real epd13in3e driver from vendor code; fix wire-raster stride bug
Build and push server image / test (push) Successful in 43s
Firmware build check / build-check (push) Successful in 2m47s
Build and push server image / build-and-push (push) Successful in 4m34s
Build and push server image / deploy (push) Failing after 1m27s

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.
This commit is contained in:
2026-08-04 22:00:18 +00:00
parent fe5a2df074
commit 454c03586e
6 changed files with 464 additions and 160 deletions
+23 -14
View File
@@ -2,9 +2,9 @@
ESP-IDF firmware for the ESP32-C6 (devkit/xiao boards, 7.3" panel) or
ESP32-S3 (ee02 board, 13.3" panel -- see
[Building for Seeed's EE02](#building-for-seeeds-ee02-esp32-s3--133-panel-driver-not-yet-functional)
below; the driver for that panel doesn't work yet). On first boot it
provisions itself over
[Building for Seeed's EE02](#building-for-seeeds-ee02-esp32-s3--133-panel-driver-ported-board-still-doesnt-build-end-to-end)
below; the panel driver itself works now, but the board doesn't build
end-to-end yet for an unrelated reason). On first boot it provisions itself over
a WiFi captive portal; after that it wakes on a timer, fetches an
already-processed frame from the [server](../server/), streams it straight
to the panel over SPI, and goes back to deep sleep.
@@ -52,7 +52,7 @@ never clobbers the other:
(`./build_for_board.sh devkit ...` does the same for the dev board --
equivalent to a plain `idf.py`, just consistent with the XIAO invocation.)
### Building for Seeed's EE02 (ESP32-S3 + 13.3" panel, driver not yet functional)
### Building for Seeed's EE02 (ESP32-S3 + 13.3" panel, driver ported; board still doesn't build end-to-end)
EE02 is a different chip (ESP32-S3, not C6), so it needs `set-target
esp32s3` instead of `esp32c6`, and its own partition table/flash-size
@@ -64,16 +64,25 @@ Kconfig sized for its 16MB flash
./build_for_board.sh ee02 build
```
**This will fail to compile.** `firmware/components/epd13in3e`'s panel
init/LUT/refresh register sequence hasn't been ported from vendor demo
code yet (see that file's own top comment and
[`docs/hardware.md`](../docs/hardware.md#133-spectra-6-panel-on-seeeds-ee02-board-driver-not-yet-functional))
-- a deliberate `#error`, not a bug in this build path. Everything
around it (target selection, Kconfig, partition table, sdkconfig
layering, `main/CMakeLists.txt`'s component selection) is in place and
exercised by CI (`.gitea/workflows/firmware-build-check.yml`/
`firmware-release-build.yml`, both with `continue-on-error` on this
board's step until the driver is real).
**This still fails to compile, but no longer because of the panel
driver.** `firmware/components/epd13in3e`'s panel init/LUT/refresh
register sequence is now a real, vendor-confirmed port (see that
component's own top comment and
[`docs/hardware.md`](../docs/hardware.md#133-spectra-6-panel-on-seeeds-ee02-board-panel-driver-ported-board-still-doesnt-build-end-to-end)
for the vendor sources and the load-bearing native-raster-orientation
correction that came with it) -- it compiles clean on its own. The build
now fails one step later, in `main/{back,next,combo}_button.c`: they call
an ESP32-C6-only deep-sleep GPIO-wakeup API with no ESP32-S3 fallback, a
pre-existing gap that the panel driver's old `#error` simply stopped the
build before reaching. See `docs/hardware.md`'s same section for what a
real fix looks like (not a mechanical swap -- ESP32-S3's equivalent API
takes one combined mask across all three button files' independent
calls). Everything else around the panel driver (target selection,
Kconfig, partition table, sdkconfig layering, `main/CMakeLists.txt`'s
component selection) is in place and exercised by CI
(`.gitea/workflows/firmware-build-check.yml`/`firmware-release-build.yml`,
both with `continue-on-error` on this board's step until a full build is
actually green).
## Configuration (`idf.py menuconfig`)
+235 -89
View File
@@ -2,6 +2,7 @@
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_check.h"
@@ -10,36 +11,31 @@
#include "epd13in3e.h"
/* This component only ever gets compiled in when CONFIG_FRAME_PANEL_EE02_
* 13IN3=y selects it as main/CMakeLists.txt's linked EPD driver (see
* main/epd_board.h) -- i.e. only when someone deliberately builds for the
* EE02 board. epd7in3e.c's own top comment explains why its exact
* command bytes/register values are trustworthy: they're a line-for-line
* transcription of Waveshare's own reference driver, since this class of
* panel controller has no public datasheet. No equivalent reference
* driver for the 13.3" panel + EE02 exists in this tree yet, and guessing
* at register/LUT/timing values is not a safe substitute -- wrong values
* can under-refresh (ghosting) or over-drive a real panel. Get Waveshare's
* or Seeed's official demo/reference code for this exact panel+board
* combo, port its command sequences the same way epd7in3e.c's were
* ported, and remove this #error as part of that. The SPI/GPIO plumbing
* below (bus init, busy-wait, chunked writes, the streaming/CRC contract)
* is NOT panel-specific and should carry over unchanged once that
* happens -- only the register sequences inside epd_init()/
* epd_turn_on_display()/epd_sleep() need real vendor values.
/* Command bytes/register values below are a line-for-line transcription of
* Waveshare's official reference drivers for this exact panel+controller --
* confirmed identical across three independent sources (RaspberryPi/c,
* ESP32, and the ESP32-S3-ePaper-13.3E6 ESP-IDF example; see this
* component's header for repo paths). Same "don't clean these up" rule as
* epd7in3e.c: this class of panel controller has no public datasheet, so
* the vendor driver is the source of truth for every byte.
*
* One more thing the real driver logic will need to account for, beyond
* epd7in3e.c's shape: this panel is driven as two halves sharing one
* CLK/MOSI/DC/RST/BUSY bus but with two independent chip-selects
* (EPD_PIN_CS_MASTER/EPD_PIN_CS_SLAVE, see Kconfig) -- epd_send_command/
* epd_send_data below still only assert CS_MASTER, which is wrong for
* whichever commands/data need to go to the slave half instead. Source
* for the dual-CS pinout itself (not the command sequence) is a
* community-verified ESPHome integration for this exact board
* (github.com/rkaramandi/esphome-seeed-ee02), not an official Seeed/
* Waveshare reference -- treat it as a reasonable starting point, not
* gospel, until confirmed against real hardware. */
#error "epd13in3e: panel init/LUT/refresh register sequence not yet ported from vendor demo code -- see this file's top comment"
* Unlike epd7in3e's single chip-select, this panel is driven as two
* independent controllers sharing one CLK/MOSI/DC/RST/BUSY bus but with
* separate chip-selects (EPD_PIN_CS_MASTER/EPD_PIN_CS_SLAVE) -- most init
* commands broadcast to both (CS_ALL), a handful of power/boost commands
* go only to the master (which owns the shared analog rails), and actual
* frame data is split per-row into a left half (master) and right half
* (slave), 300 bytes each out of each 600-byte row. That split is why
* epd_write_frame below buffers the whole frame in PSRAM before sending
* anything (every row needs slicing in half before either half can go out),
* unlike epd7in3e.c's straight single-CS passthrough streaming.
*
* Pin numbers themselves are NOT from this vendor code -- Waveshare's
* ESP32-S3-ePaper-13.3E6 example targets Waveshare's own driver board, a
* different carrier than Seeed's EE02 this project actually uses, so its
* GPIO numbers don't apply here. EE02's pins remain sourced from a
* community-verified ESPHome integration (see this component's Kconfig),
* not an official reference. */
#define EPD_SPI_HOST SPI2_HOST
#define EPD_SPI_CHUNK_SIZE 4096
@@ -48,6 +44,49 @@ static const char *TAG = "epd13in3e";
#define EPD_CHECK(expr) ESP_RETURN_ON_ERROR((expr), TAG, #expr)
/* --- panel command opcodes --- */
#define PSR 0x00
#define PWR 0x01
#define POF 0x02
#define PON 0x04
#define BTST_N 0x05
#define BTST_P 0x06
#define DTM 0x10 /* data transfer (frame data) */
#define DRF 0x12 /* display refresh */
#define CDI 0x50
#define TCON 0x60
#define TRES 0x61
#define AN_TM 0x74
#define AGID 0x86
#define BUCK_BOOST_VDDN 0xB0
#define TFT_VCOM_POWER 0xB1
#define EN_BUF 0xB6
#define BOOST_VDDP_EN 0xB7
#define CCSET 0xE0
#define PWS 0xE3
#define CMD66 0xF0
#define DEEP_SLEEP 0x07
/* --- canned init parameter blobs (do NOT edit -- see top comment) --- */
static const uint8_t PSR_V[] = {0xDF, 0x69};
static const uint8_t PWR_V[] = {0x0F, 0x00, 0x28, 0x2C, 0x28, 0x38};
static const uint8_t POF_V[] = {0x00};
static const uint8_t DRF_V[] = {0x00};
static const uint8_t CDI_V[] = {0xF7};
static const uint8_t TCON_V[] = {0x03, 0x03};
static const uint8_t TRES_V[] = {0x04, 0xB0, 0x03, 0x20};
static const uint8_t CMD66_V[] = {0x49, 0x55, 0x13, 0x5D, 0x05, 0x10};
static const uint8_t EN_BUF_V[] = {0x07};
static const uint8_t CCSET_V[] = {0x01};
static const uint8_t PWS_V[] = {0x22};
static const uint8_t AN_TM_V[] = {0xC0, 0x1C, 0x1C, 0xCC, 0xCC, 0xCC, 0x15, 0x15, 0x55};
static const uint8_t AGID_V[] = {0x10};
static const uint8_t BTST_P_V[] = {0xE8, 0x28};
static const uint8_t BOOST_VDDP_EN_V[] = {0x01};
static const uint8_t BTST_N_V[] = {0xE8, 0x28};
static const uint8_t BUCK_BOOST_VDDN_V[] = {0x01};
static const uint8_t TFT_VCOM_POWER_V[] = {0x02};
static spi_device_handle_t s_spi;
static void epd_delay_ms(uint32_t ms)
@@ -55,10 +94,9 @@ static void epd_delay_ms(uint32_t ms)
vTaskDelay(pdMS_TO_TICKS(ms));
}
/* BUSY: LOW = busy, HIGH = idle -- same polarity convention as epd7in3e.c;
* confirm against the vendor demo code once it exists (some EPD
* controllers invert this). See epd7in3e.c's own comment for why this
* polls in >= 1 FreeRTOS tick increments rather than a tight spin. */
/* BUSY: LOW = busy, HIGH = idle -- same polarity/poll-interval reasoning
* as epd7in3e.c's identical comment (a tight 1ms-rounds-to-0-ticks poll
* starves the idle task badly enough to trip the watchdog). */
static void epd_wait_busy(void)
{
while (gpio_get_level((gpio_num_t)CONFIG_EPD_PIN_BUSY) == 0) {
@@ -81,22 +119,20 @@ static esp_err_t epd_spi_write(const uint8_t *data, size_t len)
return ESP_OK;
}
/* Unlike epd7in3e.c's send_command/send_data, these do NOT touch CS --
* this panel's two independent chip-selects (and the "broadcast to both"
* vs "master only" split the init sequence needs) mean CS bracketing has
* to be the caller's decision, not baked into the byte-send primitive. */
static esp_err_t epd_send_command(uint8_t cmd)
{
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_DC, 0);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_MASTER, 0);
esp_err_t err = epd_spi_write(&cmd, 1);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_MASTER, 1);
return err;
return epd_spi_write(&cmd, 1);
}
static esp_err_t epd_send_data(const uint8_t *data, size_t len)
{
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_DC, 1);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_MASTER, 0);
esp_err_t err = epd_spi_write(data, len);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_MASTER, 1);
return err;
return epd_spi_write(data, len);
}
static esp_err_t epd_send_data_byte(uint8_t data)
@@ -104,36 +140,74 @@ static esp_err_t epd_send_data_byte(uint8_t data)
return epd_send_data(&data, 1);
}
static void epd_cs_both(int level)
{
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_MASTER, level);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_SLAVE, level);
}
/* Sends `cmd` + its data blob to both controllers at once (most of the
* init sequence -- shared display-timing/power registers). */
static esp_err_t epd_cmd_both(uint8_t cmd, const uint8_t *data, size_t len)
{
epd_cs_both(0);
esp_err_t err = epd_send_command(cmd);
if (err == ESP_OK && data != NULL) {
err = epd_send_data(data, len);
}
epd_cs_both(1);
return err;
}
/* Sends `cmd` + its data blob to the master controller only -- the boost/
* VCOM power registers the master alone owns. */
static esp_err_t epd_cmd_master(uint8_t cmd, const uint8_t *data, size_t len)
{
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_MASTER, 0);
esp_err_t err = epd_send_command(cmd);
if (err == ESP_OK && data != NULL) {
err = epd_send_data(data, len);
}
epd_cs_both(1);
return err;
}
/* 5-edge reset sequence (30ms each) -- per-vendor-source exact, more edges
* than epd7in3e.c's 3-edge/20ms reset. */
static void epd_reset(void)
{
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_RST, 1);
epd_delay_ms(20);
epd_delay_ms(30);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_RST, 0);
epd_delay_ms(2);
epd_delay_ms(30);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_RST, 1);
epd_delay_ms(20);
epd_delay_ms(30);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_RST, 0);
epd_delay_ms(30);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_RST, 1);
epd_delay_ms(30);
}
/* TODO(epd13in3e): power-on/refresh/power-off register sequence -- see
* this file's top #error. epd7in3e.c's epd_turn_on_display() is the
* shape to mirror once the real command bytes are known. */
/* Power on, refresh, power off -- mirrors EPD_TurnOnDisplay()/
* EPD_13IN3E_TurnOnDisplay() in the reference drivers. */
esp_err_t epd_turn_on_display(void)
{
(void)epd_send_command;
(void)epd_send_data;
(void)epd_send_data_byte;
(void)epd_wait_busy;
return ESP_ERR_NOT_SUPPORTED;
EPD_CHECK(epd_cmd_both(PON, NULL, 0));
epd_wait_busy();
epd_delay_ms(50);
EPD_CHECK(epd_cmd_both(DRF, DRF_V, sizeof(DRF_V)));
epd_wait_busy();
epd_delay_ms(50);
EPD_CHECK(epd_cmd_both(POF, POF_V, sizeof(POF_V)));
/* No busy-wait after POF -- matches every reference driver. */
return ESP_OK;
}
esp_err_t epd_init(void)
{
/* CS_SLAVE and POWER_EN are configured as outputs here (safe,
* mechanical) but not yet driven anywhere below -- the dual-CS
* command routing and whatever power-on-vs-reset sequencing
* POWER_EN needs are both part of the still-unported vendor
* register sequence (see this file's top comment), not something
* to guess at. */
gpio_config_t out_cfg = {
.pin_bit_mask = (1ULL << CONFIG_EPD_PIN_DC) | (1ULL << CONFIG_EPD_PIN_RST) |
(1ULL << CONFIG_EPD_PIN_CS_MASTER) | (1ULL << CONFIG_EPD_PIN_CS_SLAVE) |
@@ -148,8 +222,9 @@ esp_err_t epd_init(void)
};
EPD_CHECK(gpio_config(&busy_cfg));
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_MASTER, 1);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_SLAVE, 1);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_RST, 1);
epd_cs_both(1);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_POWER_EN, 0);
spi_bus_config_t bus_cfg = {
.mosi_io_num = CONFIG_EPD_PIN_MOSI,
@@ -164,59 +239,123 @@ esp_err_t epd_init(void)
spi_device_interface_config_t dev_cfg = {
.clock_speed_hz = CONFIG_EPD_SPI_CLOCK_HZ,
.mode = 0,
.spics_io_num = -1,
.spics_io_num = -1, /* both chip-selects are bit-banged by hand above */
.queue_size = 1,
};
EPD_CHECK(spi_bus_add_device(EPD_SPI_HOST, &dev_cfg, &s_spi));
/* Panel power-enable rail (no equivalent on epd7in3e's board -- EE02
* gates it separately from the ESP32-S3 module's own supply). */
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_POWER_EN, 1);
epd_delay_ms(10);
epd_reset();
epd_wait_busy();
epd_delay_ms(30);
/* TODO(epd13in3e): panel-specific power-on register sequence goes
* here, see this file's top #error. */
ESP_LOGE(TAG, "epd_init: panel register sequence not yet ported, EPD will not actually work");
/* Master-only: shared analog-timing register. */
EPD_CHECK(epd_cmd_master(AN_TM, AN_TM_V, sizeof(AN_TM_V)));
return ESP_ERR_NOT_SUPPORTED;
/* Broadcast: display-timing/power-sequencing registers both
* controllers need identically. */
EPD_CHECK(epd_cmd_both(CMD66, CMD66_V, sizeof(CMD66_V)));
EPD_CHECK(epd_cmd_both(PSR, PSR_V, sizeof(PSR_V)));
EPD_CHECK(epd_cmd_both(CDI, CDI_V, sizeof(CDI_V)));
EPD_CHECK(epd_cmd_both(TCON, TCON_V, sizeof(TCON_V)));
EPD_CHECK(epd_cmd_both(AGID, AGID_V, sizeof(AGID_V)));
EPD_CHECK(epd_cmd_both(PWS, PWS_V, sizeof(PWS_V)));
EPD_CHECK(epd_cmd_both(CCSET, CCSET_V, sizeof(CCSET_V)));
EPD_CHECK(epd_cmd_both(TRES, TRES_V, sizeof(TRES_V)));
/* Master-only: boost/VCOM power programming. */
EPD_CHECK(epd_cmd_master(PWR, PWR_V, sizeof(PWR_V)));
EPD_CHECK(epd_cmd_master(EN_BUF, EN_BUF_V, sizeof(EN_BUF_V)));
EPD_CHECK(epd_cmd_master(BTST_P, BTST_P_V, sizeof(BTST_P_V)));
EPD_CHECK(epd_cmd_master(BOOST_VDDP_EN, BOOST_VDDP_EN_V, sizeof(BOOST_VDDP_EN_V)));
EPD_CHECK(epd_cmd_master(BTST_N, BTST_N_V, sizeof(BTST_N_V)));
EPD_CHECK(epd_cmd_master(BUCK_BOOST_VDDN, BUCK_BOOST_VDDN_V, sizeof(BUCK_BOOST_VDDN_V)));
EPD_CHECK(epd_cmd_master(TFT_VCOM_POWER, TFT_VCOM_POWER_V, sizeof(TFT_VCOM_POWER_V)));
ESP_LOGI(TAG, "EPD initialized (CLK=%d MOSI=%d CS_M=%d CS_S=%d DC=%d RST=%d BUSY=%d PWR_EN=%d)",
CONFIG_EPD_PIN_CLK, CONFIG_EPD_PIN_MOSI, CONFIG_EPD_PIN_CS_MASTER,
CONFIG_EPD_PIN_CS_SLAVE, CONFIG_EPD_PIN_DC, CONFIG_EPD_PIN_RST,
CONFIG_EPD_PIN_BUSY, CONFIG_EPD_PIN_POWER_EN);
return ESP_OK;
}
esp_err_t epd_write_frame(epd_read_fn_t read_fn, void *ctx, uint32_t *out_crc32)
{
ESP_RETURN_ON_FALSE(read_fn != NULL, ESP_ERR_INVALID_ARG, TAG, "read_fn required");
EPD_CHECK(epd_send_command(0x10));
/* Every row has to be sliced into a left (master) and right (slave)
* half before either half can go out over SPI, so -- unlike
* epd7in3e.c's single-CS passthrough -- bytes can't be forwarded to
* the wire as they arrive. Buffer the whole ~938KB frame in PSRAM
* first (EE02's XIAO ESP32-S3 Plus has 8MB of it). */
uint8_t *frame = heap_caps_malloc(EPD_FRAME_BYTES, MALLOC_CAP_SPIRAM);
if (frame == NULL) {
ESP_LOGE(TAG, "OOM allocating %u-byte frame buffer", (unsigned)EPD_FRAME_BYTES);
return ESP_ERR_NO_MEM;
}
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_DC, 1);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_MASTER, 0);
/* Static rather than a stack local -- see epd7in3e.c's identical
* comment on why (default main task stack is smaller than this
* chunk buffer alone). */
static uint8_t chunk[EPD_SPI_CHUNK_SIZE];
size_t total = 0;
uint32_t crc = 0;
size_t n;
esp_err_t err = ESP_OK;
while ((n = read_fn(chunk, sizeof(chunk), ctx)) > 0) {
err = epd_spi_write(chunk, n);
if (err != ESP_OK) {
break;
}
crc = esp_rom_crc32_le(crc, chunk, n);
while (total < EPD_FRAME_BYTES &&
(n = read_fn(frame + total, EPD_FRAME_BYTES - total, ctx)) > 0) {
crc = esp_rom_crc32_le(crc, frame + total, n);
total += n;
}
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_MASTER, 1);
EPD_CHECK(err);
if (total != EPD_FRAME_BYTES) {
/* Same invariant as epd7in3e.c: never trigger a refresh on a
* short/wrong-size stream. */
/* Same invariant as epd7in3e.c: never touch the panel on a
* short/wrong-size stream -- the visible screen is left exactly
* as it was. */
ESP_LOGE(TAG, "Stream supplied %u bytes, expected %u -- aborting refresh",
(unsigned)total, (unsigned)EPD_FRAME_BYTES);
free(frame);
return ESP_ERR_INVALID_SIZE;
}
/* De-interleave into one half-buffer at a time and DMA it out as a
* single contiguous transfer (chunked internally by epd_spi_write) --
* far fewer, far larger SPI transactions than sending 1600 separate
* 300-byte rows per side. */
const size_t HALF_ROW = EPD_BYTES_PER_ROW / 2; /* 300 */
const size_t HALF_BUF = HALF_ROW * EPD_HEIGHT; /* 480000 */
uint8_t *half = heap_caps_malloc(HALF_BUF, MALLOC_CAP_SPIRAM);
if (half == NULL) {
ESP_LOGE(TAG, "OOM allocating %u-byte half-frame scratch buffer", (unsigned)HALF_BUF);
free(frame);
return ESP_ERR_NO_MEM;
}
for (size_t r = 0; r < EPD_HEIGHT; r++) {
memcpy(half + r * HALF_ROW, frame + r * EPD_BYTES_PER_ROW, HALF_ROW);
}
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_MASTER, 0);
esp_err_t err = epd_send_command(DTM);
if (err == ESP_OK) {
err = epd_send_data(half, HALF_BUF);
}
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_MASTER, 1);
if (err == ESP_OK) {
for (size_t r = 0; r < EPD_HEIGHT; r++) {
memcpy(half + r * HALF_ROW, frame + r * EPD_BYTES_PER_ROW + HALF_ROW, HALF_ROW);
}
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_SLAVE, 0);
err = epd_send_command(DTM);
if (err == ESP_OK) {
err = epd_send_data(half, HALF_BUF);
}
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS_SLAVE, 1);
}
free(half);
free(frame);
EPD_CHECK(err);
if (out_crc32 != NULL) {
*out_crc32 = crc;
}
@@ -284,9 +423,16 @@ esp_err_t epd_clear(epd_color_t color)
return epd_display_stream(epd_fill_read, &fill_ctx);
}
/* TODO(epd13in3e): power-off/deep-sleep register sequence -- see this
* file's top #error. */
esp_err_t epd_sleep(void)
{
return ESP_ERR_NOT_SUPPORTED;
epd_cs_both(0);
EPD_CHECK(epd_send_command(DEEP_SLEEP));
EPD_CHECK(epd_send_data_byte(0xA5)); /* magic deep-sleep arg per every reference driver */
epd_cs_both(1);
epd_delay_ms(100);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_POWER_EN, 0);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_RST, 0);
return ESP_OK;
}
@@ -5,31 +5,42 @@
#include "esp_err.h"
/* Waveshare 13.3" e-Paper (E) Spectra 6 panel, driven by Seeed's EE02
* board (XIAO ESP32-S3 Plus): 1600x1200, 4 bits/pixel packed
* 2-pixels-per-byte -- same packing convention and public function
* shapes as epd7in3e.h, just a different resolution, so main's own
* sources don't need to branch on which panel is active beyond
* main/epd_board.h's
* header selection. Confirmed from Waveshare's/Seeed's public product
* pages (270.40x202.80mm, 1600x1200px). NOT yet confirmed against the
* vendor's own reference driver code, which doesn't exist in this tree
* yet -- see epd13in3e.c's top comment and epd_init()'s stub. The panel
* is also driven as two halves over a shared bus with two independent
* chip-selects (see Kconfig's EPD_PIN_CS_MASTER/EPD_PIN_CS_SLAVE), unlike
* epd7in3e's single-CS interface -- that's an implementation detail of
* epd13in3e.c, not something callers of this header need to know about. */
#define EPD_WIDTH 1600
#define EPD_HEIGHT 1200
* board (XIAO ESP32-S3 Plus). The panel is marketed/mounted as a
* 1600x1200 landscape rectangle (270.40x202.80mm), but its SPI
* controller addresses a native raster of 1200 columns x 1600 rows --
* i.e. the wire format is portrait, rotated 90 degrees from how the
* panel physically hangs. Confirmed identically across three independent
* vendor sources: Waveshare's RaspberryPi/c and ESP32 reference drivers
* for this exact panel (E-paper_Separate_Program/13.3inch_e-Paper_E in
* waveshare/e-Paper), and Waveshare's own ESP-IDF example for their
* ESP32-S3-ePaper-13.3E6 driver board (a *different* carrier board than
* Seeed's EE02, but the same panel+controller, hence the same command
* bytes/geometry -- only the GPIO numbers differ, and those come from
* EE02-specific sources, see this component's Kconfig). All three define
* EPD_WIDTH=1200/EPD_HEIGHT=1600 and split each row into two 600-byte
* (300px) halves sent to independent chip-selects: EPD_PIN_CS_MASTER
* gets the left half, EPD_PIN_CS_SLAVE the right -- see epd13in3e.c.
*
* Getting this backwards (assuming the wire raster matches the
* 1600x1200 mount/marketing size) doesn't just rotate the image -- 1600
* and 1200 don't share a row stride with 1200 and 1600 the other way
* (800 bytes/row x 1200 rows vs 600 bytes/row x 1600 rows), so a mismatch
* here slices real image rows at the wrong byte offsets and shreds the
* picture into a repeating diagonal garble, not a clean rotation.
* server/app/image_pipeline.py's PANEL_WIRE_TRANSPOSE handles the
* corresponding rotation server-side before packing bytes for this
* panel_type -- this header and that dict must agree on which axis is
* native. */
#define EPD_WIDTH 1200
#define EPD_HEIGHT 1600
#define EPD_BYTES_PER_ROW ((EPD_WIDTH + 1) / 2)
#define EPD_FRAME_BYTES (EPD_BYTES_PER_ROW * EPD_HEIGHT)
/* Same 6-ink Spectra family as the 7.3" panel, so the same 6 named
* colors -- but whether this panel's controller uses the SAME nibble
* values as epd7in3e.h's epd_color_t is UNCONFIRMED (see this file's own
* top comment). Left identical to epd7in3e.h's values as the working
* assumption; correct these against the vendor demo code once it exists,
* alongside server/app/image_pipeline.py's PANEL_CODES if they turn out
* to differ (see that file's own comment on PANEL_CODES). */
/* Same 6-ink Spectra family as the 7.3" panel, and (now confirmed by the
* same three vendor sources as the geometry above) the same 4-bit nibble
* codes as epd7in3e.h's epd_color_t -- matches
* server/app/image_pipeline.py's PANEL_CODES unconditionally, no
* panel-specific table needed there. */
typedef enum {
EPD_COLOR_BLACK = 0x0,
EPD_COLOR_WHITE = 0x1,