#include #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" #include "esp_log.h" #include "esp_rom_crc.h" #include "epd13in3e.h" /* 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. * * 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 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) { vTaskDelay(pdMS_TO_TICKS(ms)); } /* 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) { 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; } /* 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); 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); return epd_spi_write(data, len); } 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(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); 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); } /* Power on, refresh, power off -- mirrors EPD_TurnOnDisplay()/ * EPD_13IN3E_TurnOnDisplay() in the reference drivers. */ esp_err_t epd_turn_on_display(void) { 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) { 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_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, .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, /* 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(); /* Master-only: shared analog-timing register. */ EPD_CHECK(epd_cmd_master(AN_TM, AN_TM_V, sizeof(AN_TM_V))); /* 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"); /* 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; } size_t total = 0; uint32_t crc = 0; size_t 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; } if (total != EPD_FRAME_BYTES) { /* 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; } 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); } esp_err_t epd_sleep(void) { 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; }