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

520 lines
14 KiB
Markdown

# 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`:
```c
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[]`:
```c
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`:
```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`:
```c
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`:
```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`:
```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
```cmake
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
```c
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
```c
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`:
```c
// 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:
```c
// 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:
```c
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):
```c
// 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):
```c
// 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`:
```c
#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
- [Good Display Wiki](https://www.good-display.com/companyfile/101.html) - Datasheets and app notes
- [Waveshare E-Paper Wiki](https://www.waveshare.com/wiki/E-Paper) - Reference implementations
- [ESP-IDF SPI Master](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/spi_master.html)