mirror of
https://github.com/esphome/esphome.git
synced 2026-09-21 03:58:41 +00:00
[it8951] Add IT8951 e-paper controller support to epaper_spi (#15346)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Citric Li <37475446+limengdu@users.noreply.github.com> Co-authored-by: koosoli <koosoli@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
pre-commit-ci-lite[bot]
Copilot
Citric Li
koosoli
Cursor
Clyde Stubbs
Claude Opus 4.8
parent
d3892b8399
commit
8e23065b86
@@ -266,6 +266,7 @@ esphome/components/integration/* @OttoWinter
|
||||
esphome/components/internal_temperature/* @Mat931
|
||||
esphome/components/interval/* @esphome/core
|
||||
esphome/components/ir_rf_proxy/* @kbx81
|
||||
esphome/components/it8951/* @koosoli @limengdu @Passific
|
||||
esphome/components/jsn_sr04t/* @Mafus1
|
||||
esphome/components/json/* @esphome/core
|
||||
esphome/components/kamstrup_kmp/* @cfeenstra1024
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@Passific", "@koosoli", "@limengdu"]
|
||||
@@ -0,0 +1,433 @@
|
||||
"""
|
||||
ESPHome configuration for the IT8951 e-paper controller.
|
||||
"""
|
||||
|
||||
from esphome import automation, core, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, spi
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD, validate_rotation
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import update_interval
|
||||
from esphome.const import (
|
||||
CONF_BUSY_PIN,
|
||||
CONF_CS_PIN,
|
||||
CONF_DATA_RATE,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_ENABLE_PIN,
|
||||
CONF_FULL_UPDATE_EVERY,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
CONF_INVERT_COLORS,
|
||||
CONF_LAMBDA,
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_MODE,
|
||||
CONF_MODEL,
|
||||
CONF_PAGES,
|
||||
CONF_RESET_DURATION,
|
||||
CONF_RESET_PIN,
|
||||
CONF_ROTATION,
|
||||
CONF_SLEEP_WHEN_DONE,
|
||||
CONF_SWAP_XY,
|
||||
CONF_TRANSFORM,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.cpp_generator import RawExpression
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
AUTO_LOAD = ["split_buffer"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
CONF_VCOM = "vcom"
|
||||
CONF_VCOM_REGISTER = "vcom_register"
|
||||
CONF_FORCE_TEMPERATURE = "force_temperature"
|
||||
CONF_GRAYSCALE = "grayscale"
|
||||
CONF_DITHERING = "dithering"
|
||||
CONF_UPDATE_MODE = "update_mode"
|
||||
CONF_USE_LEGACY_DPY_AREA = "use_legacy_dpy_area"
|
||||
|
||||
# VCOM SET sub-command selectors. The IT8951 firmware accepts different
|
||||
# values across panels; most respond to 0x0001, but a few — e.g. the Seeed
|
||||
# reTerminal E1003 — only respond to 0x0002 and silently drop 0x0001.
|
||||
VCOM_REGISTER_DEFAULT = 0x0001
|
||||
VCOM_REGISTER_ALT = 0x0002
|
||||
VCOM_REGISTER_OPTIONS = (VCOM_REGISTER_DEFAULT, VCOM_REGISTER_ALT)
|
||||
|
||||
it8951_ns = cg.esphome_ns.namespace("it8951")
|
||||
IT8951Display = it8951_ns.class_("IT8951Display", display.Display, spi.SPIDevice)
|
||||
IT8951UpdateAction = it8951_ns.class_("IT8951UpdateAction", automation.Action)
|
||||
|
||||
# Hardware waveform modes exposed to YAML. Strings are mapped to the C++
|
||||
# UpdateMode enum so the runtime can store the mode as a uint16_t rather
|
||||
# than a std::string (avoiding a heap-resident member; see ESPHome
|
||||
# CLAUDE.md "STL Container Guidelines"). "fast" and "full" are
|
||||
# convenience aliases for DU and GC16 respectively.
|
||||
UpdateMode = it8951_ns.enum("UpdateMode")
|
||||
UPDATE_MODE_OPTIONS = {
|
||||
"INIT": UpdateMode.UPDATE_MODE_INIT,
|
||||
"DU": UpdateMode.UPDATE_MODE_DU,
|
||||
"GC16": UpdateMode.UPDATE_MODE_GC16,
|
||||
"GL16": UpdateMode.UPDATE_MODE_GL16,
|
||||
"GLR16": UpdateMode.UPDATE_MODE_GLR16,
|
||||
"GLD16": UpdateMode.UPDATE_MODE_GLD16,
|
||||
"DU4": UpdateMode.UPDATE_MODE_DU4,
|
||||
"A2": UpdateMode.UPDATE_MODE_A2,
|
||||
"FAST": UpdateMode.UPDATE_MODE_DU,
|
||||
"FULL": UpdateMode.UPDATE_MODE_GC16,
|
||||
}
|
||||
# Maps the YAML mode string directly to the C++ UpdateMode enum value, so the
|
||||
# config option and the it8951.update action share one validator.
|
||||
update_mode = cv.enum(UPDATE_MODE_OPTIONS, upper=True)
|
||||
|
||||
# Transform flag values mirror the C++ TRANSFORM_* constants.
|
||||
_TRANSFORM_NONE = 0
|
||||
_TRANSFORM_MIRROR_X = 1
|
||||
_TRANSFORM_MIRROR_Y = 2
|
||||
_TRANSFORM_SWAP_XY = 4
|
||||
_TRANSFORM_FLAGS = {
|
||||
CONF_MIRROR_X: _TRANSFORM_MIRROR_X,
|
||||
CONF_MIRROR_Y: _TRANSFORM_MIRROR_Y,
|
||||
CONF_SWAP_XY: _TRANSFORM_SWAP_XY,
|
||||
}
|
||||
|
||||
|
||||
class IT8951Model:
|
||||
"""A specific board / panel preset for the IT8951 controller."""
|
||||
|
||||
models: dict[str, "IT8951Model"] = {}
|
||||
|
||||
def __init__(self, name: str, **defaults):
|
||||
name = name.upper()
|
||||
self.name = name
|
||||
self.defaults = defaults
|
||||
IT8951Model.models[name] = self
|
||||
|
||||
def get_default(self, key, fallback=None):
|
||||
return self.defaults.get(key, fallback)
|
||||
|
||||
def get_dimensions(self, config) -> tuple[int, int]:
|
||||
# If dimensions are in config, use them; otherwise fall back to model defaults.
|
||||
if CONF_DIMENSIONS in config:
|
||||
dimensions = config[CONF_DIMENSIONS]
|
||||
if isinstance(dimensions, dict):
|
||||
return dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT]
|
||||
return tuple(dimensions)
|
||||
# Model must have defaults if dimensions not in config.
|
||||
return self.get_default(CONF_WIDTH), self.get_default(CONF_HEIGHT)
|
||||
|
||||
|
||||
# --- Model presets ----------------------------------------------------------
|
||||
# The generic model leaves dimensions and pin choices up to the user.
|
||||
IT8951Model("it8951", vcom=2300, sleep_when_done=True, data_rate=12_000_000)
|
||||
|
||||
IT8951Model(
|
||||
"m5stack-m5paper",
|
||||
width=960,
|
||||
height=540,
|
||||
busy_pin=27,
|
||||
reset_pin=23,
|
||||
cs_pin=15,
|
||||
vcom=2300,
|
||||
sleep_when_done=True,
|
||||
data_rate=20_000_000,
|
||||
)
|
||||
|
||||
IT8951Model(
|
||||
"seeed-reterminal-e1003",
|
||||
width=1872,
|
||||
height=1404,
|
||||
busy_pin=13,
|
||||
reset_pin=12,
|
||||
cs_pin=10,
|
||||
# Board power-enable rails: 1.8V logic supply (GPIO21) and the EPD supply
|
||||
# (GPIO11). Driven high during setup so no separate power_supply is needed.
|
||||
enable_pin=[21, 11],
|
||||
vcom=1400,
|
||||
# reTerminal E1003 panel firmware only accepts the 0x0002 VCOM SET
|
||||
# selector; using the default 0x0001 leaves VCOM unchanged and breaks
|
||||
# grayscale waveforms (GC16/GL16) — INIT still works because it does
|
||||
# not depend on VCOM accuracy.
|
||||
vcom_register=VCOM_REGISTER_ALT,
|
||||
# The reTerminal E1003 ships with on-die temperature sensing disabled,
|
||||
# so the host must declare an operating temperature; otherwise the
|
||||
# waveform LUT defaults to a value that produces no visible change
|
||||
# for grayscale modes.
|
||||
force_temperature=25,
|
||||
sleep_when_done=False,
|
||||
data_rate=20_000_000,
|
||||
mirror_x=True,
|
||||
)
|
||||
|
||||
IT8951Model(
|
||||
"seeed-ee03",
|
||||
width=1872,
|
||||
height=1404,
|
||||
busy_pin=4,
|
||||
reset_pin=38,
|
||||
cs_pin=44,
|
||||
vcom=1400,
|
||||
sleep_when_done=False,
|
||||
data_rate=4_000_000,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DIMENSION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_WIDTH): cv.int_,
|
||||
cv.Required(CONF_HEIGHT): cv.int_,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _model_pin_option(model, key, schema):
|
||||
default = model.get_default(key)
|
||||
if default is None:
|
||||
return cv.Required(key), schema
|
||||
return cv.Optional(key, default=default), schema
|
||||
|
||||
|
||||
def _model_schema(config):
|
||||
model = IT8951Model.models[config[CONF_MODEL]]
|
||||
has_default_dimensions = (
|
||||
model.get_default(CONF_WIDTH) is not None
|
||||
and model.get_default(CONF_HEIGHT) is not None
|
||||
)
|
||||
dimensions_key = (
|
||||
cv.Optional(
|
||||
CONF_DIMENSIONS,
|
||||
default={
|
||||
CONF_WIDTH: model.get_default(CONF_WIDTH),
|
||||
CONF_HEIGHT: model.get_default(CONF_HEIGHT),
|
||||
},
|
||||
)
|
||||
if has_default_dimensions
|
||||
else cv.Required(CONF_DIMENSIONS)
|
||||
)
|
||||
|
||||
schema = display.FULL_DISPLAY_SCHEMA.extend(
|
||||
spi.spi_device_schema(
|
||||
cs_pin_required=False,
|
||||
default_mode="MODE0",
|
||||
default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000),
|
||||
)
|
||||
).extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(IT8951Display),
|
||||
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True, space="-"),
|
||||
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
|
||||
cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): update_interval,
|
||||
cv.Optional(CONF_FULL_UPDATE_EVERY, default=30): cv.int_range(1, 255),
|
||||
cv.Optional(CONF_TRANSFORM): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
||||
cv.Optional(CONF_SWAP_XY, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_INVERT_COLORS, default=model.get_default(CONF_INVERT_COLORS, False)
|
||||
): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_SLEEP_WHEN_DONE,
|
||||
default=model.get_default(CONF_SLEEP_WHEN_DONE, False),
|
||||
): cv.boolean,
|
||||
# Pixel format: true = 4bpp grayscale, false = packed 1bpp
|
||||
# monochrome. Monochrome halves the framebuffer and enables fast DU
|
||||
# partial refreshes; grayscale gives 16 levels but always uses GC16.
|
||||
cv.Optional(
|
||||
CONF_GRAYSCALE, default=model.get_default(CONF_GRAYSCALE, True)
|
||||
): cv.boolean,
|
||||
# Monochrome only: ordered-dither pale colours so they render as
|
||||
# visible stipple. Disable for a crisp hard black/white threshold
|
||||
# (better for purely black/white text). No effect in grayscale mode.
|
||||
cv.Optional(
|
||||
CONF_DITHERING, default=model.get_default(CONF_DITHERING, True)
|
||||
): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_VCOM, default=model.get_default(CONF_VCOM, 2300)
|
||||
): cv.int_range(0, 5000),
|
||||
cv.Optional(
|
||||
CONF_VCOM_REGISTER,
|
||||
default=model.get_default(CONF_VCOM_REGISTER, VCOM_REGISTER_DEFAULT),
|
||||
): cv.one_of(*VCOM_REGISTER_OPTIONS, int=True),
|
||||
**(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_FORCE_TEMPERATURE,
|
||||
default=model.get_default(CONF_FORCE_TEMPERATURE),
|
||||
): cv.int_range(min=-40, max=85)
|
||||
}
|
||||
if model.get_default(CONF_FORCE_TEMPERATURE) is not None
|
||||
else {}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_USE_LEGACY_DPY_AREA,
|
||||
default=model.get_default(CONF_USE_LEGACY_DPY_AREA, False),
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_UPDATE_MODE): update_mode,
|
||||
# One or more GPIOs driven high during setup to power on the panel
|
||||
# (e.g. board power-enable rails), before reset and init.
|
||||
cv.Optional(
|
||||
CONF_ENABLE_PIN, default=model.get_default(CONF_ENABLE_PIN, [])
|
||||
): cv.ensure_list(pins.gpio_output_pin_schema),
|
||||
cv.Optional(CONF_RESET_DURATION): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=core.TimePeriod(milliseconds=500)),
|
||||
),
|
||||
dimensions_key: DIMENSION_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
# Pin options: required if the model doesn't supply a default.
|
||||
pin_specs = (
|
||||
(CONF_BUSY_PIN, pins.gpio_input_pin_schema),
|
||||
(CONF_RESET_PIN, pins.gpio_output_pin_schema),
|
||||
(CONF_CS_PIN, pins.gpio_output_pin_schema),
|
||||
)
|
||||
pin_extra = {}
|
||||
for key, schema_value in pin_specs:
|
||||
opt, sv = _model_pin_option(model, key, schema_value)
|
||||
pin_extra[opt] = sv
|
||||
return schema.extend(pin_extra)
|
||||
|
||||
|
||||
def _customise_schema(config):
|
||||
config = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MODEL): cv.one_of(
|
||||
*IT8951Model.models, upper=True, space="-"
|
||||
)
|
||||
},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
)(config)
|
||||
|
||||
model_config = _model_schema(config)(config)
|
||||
|
||||
model = IT8951Model.models[config[CONF_MODEL].upper()]
|
||||
width, height = model.get_dimensions(model_config)
|
||||
|
||||
display.add_metadata(
|
||||
model_config[CONF_ID],
|
||||
width,
|
||||
height,
|
||||
# Rotation is applied per-pixel in draw_pixel_at at no extra cost, so we
|
||||
# advertise hardware rotation: LVGL routes its rotation to the driver via
|
||||
# set_rotation rather than rotating the framebuffer in software.
|
||||
has_hardware_rotation=True,
|
||||
has_writer=any(
|
||||
model_config.get(key)
|
||||
for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD)
|
||||
),
|
||||
# Report the configured rotation so LVGL can detect (and reject) a
|
||||
# rotation set in the display config instead of the LVGL config.
|
||||
rotation=model_config.get(CONF_ROTATION, 0),
|
||||
# The IT8951 snaps partial display refreshes to a 32-pixel X boundary
|
||||
# (see prepare_update_region_), so have LVGL round its redraw areas to
|
||||
# 32px too — this keeps flush rectangles aligned with what the panel
|
||||
# actually refreshes and avoids redundant re-rounding/over-draw.
|
||||
draw_rounding=32,
|
||||
)
|
||||
|
||||
return model_config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = _customise_schema
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
# IT8951 reads from SPI (DevInfo, VCOM, register reads) so MISO is required.
|
||||
spi.final_validate_device_schema("it8951", require_miso=True, require_mosi=True)(
|
||||
config
|
||||
)
|
||||
|
||||
global_config = full_config.get()
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
if CONF_LAMBDA not in config and CONF_PAGES not in config:
|
||||
if LVGL_DOMAIN in global_config:
|
||||
if CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("never")
|
||||
else:
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
model = IT8951Model.models[config[CONF_MODEL]]
|
||||
width, height = model.get_dimensions(config)
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID], model.name, width, height)
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=False)
|
||||
|
||||
if lambda_config := config.get(CONF_LAMBDA):
|
||||
lambda_ = await cg.process_lambda(
|
||||
lambda_config, [(display.DisplayRef, "it")], return_type=cg.void
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
if reset_pin := config.get(CONF_RESET_PIN):
|
||||
cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin)))
|
||||
if busy_pin := config.get(CONF_BUSY_PIN):
|
||||
cg.add(var.set_busy_pin(await cg.gpio_pin_expression(busy_pin)))
|
||||
if enable_pins := config.get(CONF_ENABLE_PIN):
|
||||
cg.add(
|
||||
var.set_enable_pins(
|
||||
[await cg.gpio_pin_expression(pin) for pin in enable_pins]
|
||||
)
|
||||
)
|
||||
cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY]))
|
||||
if (reset_duration := config.get(CONF_RESET_DURATION)) is not None:
|
||||
cg.add(var.set_reset_duration(reset_duration))
|
||||
if config.get(CONF_INVERT_COLORS):
|
||||
cg.add(var.set_invert_colors(True))
|
||||
if config.get(CONF_SLEEP_WHEN_DONE):
|
||||
cg.add(var.set_sleep_when_done(True))
|
||||
cg.add(var.set_vcom(config[CONF_VCOM]))
|
||||
cg.add(var.set_vcom_register(config[CONF_VCOM_REGISTER]))
|
||||
if CONF_FORCE_TEMPERATURE in config:
|
||||
cg.add(var.set_force_temperature(config[CONF_FORCE_TEMPERATURE]))
|
||||
if config.get(CONF_USE_LEGACY_DPY_AREA):
|
||||
cg.add(var.set_use_legacy_dpy_area(True))
|
||||
cg.add(var.set_grayscale(config[CONF_GRAYSCALE]))
|
||||
cg.add(var.set_dithering(config[CONF_DITHERING]))
|
||||
if (mode := config.get(CONF_UPDATE_MODE)) is not None:
|
||||
cg.add(var.set_update_mode(mode))
|
||||
|
||||
transform = config.get(
|
||||
CONF_TRANSFORM,
|
||||
{
|
||||
CONF_MIRROR_X: model.get_default(CONF_MIRROR_X),
|
||||
CONF_MIRROR_Y: model.get_default(CONF_MIRROR_Y),
|
||||
},
|
||||
)
|
||||
|
||||
transform_value = sum(
|
||||
flag for key, flag in _TRANSFORM_FLAGS.items() if transform.get(key)
|
||||
)
|
||||
if transform_value:
|
||||
cg.add(var.set_transform(RawExpression(str(transform_value))))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"it8951.update",
|
||||
IT8951UpdateAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(IT8951Display),
|
||||
cv.Optional(CONF_MODE): cv.templatable(update_mode),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def it8951_update_action_to_code(config, action_id, template_arg, args):
|
||||
display_var = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, display_var)
|
||||
if mode := config.get(CONF_MODE):
|
||||
mode = await cg.templatable(mode, args, UpdateMode)
|
||||
cg.add(var.set_mode(mode))
|
||||
return var
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,373 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "it8951_defs.h"
|
||||
|
||||
namespace esphome::it8951 {
|
||||
|
||||
using namespace display;
|
||||
|
||||
// --- Bounded op queue --------------------------------------------------------
|
||||
// Fixed-capacity ring buffer used by the loop scheduler. Replaces std::deque
|
||||
// to comply with ESPHome's STL container guidelines (std::deque allocates in
|
||||
// 512-byte blocks regardless of element size). Size analysis: the deepest
|
||||
// observed scenario is UPDATE_REFRESH (10 enqueued ops) + CHECK_LUT_IDLE's
|
||||
// 5 push_front rescheduling = 14 simultaneous entries. We use 32 for a
|
||||
// comfortable margin while keeping RAM cost low (~192 bytes per instance vs
|
||||
// 512+ bytes for std::deque).
|
||||
template<typename T, size_t N> class StaticOpQueue {
|
||||
public:
|
||||
bool empty() const { return this->count_ == 0; }
|
||||
size_t size() const { return this->count_; }
|
||||
static constexpr size_t capacity() { return N; }
|
||||
|
||||
bool push_back(const T &value) {
|
||||
if (this->count_ >= N)
|
||||
return false;
|
||||
this->data_[(this->head_ + this->count_) % N] = value;
|
||||
++this->count_;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool push_front(const T &value) {
|
||||
if (this->count_ >= N)
|
||||
return false;
|
||||
this->head_ = (this->head_ + N - 1) % N;
|
||||
this->data_[this->head_] = value;
|
||||
++this->count_;
|
||||
return true;
|
||||
}
|
||||
|
||||
void pop_front() {
|
||||
if (this->count_ == 0)
|
||||
return;
|
||||
this->head_ = (this->head_ + 1) % N;
|
||||
--this->count_;
|
||||
}
|
||||
|
||||
const T &front() const { return this->data_[this->head_]; }
|
||||
T &front() { return this->data_[this->head_]; }
|
||||
|
||||
void clear() {
|
||||
this->head_ = 0;
|
||||
this->count_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
T data_[N]{};
|
||||
size_t head_{0};
|
||||
size_t count_{0};
|
||||
};
|
||||
|
||||
// Op queue capacity. See StaticOpQueue comment for sizing analysis.
|
||||
static constexpr size_t OP_QUEUE_SIZE = 32;
|
||||
|
||||
// --- Op queue ---------------------------------------------------------------
|
||||
// Each Op is a single CS-asserted SPI transaction (or a tiny bookkeeping
|
||||
// step). The loop processes one Op per iteration after gating on HW_RDY, so
|
||||
// the natural ESPHome loop cadence (~8-16 ms) provides inter-op pacing
|
||||
// without any blocking waits.
|
||||
//
|
||||
// Compound Ops (READ_DEV_INFO, XFER_*, DPY_BUF_AREA, ENABLE_1BPP, ...) are
|
||||
// short self-contained methods that do all their SPI work inside a single
|
||||
// CS cycle (or a small handful of cycles) and complete well under 2ms, so
|
||||
// they don't break the no-blocking budget.
|
||||
//
|
||||
// Each write-type op is a SINGLE CS-asserted transaction. The loop-level
|
||||
// HW_RDY gate ensures the controller is ready before dispatching any op, so
|
||||
// no blocking waits are needed within write ops.
|
||||
//
|
||||
// Read ops are decomposed: the command/address that triggers data preparation
|
||||
// is sent as write ops (CMD, WRITE_W), then a separate read op runs only
|
||||
// after the loop confirms HW_RDY is back HIGH (data ready). No blocking.
|
||||
enum class OpType : uint8_t {
|
||||
CMD, // single CS: CMD preamble + command word (a)
|
||||
WRITE_W, // single CS: WRITE preamble + data word (a)
|
||||
WRITE_REG, // single CS: WRITE preamble + addr(a) + value(b)
|
||||
// (caller must enqueue CMD(TCON_REG_WR) before this)
|
||||
READ_DEV_INFO, // single CS: READ preamble + dummy + read DevInfo struct
|
||||
// (caller enqueues CMD(GET_DEV_INFO) first; loop HW_RDY gate
|
||||
// ensures data is ready before this op runs)
|
||||
READ_WORD, // single CS: READ preamble + dummy + read one 16-bit word
|
||||
// into read_result_. Loop HW_RDY gate ensures data ready.
|
||||
CHECK_LUT_IDLE, // checks read_result_; if non-zero, re-enqueues read sequence
|
||||
SET_1BPP, // uses read_result_ to set UP1SR bit 2, enqueues writes
|
||||
XFER_LISAR, // set image-buffer target address (2× reg write: 4 CS transactions)
|
||||
XFER_AREA_CMD, // single CS: CMD preamble + TCON_LD_IMG_AREA
|
||||
XFER_AREA_ARGS, // single CS: WRITE preamble + 5 area-parameter words
|
||||
XFER_ROWS, // single CS: WRITE preamble + row pixel data (time-sliced)
|
||||
XFER_AREA_END, // single CS: CMD preamble + TCON_LD_IMG_END
|
||||
DPY_BUF_CMD, // single CS: CMD preamble + I80_CMD_DPY_BUF_AREA
|
||||
DPY_BUF_ARGS, // single CS: WRITE preamble + 7 display-area words
|
||||
GPIO_RESET_LOW, // drive RESET pin low
|
||||
GPIO_RESET_HIGH, // drive RESET pin high
|
||||
DELAY_MS, // park `delay_until_` for a few ms (no SPI)
|
||||
};
|
||||
|
||||
struct Op {
|
||||
OpType type;
|
||||
uint16_t a{0};
|
||||
uint16_t b{0};
|
||||
};
|
||||
|
||||
// High-level controller phases. Each phase enqueues a sequence of Ops; when
|
||||
// the queue drains, advance_phase_() runs the next phase.
|
||||
// This separation keeps per-Op work tiny and predictable.
|
||||
enum class Phase : uint8_t {
|
||||
IDLE,
|
||||
// Initialisation
|
||||
INIT_RESET, // reset pulse + wake controller + packed-write enable
|
||||
INIT_DEV_INFO, // GET_DEV_INFO and validate
|
||||
INIT_VCOM, // write configured VCOM
|
||||
INIT_TEMP, // force temperature for waveform LUT selection
|
||||
INIT_DONE, // allocate framebuffer; transition to IDLE
|
||||
// Update flow
|
||||
UPDATE_PREPARE, // do_update_, compute dirty region, decide 4bpp/1bpp
|
||||
UPDATE_TRANSFER, // one LD_IMG_AREA, time-sliced row streaming, one LD_IMG_END
|
||||
UPDATE_REFRESH, // wait LUT idle, optionally enable 1bpp, send DPY_BUF_AREA
|
||||
UPDATE_SLEEP, // optional deep sleep
|
||||
};
|
||||
|
||||
class IT8951Display : public Display,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||
spi::DATA_RATE_2MHZ> {
|
||||
public:
|
||||
IT8951Display(const char *name, uint16_t width, uint16_t height) : name_(name), width_(width), height_(height) {
|
||||
this->row_width_ = this->compute_row_width_();
|
||||
this->buffer_length_ = static_cast<size_t>(this->row_width_) * static_cast<size_t>(height);
|
||||
}
|
||||
|
||||
// --- Component lifecycle ---
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void on_safe_shutdown() override;
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
|
||||
// --- Config setters (called from generated code) ---
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
void set_busy_pin(GPIOPin *pin) { this->busy_pin_ = pin; }
|
||||
void set_enable_pins(std::vector<GPIOPin *> pins) { this->enable_pins_ = std::move(pins); }
|
||||
void set_reset_duration(uint32_t ms) { this->reset_duration_ = ms; }
|
||||
void set_full_update_every(uint8_t n) {
|
||||
this->full_update_every_ = n;
|
||||
// Seed the counter so the very first update trips the full-update branch in
|
||||
// prepare_update_region_, giving a freshly-booted panel a clean GC16 refresh
|
||||
// before any partial (fast-waveform) updates begin.
|
||||
this->partial_update_count_ = n;
|
||||
}
|
||||
void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; }
|
||||
void set_sleep_when_done(bool s) { this->sleep_when_done_ = s; }
|
||||
void set_vcom(uint16_t vcom_mv) { this->vcom_ = vcom_mv; }
|
||||
void set_vcom_register(uint16_t selector) { this->vcom_register_ = selector; }
|
||||
void set_force_temperature(int16_t celsius) {
|
||||
this->force_temperature_ = celsius;
|
||||
this->force_temperature_set_ = true;
|
||||
}
|
||||
void set_use_legacy_dpy_area(bool use) { this->use_legacy_dpy_area_ = use; }
|
||||
// Pixel format: true = 4bpp grayscale framebuffer, false = packed 1bpp
|
||||
// monochrome framebuffer. Chosen at config time; the framebuffer is stored
|
||||
// in this native format and every update uses the matching transfer path.
|
||||
void set_grayscale(bool g) { this->grayscale_ = g; }
|
||||
// Monochrome only: ordered-dither pale colours (true) vs a hard 50% threshold.
|
||||
void set_dithering(bool d) { this->dithering_ = d; }
|
||||
void set_update_mode(uint16_t m) { this->default_update_mode_ = static_cast<UpdateMode>(m); }
|
||||
void set_transform(uint8_t t) {
|
||||
this->transform_ = t;
|
||||
this->update_effective_transform_();
|
||||
}
|
||||
void set_rotation(DisplayRotation rotation) override {
|
||||
Display::set_rotation(rotation);
|
||||
this->update_effective_transform_();
|
||||
}
|
||||
|
||||
// --- Display API ---
|
||||
void update() override;
|
||||
void update_mode(UpdateMode mode);
|
||||
DisplayType get_display_type() override { return this->grayscale_ ? DISPLAY_TYPE_GRAYSCALE : DISPLAY_TYPE_BINARY; }
|
||||
void fill(Color color) override;
|
||||
void clear() override { this->fill(Color::WHITE); }
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
// Bulk pixel blit (used by LVGL and image rendering). Overridden to write
|
||||
// straight into the framebuffer, avoiding the base class's per-pixel
|
||||
// draw_pixel_at overhead (watchdog feed, clipping test, dirty-box clamps).
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
int get_width() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->height_ : this->width_; }
|
||||
int get_height() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->width_ : this->height_; }
|
||||
|
||||
protected:
|
||||
int get_height_internal() override { return this->height_; }
|
||||
int get_width_internal() override { return this->width_; }
|
||||
|
||||
// --- Coord transform / dirty region ---
|
||||
void update_effective_transform_();
|
||||
// Map display (logical) coordinates to native framebuffer coordinates by
|
||||
// applying effective_transform_ (swap/mirror). Shared by rotate_coordinates_
|
||||
// and the bulk draw_pixels_at path.
|
||||
void apply_transform_(int &x, int &y) const;
|
||||
bool rotate_coordinates_(int &x, int &y);
|
||||
void reset_dirty_region_();
|
||||
|
||||
// --- Framebuffer geometry / monochrome packing ---
|
||||
// Bytes per row for the configured pixel format: 4bpp grayscale packs two
|
||||
// pixels per byte; monochrome packs eight bits per byte, rounded up to a
|
||||
// whole 16-pixel group (matching the controller's 8bpp-load / 1bpp trick).
|
||||
uint16_t compute_row_width_() const {
|
||||
return this->grayscale_ ? static_cast<uint16_t>((static_cast<uint32_t>(this->width_) + 1) / 2)
|
||||
: static_cast<uint16_t>(((static_cast<uint32_t>(this->width_) + 15) / 16) * 2);
|
||||
}
|
||||
void set_mono_pixel_(uint16_t x, uint16_t y, bool value) const;
|
||||
// Write a 4bpp grayscale nibble into the framebuffer (two pixels per byte).
|
||||
void set_gray_pixel_(uint16_t x, uint16_t y, uint8_t nibble) const;
|
||||
// Convert a color and write it at native framebuffer coordinates: a 4bpp
|
||||
// nibble in grayscale mode, or an ordered-dithered bit in monochrome mode.
|
||||
void write_pixel_native_(uint16_t x, uint16_t y, const Color &color) const;
|
||||
|
||||
// --- Op queue / loop machinery ---
|
||||
void enqueue_(OpType type, uint16_t a = 0, uint16_t b = 0);
|
||||
void prepend_(OpType type, uint16_t a = 0, uint16_t b = 0);
|
||||
bool is_busy_() const;
|
||||
void process_op_(const Op &op);
|
||||
void advance_phase_();
|
||||
void set_phase_(Phase next);
|
||||
void start_update_(UpdateMode mode);
|
||||
|
||||
// --- SPI primitives (each is one CS-asserted burst, fully non-blocking) ---
|
||||
void spi_cmd_(uint16_t cmd);
|
||||
void spi_write_word_(uint16_t value);
|
||||
void spi_write_reg_(uint16_t addr, uint16_t value);
|
||||
void spi_write_args_(const uint16_t *args, uint16_t count);
|
||||
uint16_t spi_read_word_(); // non-blocking: HW_RDY confirmed by loop gate
|
||||
void spi_read_dev_info_(); // non-blocking: HW_RDY confirmed by loop gate
|
||||
|
||||
// --- Compound Ops (small bounded helpers) ---
|
||||
void op_xfer_lisar_();
|
||||
void op_xfer_area_args_();
|
||||
void op_xfer_area_end_();
|
||||
bool op_xfer_rows_(); // returns true when current update area fully sent
|
||||
void op_dpy_buf_args_();
|
||||
void op_check_lut_idle_();
|
||||
void op_set_1bpp_();
|
||||
|
||||
// --- Phase enqueuers ---
|
||||
void enqueue_init_reset_();
|
||||
void enqueue_init_dev_info_();
|
||||
void enqueue_init_vcom_();
|
||||
void enqueue_init_temp_();
|
||||
void enqueue_update_transfer_();
|
||||
void enqueue_update_refresh_();
|
||||
void enqueue_update_sleep_();
|
||||
|
||||
bool prepare_update_region_(UpdateMode &mode);
|
||||
|
||||
// --- Recovery ---
|
||||
void recover_();
|
||||
|
||||
// --- State ---
|
||||
static constexpr uint32_t BUSY_TIMEOUT_MS = 5000;
|
||||
|
||||
StaticOpQueue<Op, OP_QUEUE_SIZE> queue_;
|
||||
Phase phase_{Phase::IDLE};
|
||||
uint32_t delay_until_{0};
|
||||
uint32_t phase_started_at_{0};
|
||||
// Requests a continuous (non-throttled) main loop while streaming image data
|
||||
// so 20ms transfer slices aren't separated by the ~16ms default loop interval.
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
|
||||
// Pending update bookkeeping
|
||||
bool update_pending_{false};
|
||||
UpdateMode pending_update_mode_{UPDATE_MODE_NONE};
|
||||
UpdateMode active_mode_{UPDATE_MODE_NONE};
|
||||
uint16_t area_x_{0}, area_y_{0}, area_w_{0}, area_h_{0};
|
||||
uint16_t transfer_row_{0};
|
||||
bool initialised_{false};
|
||||
// True once TCON_SLEEP has been sent and the controller has not been woken
|
||||
// since. The next update must issue TCON_SYS_RUN before any SPI op.
|
||||
bool asleep_{false};
|
||||
uint32_t partial_update_count_{0};
|
||||
uint32_t update_started_at_{0};
|
||||
|
||||
// Read result storage for decomposed read-modify-write op sequences
|
||||
uint16_t read_result_{0};
|
||||
|
||||
// Device info
|
||||
DevInfo dev_info_{};
|
||||
uint16_t img_buf_addr_l_{0};
|
||||
uint16_t img_buf_addr_h_{0};
|
||||
|
||||
// Configured properties
|
||||
const char *name_;
|
||||
uint16_t width_;
|
||||
uint16_t height_;
|
||||
uint16_t row_width_;
|
||||
size_t buffer_length_{};
|
||||
uint8_t *buffer_{};
|
||||
uint8_t transform_{0};
|
||||
uint8_t effective_transform_{0};
|
||||
uint8_t full_update_every_{1};
|
||||
uint32_t reset_duration_{10};
|
||||
uint16_t vcom_{2300};
|
||||
uint16_t vcom_register_{I80_CMD_VCOM_WRITE};
|
||||
int16_t force_temperature_{DEFAULT_FORCE_TEMP_C};
|
||||
bool force_temperature_set_{false};
|
||||
bool use_legacy_dpy_area_{false};
|
||||
bool invert_colors_{false};
|
||||
bool sleep_when_done_{false};
|
||||
// Pixel format selector (see set_grayscale): true = 4bpp grayscale,
|
||||
// false = packed 1bpp monochrome.
|
||||
bool grayscale_{true};
|
||||
// Monochrome dithering (see set_dithering): true = ordered dither.
|
||||
bool dithering_{true};
|
||||
UpdateMode default_update_mode_{UPDATE_MODE_NONE};
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
GPIOPin *busy_pin_{nullptr};
|
||||
// GPIOs driven high during setup to power on the panel (empty if unused).
|
||||
std::vector<GPIOPin *> enable_pins_;
|
||||
|
||||
// Dirty region (pixel coordinates of bounding box of changes since last update)
|
||||
uint16_t x_low_{0}, y_low_{0}, x_high_{0}, y_high_{0};
|
||||
|
||||
// Saved data rate so we can probe slow then run fast
|
||||
uint32_t configured_data_rate_{0};
|
||||
|
||||
// Consecutive recovery attempts; used to give up rather than infinite-loop
|
||||
// when the controller is unresponsive (e.g. wiring issue).
|
||||
uint8_t recovery_attempts_{0};
|
||||
|
||||
// DevInfo read retry counter (controller often returns garbage on the first
|
||||
// read after reset; the original driver retried up to 3 times with 100ms
|
||||
// between attempts).
|
||||
uint8_t dev_info_attempts_{0};
|
||||
};
|
||||
|
||||
// --- Automation action ---
|
||||
template<typename... Ts> class IT8951UpdateAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit IT8951UpdateAction(IT8951Display *display) : display_(display) {}
|
||||
TEMPLATABLE_VALUE(UpdateMode, mode)
|
||||
|
||||
protected:
|
||||
void play(const Ts &...x) override {
|
||||
if (!this->display_->is_ready())
|
||||
return;
|
||||
if (this->mode_.has_value()) {
|
||||
this->display_->update_mode(this->mode_.value(x...));
|
||||
} else {
|
||||
this->display_->update();
|
||||
}
|
||||
}
|
||||
|
||||
IT8951Display *display_;
|
||||
};
|
||||
|
||||
} // namespace esphome::it8951
|
||||
@@ -0,0 +1,168 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::it8951 {
|
||||
|
||||
struct DevInfo {
|
||||
uint16_t panel_width{0};
|
||||
uint16_t panel_height{0};
|
||||
uint16_t img_buf_addr_l{0};
|
||||
uint16_t img_buf_addr_h{0};
|
||||
uint16_t fw_version[8]{};
|
||||
uint16_t lut_version[8]{};
|
||||
};
|
||||
|
||||
// --- IT8951 SPI packet preambles ---
|
||||
static constexpr uint16_t PACKET_TYPE_CMD = 0x6000;
|
||||
static constexpr uint16_t PACKET_TYPE_WRITE = 0x0000;
|
||||
static constexpr uint16_t PACKET_TYPE_READ = 0x1000;
|
||||
|
||||
// --- Built-in I80 commands ---
|
||||
static constexpr uint16_t TCON_SYS_RUN = 0x0001;
|
||||
static constexpr uint16_t TCON_STANDBY = 0x0002;
|
||||
static constexpr uint16_t TCON_SLEEP = 0x0003;
|
||||
static constexpr uint16_t TCON_REG_RD = 0x0010;
|
||||
static constexpr uint16_t TCON_REG_WR = 0x0011;
|
||||
|
||||
static constexpr uint16_t TCON_LD_IMG = 0x0020;
|
||||
static constexpr uint16_t TCON_LD_IMG_AREA = 0x0021;
|
||||
static constexpr uint16_t TCON_LD_IMG_END = 0x0022;
|
||||
|
||||
// --- I80 user-defined commands ---
|
||||
static constexpr uint16_t I80_CMD_DPY_AREA = 0x0034;
|
||||
static constexpr uint16_t I80_CMD_GET_DEV_INFO = 0x0302;
|
||||
static constexpr uint16_t I80_CMD_DPY_BUF_AREA = 0x0037;
|
||||
static constexpr uint16_t I80_CMD_VCOM = 0x0039;
|
||||
static constexpr uint16_t I80_CMD_VCOM_READ = 0x0000;
|
||||
// VCOM write selectors. Different IT8951-driven panels accept different
|
||||
// selector values for the VCOM SET sub-command. Most panels (m5stack-m5paper,
|
||||
// generic dev kits) accept 0x0001. Some panels — notably the Seeed
|
||||
// reTerminal E1003 — only respond to selector 0x0002 and silently ignore
|
||||
// 0x0001, leaving VCOM at its default and making grayscale waveforms
|
||||
// (GC16/GL16) ineffective even though INIT still works.
|
||||
static constexpr uint16_t I80_CMD_VCOM_WRITE = 0x0001;
|
||||
static constexpr uint16_t I80_CMD_VCOM_WRITE_ALT = 0x0002;
|
||||
|
||||
// Force temperature command. The IT8951 selects waveform LUTs based on
|
||||
// panel temperature; if it is left at the controller default, panels with
|
||||
// auto-temperature disabled (notably the Seeed reTerminal E1003) will
|
||||
// run waveforms against a mismatched LUT, leaving pixels visually
|
||||
// unchanged even though the LUT engine completes a full cycle. The
|
||||
// selector word selects the operation (0x0001 = write); the value word
|
||||
// is the temperature in degrees Celsius.
|
||||
static constexpr uint16_t I80_CMD_FORCE_TEMP = 0x0040;
|
||||
static constexpr uint16_t I80_CMD_FORCE_TEMP_WRITE = 0x0001;
|
||||
static constexpr int16_t DEFAULT_FORCE_TEMP_C = 25;
|
||||
|
||||
// --- Pixel mode (bits per pixel encoding) ---
|
||||
static constexpr uint8_t PIXEL_2BPP = 0;
|
||||
static constexpr uint8_t PIXEL_3BPP = 1;
|
||||
static constexpr uint8_t PIXEL_4BPP = 2;
|
||||
static constexpr uint8_t PIXEL_8BPP = 3;
|
||||
|
||||
// --- Endian flags for LD_IMG_AREA ---
|
||||
static constexpr uint8_t LDIMG_L_ENDIAN = 0;
|
||||
static constexpr uint8_t LDIMG_B_ENDIAN = 1;
|
||||
|
||||
// --- SPI probe frequency used for initial controller handshake ---
|
||||
static constexpr uint32_t SPI_PROBE_FREQUENCY = 1'000'000;
|
||||
|
||||
// --- Refresh modes ---
|
||||
/*
|
||||
INIT The initialization (INIT) mode is
|
||||
used to completely erase the display and leave it in the white state. It is
|
||||
useful for situations where the display information in memory is not a faithful
|
||||
representation of the optical state of the display, for example, after the
|
||||
device receives power after it has been fully powered down. This waveform
|
||||
switches the display several times and leaves it in the white state.
|
||||
|
||||
DU
|
||||
The direct update (DU) is a very fast, non-flashy update. This mode supports
|
||||
transitions from any graytone to black or white only. It cannot be used to
|
||||
update to any graytone other than black or white. The fast update time for this
|
||||
mode makes it useful for response to touch sensor or pen input or menu selection
|
||||
indictors.
|
||||
|
||||
GC16
|
||||
The grayscale clearing (GC16) mode is used to update the full display and
|
||||
provide a high image quality. When GC16 is used with Full Display Update the
|
||||
entire display will update as the new image is written. If a Partial Update
|
||||
command is used the only pixels with changing graytone values will update. The
|
||||
GC16 mode has 16 unique gray levels.
|
||||
|
||||
GL16
|
||||
The GL16 waveform is primarily used to update sparse content on a white
|
||||
background, such as a page of anti-aliased text, with reduced flash. The
|
||||
GL16 waveform has 16 unique gray levels.
|
||||
|
||||
GLR16
|
||||
The GLR16 mode is used in conjunction with an image preprocessing algorithm to
|
||||
update sparse content on a white background with reduced flash and reduced image
|
||||
artifacts. The GLR16 mode supports 16 graytones. If only the even pixel states
|
||||
are used (0, 2, 4, … 30), the mode will behave exactly as a traditional GL16
|
||||
waveform mode. If a separately-supplied image preprocessing algorithm is used,
|
||||
the transitions invoked by the pixel states 29 and 31 are used to improve
|
||||
display quality. For the AF waveform, it is assured that the GLR16 waveform data
|
||||
will point to the same voltage lists as the GL16 data and does not need to be
|
||||
stored in a separate memory.
|
||||
|
||||
GLD16
|
||||
The GLD16 mode is used in conjunction with an image preprocessing algorithm to
|
||||
update sparse content on a white background with reduced flash and reduced image
|
||||
artifacts. It is recommended to be used only with the full display update. The
|
||||
GLD16 mode supports 16 graytones. If only the even pixel states are used (0, 2,
|
||||
4, … 30), the mode will behave exactly as a traditional GL16 waveform mode. If a
|
||||
separately-supplied image preprocessing algorithm is used, the transitions
|
||||
invoked by the pixel states 29 and 31 are used to refresh the background with a
|
||||
lighter flash compared to GC16 mode following a predetermined pixel map as
|
||||
encoded in the waveform file, and reduce image artifacts even more compared to
|
||||
the GLR16 mode. For the AF waveform, it is assured that the GLD16 waveform data
|
||||
will point to the same voltage lists as the GL16 data and does not need to be
|
||||
stored in a separate memory.
|
||||
|
||||
DU4
|
||||
The DU4 is a fast update time (similar to DU), non-flashy waveform. This mode
|
||||
supports transitions from any gray tone to gray tones 1,6,11,16 represented by
|
||||
pixel states [0 10 20 30]. The combination of fast update time and four gray
|
||||
tones make it useful for anti-aliased text in menus. There is a moderate
|
||||
increase in ghosting compared with GC16.
|
||||
|
||||
A2
|
||||
The A2 mode is a fast, non-flash update mode designed for fast paging turning or
|
||||
simple black/white animation. This mode supports transitions from and to black
|
||||
or white only. It cannot be used to update to any graytone other than black or
|
||||
white. The recommended update sequence to transition into repeated A2 updates is
|
||||
shown in Figure 1. The use of a white image in the transition from 4-bit to
|
||||
1-bit images will reduce ghosting and improve image quality for A2 updates.
|
||||
*/
|
||||
enum UpdateMode : uint16_t {
|
||||
UPDATE_MODE_INIT = 0,
|
||||
UPDATE_MODE_DU = 1,
|
||||
UPDATE_MODE_GC16 = 2,
|
||||
UPDATE_MODE_GL16 = 3,
|
||||
UPDATE_MODE_GLR16 = 4,
|
||||
UPDATE_MODE_GLD16 = 5,
|
||||
UPDATE_MODE_DU4 = 6,
|
||||
UPDATE_MODE_A2 = 7,
|
||||
UPDATE_MODE_NONE = 8,
|
||||
};
|
||||
|
||||
// --- Registers ---
|
||||
static constexpr uint16_t DISPLAY_REG_BASE = 0x1000;
|
||||
static constexpr uint16_t UP1SR = DISPLAY_REG_BASE + 0x138;
|
||||
static constexpr uint16_t LUTAFSR = DISPLAY_REG_BASE + 0x224;
|
||||
static constexpr uint16_t BGVR = DISPLAY_REG_BASE + 0x250;
|
||||
|
||||
static constexpr uint16_t I80CPCR = 0x0004;
|
||||
|
||||
static constexpr uint16_t MCSR_BASE_ADDR = 0x0200;
|
||||
static constexpr uint16_t LISAR = MCSR_BASE_ADDR + 0x0008;
|
||||
|
||||
// Display orientation flags
|
||||
static constexpr uint8_t TRANSFORM_NONE = 0;
|
||||
static constexpr uint8_t TRANSFORM_MIRROR_X = 1;
|
||||
static constexpr uint8_t TRANSFORM_MIRROR_Y = 2;
|
||||
static constexpr uint8_t TRANSFORM_SWAP_XY = 4;
|
||||
|
||||
} // namespace esphome::it8951
|
||||
@@ -0,0 +1,109 @@
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
|
||||
|
||||
display:
|
||||
# Generic IT8951 with explicit dimensions
|
||||
- platform: it8951
|
||||
spi_id: spi_bus
|
||||
model: it8951
|
||||
dimensions:
|
||||
width: 1872
|
||||
height: 1404
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
enable_pin:
|
||||
- GPIO17
|
||||
- GPIO18
|
||||
vcom: 1500
|
||||
update_interval: 60s
|
||||
# Exercise an alias for the update_mode config option.
|
||||
update_mode: fast
|
||||
lambda: |-
|
||||
it.circle(64, 64, 50, Color::BLACK);
|
||||
|
||||
# m5stack-m5paper (960x540) — model supplies pin defaults
|
||||
- platform: it8951
|
||||
id: m5epd_display
|
||||
spi_id: spi_bus
|
||||
model: m5stack-m5paper
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
full_update_every: 30
|
||||
invert_colors: false
|
||||
sleep_when_done: true
|
||||
grayscale: true
|
||||
update_mode: GC16
|
||||
rotation: 270
|
||||
transform:
|
||||
mirror_x: false
|
||||
mirror_y: false
|
||||
lambda: |-
|
||||
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 30, Color::BLACK);
|
||||
|
||||
# seeed-reterminal-e1003 (1872x1404)
|
||||
- platform: it8951
|
||||
spi_id: spi_bus
|
||||
model: seeed-reterminal-e1003
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
vcom: 1400
|
||||
sleep_when_done: false
|
||||
lambda: |-
|
||||
it.filled_rectangle(0, 0, 128, 128, Color::BLACK);
|
||||
|
||||
# seeed-ee03 (1872x1404), monochrome fast path
|
||||
- platform: it8951
|
||||
spi_id: spi_bus
|
||||
model: seeed-ee03
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
grayscale: false
|
||||
dithering: false
|
||||
update_mode: DU
|
||||
lambda: |-
|
||||
it.circle(128, 128, 64, Color::BLACK);
|
||||
|
||||
# Exercise the it8951.update automation: alias modes, a direct enum-name mode,
|
||||
# and the bare (default-mode) form.
|
||||
interval:
|
||||
- interval: 30s
|
||||
then:
|
||||
- it8951.update:
|
||||
id: m5epd_display
|
||||
mode: fast
|
||||
- it8951.update:
|
||||
id: m5epd_display
|
||||
mode: full
|
||||
- it8951.update:
|
||||
id: m5epd_display
|
||||
mode: A2
|
||||
- it8951.update: m5epd_display
|
||||
@@ -18,6 +18,9 @@ class MockUARTComponent : public uart::UARTComponent {
|
||||
MOCK_METHOD(size_t, available, (), (override));
|
||||
MOCK_METHOD(uart::UARTFlushResult, flush, (), (override));
|
||||
MOCK_METHOD(void, check_logger_conflict, (), (override));
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
void load_settings(bool dump_config) override {}
|
||||
#endif // USE_ESP8266 || USE_ESP32
|
||||
};
|
||||
|
||||
// Expose protected members for testing.
|
||||
|
||||
Reference in New Issue
Block a user