Tactility/Libraries/esp_epaper/ADDING_PANELS.md
Ken Van Hoeylandt 65ee223876
CL-32 drivers & more (#652)
- CL-32 features added:
  - Hardware revision detection
  - v0.3 and v0.4 keyboard 
  - v0.4 power supply
  - Inlined epd library (was a module) and made changes to the device definition (don't do full update after 5 small updates)
- Statusbar: add support for inverted colors
- e-paper driver improvements and warning fixes
- Implemented gpio_controller_get_level()
- Keyboard support improvements (unicode for tca drivers)
2026-09-14 00:06:55 +02:00

14 KiB

Adding a New Panel

This guide explains how to add support for a new e-paper panel using the data-driven architecture.

Architecture Overview

epaper_config.h          epaper_registry.c         controllers/
+----------------+       +-------------------+      +----------------+
| EPD_PANEL_xxx  | ----> | panel_registry[]  | ---> | ssd16xx.c      |
| (enum)         |       | - name            |      | gdey0154_lut.c |
+----------------+       | - width/height    |      | acep_6color.c  |
                         | - color_mode      |      | bwry_4color.c  |
                         | - bits_per_pixel  |      +----------------+
                         | - caps            |
                         | - ctrl (enum)     | ---> controller_ops[]
                         +-------------------+      (vtable lookup)

Quick Start: Same Controller, Different Size

For BW panels using existing SSD16xx controller (covers 90% of GoodDisplay panels), adding support requires only 1 line of code:

Step 1: Add Panel Type Enum

In include/epaper_config.h:

typedef enum {
    // ... existing panels ...
    EPD_PANEL_SSD16XX_290,      // 2.9" BW 128x296
    EPD_PANEL_SSD16XX_YOUR_NEW, // <-- Add here
    EPD_PANEL_SSD16XX_420,      // 4.2" BW 400x300

    EPD_PANEL_COUNT,            // Must be BEFORE aliases!
} epd_panel_type_t;

Step 2: Add to Panel Registry

In src/epaper_registry.c, add one line to panel_registry[]:

static const epd_panel_desc_t panel_registry[EPD_PANEL_COUNT] = {
    // ... existing panels ...

    [EPD_PANEL_SSD16XX_YOUR_NEW] = {
        .name = "SSD16xx_YOUR",
        .width = YOUR_WIDTH,
        .height = YOUR_HEIGHT,
        .color_mode = EPD_COLOR_BW,
        .bits_per_pixel = 1,
        .caps = EPD_CAP_PARTIAL | EPD_CAP_FAST,
        .ctrl = EPD_CTRL_SSD16XX,    // Reuse existing controller
    },
};

That's it! The existing EPD_CTRL_SSD16XX controller handles everything.

Currently Supported Generic Panels

Panel Type Size Resolution Notes
EPD_PANEL_SSD16XX_154 1.54" 200x200 GDEM0154I61, generic 1.54"
EPD_PANEL_SSD16XX_213 2.13" 122x250 GDEY0213B74, GDEM0213I61
EPD_PANEL_SSD16XX_266 2.66" 152x296 GDEY0266T90, GDEY0266T90H
EPD_PANEL_SSD16XX_270 2.7" 176x264 GDEY027T91, GDEM027Q72
EPD_PANEL_SSD16XX_290 2.9" 128x296 GDEY029T94, GDEY029T71H
EPD_PANEL_SSD16XX_370 3.7" 280x480 GDEY037T03
EPD_PANEL_SSD16XX_420 4.2" 400x300 GDEY042T81, GDEQ0426T82

Adding a New Controller

For panels with custom LUT tables, different controllers, or special features, you need to add a new controller.

Step 1: Create Controller File

Create src/controllers/your_controller.c:

/**
 * @file your_controller.c
 * @brief Driver for YOUR_CONTROLLER e-paper displays
 */

#include "../epaper_common.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include <string.h>

static const char *TAG = "your_ctrl";

/*=============================================================================
 * Initialization
 *============================================================================*/

esp_err_t your_controller_init(epd_device_t *dev)
{
    epd_spi_t *spi = epd_get_spi(dev);
    uint16_t w = epd_get_width(dev);
    uint16_t h = epd_get_height(dev);

    RESET(spi);
    vTaskDelay(pdMS_TO_TICKS(50));
    WAIT(dev);  // Uses polarity from panel caps

    CMD(spi, 0x12);  // SWRESET
    WAIT(dev);

    CMD(spi, 0x01);  // Driver output control
    DATA(spi, (h - 1) & 0xFF);
    DATA(spi, ((h - 1) >> 8) & 0xFF);
    DATA(spi, 0x00);

    CMD(spi, 0x11);  // Data entry mode
    DATA(spi, 0x03);

    // Set RAM address
    CMD(spi, 0x44);
    DATA(spi, 0x00);
    DATA(spi, (w / 8) - 1);

    CMD(spi, 0x45);
    DATA(spi, 0x00);
    DATA(spi, 0x00);
    DATA(spi, (h - 1) & 0xFF);
    DATA(spi, ((h - 1) >> 8) & 0xFF);

    WAIT(dev);

    ESP_LOGI(TAG, "Panel initialized: %dx%d", w, h);
    return ESP_OK;
}

/*=============================================================================
 * Update Functions
 *============================================================================*/

esp_err_t your_controller_update(epd_device_t *dev, epd_update_mode_t mode)
{
    epd_spi_t *spi = epd_get_spi(dev);

    switch (mode) {
        case EPD_UPDATE_PARTIAL:
            CMD(spi, 0x22);
            DATA(spi, 0xFF);  // Partial update sequence
            break;
        case EPD_UPDATE_FAST:
            CMD(spi, 0x22);
            DATA(spi, 0xC7);  // Fast update
            break;
        case EPD_UPDATE_FULL:
        default:
            CMD(spi, 0x22);
            DATA(spi, 0xF7);  // Full update
            break;
    }

    CMD(spi, 0x20);  // Master activation
    WAIT(dev);

    return ESP_OK;
}

/*=============================================================================
 * RAM Write Functions
 *============================================================================*/

esp_err_t your_controller_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len)
{
    epd_spi_t *spi = epd_get_spi(dev);
    bool partial_ready = epd_is_partial_ready(dev);

    // Reset RAM address
    CMD(spi, 0x4E);
    DATA(spi, 0x00);
    CMD(spi, 0x4F);
    DATA(spi, 0x00);
    DATA(spi, 0x00);

    // Write to current RAM (0x24)
    CMD(spi, 0x24);
    epd_spi_write_data_bulk(spi, data, len);

    // Only write to base RAM (0x26) when setting up partial refresh
    if (!partial_ready) {
        CMD(spi, 0x4E);
        DATA(spi, 0x00);
        CMD(spi, 0x4F);
        DATA(spi, 0x00);
        DATA(spi, 0x00);

        CMD(spi, 0x26);
        epd_spi_write_data_bulk(spi, data, len);
    }

    return ESP_OK;
}

/*=============================================================================
 * Power Management
 *============================================================================*/

esp_err_t your_controller_sleep(epd_device_t *dev)
{
    epd_spi_t *spi = epd_get_spi(dev);

    CMD(spi, 0x10);  // Deep sleep
    DATA(spi, 0x01);
    vTaskDelay(pdMS_TO_TICKS(100));

    ESP_LOGI(TAG, "Panel in deep sleep");
    return ESP_OK;
}

esp_err_t your_controller_wake(epd_device_t *dev)
{
    return your_controller_init(dev);
}

Step 2: Add Controller Type Enum

In include/epaper_panel.h:

typedef enum {
    EPD_CTRL_SSD16XX,
    EPD_CTRL_GDEY0154_LUT,
    EPD_CTRL_ACEP_6COLOR,
    EPD_CTRL_BWRY_4COLOR,
    EPD_CTRL_YOUR_CONTROLLER,  // <-- Add here
    EPD_CTRL_COUNT
} epd_controller_type_t;

Step 3: Register Controller Operations

In src/epaper_registry.c:

// Declare functions
extern esp_err_t your_controller_init(epd_device_t *dev);
extern esp_err_t your_controller_update(epd_device_t *dev, epd_update_mode_t mode);
extern esp_err_t your_controller_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len);
extern esp_err_t your_controller_sleep(epd_device_t *dev);
extern esp_err_t your_controller_wake(epd_device_t *dev);

// Add to controller_ops array
static const epd_controller_ops_t controller_ops[EPD_CTRL_COUNT] = {
    // ... existing controllers ...

    [EPD_CTRL_YOUR_CONTROLLER] = {
        .init = your_controller_init,
        .update = your_controller_update,
        .write_ram = your_controller_write_ram,
        .write_ram_partial = NULL,  // Optional
        .sleep = your_controller_sleep,
        .wake = your_controller_wake,
    },
};

Step 4: Add Panel to Registry

In src/epaper_registry.c:

static const epd_panel_desc_t panel_registry[EPD_PANEL_COUNT] = {
    // ... existing panels ...

    [EPD_PANEL_YOUR_PANEL] = {
        .name = "YOUR_PANEL",
        .width = 200,
        .height = 200,
        .color_mode = EPD_COLOR_BW,
        .bits_per_pixel = 1,
        .caps = EPD_CAP_PARTIAL | EPD_CAP_FAST,
        .ctrl = EPD_CTRL_YOUR_CONTROLLER,
    },
};

Step 5: Update CMakeLists.txt

idf_component_register(
    SRCS
        "src/epaper.c"
        "src/epaper_spi.c"
        "src/epaper_lvgl.c"
        "src/epaper_common.c"
        "src/epaper_registry.c"
        "src/controllers/ssd16xx.c"
        "src/controllers/gdey0154_lut.c"
        "src/controllers/acep_6color.c"
        "src/controllers/bwry_4color.c"
        "src/controllers/your_controller.c"  # <-- Add here
    INCLUDE_DIRS "include"
    REQUIRES driver esp_driver_gpio esp_driver_spi esp_timer
    PRIV_REQUIRES lvgl esp_psram
)

Panel Descriptor Reference

typedef struct {
    const char *name;               // Panel name for logging
    uint16_t width;                 // Width in pixels
    uint16_t height;                // Height in pixels
    epd_color_mode_t color_mode;    // EPD_COLOR_BW, EPD_COLOR_6COLOR, etc.
    uint8_t bits_per_pixel;         // 1, 2, or 4
    uint32_t caps;                  // EPD_CAP_* flags
    epd_controller_type_t ctrl;     // Controller type
    const void *init_data;          // Optional panel-specific data
} epd_panel_desc_t;

Capability Flags

Flag Description
EPD_CAP_PARTIAL Supports partial refresh
EPD_CAP_FAST Supports fast refresh mode
EPD_CAP_GRAYSCALE Supports grayscale mode
EPD_CAP_BUSY_INV Inverted busy signal (HIGH = ready)

Color Modes

Mode BPP Buffer Size Description
EPD_COLOR_BW 1 W*H/8 Black/White
EPD_COLOR_BWR 2 W*H/4 Black/White/Red
EPD_COLOR_BWY 2 W*H/4 Black/White/Yellow
EPD_COLOR_4GRAY 2 W*H/4 4 Gray levels
EPD_COLOR_4COLOR 2 W*H/4 4-color BWRY
EPD_COLOR_6COLOR 4 W*H/2 6 Colors
EPD_COLOR_7COLOR 4 W*H/2 7 Colors

Controller Operations Interface

typedef struct {
    esp_err_t (*init)(epd_device_t *dev);
    esp_err_t (*update)(epd_device_t *dev, epd_update_mode_t mode);
    esp_err_t (*write_ram)(epd_device_t *dev, const uint8_t *data, uint32_t len);
    esp_err_t (*write_ram_partial)(epd_device_t *dev, uint16_t x, uint16_t y,
                                    uint16_t w, uint16_t h, const uint8_t *data);
    esp_err_t (*sleep)(epd_device_t *dev);
    esp_err_t (*wake)(epd_device_t *dev);
} epd_controller_ops_t;
Function Required Description
init Yes Initialize panel hardware
update Yes Trigger display refresh (handles all modes)
write_ram Yes Write framebuffer to display RAM
write_ram_partial No Windowed RAM write for partial regions
sleep No Enter deep sleep mode
wake No Wake from sleep (usually calls init)

Helper Functions & Macros

