[epaper_spi] Add T133A01 6-color e-paper driver for reTerminal E1004 (#16706)

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
Citric Li
2026-07-04 11:50:54 +08:00
committed by GitHub
parent e94fcda8b7
commit 7878052533
8 changed files with 594 additions and 0 deletions
+2
View File
@@ -112,6 +112,7 @@ def model_schema(config):
cv.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(milliseconds=500)),
),
**model.get_config_options(),
}
)
@@ -198,6 +199,7 @@ async def to_code(config):
)
await display.register_display(var, config)
config = await model.to_code(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
@@ -0,0 +1,367 @@
#include "epaper_spi_t133a01.h"
#include <algorithm>
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.t133a01";
// Color indices used in the 4bpp buffer (sprite-side)
// These MUST match the Arduino GFX TFT_eSPI.h color definitions and
// the remap_color()/COLOR_GET mapping:
// 0x0F=BLACK, 0x00=WHITE, 0x02=GREEN, 0x06=RED, 0x0B=YELLOW, 0x0D=BLUE
static constexpr uint8_t T133A01_BLACK = 0x0F;
static constexpr uint8_t T133A01_WHITE = 0x00;
static constexpr uint8_t T133A01_GREEN = 0x02;
static constexpr uint8_t T133A01_RED = 0x06;
static constexpr uint8_t T133A01_YELLOW = 0x0B;
static constexpr uint8_t T133A01_BLUE = 0x0D;
// T133A01 register addresses
static constexpr uint8_t R00_PSR = 0x00;
static constexpr uint8_t R01_PWR = 0x01;
static constexpr uint8_t R02_POF = 0x02;
static constexpr uint8_t R04_PON = 0x04;
static constexpr uint8_t R05_BTST_N = 0x05;
static constexpr uint8_t R06_BTST_P = 0x06;
static constexpr uint8_t R10_DTM = 0x10;
static constexpr uint8_t R12_DRF = 0x12;
static constexpr uint8_t R50_CDI = 0x50;
static constexpr uint8_t R61_TRES = 0x61;
static constexpr uint8_t RA5_DCDC = 0xA5;
static constexpr uint8_t RE0_CCSET = 0xE0;
static constexpr uint8_t RE3_PWS = 0xE3;
/**
* COLOR_GET remap table from T133A01_Defines.h.
* Translates 4bpp sprite color index to the hardware pixel encoding.
* Sprite: 0x0F=BLACK 0x00=WHITE 0x02=GREEN 0x06=RED 0x0B=YELLOW 0x0D=BLUE
* HW: 0x00=BLACK 0x01=WHITE 0x06=GREEN 0x03=RED 0x02=YELLOW 0x05=BLUE
*/
uint8_t EPaperT133A01::remap_color(uint8_t index) {
switch (index & 0x0F) {
case 0x0F:
return 0x00; // Black
case 0x00:
return 0x01; // White
case 0x02:
return 0x06; // Green
case 0x06:
return 0x03; // Red
case 0x0B:
return 0x02; // Yellow
case 0x0D:
return 0x05; // Blue
default:
return 0x01; // White fallback
}
}
/**
* Map an ESPHome Color to a 4-bit sprite color index.
* Index values match the Arduino GFX TFT_eSPI color definitions:
* 0x00=WHITE, 0x02=GREEN, 0x06=RED, 0x0B=YELLOW, 0x0D=BLUE, 0x0F=BLACK
*/
uint8_t EPaperT133A01::color_to_index(Color color) {
unsigned char max_rgb = std::max({color.r, color.g, color.b});
unsigned char min_rgb = std::min({color.r, color.g, color.b});
// Check for grayscale
if ((max_rgb - min_rgb) < 50) {
if ((static_cast<int>(color.r) + color.g + color.b) > 382) {
return T133A01_WHITE;
}
return T133A01_BLACK;
}
bool r_on = (color.r > 128);
bool g_on = (color.g > 128);
bool b_on = (color.b > 128);
if (r_on && g_on && !b_on)
return T133A01_YELLOW;
if (r_on && !g_on && !b_on)
return T133A01_RED;
if (!r_on && g_on && !b_on)
return T133A01_GREEN;
if (!r_on && !g_on && b_on)
return T133A01_BLUE;
// Handle mixed colors: map to nearest primary
if (!r_on && g_on && b_on)
return T133A01_GREEN; // Cyan -> Green
if (r_on && !g_on)
return T133A01_RED; // Magenta -> Red
if (r_on)
return T133A01_WHITE;
return T133A01_BLACK;
}
void EPaperT133A01::setup() {
// Base setup initialises the buffer, the standard pins and the SPI bus.
EPaperBase::setup();
// Both chip-selects are driven directly by this driver (the dual-CS
// protocol needs CS held HIGH while CS1 receives data, which the SPI
// bus cannot do). Start both deselected (HIGH).
this->cs_pin_->setup();
this->cs_pin_->digital_write(true);
this->cs1_pin_->setup();
this->cs1_pin_->digital_write(true);
}
bool EPaperT133A01::reset() {
for (auto *enable_pin : this->enable_pins_) {
enable_pin->digital_write(true);
}
if (this->reset_pin_ != nullptr) {
if (this->state_ == EPaperState::RESET) {
this->reset_pin_->digital_write(false);
return false;
}
this->reset_pin_->digital_write(true);
}
return true;
}
/**
* Initialise the T133A01 display.
*
* The init sequence uses a mix of CS and CS1 commands as per the Arduino driver.
* The base class init_sequence is NOT used for T133A01 because the dual-CS
* protocol requires per-command routing.
*/
bool EPaperT133A01::initialise(bool partial) {
// Init sequence mirrors the Arduino GFX library's EPD_INIT() macro
// (T133A01_Defines.h). Commands routed to CS only leave CS1 deselected;
// commands routed to both controllers assert CS and CS1 together.
// 0x74 - panel config (CS only)
this->write_command_(0x74, {0x00, 0x0C, 0x0C, 0xD9, 0xDD, 0xDD, 0x15, 0x15, 0x55}, true, false);
delay(10);
// 0xF0 - panel config (CS + CS1)
this->write_command_(0xF0, {0x49, 0x55, 0x13, 0x5D, 0x05, 0x10}, true, true);
delay(10);
// PSR - Panel Setting Register (CS + CS1)
this->write_command_(0x00, {0xDF, 0x69}, true, true);
delay(10);
// DCDC (CS only)
this->write_command_(RA5_DCDC, {0x44, 0x54, 0x00}, true, false);
delay(10);
// CDI (CS + CS1)
this->write_command_(R50_CDI, {0x37}, true, true);
delay(10);
// 0x60 (CS + CS1)
this->write_command_(0x60, {0x03, 0x03}, true, true);
delay(10);
// 0x86 (CS + CS1)
this->write_command_(0x86, {0x10}, true, true);
delay(10);
// PWS - Phase Width Setting (CS + CS1)
this->write_command_(RE3_PWS, {0x22}, true, true);
delay(10);
// TRES - Resolution Setting (CS + CS1).
// With width=1200, height=1600: first word = width = 1200, second word = height/2 = 800.
this->write_command_(R61_TRES,
{(uint8_t) (this->width_ >> 8), (uint8_t) (this->width_ & 0xFF),
(uint8_t) ((this->height_ / 2) >> 8), (uint8_t) ((this->height_ / 2) & 0xFF)},
true, true);
delay(10);
// PWR - Power Setting (CS only)
this->write_command_(R01_PWR, {0x0F, 0x00, 0x28, 0x2C, 0x28, 0x38}, true, false);
delay(10);
// 0xB6 (CS only)
this->write_command_(0xB6, {0x07}, true, false);
delay(10);
// BTST_P (CS only)
this->write_command_(R06_BTST_P, {0xE0, 0x20}, true, false);
delay(10);
// 0xB7 (CS only)
this->write_command_(0xB7, {0x01}, true, false);
delay(10);
// BTST_N (CS only)
this->write_command_(R05_BTST_N, {0xE0, 0x20}, true, false);
delay(10);
// 0xB0 (CS only)
this->write_command_(0xB0, {0x01}, true, false);
delay(10);
// 0xB1 (CS only)
this->write_command_(0xB1, {0x02}, true, false);
delay(10);
return true;
}
void EPaperT133A01::write_command_(uint8_t command, const uint8_t *data, size_t length, bool use_cs, bool use_cs1) {
ESP_LOGV(TAG, "Command: 0x%02X, Length: %u, CS: %d, CS1: %d", command, (unsigned) length, use_cs, use_cs1);
// Chip-selects are active-low: assert the requested controllers.
this->cs_pin_->digital_write(!use_cs);
this->cs1_pin_->digital_write(!use_cs1);
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(command);
if (length > 0) {
this->dc_pin_->digital_write(true);
this->write_array(data, length);
}
this->disable();
this->cs_pin_->digital_write(true);
this->cs1_pin_->digital_write(true);
}
void EPaperT133A01::fill(Color color) {
if (this->get_clipping().is_set()) {
EPaperBase::fill(color);
return;
}
auto pixel_color = color_to_index(color);
this->buffer_.fill(pixel_color + (pixel_color << 4));
}
void EPaperT133A01::draw_pixel_at(int x, int y, Color color) {
if (!this->rotate_coordinates_(x, y))
return;
auto pixel_bits = color_to_index(color);
uint32_t pixel_position = x + y * this->get_width_internal();
uint32_t byte_position = pixel_position / 2;
auto original = this->buffer_[byte_position];
if ((pixel_position & 1) != 0) {
this->buffer_[byte_position] = (original & 0xF0) | pixel_bits;
} else {
this->buffer_[byte_position] = (original & 0x0F) | (pixel_bits << 4);
}
}
void EPaperT133A01::power_on() {
ESP_LOGV(TAG, "Power on");
this->write_command_(R04_PON, true, true);
}
void EPaperT133A01::power_off() {
ESP_LOGV(TAG, "Power off");
this->write_command_(R02_POF, {0x00}, true, true);
}
void EPaperT133A01::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh screen");
// Display Refresh
this->write_command_(R12_DRF, {0x01}, true, true);
}
void EPaperT133A01::deep_sleep() {
ESP_LOGV(TAG, "Deep sleep");
this->write_command_(0x07, {0xA5}, true, true);
}
bool HOT EPaperT133A01::transfer_data() {
const uint32_t start_time = millis();
const uint16_t bytes_per_half_row = this->width_ / 4;
const uint16_t total_rows = this->height_;
const uint16_t bytes_per_row = this->width_ / 2;
uint8_t line_data[400] = {};
size_t half = this->current_data_index_;
// --- CCSET: select color set before data transfer (CS + CS1) ---
if (half == 0) {
this->write_command_(RE0_CCSET, {0x01}, true, true);
this->wait_for_idle_(true);
delay(10);
}
// --- CS phase: left half of each row via CS ---
// T133A01 requires CS to stay LOW for the ENTIRE DTM data stream.
// Toggling CS between chunks resets the controller's data pointer,
// causing only the last chunk to be retained. Keep CS asserted
// across timeout boundaries by NOT deselecting on yield.
if (half < total_rows) {
if (half == 0) {
this->cs_pin_->digital_write(false); // select CS
this->cs1_pin_->digital_write(true); // deselect CS1
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(R10_DTM);
this->dc_pin_->digital_write(true);
}
while (half < total_rows) {
size_t buf_offset = half * bytes_per_row;
for (uint16_t col = 0; col < bytes_per_half_row; col++) {
uint8_t b = this->buffer_[buf_offset + col];
line_data[col] = (remap_color(b >> 4) << 4) | remap_color(b & 0x0F);
}
this->write_array(line_data, bytes_per_half_row);
half++;
this->current_data_index_ = half;
if (millis() - start_time > MAX_TRANSFER_TIME) {
return false;
}
}
ESP_LOGD(TAG, "CS phase done");
this->disable();
this->cs_pin_->digital_write(true); // deselect CS
}
// --- CS1 phase: right half of each row via CS1 ---
// Same continuous-transaction requirement as the CS phase.
// CS is held HIGH so only CS1 receives the data.
if (half >= total_rows && half < total_rows * 2) {
size_t cs1_row = half - total_rows;
if (cs1_row == 0) {
this->cs_pin_->digital_write(true); // deselect CS
this->cs1_pin_->digital_write(false); // select CS1
this->enable();
this->dc_pin_->digital_write(false);
this->write_byte(R10_DTM);
this->dc_pin_->digital_write(true);
}
while (half < total_rows * 2) {
size_t row = half - total_rows;
size_t buf_offset = row * bytes_per_row + bytes_per_half_row;
for (uint16_t col = 0; col < bytes_per_half_row; col++) {
uint8_t b = this->buffer_[buf_offset + col];
line_data[col] = (remap_color(b >> 4) << 4) | remap_color(b & 0x0F);
}
this->write_array(line_data, bytes_per_half_row);
half++;
this->current_data_index_ = half;
if (millis() - start_time > MAX_TRANSFER_TIME) {
return false;
}
}
ESP_LOGD(TAG, "CS1 phase done");
this->disable();
this->cs1_pin_->digital_write(true); // deselect CS1
}
this->current_data_index_ = 0;
return true;
}
void EPaperT133A01::dump_config() {
EPaperBase::dump_config();
LOG_PIN(" CS Pin: ", this->cs_pin_);
LOG_PIN(" CS1 Pin: ", this->cs1_pin_);
}
} // namespace esphome::epaper_spi
@@ -0,0 +1,77 @@
#pragma once
#include "epaper_spi.h"
namespace esphome::epaper_spi {
/**
* T133A01-based 6-color e-paper display driver.
*
* The T133A01 controller uses a dual-CS SPI architecture:
* - CS (primary): Controls the first half of pixel data transfer
* - CS1 (secondary): Controls panel commands (init, power, refresh) and
* the second half of pixel data transfer
*
* Color depth: 4 bits per pixel, supporting 6 colors:
* White, Green, Red, Yellow, Blue, Black
*
* Buffer layout: 2 pixels per byte (4bpp packed), total buffer size
* is width * height / 2 bytes.
*/
class EPaperT133A01 : public EPaperBase {
public:
EPaperT133A01(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) {
this->buffer_length_ = (size_t) width * height / 2; // 2 pixels per byte at 4bpp
}
void set_cs_pins(GPIOPin *cs, GPIOPin *cs1) {
this->cs_pin_ = cs;
this->cs1_pin_ = cs1;
}
void fill(Color color) override;
void setup() override;
void dump_config() override;
void draw_pixel_at(int x, int y, Color color) override;
protected:
bool reset() override;
bool initialise(bool partial) override;
void refresh_screen(bool partial) override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
bool transfer_data() override;
/**
* Send a command (and optional data) selecting one or both controllers.
* Both chip-selects are active-low and managed directly by this driver.
* @param command The command byte to send
* @param data Optional pointer to data bytes to send after the command
* @param length Number of data bytes to send after the command
* @param use_cs assert CS (left controller) for this transaction
* @param use_cs1 assert CS1 (right controller) for this transaction
*/
void write_command_(uint8_t command, const uint8_t *data, size_t length, bool use_cs, bool use_cs1);
void write_command_(uint8_t command, std::initializer_list<uint8_t> data, bool use_cs, bool use_cs1) {
this->write_command_(command, data.begin(), data.size(), use_cs, use_cs1);
}
void write_command_(uint8_t command, bool use_cs, bool use_cs1) {
this->write_command_(command, nullptr, 0, use_cs, use_cs1);
}
/// Convert Color to 4-bit T133A01 color index
static uint8_t color_to_index(Color color);
/// Apply COLOR_GET remap table to translate sprite indices to hardware values
static uint8_t remap_color(uint8_t index);
GPIOPin *cs_pin_{nullptr};
GPIOPin *cs1_pin_{nullptr};
};
} // namespace esphome::epaper_spi
@@ -2,11 +2,15 @@ from typing import Any, Self
import esphome.config_validation as cv
from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_WIDTH
from esphome.cpp_generator import MockObj
class EpaperModel:
models: dict[str, Self] = {}
# Whether the driver manages chip-select itself instead of via the SPI bus.
manages_cs: bool = False
def __init__(
self,
name: str,
@@ -35,6 +39,25 @@ class EpaperModel:
def get_constructor_args(self, config) -> tuple:
return ()
def get_config_options(self) -> dict:
"""
Return model-specific configuration schema options.
The base implementation adds nothing; specific models override this to
declare extra options without cluttering the shared schema.
:return: A mapping suitable for cv.Schema.extend()
"""
return {}
async def to_code(self, var: MockObj, config: dict) -> dict:
"""
Generate model-specific code for the options added by add_options().
The base implementation does nothing; specific models override this.
The config can be updated in place to add or remove options.
:param var: The component variable
:param config: The validated configuration
"""
return config
def get_dimensions(self, config) -> tuple[int, int]:
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is
@@ -0,0 +1,71 @@
"""T133A01-based e-paper displays.
The T133A01 is a 6-color e-paper controller IC that drives large panels
(1200x1600 portrait). It uses a dual-CS SPI architecture where CS
controls one half of the pixel data and CS1 controls the other half,
as well as panel-level commands (power on, refresh, power off).
Supported models:
- Seeed-reTerminal-E1004: 1200x1600 pixels, 6-color (T133A01 panel)
"""
from esphome import pins
import esphome.codegen as cg
from esphome.const import CONF_CS_PIN
from esphome.cpp_generator import MockObj
from . import EpaperModel
CONF_CS1_PIN = "cs1_pin"
class T133A01Model(EpaperModel):
"""EpaperModel subclass for T133A01-based 6-color e-paper displays."""
# The driver drives CS and CS1 directly for the dual-CS protocol.
manages_cs = True
def __init__(self, name, class_name="EPaperT133A01", **defaults):
super().__init__(name, class_name, **defaults)
def get_config_options(self) -> dict:
# CS1 is the second chip-select required by the dual-CS architecture.
# fallback=None makes it required unless the model provides a default.
return {
self.option(CONF_CS1_PIN, fallback=None): pins.gpio_output_pin_schema,
}
async def to_code(self, var: MockObj, config: dict) -> dict:
cs = await cg.gpio_pin_expression(config[CONF_CS_PIN])
cs1 = await cg.gpio_pin_expression(config[CONF_CS1_PIN])
cg.add(var.set_cs_pins(cs, cs1))
# Remove CS and CS1 from the config so that the base class doesn't try to handle them.
return {k: v for k, v in config.items() if k not in (CONF_CS_PIN, CONF_CS1_PIN)}
t133a01_base = T133A01Model(
"t133a01",
minimum_update_interval="30s",
data_rate="10MHz",
)
# Seeed reTerminal E1004 - 13.3" 6-color e-paper (1200x1600, T133A01)
# Portrait orientation (1200 wide × 1600 tall), matching the Arduino
# Setup523 defines TFT_WIDTH=1200, TFT_HEIGHT=1600.
# CS and CS1 each receive half of each row's pixel data
# (300 bytes = 600 pixels per controller, for all 1600 rows).
Seeed_reTerminal_E1004 = t133a01_base.extend(
"Seeed-reTerminal-E1004",
width=1200,
height=1600,
cs_pin=10,
cs1_pin=2,
dc_pin=11,
reset_pin=38,
busy_pin={
"number": 13,
"inverted": True,
"mode": {"input": True},
},
enable_pin=12,
)
@@ -154,6 +154,10 @@ def test_all_predefined_models(
if not model.get_default(CONF_CS_PIN):
config[CONF_CS_PIN] = 5
# Dual-CS models (e.g. T133A01) require a second chip-select pin
if model.manages_cs and not model.get_default("cs1_pin"):
config["cs1_pin"] = 4
# Select an ESP32 variant on which all of this model's pins are valid
# (some models default to high-numbered pins only present on the S3).
choose_variant_with_pins(_pins_for(model, config))
@@ -204,6 +208,10 @@ def test_individual_models(
if not model.get_default(CONF_CS_PIN):
config[CONF_CS_PIN] = 5
# Dual-CS models (e.g. T133A01) require a second chip-select pin
if model.manages_cs and not model.get_default("cs1_pin"):
config["cs1_pin"] = 4
# Select an ESP32 variant on which all of this model's pins are valid
# (some models default to high-numbered pins only present on the S3).
choose_variant_with_pins(_pins_for(model, config))
@@ -76,6 +76,14 @@ display:
- platform: epaper_spi
model: seeed-reterminal-e1002
- platform: epaper_spi
model: seeed-reterminal-e1004
cs_pin: 33
cs1_pin: 34
dc_pin: 35
reset_pin: 36
busy_pin: 37
enable_pin: 39
- platform: epaper_spi
model: seeed-ee04-mono-4.26
full_update_every: 10
@@ -0,0 +1,38 @@
esphome:
name: e1004-test
friendly_name: E1004 Test
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
framework:
type: esp-idf
psram:
mode: octal
spi:
- id: epaper_spi_bus
clk_pin: GPIO7
mosi_pin: GPIO9
display:
- platform: epaper_spi
spi_id: epaper_spi_bus
model: seeed-reterminal-e1004
update_interval: never
lambda: |-
it.fill(Color::WHITE);
it.rectangle(10, 10, it.get_width() - 20, it.get_height() - 20, Color::BLACK);
it.print(it.get_width() / 2, it.get_height() / 2, id(my_font), Color::BLACK, TextAlign::CENTER, "E1004 Test");
it.circle(100, 100, 30, Color(255, 0, 0));
it.circle(200, 100, 30, Color(0, 255, 0));
it.circle(300, 100, 30, Color(0, 0, 255));
it.circle(400, 100, 30, Color(255, 255, 0));
font:
- file: "gfonts://Roboto"
id: my_font
size: 20
logger: