[epaper_spi] Add the UC8151D controller and the Waveshare 2.13" B V3 (#21022)

This commit is contained in:
Clyde Stubbs
2026-10-19 07:40:16 +11:00
committed by GitHub
parent dd456ea0b2
commit 3e68e0f5e1
6 changed files with 100 additions and 17 deletions
@@ -20,7 +20,7 @@ static BwrState color_to_bwr(Color color) {
return BwrState::BWR_BLACK;
}
// UC8179 3-color display buffer layout:
// 3-color display buffer layout (UC8179 and UC8151D):
// - 1 bit per pixel, 8 pixels per byte
// - Buffer first half: Black/White plane (1=black, 0=white)
// - Buffer second half: Red plane (1=red, 0=white)
@@ -79,7 +79,7 @@ bool HOT EPaperWaveshareBWR::transfer_data() {
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
// Phase 1: send Black/White plane (first half) via command 0x10 (DTM1)
// UC8179 DTM1 (0x10): inverted to get 0=black, 1=white
// DTM1 (0x10): inverted to get 0=black, 1=white
if (this->current_data_index_ < half_buffer) {
if (this->current_data_index_ == 0) {
this->command(0x10); // DATA START TRANSMISSION 1 (black channel)
@@ -101,7 +101,7 @@ bool HOT EPaperWaveshareBWR::transfer_data() {
}
// Phase 2: send Red plane (second half) via command 0x13 (DTM2)
// UC8179 DTM2 (0x13): 1=red, 0=white
// DTM2 (0x13): 1=red, 0=white; on the UC8151D this relies on the 0x50 value in uc8151.py
if (this->current_data_index_ < buffer_length) {
if (this->current_data_index_ == half_buffer) {
this->command(0x13); // DATA START TRANSMISSION 2 (red channel)
@@ -127,8 +127,7 @@ bool HOT EPaperWaveshareBWR::transfer_data() {
}
void EPaperWaveshareBWR::power_on() {
this->cmd_data(0x01, {0x07, 0x17, 0x3F, 0x3F}); // POWER SETTING
this->command(0x04); // POWER ON
this->command(0x04); // POWER ON
}
void EPaperWaveshareBWR::refresh_screen(bool /*partial*/) {
@@ -5,8 +5,8 @@
namespace esphome::epaper_spi {
/**
* Waveshare 3-color e-paper displays (UC8179 controller).
* Supports: 7.5" V2 BWR (EDP_7in5b_V2), 800x480 pixels.
* Waveshare 3-color e-paper displays (UC8179 and UC8151D controllers).
* Supports: 7.5" V2 BWR (EPD_7in5b_V2), 800x480 pixels; 2.13" B V3 (EPD_2in13b_V3), 104x212 pixels.
*
* Color scheme: Black, White, Red (BWR)
* Buffer layout: 1 bit per pixel, separate planes
@@ -14,8 +14,8 @@ namespace esphome::epaper_spi {
* - Buffer second half: Red plane (1=red, 0=no red)
* - Total buffer: width * height / 4 bytes (2 * width * height / 8)
*
* The init sequence (INITIALISE state) sends panel configuration only.
* Power-on (0x01 + 0x04) is sent in the POWER_ON state after data transfer;
* The init sequence (INITIALISE state) sends the power and panel configuration.
* Power-on (0x04) is sent in the POWER_ON state after data transfer;
* the state machine then busy-waits before triggering REFRESH_SCREEN (0x12).
*/
class EPaperWaveshareBWR : public EPaperBase {
@@ -0,0 +1,60 @@
"""Black/white/red e-paper displays using the UltraChip UC8151D controller.
Supported models:
- uc8151: the controller on its own; dimensions and pins must be configured
- waveshare-2.13in-bv3: 104x212 pixels (2.13" B V3, EPD_2in13b_V3)
The busy pin of these displays is low while busy, so configure it with ``inverted: true``.
"""
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_DIMENSIONS
from esphome.types import ConfigType
from . import EpaperModel
MAX_SOURCES = 160
MAX_GATES = 296
class UC8151(EpaperModel):
"""EpaperModel class for black/white/red displays using the UC8151D controller."""
def __init__(self, name: str, **defaults: Any) -> None:
super().__init__(name, "EPaperWaveshareBWR", **defaults)
def validate_config(self, config: ConfigType) -> ConfigType:
width, height = self.get_dimensions(config)
if width > MAX_SOURCES or height > MAX_GATES:
raise cv.Invalid(
f"{self.name} supports at most {MAX_SOURCES}x{MAX_GATES} pixels (width x height)",
path=[CONF_DIMENSIONS],
)
return config
def get_init_sequence(self, config: dict) -> tuple:
width, height = self.get_dimensions(config)
return (
# PANEL SETTING (black/white/red mode, LUT from OTP)
(0x00, 0x0F, 0x89),
# RESOLUTION SETTING: sources, then gates
(0x61, width, height >> 8, height & 0xFF),
# VCOM AND DATA INTERVAL SETTING; 0x57, not Waveshare's 0x77, so that 1 in DTM2 is red
(0x50, 0x57),
)
uc8151 = UC8151(
"uc8151",
data_rate="10MHz",
minimum_update_interval="15s",
width_multiple=8,
)
uc8151.extend(
"waveshare-2.13in-bv3",
width=104,
height=212,
)
@@ -1,10 +1,10 @@
"""Waveshare Black/White/Red e-paper displays using UC8179 controller.
Supported models:
- waveshare-7.5in-bv2-bwr: 800x480 pixels (7.5" BWR display, EDP_7in5b_V2)
- waveshare-7.5in-bv2-bwr: 800x480 pixels (7.5" BWR display, EPD_7in5b_V2)
These displays use the UC8179 controller. Panel configuration is sent during
the INITIALISE state. Power-on is handled in the POWER_ON state, after data
These displays use the UC8179 controller. Power and panel configuration are sent
during the INITIALISE state. Power-on is handled in the POWER_ON state, after data
transfer, so the state machine's built-in busy wait covers the power-on delay.
"""
@@ -20,8 +20,8 @@ class WaveshareBWR(EpaperModel):
def get_init_sequence(self, config):
"""Generate initialization sequence for UC8179 BWR displays.
Panel configuration only — power-on is handled separately in power_on()
after data transfer, with the state machine busy-waiting before refresh.
Power and panel configuration only — power-on is handled separately in
power_on() after data transfer, with the state machine busy-waiting before refresh.
"""
width, height = self.get_dimensions(config)
return (
@@ -43,10 +43,12 @@ class WaveshareBWR(EpaperModel):
(0x60, 0x22),
# RESOLUTION GATE SETTING
(0x65, 0x00, 0x00, 0x00, 0x00),
# POWER SETTING
(0x01, 0x07, 0x17, 0x3F, 0x3F),
)
# Model: Waveshare 7.5" V2 BWR (EDP_7in5b_V2) — 800x480, UC8179 controller
# Model: Waveshare 7.5" V2 BWR (EPD_7in5b_V2) — 800x480, UC8179 controller
WaveshareBWR(
"waveshare-7.5in-bv2-bwr",
width=800,
@@ -143,7 +143,7 @@ def test_all_predefined_models(
# Add dimensions if not provided by model
if not model.get_default(CONF_WIDTH):
config[CONF_DIMENSIONS] = {CONF_HEIGHT: 240, CONF_WIDTH: 320}
config[CONF_DIMENSIONS] = {CONF_HEIGHT: 120, CONF_WIDTH: 120}
# Add init sequence if model doesn't provide one
if model.initsequence is None:
@@ -199,7 +199,7 @@ def test_individual_models(
config[CONF_DC_PIN] = 21
if not model.get_default(CONF_WIDTH):
config[CONF_DIMENSIONS] = {CONF_HEIGHT: 240, CONF_WIDTH: 320}
config[CONF_DIMENSIONS] = {CONF_HEIGHT: 120, CONF_WIDTH: 120}
if model.initsequence is None:
config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]]
@@ -285,6 +285,28 @@ display:
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));
# Waveshare 2.13" V3 B series 3-color e-paper (104x212, BWR, UC8151D)
- platform: epaper_spi
spi_id: spi_bus
model: waveshare-2.13in-bv3
cs_pin:
allow_other_uses: true
number: GPIO5
dc_pin:
allow_other_uses: true
number: GPIO17
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
inverted: true
lambda: |-
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));
# Waveshare 2.66" B 3-color e-paper (152x296, BWR, SSD1680)
- platform: epaper_spi
spi_id: spi_bus