Files
espresso_frame/firmware/components/epd13in3e/epd13in3e.c
T
tfaour 474b92a282
Build and push server image / test (push) Successful in 45s
Firmware build check / build-check (push) Successful in 2m50s
Build and push server image / build-and-push (push) Successful in 4m36s
Build and push server image / deploy (push) Failing after 1m34s
Add server-side support for a second panel (13.3in Spectra 6 / EE02) and scaffold its firmware target
Server: Frame.panel_type (new column + migration) is auto-derived from
the device's reported board (X-Frame-Board), never user-set -- the
panel is a property of the hardware, not a picker in the UI.
image_pipeline's packing/render pipeline is parameterized by panel
geometry instead of hardcoded 800x480 globals, with the real confirmed
13.3in geometry (1600x1200) registered alongside the original 7.3in
panel. Existing 7.3in frames are unaffected (column default + board
mapping both resolve to the original panel).

Board identifiers are also renamed (devkit/xiao -> devkit_esp32c6/
xiao_esp32c6, plus new "ee02") since the EE02 board also carries a XIAO
module -- "xiao" alone stopped disambiguating hardware. The server
keeps accepting the legacy bare names indefinitely for already-flashed
devices.

Firmware: scaffolds a third build target (ee02, ESP32-S3 -- a real
chip-target change, not just a same-chip Kconfig variant like xiao) and
a new epd13in3e driver component skeleton. The actual panel init/LUT/
refresh register sequence isn't ported from vendor demo code yet (none
was available), so that component deliberately fails to compile
(#error) rather than risk sending unverified register values to real
hardware -- devkit/xiao are unaffected and build identically to before.
CI's ee02 build step is continue-on-error for the same reason.
2026-08-04 20:08:22 +00:00

293 lines
9.4 KiB
C

#include <string.h>
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_check.h"
#include "esp_log.h"
#include "esp_rom_crc.h"
#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.
*
* 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"
#define EPD_SPI_HOST SPI2_HOST
#define EPD_SPI_CHUNK_SIZE 4096
static const char *TAG = "epd13in3e";
#define EPD_CHECK(expr) ESP_RETURN_ON_ERROR((expr), TAG, #expr)
static spi_device_handle_t s_spi;
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. */
static void epd_wait_busy(void)
{
while (gpio_get_level((gpio_num_t)CONFIG_EPD_PIN_BUSY) == 0) {
epd_delay_ms(20);
}
}
static esp_err_t epd_spi_write(const uint8_t *data, size_t len)
{
while (len > 0) {
size_t n = len > EPD_SPI_CHUNK_SIZE ? EPD_SPI_CHUNK_SIZE : len;
spi_transaction_t t = {
.length = n * 8,
.tx_buffer = data,
};
EPD_CHECK(spi_device_polling_transmit(s_spi, &t));
data += n;
len -= n;
}
return ESP_OK;
}
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;
}
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;
}
static esp_err_t epd_send_data_byte(uint8_t data)
{
return epd_send_data(&data, 1);
}
static void epd_reset(void)
{
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_RST, 1);
epd_delay_ms(20);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_RST, 0);
epd_delay_ms(2);
gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_RST, 1);
epd_delay_ms(20);
}
/* 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. */
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;
}
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) |
(1ULL << CONFIG_EPD_PIN_POWER_EN),
.mode = GPIO_MODE_OUTPUT,
};
EPD_CHECK(gpio_config(&out_cfg));
gpio_config_t busy_cfg = {
.pin_bit_mask = (1ULL << CONFIG_EPD_PIN_BUSY),
.mode = GPIO_MODE_INPUT,
};
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);
spi_bus_config_t bus_cfg = {
.mosi_io_num = CONFIG_EPD_PIN_MOSI,
.miso_io_num = -1,
.sclk_io_num = CONFIG_EPD_PIN_CLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = EPD_SPI_CHUNK_SIZE,
};
EPD_CHECK(spi_bus_initialize(EPD_SPI_HOST, &bus_cfg, SPI_DMA_CH_AUTO));
spi_device_interface_config_t dev_cfg = {
.clock_speed_hz = CONFIG_EPD_SPI_CLOCK_HZ,
.mode = 0,
.spics_io_num = -1,
.queue_size = 1,
};
EPD_CHECK(spi_bus_add_device(EPD_SPI_HOST, &dev_cfg, &s_spi));
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");
return ESP_ERR_NOT_SUPPORTED;
}
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));
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);
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. */
ESP_LOGE(TAG, "Stream supplied %u bytes, expected %u -- aborting refresh",
(unsigned)total, (unsigned)EPD_FRAME_BYTES);
return ESP_ERR_INVALID_SIZE;
}
if (out_crc32 != NULL) {
*out_crc32 = crc;
}
return ESP_OK;
}
esp_err_t epd_display_stream(epd_read_fn_t read_fn, void *ctx)
{
esp_err_t err = epd_write_frame(read_fn, ctx, NULL);
if (err != ESP_OK) {
return err;
}
return epd_turn_on_display();
}
typedef struct {
const uint8_t *data;
size_t len;
size_t pos;
} epd_buf_ctx_t;
static size_t epd_buf_read(uint8_t *chunk, size_t chunk_size, void *ctx_)
{
epd_buf_ctx_t *c = (epd_buf_ctx_t *)ctx_;
size_t remaining = c->len - c->pos;
size_t n = remaining < chunk_size ? remaining : chunk_size;
if (n == 0) {
return 0;
}
memcpy(chunk, c->data + c->pos, n);
c->pos += n;
return n;
}
esp_err_t epd_display_buffer(const uint8_t *frame, size_t len)
{
epd_buf_ctx_t buf_ctx = { .data = frame, .len = len, .pos = 0 };
return epd_display_stream(epd_buf_read, &buf_ctx);
}
typedef struct {
uint8_t fill_byte;
size_t remaining;
} epd_fill_ctx_t;
static size_t epd_fill_read(uint8_t *chunk, size_t chunk_size, void *ctx_)
{
epd_fill_ctx_t *c = (epd_fill_ctx_t *)ctx_;
size_t n = c->remaining < chunk_size ? c->remaining : chunk_size;
if (n == 0) {
return 0;
}
memset(chunk, c->fill_byte, n);
c->remaining -= n;
return n;
}
esp_err_t epd_clear(epd_color_t color)
{
epd_fill_ctx_t fill_ctx = {
.fill_byte = (uint8_t)((color << 4) | color),
.remaining = EPD_FRAME_BYTES,
};
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;
}