Available in epaper_common.h:

// Device access
epd_spi_t* epd_get_spi(epd_device_t *dev);
uint16_t epd_get_width(epd_device_t *dev);
uint16_t epd_get_height(epd_device_t *dev);
const epd_panel_desc_t* epd_get_panel(epd_device_t *dev);
bool epd_is_partial_ready(epd_device_t *dev);

// SPI macros
#define CMD(spi, c)     epd_spi_write_cmd(spi, c)
#define DATA(spi, d)    epd_spi_write_data(spi, d)
#define RESET(spi)      epd_spi_reset(spi)

// Busy wait with polarity support
#define WAIT(dev) epd_wait_busy_polarity(epd_get_spi(dev), epd_get_panel(dev)->caps, 0)

Common Issues and Solutions

Busy Signal Polarity

Some panels use inverted busy signal. Add EPD_CAP_BUSY_INV to capabilities:

// Standard: LOW = ready, HIGH = busy
.caps = EPD_CAP_PARTIAL,

// Inverted: HIGH = ready, LOW = busy (e.g., GDEP073E01)
.caps = EPD_CAP_BUSY_INV,

The WAIT(dev) macro automatically handles polarity based on panel caps.

Partial Refresh RAM Management

For partial refresh to work correctly:

  1. First call (partial_ready = false): Write to both RAM 0x24 and 0x26
  2. Subsequent calls (partial_ready = true): Write only to RAM 0x24

Use epd_is_partial_ready(dev) to check the state:

esp_err_t your_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len)
{
    bool partial_ready = epd_is_partial_ready(dev);

    // Always write to current RAM
    CMD(spi, 0x24);
    epd_spi_write_data_bulk(spi, data, len);

    // Only write to base RAM when setting up partial mode
    if (!partial_ready) {
        CMD(spi, 0x26);
        epd_spi_write_data_bulk(spi, data, len);
    }

    return ESP_OK;
}

Multi-Color Panel RAM Format

6/7-color panels use 4-bit per pixel (2 pixels per byte):

// High nibble = even pixel, Low nibble = odd pixel
static void set_pixel_4bpp(uint8_t *fb, int x, int y, int width, uint8_t color)
{
    uint32_t addr = (y * width + x) / 2;
    if (x % 2 == 0) {
        fb[addr] = (fb[addr] & 0x0F) | (color << 4);
    } else {
        fb[addr] = (fb[addr] & 0xF0) | (color & 0x0F);
    }
}

4-color BWRY panels use 2-bit per pixel (4 pixels per byte):

// Pixel order: [P0:7-6][P1:5-4][P2:3-2][P3:1-0]
static void set_pixel_2bpp(uint8_t *fb, int x, int y, int width, uint8_t color)
{
    uint32_t pixel_idx = y * width + x;
    uint32_t byte_idx = pixel_idx / 4;
    uint8_t bit_offset = (3 - (pixel_idx % 4)) * 2;  // 6, 4, 2, 0
    uint8_t mask = 0x03 << bit_offset;
    fb[byte_idx] = (fb[byte_idx] & ~mask) | ((color & 0x03) << bit_offset);
}

Adding Board Preset (Optional)

Create a configuration macro for easy setup in include/epaper_config.h:

#define EPD_CONFIG_YOUR_BOARD() { \
    .pins = { \
        .busy = GPIO_NUM_8, \
        .rst = GPIO_NUM_9, \
        .dc = GPIO_NUM_10, \
        .cs = GPIO_NUM_11, \
        .sck = GPIO_NUM_12, \
        .mosi = GPIO_NUM_13, \
    }, \
    .spi = { \
        .host = SPI2_HOST, \
        .speed_hz = 10000000, \
    }, \
    .panel = { \
        .type = EPD_PANEL_YOUR_PANEL, \
        .width = 0, \
        .height = 0, \
        .mirror_x = false, \
        .mirror_y = false, \
        .rotation = 0, \
    }, \
}

Testing

  1. Build test: idf.py build
  2. Basic test: Fill screen with white, then black
  3. Pattern test: Draw checkerboard or gradient
  4. Partial test: Update small region repeatedly
  5. Sleep/wake test: Verify low power mode

Resources