[mipi_spi] Template code, partial buffer support (#9314)

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
Clyde Stubbs 2025-07-16 11:05:27 +10:00 committed by GitHub
parent 5480675dd8
commit 6486147da1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 1430 additions and 1113 deletions

View File

@ -3,6 +3,7 @@
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
CONF_BYTE_ORDER = "byte_order" CONF_BYTE_ORDER = "byte_order"
CONF_COLOR_DEPTH = "color_depth"
CONF_DRAW_ROUNDING = "draw_rounding" CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ON_STATE_CHANGE = "on_state_change" CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers" CONF_REQUEST_HEADERS = "request_headers"

View File

@ -2,10 +2,8 @@ CODEOWNERS = ["@clydebarrow"]
DOMAIN = "mipi_spi" DOMAIN = "mipi_spi"
CONF_DRAW_FROM_ORIGIN = "draw_from_origin"
CONF_SPI_16 = "spi_16" CONF_SPI_16 = "spi_16"
CONF_PIXEL_MODE = "pixel_mode" CONF_PIXEL_MODE = "pixel_mode"
CONF_COLOR_DEPTH = "color_depth"
CONF_BUS_MODE = "bus_mode" CONF_BUS_MODE = "bus_mode"
CONF_USE_AXIS_FLIPS = "use_axis_flips" CONF_USE_AXIS_FLIPS = "use_axis_flips"
CONF_NATIVE_WIDTH = "native_width" CONF_NATIVE_WIDTH = "native_width"

View File

@ -3,11 +3,18 @@ import logging
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import display, spi from esphome.components import display, spi
from esphome.components.const import (
CONF_BYTE_ORDER,
CONF_COLOR_DEPTH,
CONF_DRAW_ROUNDING,
)
from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.config_validation import ALLOW_EXTRA from esphome.config_validation import ALLOW_EXTRA
from esphome.const import ( from esphome.const import (
CONF_BRIGHTNESS, CONF_BRIGHTNESS,
CONF_BUFFER_SIZE,
CONF_COLOR_ORDER, CONF_COLOR_ORDER,
CONF_CS_PIN, CONF_CS_PIN,
CONF_DATA_RATE, CONF_DATA_RATE,
@ -24,19 +31,19 @@ from esphome.const import (
CONF_MODEL, CONF_MODEL,
CONF_OFFSET_HEIGHT, CONF_OFFSET_HEIGHT,
CONF_OFFSET_WIDTH, CONF_OFFSET_WIDTH,
CONF_PAGES,
CONF_RESET_PIN, CONF_RESET_PIN,
CONF_ROTATION, CONF_ROTATION,
CONF_SWAP_XY, CONF_SWAP_XY,
CONF_TRANSFORM, CONF_TRANSFORM,
CONF_WIDTH, CONF_WIDTH,
) )
from esphome.core import TimePeriod from esphome.core import CORE, TimePeriod
from esphome.cpp_generator import TemplateArguments
from esphome.final_validate import full_config
from ..const import CONF_DRAW_ROUNDING
from ..lvgl.defines import CONF_COLOR_DEPTH
from . import ( from . import (
CONF_BUS_MODE, CONF_BUS_MODE,
CONF_DRAW_FROM_ORIGIN,
CONF_NATIVE_HEIGHT, CONF_NATIVE_HEIGHT,
CONF_NATIVE_WIDTH, CONF_NATIVE_WIDTH,
CONF_PIXEL_MODE, CONF_PIXEL_MODE,
@ -55,6 +62,7 @@ from .models import (
MADCTL_XFLIP, MADCTL_XFLIP,
MADCTL_YFLIP, MADCTL_YFLIP,
DriverChip, DriverChip,
adafruit,
amoled, amoled,
cyd, cyd,
ili, ili,
@ -69,43 +77,112 @@ DEPENDENCIES = ["spi"]
LOGGER = logging.getLogger(DOMAIN) LOGGER = logging.getLogger(DOMAIN)
mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi") mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi")
MipiSpi = mipi_spi_ns.class_( MipiSpi = mipi_spi_ns.class_("MipiSpi", display.Display, cg.Component, spi.SPIDevice)
"MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice MipiSpiBuffer = mipi_spi_ns.class_(
"MipiSpiBuffer", MipiSpi, display.Display, cg.Component, spi.SPIDevice
) )
ColorOrder = display.display_ns.enum("ColorMode") ColorOrder = display.display_ns.enum("ColorMode")
ColorBitness = display.display_ns.enum("ColorBitness") ColorBitness = display.display_ns.enum("ColorBitness")
Model = mipi_spi_ns.enum("Model") Model = mipi_spi_ns.enum("Model")
PixelMode = mipi_spi_ns.enum("PixelMode")
BusType = mipi_spi_ns.enum("BusType")
COLOR_ORDERS = { COLOR_ORDERS = {
MODE_RGB: ColorOrder.COLOR_ORDER_RGB, MODE_RGB: ColorOrder.COLOR_ORDER_RGB,
MODE_BGR: ColorOrder.COLOR_ORDER_BGR, MODE_BGR: ColorOrder.COLOR_ORDER_BGR,
} }
COLOR_DEPTHS = { COLOR_DEPTHS = {
8: ColorBitness.COLOR_BITNESS_332, 8: PixelMode.PIXEL_MODE_8,
16: ColorBitness.COLOR_BITNESS_565, 16: PixelMode.PIXEL_MODE_16,
18: PixelMode.PIXEL_MODE_18,
} }
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
BusTypes = {
TYPE_SINGLE: BusType.BUS_TYPE_SINGLE,
TYPE_QUAD: BusType.BUS_TYPE_QUAD,
TYPE_OCTAL: BusType.BUS_TYPE_OCTAL,
}
DriverChip("CUSTOM", initsequence={}) DriverChip("CUSTOM")
MODELS = DriverChip.models MODELS = DriverChip.models
# These statements are noops, but serve to suppress linting of side-effect-only imports # This loop is a noop, but suppresses linting of side-effect-only imports
for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare): for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare, adafruit):
pass pass
PixelMode = mipi_spi_ns.enum("PixelMode")
PIXEL_MODE_18BIT = "18bit" DISPLAY_18BIT = "18bit"
PIXEL_MODE_16BIT = "16bit" DISPLAY_16BIT = "16bit"
PIXEL_MODES = { DISPLAY_PIXEL_MODES = {
PIXEL_MODE_16BIT: 0x55, DISPLAY_16BIT: (0x55, PixelMode.PIXEL_MODE_16),
PIXEL_MODE_18BIT: 0x66, DISPLAY_18BIT: (0x66, PixelMode.PIXEL_MODE_18),
} }
def get_dimensions(config):
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
width = dimensions[CONF_WIDTH]
height = dimensions[CONF_HEIGHT]
offset_width = dimensions[CONF_OFFSET_WIDTH]
offset_height = dimensions[CONF_OFFSET_HEIGHT]
return width, height, offset_width, offset_height
(width, height) = dimensions
return width, height, 0, 0
# Default dimensions, use model defaults
transform = get_transform(config)
model = MODELS[config[CONF_MODEL]]
width = model.get_default(CONF_WIDTH)
height = model.get_default(CONF_HEIGHT)
offset_width = model.get_default(CONF_OFFSET_WIDTH, 0)
offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0)
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
# the offset is asymmetric
if transform[CONF_MIRROR_X]:
native_width = model.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2)
offset_width = native_width - width - offset_width
if transform[CONF_MIRROR_Y]:
native_height = model.get_default(
CONF_NATIVE_HEIGHT, height + offset_height * 2
)
offset_height = native_height - height - offset_height
# Swap default dimensions if swap_xy is set
if transform[CONF_SWAP_XY] is True:
width, height = height, width
offset_height, offset_width = offset_width, offset_height
return width, height, offset_width, offset_height
def denominator(config):
"""
Calculate the best denominator for a buffer size fraction.
The denominator must be a number between 2 and 16 that divides the display height evenly,
and the fraction represented by the denominator must be less than or equal to the given fraction.
:config: The configuration dictionary containing the buffer size fraction and display dimensions
:return: The denominator to use for the buffer size fraction
"""
frac = config.get(CONF_BUFFER_SIZE)
if frac is None or frac > 0.75:
return 1
height, _width, _offset_width, _offset_height = get_dimensions(config)
try:
return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0)
except StopIteration:
raise cv.Invalid(
f"Buffer size fraction {frac} is not compatible with display height {height}"
) from StopIteration
def validate_dimension(rounding): def validate_dimension(rounding):
def validator(value): def validator(value):
value = cv.positive_int(value) value = cv.positive_int(value)
@ -158,41 +235,50 @@ def dimension_schema(rounding):
) )
def model_schema(bus_mode, model: DriverChip, swapsies: bool): def swap_xy_schema(model):
uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
def validator(value):
if value:
raise cv.Invalid("Axis swapping not supported by this model")
return cv.boolean(value)
if uses_swap:
return {cv.Required(CONF_SWAP_XY): cv.boolean}
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
bus_mode = config.get(CONF_BUS_MODE, model.modes[0])
transform = cv.Schema( transform = cv.Schema(
{ {
cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean, cv.Required(CONF_MIRROR_Y): cv.boolean,
**swap_xy_schema(model),
} }
) )
if model.get_default(CONF_SWAP_XY, False) == cv.UNDEFINED:
transform = transform.extend(
{
cv.Optional(CONF_SWAP_XY): cv.invalid(
"Axis swapping not supported by this model"
)
}
)
else:
transform = transform.extend(
{
cv.Required(CONF_SWAP_XY): cv.boolean,
}
)
# CUSTOM model will need to provide a custom init sequence # CUSTOM model will need to provide a custom init sequence
iseqconf = ( iseqconf = (
cv.Required(CONF_INIT_SEQUENCE) cv.Required(CONF_INIT_SEQUENCE)
if model.initsequence is None if model.initsequence is None
else cv.Optional(CONF_INIT_SEQUENCE) else cv.Optional(CONF_INIT_SEQUENCE)
) )
# Dimensions are optional if the model has a default width and the transform is not overridden # Dimensions are optional if the model has a default width and the x-y transform is not overridden
is_swapped = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True
cv_dimensions = ( cv_dimensions = (
cv.Optional if model.get_default(CONF_WIDTH) and not swapsies else cv.Required cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required
) )
pixel_modes = PIXEL_MODES if bus_mode == TYPE_SINGLE else (PIXEL_MODE_16BIT,) pixel_modes = DISPLAY_PIXEL_MODES if bus_mode == TYPE_SINGLE else (DISPLAY_16BIT,)
color_depth = ( color_depth = (
("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit") ("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit")
) )
other_options = [
CONF_INVERT_COLORS,
CONF_USE_AXIS_FLIPS,
]
if bus_mode == TYPE_SINGLE:
other_options.append(CONF_SPI_16)
schema = ( schema = (
display.FULL_DISPLAY_SCHEMA.extend( display.FULL_DISPLAY_SCHEMA.extend(
spi.spi_device_schema( spi.spi_device_schema(
@ -220,11 +306,13 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum( model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(
COLOR_ORDERS, upper=True COLOR_ORDERS, upper=True
), ),
model.option(CONF_BYTE_ORDER, "big_endian"): cv.one_of(
"big_endian", "little_endian", lower=True
),
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True), model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
model.option(CONF_DRAW_ROUNDING, 2): power_of_two, model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any( model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of(
cv.one_of(*pixel_modes, lower=True), *pixel_modes, lower=True
cv.int_range(0, 255, min_included=True, max_included=True),
), ),
cv.Optional(CONF_TRANSFORM): transform, cv.Optional(CONF_TRANSFORM): transform,
cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of( cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of(
@ -232,19 +320,12 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
), ),
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
iseqconf: cv.ensure_list(map_sequence), iseqconf: cv.ensure_list(map_sequence),
cv.Optional(CONF_BUFFER_SIZE): cv.All(
cv.percentage, cv.Range(0.12, 1.0)
),
} }
) )
.extend( .extend({model.option(x): cv.boolean for x in other_options})
{
model.option(x): cv.boolean
for x in [
CONF_DRAW_FROM_ORIGIN,
CONF_SPI_16,
CONF_INVERT_COLORS,
CONF_USE_AXIS_FLIPS,
]
}
)
) )
if brightness := model.get_default(CONF_BRIGHTNESS): if brightness := model.get_default(CONF_BRIGHTNESS):
schema = schema.extend( schema = schema.extend(
@ -259,18 +340,25 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
return schema return schema
def rotation_as_transform(model, config): def is_rotation_transformable(config):
""" """
Check if a rotation can be implemented in hardware using the MADCTL register. Check if a rotation can be implemented in hardware using the MADCTL register.
A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y.
""" """
model = MODELS[config[CONF_MODEL]]
rotation = config.get(CONF_ROTATION, 0) rotation = config.get(CONF_ROTATION, 0)
return rotation and ( return rotation and (
model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180
) )
def config_schema(config): def customise_schema(config):
"""
Create a customised config schema for a specific model and validate the configuration.
:param config: The configuration dictionary to validate
:return: The validated configuration dictionary
:raises cv.Invalid: If the configuration is invalid
"""
# First get the model and bus mode # First get the model and bus mode
config = cv.Schema( config = cv.Schema(
{ {
@ -288,29 +376,94 @@ def config_schema(config):
extra=ALLOW_EXTRA, extra=ALLOW_EXTRA,
)(config) )(config)
bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) bus_mode = config.get(CONF_BUS_MODE, model.modes[0])
swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True config = model_schema(config)(config)
config = model_schema(bus_mode, model, swapsies)(config)
# Check for invalid combinations of MADCTL config # Check for invalid combinations of MADCTL config
if init_sequence := config.get(CONF_INIT_SEQUENCE): if init_sequence := config.get(CONF_INIT_SEQUENCE):
if MADCTL in [x[0] for x in init_sequence] and CONF_TRANSFORM in config: commands = [x[0] for x in init_sequence]
if MADCTL in commands and CONF_TRANSFORM in config:
raise cv.Invalid( raise cv.Invalid(
f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence" f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence"
) )
if PIXFMT in commands:
raise cv.Invalid(
f"PIXFMT ({PIXFMT:#X}) should not be in the init sequence, it will be set automatically"
)
if bus_mode == TYPE_QUAD and CONF_DC_PIN in config: if bus_mode == TYPE_QUAD and CONF_DC_PIN in config:
raise cv.Invalid("DC pin is not supported in quad mode") raise cv.Invalid("DC pin is not supported in quad mode")
if config[CONF_PIXEL_MODE] == PIXEL_MODE_18BIT and bus_mode != TYPE_SINGLE:
raise cv.Invalid("18-bit pixel mode is not supported on a quad or octal bus")
if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config: if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config:
raise cv.Invalid(f"DC pin is required in {bus_mode} mode") raise cv.Invalid(f"DC pin is required in {bus_mode} mode")
denominator(config)
return config return config
CONFIG_SCHEMA = config_schema CONFIG_SCHEMA = customise_schema
def get_transform(model, config): def requires_buffer(config):
can_transform = rotation_as_transform(model, config) """
Check if the display configuration requires a buffer. It will do so if any drawing methods are configured.
:param config:
:return: True if a buffer is required, False otherwise
"""
return any(
config.get(key) for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD)
)
def get_color_depth(config):
return int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
def _final_validate(config):
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
if not requires_buffer(config) and LVGL_DOMAIN not in global_config:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
if "psram" not in global_config and CONF_BUFFER_SIZE not in config:
if not requires_buffer(config):
return config # No buffer needed, so no need to set a buffer size
# If PSRAM is not enabled, choose a small buffer size by default
if not requires_buffer(config):
# not our problem.
return config
color_depth = get_color_depth(config)
frac = denominator(config)
height, width, _offset_width, _offset_height = get_dimensions(config)
buffer_size = color_depth // 8 * width * height // frac
# Target a buffer size of 20kB
fraction = 20000.0 / buffer_size
try:
config[CONF_BUFFER_SIZE] = 1.0 / next(
x for x in range(2, 17) if fraction >= 1 / x and height % x == 0
)
except StopIteration:
# Either the screen is too big, or the height is not divisible by any of the fractions, so use 1.0
# PSRAM will be needed.
if CORE.is_esp32:
raise cv.Invalid(
"PSRAM is required for this display"
) from StopIteration
return config
FINAL_VALIDATE_SCHEMA = _final_validate
def get_transform(config):
"""
Get the transformation configuration for the display.
:param config:
:return:
"""
model = MODELS[config[CONF_MODEL]]
can_transform = is_rotation_transformable(config)
transform = config.get( transform = config.get(
CONF_TRANSFORM, CONF_TRANSFORM,
{ {
@ -350,16 +503,13 @@ def get_sequence(model, config):
sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] sequence = [x if isinstance(x, tuple) else (x,) for x in sequence]
commands = [x[0] for x in sequence] commands = [x[0] for x in sequence]
# Set pixel format if not already in the custom sequence # Set pixel format if not already in the custom sequence
if PIXFMT not in commands: pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]]
pixel_mode = config[CONF_PIXEL_MODE] sequence.append((PIXFMT, pixel_mode[0]))
if not isinstance(pixel_mode, int):
pixel_mode = PIXEL_MODES[pixel_mode]
sequence.append((PIXFMT, pixel_mode))
# Does the chip use the flipping bits for mirroring rather than the reverse order bits? # Does the chip use the flipping bits for mirroring rather than the reverse order bits?
use_flip = config[CONF_USE_AXIS_FLIPS] use_flip = config[CONF_USE_AXIS_FLIPS]
if MADCTL not in commands: if MADCTL not in commands:
madctl = 0 madctl = 0
transform = get_transform(model, config) transform = get_transform(config)
if transform.get(CONF_TRANSFORM): if transform.get(CONF_TRANSFORM):
LOGGER.info("Using hardware transform to implement rotation") LOGGER.info("Using hardware transform to implement rotation")
if transform.get(CONF_MIRROR_X): if transform.get(CONF_MIRROR_X):
@ -396,63 +546,62 @@ def get_sequence(model, config):
) )
def get_instance(config):
"""
Get the type of MipiSpi instance to create based on the configuration,
and the template arguments.
:param config:
:return: type, template arguments
"""
width, height, offset_width, offset_height = get_dimensions(config)
color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
bufferpixels = COLOR_DEPTHS[color_depth]
display_pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]][1]
bus_type = config[CONF_BUS_MODE]
if bus_type == TYPE_SINGLE and config.get(CONF_SPI_16, False):
# If the bus mode is single and spi_16 is set, use single 16-bit mode
bus_type = BusType.BUS_TYPE_SINGLE_16
else:
bus_type = BusTypes[bus_type]
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
frac = denominator(config)
rotation = DISPLAY_ROTATIONS[
0 if is_rotation_transformable(config) else config.get(CONF_ROTATION, 0)
]
templateargs = [
buffer_type,
bufferpixels,
config[CONF_BYTE_ORDER] == "big_endian",
display_pixel_mode,
bus_type,
width,
height,
offset_width,
offset_height,
]
# If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi
if requires_buffer(config):
templateargs.append(rotation)
templateargs.append(frac)
return MipiSpiBuffer, templateargs
return MipiSpi, templateargs
async def to_code(config): async def to_code(config):
model = MODELS[config[CONF_MODEL]] model = MODELS[config[CONF_MODEL]]
transform = get_transform(model, config) var_id = config[CONF_ID]
if CONF_DIMENSIONS in config: var_id.type, templateargs = get_instance(config)
# Explicit dimensions, just use as is var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs))
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
width = dimensions[CONF_WIDTH]
height = dimensions[CONF_HEIGHT]
offset_width = dimensions[CONF_OFFSET_WIDTH]
offset_height = dimensions[CONF_OFFSET_HEIGHT]
else:
(width, height) = dimensions
offset_width = 0
offset_height = 0
else:
# Default dimensions, use model defaults and transform if needed
width = model.get_default(CONF_WIDTH)
height = model.get_default(CONF_HEIGHT)
offset_width = model.get_default(CONF_OFFSET_WIDTH, 0)
offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0)
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
# the offset is asymmetric
if transform[CONF_MIRROR_X]:
native_width = model.get_default(
CONF_NATIVE_WIDTH, width + offset_width * 2
)
offset_width = native_width - width - offset_width
if transform[CONF_MIRROR_Y]:
native_height = model.get_default(
CONF_NATIVE_HEIGHT, height + offset_height * 2
)
offset_height = native_height - height - offset_height
# Swap default dimensions if swap_xy is set
if transform[CONF_SWAP_XY] is True:
width, height = height, width
offset_height, offset_width = offset_width, offset_height
color_depth = config[CONF_COLOR_DEPTH]
if color_depth.endswith("bit"):
color_depth = color_depth[:-3]
color_depth = COLOR_DEPTHS[int(color_depth)]
var = cg.new_Pvariable(
config[CONF_ID], width, height, offset_width, offset_height, color_depth
)
cg.add(var.set_init_sequence(get_sequence(model, config))) cg.add(var.set_init_sequence(get_sequence(model, config)))
if rotation_as_transform(model, config): if is_rotation_transformable(config):
if CONF_TRANSFORM in config: if CONF_TRANSFORM in config:
LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") LOGGER.warning("Use of 'transform' with 'rotation' is not recommended")
else: else:
config[CONF_ROTATION] = 0 config[CONF_ROTATION] = 0
cg.add(var.set_model(config[CONF_MODEL])) cg.add(var.set_model(config[CONF_MODEL]))
cg.add(var.set_draw_from_origin(config[CONF_DRAW_FROM_ORIGIN]))
cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING])) cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING]))
cg.add(var.set_spi_16(config[CONF_SPI_16]))
if enable_pin := config.get(CONF_ENABLE_PIN): if enable_pin := config.get(CONF_ENABLE_PIN):
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
cg.add(var.set_enable_pins(enable)) cg.add(var.set_enable_pins(enable))
@ -472,4 +621,5 @@ async def to_code(config):
cg.add(var.set_writer(lambda_)) cg.add(var.set_writer(lambda_))
await display.register_display(var, config) await display.register_display(var, config)
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)
# Displays are write-only, set the SPI device to write-only as well
cg.add(var.set_write_only(True)) cg.add(var.set_write_only(True))

View File

@ -2,489 +2,5 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
namespace mipi_spi { namespace mipi_spi {} // namespace mipi_spi
void MipiSpi::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
this->spi_setup();
if (this->dc_pin_ != nullptr) {
this->dc_pin_->setup();
this->dc_pin_->digital_write(false);
}
for (auto *pin : this->enable_pins_) {
pin->setup();
pin->digital_write(true);
}
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
}
this->bus_width_ = this->parent_->get_bus_width();
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
auto when = millis() + 120;
delay(10);
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
ESP_LOGD(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
auto arg_byte = vec[index];
switch (cmd) {
case SLEEP_OUT: {
// are we ready, boots?
int duration = when - millis();
if (duration > 0) {
ESP_LOGD(TAG, "Sleep %dms", duration);
delay(duration);
}
} break;
case INVERT_ON:
this->invert_colors_ = true;
break;
case MADCTL_CMD:
this->madctl_ = arg_byte;
break;
case PIXFMT:
this->pixel_mode_ = arg_byte & 0x11 ? PIXEL_MODE_16 : PIXEL_MODE_18;
break;
case BRIGHTNESS:
this->brightness_ = arg_byte;
break;
default:
break;
}
const auto *ptr = vec.data() + index;
ESP_LOGD(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
this->write_command_(cmd, ptr, num_args);
index += num_args;
if (cmd == SLEEP_OUT)
delay(10);
}
}
this->setup_complete_ = true;
if (this->draw_from_origin_)
check_buffer_();
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
}
void MipiSpi::update() {
if (!this->setup_complete_ || this->is_failed()) {
return;
}
this->do_update_();
if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
return;
ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_);
// Some chips require that the drawing window be aligned on certain boundaries
auto dr = this->draw_rounding_;
this->x_low_ = this->x_low_ / dr * dr;
this->y_low_ = this->y_low_ / dr * dr;
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
if (this->draw_from_origin_) {
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_ - 1;
}
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, this->y_low_,
this->width_ - w - this->x_low_);
// invalidate watermarks
this->x_low_ = this->width_;
this->y_low_ = this->height_;
this->x_high_ = 0;
this->y_high_ = 0;
}
void MipiSpi::fill(Color color) {
if (!this->check_buffer_())
return;
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->get_width_internal() - 1;
this->y_high_ = this->get_height_internal() - 1;
switch (this->color_depth_) {
case display::COLOR_BITNESS_332: {
auto new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
break;
}
default: {
auto new_color = display::ColorUtil::color_to_565(color);
if (((uint8_t) (new_color >> 8)) == ((uint8_t) new_color)) {
// Upper and lower is equal can use quicker memset operation. Takes ~20ms.
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
} else {
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
auto len = this->buffer_bytes_ / 2;
while (len--) {
*ptr_16++ = new_color;
}
}
}
}
}
void MipiSpi::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
return;
}
if (!this->check_buffer_())
return;
size_t pos = (y * this->width_) + x;
switch (this->color_depth_) {
case display::COLOR_BITNESS_332: {
uint8_t new_color = display::ColorUtil::color_to_332(color);
if (this->buffer_[pos] == new_color)
return;
this->buffer_[pos] = new_color;
break;
}
case display::COLOR_BITNESS_565: {
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5);
uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3);
uint16_t new_color = hi_byte | (lo_byte << 8); // big endian
if (ptr_16[pos] == new_color)
return;
ptr_16[pos] = new_color;
break;
}
default:
return;
}
// low and high watermark may speed up drawing from buffer
if (x < this->x_low_)
this->x_low_ = x;
if (y < this->y_low_)
this->y_low_ = y;
if (x > this->x_high_)
this->x_high_ = x;
if (y > this->y_high_)
this->y_high_ = y;
}
void MipiSpi::reset_params_() {
if (!this->is_ready())
return;
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
if (this->brightness_.has_value())
this->write_command_(BRIGHTNESS, this->brightness_.value());
}
void MipiSpi::write_init_sequence_() {
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
ESP_LOGV(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
const auto *ptr = vec.data() + index;
this->write_command_(cmd, ptr, num_args);
index += num_args;
}
}
this->setup_complete_ = true;
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
}
void MipiSpi::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
ESP_LOGVV(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
uint8_t buf[4];
x1 += this->offset_width_;
x2 += this->offset_width_;
y1 += this->offset_height_;
y2 += this->offset_height_;
put16_be(buf, y1);
put16_be(buf + 2, y2);
this->write_command_(RASET, buf, sizeof buf);
put16_be(buf, x1);
put16_be(buf + 2, x2);
this->write_command_(CASET, buf, sizeof buf);
}
void MipiSpi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
if (!this->setup_complete_ || this->is_failed())
return;
if (w <= 0 || h <= 0)
return;
if (bitness != this->color_depth_ || big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) {
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
return;
}
if (this->draw_from_origin_) {
auto stride = x_offset + w + x_pad;
for (int y = 0; y != h; y++) {
memcpy(this->buffer_ + ((y + y_start) * this->width_ + x_start) * 2,
ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2);
}
ptr = this->buffer_;
w = this->width_;
h += y_start;
x_start = 0;
y_start = 0;
x_offset = 0;
y_offset = 0;
}
this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad);
}
void MipiSpi::write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride) {
stride -= w;
uint8_t transfer_buffer[6 * 256];
size_t idx = 0; // index into transfer_buffer
while (h-- != 0) {
for (auto x = w; x-- != 0;) {
auto color_val = *ptr++;
// deal with byte swapping
transfer_buffer[idx++] = (color_val & 0xF8); // Blue
transfer_buffer[idx++] = ((color_val & 0x7) << 5) | ((color_val & 0xE000) >> 11); // Green
transfer_buffer[idx++] = (color_val >> 5) & 0xF8; // Red
if (idx == sizeof(transfer_buffer)) {
this->write_array(transfer_buffer, idx);
idx = 0;
}
}
ptr += stride;
}
if (idx != 0)
this->write_array(transfer_buffer, idx);
}
void MipiSpi::write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) {
stride -= w;
uint8_t transfer_buffer[6 * 256];
size_t idx = 0; // index into transfer_buffer
while (h-- != 0) {
for (auto x = w; x-- != 0;) {
auto color_val = *ptr++;
transfer_buffer[idx++] = color_val & 0xE0; // Red
transfer_buffer[idx++] = (color_val << 3) & 0xE0; // Green
transfer_buffer[idx++] = color_val << 6; // Blue
if (idx == sizeof(transfer_buffer)) {
this->write_array(transfer_buffer, idx);
idx = 0;
}
}
ptr += stride;
}
if (idx != 0)
this->write_array(transfer_buffer, idx);
}
void MipiSpi::write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) {
stride -= w;
uint8_t transfer_buffer[6 * 256];
size_t idx = 0; // index into transfer_buffer
while (h-- != 0) {
for (auto x = w; x-- != 0;) {
auto color_val = *ptr++;
transfer_buffer[idx++] = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
transfer_buffer[idx++] = (color_val & 0x3) << 3;
if (idx == sizeof(transfer_buffer)) {
this->write_array(transfer_buffer, idx);
idx = 0;
}
}
ptr += stride;
}
if (idx != 0)
this->write_array(transfer_buffer, idx);
}
void MipiSpi::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
int x_pad) {
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
auto stride = x_offset + w + x_pad;
const auto *offset_ptr = ptr;
if (this->color_depth_ == display::COLOR_BITNESS_332) {
offset_ptr += y_offset * stride + x_offset;
} else {
stride *= 2;
offset_ptr += y_offset * stride + x_offset * 2;
}
switch (this->bus_width_) {
case 4:
this->enable();
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
// we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't
// bother
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h * 2, 4);
} else {
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, nullptr, 0, 4);
for (int y = 0; y != h; y++) {
this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 4);
offset_ptr += stride;
}
}
break;
case 8:
this->write_command_(WDATA);
this->enable();
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h * 2, 8);
} else {
for (int y = 0; y != h; y++) {
this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 8);
offset_ptr += stride;
}
}
break;
default:
this->write_command_(WDATA);
this->enable();
if (this->color_depth_ == display::COLOR_BITNESS_565) {
// Source buffer is 16-bit RGB565
if (this->pixel_mode_ == PIXEL_MODE_18) {
// Convert RGB565 to RGB666
this->write_18_from_16_bit_(reinterpret_cast<const uint16_t *>(offset_ptr), w, h, stride / 2);
} else {
// Direct RGB565 output
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
this->write_array(ptr, w * h * 2);
} else {
for (int y = 0; y != h; y++) {
this->write_array(offset_ptr, w * 2);
offset_ptr += stride;
}
}
}
} else {
// Source buffer is 8-bit RGB332
if (this->pixel_mode_ == PIXEL_MODE_18) {
// Convert RGB332 to RGB666
this->write_18_from_8_bit_(offset_ptr, w, h, stride);
} else {
this->write_16_from_8_bit_(offset_ptr, w, h, stride);
}
break;
}
}
this->disable();
}
void MipiSpi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
if (this->bus_width_ == 4) {
this->enable();
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
this->disable();
} else if (this->bus_width_ == 8) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8);
this->disable();
}
} else {
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(cmd);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
if (this->spi_16_) {
for (size_t i = 0; i != len; i++) {
this->enable();
this->write_byte(0);
this->write_byte(bytes[i]);
this->disable();
}
} else {
this->enable();
this->write_array(bytes, len);
this->disable();
}
}
}
}
void MipiSpi::dump_config() {
ESP_LOGCONFIG(TAG,
"MIPI_SPI Display\n"
" Model: %s\n"
" Width: %u\n"
" Height: %u",
this->model_, this->width_, this->height_);
if (this->offset_width_ != 0)
ESP_LOGCONFIG(TAG, " Offset width: %u", this->offset_width_);
if (this->offset_height_ != 0)
ESP_LOGCONFIG(TAG, " Offset height: %u", this->offset_height_);
ESP_LOGCONFIG(TAG,
" Swap X/Y: %s\n"
" Mirror X: %s\n"
" Mirror Y: %s\n"
" Color depth: %d bits\n"
" Invert colors: %s\n"
" Color order: %s\n"
" Pixel mode: %s",
YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)),
YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)),
this->color_depth_ == display::COLOR_BITNESS_565 ? 16 : 8, YESNO(this->invert_colors_),
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", this->pixel_mode_ == PIXEL_MODE_18 ? "18bit" : "16bit");
if (this->brightness_.has_value())
ESP_LOGCONFIG(TAG, " Brightness: %u", this->brightness_.value());
if (this->spi_16_)
ESP_LOGCONFIG(TAG, " SPI 16bit: YES");
ESP_LOGCONFIG(TAG, " Draw rounding: %u", this->draw_rounding_);
if (this->draw_from_origin_)
ESP_LOGCONFIG(TAG, " Draw from origin: YES");
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
ESP_LOGCONFIG(TAG,
" SPI Mode: %d\n"
" SPI Data rate: %dMHz\n"
" SPI Bus width: %d",
this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), this->bus_width_);
}
} // namespace mipi_spi
} // namespace esphome } // namespace esphome

View File

@ -4,40 +4,39 @@
#include "esphome/components/spi/spi.h" #include "esphome/components/spi/spi.h"
#include "esphome/components/display/display.h" #include "esphome/components/display/display.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/display_color_utils.h" #include "esphome/components/display/display_color_utils.h"
namespace esphome { namespace esphome {
namespace mipi_spi { namespace mipi_spi {
constexpr static const char *const TAG = "display.mipi_spi"; constexpr static const char *const TAG = "display.mipi_spi";
static const uint8_t SW_RESET_CMD = 0x01; static constexpr uint8_t SW_RESET_CMD = 0x01;
static const uint8_t SLEEP_OUT = 0x11; static constexpr uint8_t SLEEP_OUT = 0x11;
static const uint8_t NORON = 0x13; static constexpr uint8_t NORON = 0x13;
static const uint8_t INVERT_OFF = 0x20; static constexpr uint8_t INVERT_OFF = 0x20;
static const uint8_t INVERT_ON = 0x21; static constexpr uint8_t INVERT_ON = 0x21;
static const uint8_t ALL_ON = 0x23; static constexpr uint8_t ALL_ON = 0x23;
static const uint8_t WRAM = 0x24; static constexpr uint8_t WRAM = 0x24;
static const uint8_t MIPI = 0x26; static constexpr uint8_t MIPI = 0x26;
static const uint8_t DISPLAY_ON = 0x29; static constexpr uint8_t DISPLAY_ON = 0x29;
static const uint8_t RASET = 0x2B; static constexpr uint8_t RASET = 0x2B;
static const uint8_t CASET = 0x2A; static constexpr uint8_t CASET = 0x2A;
static const uint8_t WDATA = 0x2C; static constexpr uint8_t WDATA = 0x2C;
static const uint8_t TEON = 0x35; static constexpr uint8_t TEON = 0x35;
static const uint8_t MADCTL_CMD = 0x36; static constexpr uint8_t MADCTL_CMD = 0x36;
static const uint8_t PIXFMT = 0x3A; static constexpr uint8_t PIXFMT = 0x3A;
static const uint8_t BRIGHTNESS = 0x51; static constexpr uint8_t BRIGHTNESS = 0x51;
static const uint8_t SWIRE1 = 0x5A; static constexpr uint8_t SWIRE1 = 0x5A;
static const uint8_t SWIRE2 = 0x5B; static constexpr uint8_t SWIRE2 = 0x5B;
static const uint8_t PAGESEL = 0xFE; static constexpr uint8_t PAGESEL = 0xFE;
static const uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
static const uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
static const uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
static const uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
static const uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
static const uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
static const uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
static const uint8_t DELAY_FLAG = 0xFF; static const uint8_t DELAY_FLAG = 0xFF;
// store a 16 bit value in a buffer, big endian. // store a 16 bit value in a buffer, big endian.
@ -46,28 +45,44 @@ static inline void put16_be(uint8_t *buf, uint16_t value) {
buf[1] = value; buf[1] = value;
} }
// Buffer mode, conveniently also the number of bytes in a pixel
enum PixelMode { enum PixelMode {
PIXEL_MODE_16, PIXEL_MODE_8 = 1,
PIXEL_MODE_18, PIXEL_MODE_16 = 2,
PIXEL_MODE_18 = 3,
}; };
class MipiSpi : public display::DisplayBuffer, enum BusType {
BUS_TYPE_SINGLE = 1,
BUS_TYPE_QUAD = 4,
BUS_TYPE_OCTAL = 8,
BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer
};
/**
* Base class for MIPI SPI displays.
* All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file.
*
* @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t
* @tparam BUFFERPIXEL Color depth of the buffer
* @tparam DISPLAYPIXEL Color depth of the display
* @tparam BUS_TYPE The type of the interface bus (single, quad, octal)
* @tparam WIDTH Width of the display in pixels
* @tparam HEIGHT Height of the display in pixels
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
* buffer
*/
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT>
class MipiSpi : public display::Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_1MHZ> { spi::DATA_RATE_1MHZ> {
public: public:
MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth) MipiSpi() {}
: width_(width), void update() override { this->stop_poller(); }
height_(height), void draw_pixel_at(int x, int y, Color color) override {}
offset_width_(offset_width),
offset_height_(offset_height),
color_depth_(color_depth) {}
void set_model(const char *model) { this->model_ = model; } void set_model(const char *model) { this->model_ = model; }
void update() override;
void setup() override;
display::ColorOrder get_color_mode() {
return this->madctl_ & MADCTL_BGR ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB;
}
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); } void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); }
void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; }
@ -79,93 +94,524 @@ class MipiSpi : public display::DisplayBuffer,
this->brightness_ = brightness; this->brightness_ = brightness;
this->reset_params_(); this->reset_params_();
} }
void set_draw_from_origin(bool draw_from_origin) { this->draw_from_origin_ = draw_from_origin; }
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
void dump_config() override;
int get_width_internal() override { return this->width_; } int get_width_internal() override { return WIDTH; }
int get_height_internal() override { return this->height_; } int get_height_internal() override { return HEIGHT; }
bool can_proceed() override { return this->setup_complete_; }
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; } void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; } void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; }
void set_spi_16(bool spi_16) { this->spi_16_ = spi_16; }
// reset the display, and write the init sequence
void setup() override {
this->spi_setup();
if (this->dc_pin_ != nullptr) {
this->dc_pin_->setup();
this->dc_pin_->digital_write(false);
}
for (auto *pin : this->enable_pins_) {
pin->setup();
pin->digital_write(true);
}
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
}
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
auto when = millis() + 120;
delay(10);
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
esph_log_e(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
esph_log_d(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
esph_log_e(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
auto arg_byte = vec[index];
switch (cmd) {
case SLEEP_OUT: {
// are we ready, boots?
int duration = when - millis();
if (duration > 0) {
esph_log_d(TAG, "Sleep %dms", duration);
delay(duration);
}
} break;
case INVERT_ON:
this->invert_colors_ = true;
break;
case MADCTL_CMD:
this->madctl_ = arg_byte;
break;
case BRIGHTNESS:
this->brightness_ = arg_byte;
break;
default:
break;
}
const auto *ptr = vec.data() + index;
esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
this->write_command_(cmd, ptr, num_args);
index += num_args;
if (cmd == SLEEP_OUT)
delay(10);
}
}
// init sequence no longer needed
this->init_sequence_.clear();
}
// Drawing operations
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override {
if (this->is_failed())
return;
if (w <= 0 || h <= 0)
return;
if (get_pixel_mode(bitness) != BUFFERPIXEL || big_endian != IS_BIG_ENDIAN) {
// note that the usual logging macros are banned in header files, so use their replacement
esph_log_e(TAG, "Unsupported color depth or bit order");
return;
}
this->write_to_display_(x_start, y_start, w, h, reinterpret_cast<const BUFFERTYPE *>(ptr), x_offset, y_offset,
x_pad);
}
void dump_config() override {
esph_log_config(TAG,
"MIPI_SPI Display\n"
" Model: %s\n"
" Width: %u\n"
" Height: %u",
this->model_, WIDTH, HEIGHT);
if constexpr (OFFSET_WIDTH != 0)
esph_log_config(TAG, " Offset width: %u", OFFSET_WIDTH);
if constexpr (OFFSET_HEIGHT != 0)
esph_log_config(TAG, " Offset height: %u", OFFSET_HEIGHT);
esph_log_config(TAG,
" Swap X/Y: %s\n"
" Mirror X: %s\n"
" Mirror Y: %s\n"
" Invert colors: %s\n"
" Color order: %s\n"
" Display pixels: %d bits\n"
" Endianness: %s\n",
YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)),
YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_),
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little");
if (this->brightness_.has_value())
esph_log_config(TAG, " Brightness: %u", this->brightness_.value());
if (this->cs_ != nullptr)
esph_log_config(TAG, " CS Pin: %s", this->cs_->dump_summary().c_str());
if (this->reset_pin_ != nullptr)
esph_log_config(TAG, " Reset Pin: %s", this->reset_pin_->dump_summary().c_str());
if (this->dc_pin_ != nullptr)
esph_log_config(TAG, " DC Pin: %s", this->dc_pin_->dump_summary().c_str());
esph_log_config(TAG,
" SPI Mode: %d\n"
" SPI Data rate: %dMHz\n"
" SPI Bus width: %d",
this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), BUS_TYPE);
}
protected: protected:
bool check_buffer_() { /* METHODS */
if (this->is_failed()) // convenience functions to write commands with or without data
return false;
if (this->buffer_ != nullptr)
return true;
auto bytes_per_pixel = this->color_depth_ == display::COLOR_BITNESS_565 ? 2 : 1;
this->init_internal_(this->width_ * this->height_ * bytes_per_pixel);
if (this->buffer_ == nullptr) {
this->mark_failed();
return false;
}
this->buffer_bytes_ = this->width_ * this->height_ * bytes_per_pixel;
return true;
}
void fill(Color color) override;
void draw_absolute_pixel_internal(int x, int y, Color color) override;
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
void write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride);
void write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
void write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
int x_pad);
/**
* the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the
* sample code.)
*
* Immediately after enabling /CS send 4 bytes in single-dataline SPI mode:
* 0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be
* sent in 1-dataline SPI. The second indicates quad mode.
* 1: 0x00
* 2: The command (register address) byte.
* 3: 0x00
*
* This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte.
* At the conclusion of the write, de-assert /CS.
*
* @param cmd
* @param bytes
* @param len
*/
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len);
void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); } void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); }
void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); } void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); }
void reset_params_();
void write_init_sequence_();
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
// Writes a command to the display, with the given bytes.
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->enable();
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
this->disable();
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8);
this->disable();
}
} else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(cmd);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
this->enable();
this->write_array(bytes, len);
this->disable();
}
} else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(cmd);
this->disable();
this->dc_pin_->digital_write(true);
for (size_t i = 0; i != len; i++) {
this->enable();
this->write_byte(0);
this->write_byte(bytes[i]);
this->disable();
}
}
}
// write changed parameters to the display
void reset_params_() {
if (!this->is_ready())
return;
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
if (this->brightness_.has_value())
this->write_command_(BRIGHTNESS, this->brightness_.value());
}
// set the address window for the next data write
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
uint8_t buf[4];
x1 += OFFSET_WIDTH;
x2 += OFFSET_WIDTH;
y1 += OFFSET_HEIGHT;
y2 += OFFSET_HEIGHT;
put16_be(buf, y1);
put16_be(buf + 2, y2);
this->write_command_(RASET, buf, sizeof buf);
put16_be(buf, x1);
put16_be(buf + 2, x2);
this->write_command_(CASET, buf, sizeof buf);
if constexpr (BUS_TYPE != BUS_TYPE_QUAD) {
this->write_command_(WDATA);
}
}
// map the display color bitness to the pixel mode
static PixelMode get_pixel_mode(display::ColorBitness bitness) {
switch (bitness) {
case display::COLOR_BITNESS_888:
return PIXEL_MODE_18; // 18 bits per pixel
case display::COLOR_BITNESS_565:
return PIXEL_MODE_16; // 16 bits per pixel
default:
return PIXEL_MODE_8; // Default to 8 bits per pixel
}
}
/**
* Writes a buffer to the display.
* @param w Width of each line in bytes
* @param h Height of the buffer in rows
* @param pad Padding in bytes after each line
*/
void write_display_data_(const uint8_t *ptr, size_t w, size_t h, size_t pad) {
if (pad == 0) {
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->write_array(ptr, w * h);
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h, 4);
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8);
}
} else {
for (size_t y = 0; y != h; y++) {
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->write_array(ptr, w);
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w, 4);
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w, 8);
}
ptr += w + pad;
}
}
}
/**
* Writes a buffer to the display.
*
* The ptr is a pointer to the pixel data
* The other parameters are all in pixel units.
*/
void write_to_display_(int x_start, int y_start, int w, int h, const BUFFERTYPE *ptr, int x_offset, int y_offset,
int x_pad) {
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
this->enable();
ptr += y_offset * (x_offset + w + x_pad) + x_offset;
if constexpr (BUFFERPIXEL == DISPLAYPIXEL) {
this->write_display_data_(reinterpret_cast<const uint8_t *>(ptr), w * sizeof(BUFFERTYPE), h,
x_pad * sizeof(BUFFERTYPE));
} else {
// type conversion required, do it in chunks
uint8_t dbuffer[DISPLAYPIXEL * 48];
uint8_t *dptr = dbuffer;
auto stride = x_offset + w + x_pad; // stride in pixels
for (size_t y = 0; y != h; y++) {
for (size_t x = 0; x != w; x++) {
auto color_val = ptr[y * stride + x];
if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) {
// 16 to 18 bit conversion
if constexpr (IS_BIG_ENDIAN) {
*dptr++ = color_val & 0xF8;
*dptr++ = ((color_val & 0x7) << 5) | (color_val & 0xE000) >> 11;
*dptr++ = (color_val >> 5) & 0xF8;
} else {
*dptr++ = (color_val >> 8) & 0xF8; // Blue
*dptr++ = (color_val & 0x7E0) >> 3;
*dptr++ = color_val << 3;
}
} else if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_8) {
// 8 bit to 18 bit conversion
*dptr++ = color_val << 6; // Blue
*dptr++ = (color_val & 0x1C) << 3; // Green
*dptr++ = (color_val & 0xE0); // Red
} else if constexpr (DISPLAYPIXEL == PIXEL_MODE_16 && BUFFERPIXEL == PIXEL_MODE_8) {
if constexpr (IS_BIG_ENDIAN) {
*dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
*dptr++ = (color_val & 3) << 3;
} else {
*dptr++ = (color_val & 3) << 3;
*dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
}
}
// buffer full? Flush.
if (dptr == dbuffer + sizeof(dbuffer)) {
this->write_display_data_(dbuffer, sizeof(dbuffer), 1, 0);
dptr = dbuffer;
}
}
}
// flush any remaining data
if (dptr != dbuffer) {
this->write_display_data_(dbuffer, dptr - dbuffer, 1, 0);
}
}
this->disable();
}
/* PROPERTIES */
// GPIO pins
GPIOPin *reset_pin_{nullptr}; GPIOPin *reset_pin_{nullptr};
std::vector<GPIOPin *> enable_pins_{}; std::vector<GPIOPin *> enable_pins_{};
GPIOPin *dc_pin_{nullptr}; GPIOPin *dc_pin_{nullptr};
uint16_t x_low_{1};
uint16_t y_low_{1};
uint16_t x_high_{0};
uint16_t y_high_{0};
bool setup_complete_{};
// other properties set by configuration
bool invert_colors_{}; bool invert_colors_{};
size_t width_;
size_t height_;
int16_t offset_width_;
int16_t offset_height_;
size_t buffer_bytes_{0};
display::ColorBitness color_depth_;
PixelMode pixel_mode_{PIXEL_MODE_16};
uint8_t bus_width_{};
bool spi_16_{};
uint8_t madctl_{};
bool draw_from_origin_{false};
unsigned draw_rounding_{2}; unsigned draw_rounding_{2};
optional<uint8_t> brightness_{}; optional<uint8_t> brightness_{};
const char *model_{"Unknown"}; const char *model_{"Unknown"};
std::vector<uint8_t> init_sequence_{}; std::vector<uint8_t> init_sequence_{};
uint8_t madctl_{};
}; };
/**
* Class for MIPI SPI displays with a buffer.
*
* @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t
* @tparam BUFFERPIXEL Color depth of the buffer
* @tparam DISPLAYPIXEL Color depth of the display
* @tparam BUS_TYPE The type of the interface bus (single, quad, octal)
* @tparam ROTATION The rotation of the display
* @tparam WIDTH Width of the display in pixels
* @tparam HEIGHT Height of the display in pixels
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
* @tparam FRACTION The fraction of the display size to use for the buffer (e.g. 4 means a 1/4 buffer).
*/
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION, int FRACTION>
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
OFFSET_WIDTH, OFFSET_HEIGHT> {
public:
MipiSpiBuffer() { this->rotation_ = ROTATION; }
void dump_config() override {
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
OFFSET_HEIGHT>::dump_config();
esph_log_config(TAG,
" Rotation: %d°\n"
" Buffer pixels: %d bits\n"
" Buffer fraction: 1/%d\n"
" Buffer bytes: %zu\n"
" Draw rounding: %u",
this->rotation_, BUFFERPIXEL * 8, FRACTION, sizeof(BUFFERTYPE) * WIDTH * HEIGHT / FRACTION,
this->draw_rounding_);
}
void setup() override {
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
OFFSET_HEIGHT>::setup();
RAMAllocator<BUFFERTYPE> allocator{};
this->buffer_ = allocator.allocate(WIDTH * HEIGHT / FRACTION);
if (this->buffer_ == nullptr) {
this->mark_failed("Buffer allocation failed");
}
}
void update() override {
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
auto now = millis();
#endif
if (this->is_failed()) {
return;
}
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
// the display height,
for (this->start_line_ = 0; this->start_line_ < HEIGHT; this->start_line_ += HEIGHT / FRACTION) {
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
auto lap = millis();
#endif
this->end_line_ = this->start_line_ + HEIGHT / FRACTION;
if (this->auto_clear_enabled_) {
this->clear();
}
if (this->page_ != nullptr) {
this->page_->get_writer()(*this);
} else if (this->writer_.has_value()) {
(*this->writer_)(*this);
} else {
this->test_card();
}
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
esph_log_v(TAG, "Drawing from line %d took %dms", this->start_line_, millis() - lap);
lap = millis();
#endif
if (this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
return;
esph_log_v(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_,
this->y_high_);
// Some chips require that the drawing window be aligned on certain boundaries
auto dr = this->draw_rounding_;
this->x_low_ = this->x_low_ / dr * dr;
this->y_low_ = this->y_low_ / dr * dr;
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
this->y_low_ - this->start_line_, WIDTH - w);
// invalidate watermarks
this->x_low_ = WIDTH;
this->y_low_ = HEIGHT;
this->x_high_ = 0;
this->y_high_ = 0;
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
esph_log_v(TAG, "Write to display took %dms", millis() - lap);
lap = millis();
#endif
}
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
esph_log_v(TAG, "Total update took %dms", millis() - now);
#endif
}
// Draw a pixel at the given coordinates.
void draw_pixel_at(int x, int y, Color color) override {
rotate_coordinates_(x, y);
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
return;
this->buffer_[(y - this->start_line_) * WIDTH + x] = convert_color_(color);
if (x < this->x_low_) {
this->x_low_ = x;
}
if (x > this->x_high_) {
this->x_high_ = x;
}
if (y < this->y_low_) {
this->y_low_ = y;
}
if (y > this->y_high_) {
this->y_high_ = y;
}
}
// Fills the display with a color.
void fill(Color color) override {
this->x_low_ = 0;
this->y_low_ = this->start_line_;
this->x_high_ = WIDTH - 1;
this->y_high_ = this->end_line_ - 1;
std::fill_n(this->buffer_, HEIGHT * WIDTH / FRACTION, convert_color_(color));
}
int get_width() override {
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
return HEIGHT;
return WIDTH;
}
int get_height() override {
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
return WIDTH;
return HEIGHT;
}
protected:
// Rotate the coordinates to match the display orientation.
void rotate_coordinates_(int &x, int &y) const {
if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) {
x = WIDTH - x - 1;
y = HEIGHT - y - 1;
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES) {
auto tmp = x;
x = WIDTH - y - 1;
y = tmp;
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_270_DEGREES) {
auto tmp = y;
y = HEIGHT - x - 1;
x = tmp;
}
}
// Convert a color to the buffer pixel format.
BUFFERTYPE convert_color_(Color &color) const {
if constexpr (BUFFERPIXEL == PIXEL_MODE_8) {
return (color.red & 0xE0) | (color.g & 0xE0) >> 3 | color.b >> 6;
} else if constexpr (BUFFERPIXEL == PIXEL_MODE_16) {
if constexpr (IS_BIG_ENDIAN) {
return (color.r & 0xF8) | color.g >> 5 | (color.g & 0x1C) << 11 | (color.b & 0xF8) << 5;
} else {
return (color.r & 0xF8) << 8 | (color.g & 0xFC) << 3 | color.b >> 3;
}
}
return static_cast<BUFFERTYPE>(0);
}
BUFFERTYPE *buffer_{};
uint16_t x_low_{WIDTH};
uint16_t y_low_{HEIGHT};
uint16_t x_high_{0};
uint16_t y_high_{0};
uint16_t start_line_{0};
uint16_t end_line_{1};
};
} // namespace mipi_spi } // namespace mipi_spi
} // namespace esphome } // namespace esphome

View File

@ -0,0 +1,30 @@
from .ili import ST7789V
ST7789V.extend(
"ADAFRUIT-FUNHOUSE",
height=240,
width=240,
offset_height=0,
offset_width=0,
cs_pin=40,
dc_pin=39,
reset_pin=41,
invert_colors=True,
mirror_x=True,
mirror_y=True,
data_rate="80MHz",
)
ST7789V.extend(
"ADAFRUIT-S2-TFT-FEATHER",
height=240,
width=135,
offset_height=52,
offset_width=40,
cs_pin=7,
dc_pin=39,
reset_pin=40,
invert_colors=True,
)
models = {}

View File

@ -67,6 +67,14 @@ RM690B0 = DriverChip(
), ),
) )
T4_S3_AMOLED = RM690B0.extend("T4-S3", width=450, offset_width=16, bus_mode=TYPE_QUAD) T4_S3_AMOLED = RM690B0.extend(
"T4-S3",
width=450,
offset_width=16,
cs_pin=11,
reset_pin=13,
enable_pin=9,
bus_mode=TYPE_QUAD,
)
models = {} models = {}

View File

@ -1,3 +1,5 @@
import esphome.config_validation as cv
from . import DriverChip from . import DriverChip
from .ili import ILI9488_A from .ili import ILI9488_A
@ -128,6 +130,7 @@ DriverChip(
ILI9488_A.extend( ILI9488_A.extend(
"PICO-RESTOUCH-LCD-3.5", "PICO-RESTOUCH-LCD-3.5",
swap_xy=cv.UNDEFINED,
spi_16=True, spi_16=True,
pixel_mode="16bit", pixel_mode="16bit",
mirror_x=True, mirror_x=True,

View File

@ -1,4 +1,5 @@
import re import re
from typing import Any
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
@ -139,6 +140,27 @@ def get_hw_interface_list():
return [] return []
def one_of_interface_validator(additional_values: list[str] | None = None) -> Any:
"""Helper to create a one_of validator for SPI interfaces.
This delays evaluation of get_hw_interface_list() until validation time,
avoiding access to CORE.data during module import.
Args:
additional_values: List of additional valid values to include
"""
if additional_values is None:
additional_values = []
def validator(value: str) -> str:
return cv.one_of(
*sum(get_hw_interface_list(), additional_values),
lower=True,
)(value)
return cv.All(cv.string, validator)
# Given an SPI name, return the index of it in the available list # Given an SPI name, return the index of it in the available list
def get_spi_index(name): def get_spi_index(name):
for i, ilist in enumerate(get_hw_interface_list()): for i, ilist in enumerate(get_hw_interface_list()):
@ -274,9 +296,8 @@ SPI_SINGLE_SCHEMA = cv.All(
cv.Optional(CONF_FORCE_SW): cv.invalid( cv.Optional(CONF_FORCE_SW): cv.invalid(
"force_sw is deprecated - use interface: software" "force_sw is deprecated - use interface: software"
), ),
cv.Optional(CONF_INTERFACE, default="any"): cv.one_of( cv.Optional(CONF_INTERFACE, default="any"): one_of_interface_validator(
*sum(get_hw_interface_list(), ["software", "hardware", "any"]), ["software", "hardware", "any"]
lower=True,
), ),
cv.Optional(CONF_DATA_PINS): cv.invalid( cv.Optional(CONF_DATA_PINS): cv.invalid(
"'data_pins' should be used with 'type: quad or octal' only" "'data_pins' should be used with 'type: quad or octal' only"
@ -309,10 +330,9 @@ def spi_mode_schema(mode):
cv.ensure_list(pins.internal_gpio_output_pin_number), cv.ensure_list(pins.internal_gpio_output_pin_number),
cv.Length(min=pin_count, max=pin_count), cv.Length(min=pin_count, max=pin_count),
), ),
cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of( cv.Optional(
*sum(get_hw_interface_list(), ["hardware"]), CONF_INTERFACE, default="hardware"
lower=True, ): one_of_interface_validator(["hardware"]),
),
cv.Optional(CONF_MISO_PIN): cv.invalid( cv.Optional(CONF_MISO_PIN): cv.invalid(
f"'miso_pin' should not be used with {mode} SPI" f"'miso_pin' should not be used with {mode} SPI"
), ),

1
tests/__init__.py Normal file
View File

@ -0,0 +1 @@
"""ESPHome tests package."""

View File

View File

View File

@ -5,18 +5,30 @@ from __future__ import annotations
from collections.abc import Callable, Generator from collections.abc import Callable, Generator
from pathlib import Path from pathlib import Path
import sys import sys
from typing import Any
import pytest import pytest
from esphome import config, final_validate
from esphome.const import (
KEY_CORE,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PlatformFramework,
)
from esphome.types import ConfigType
# Add package root to python path # Add package root to python path
here = Path(__file__).parent here = Path(__file__).parent
package_root = here.parent.parent package_root = here.parent.parent
sys.path.insert(0, package_root.as_posix()) sys.path.insert(0, package_root.as_posix())
from esphome.__main__ import generate_cpp_contents # noqa: E402 from esphome.__main__ import generate_cpp_contents # noqa: E402
from esphome.config import read_config # noqa: E402 from esphome.config import Config, read_config # noqa: E402
from esphome.core import CORE # noqa: E402 from esphome.core import CORE # noqa: E402
from .types import SetCoreConfigCallable # noqa: E402
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def config_path(request: pytest.FixtureRequest) -> Generator[None]: def config_path(request: pytest.FixtureRequest) -> Generator[None]:
@ -36,6 +48,59 @@ def config_path(request: pytest.FixtureRequest) -> Generator[None]:
CORE.config_path = original_path CORE.config_path = original_path
@pytest.fixture(autouse=True)
def reset_core() -> Generator[None]:
"""Reset CORE after each test."""
yield
CORE.reset()
@pytest.fixture
def set_core_config() -> Generator[SetCoreConfigCallable]:
"""Fixture to set up the core configuration for tests."""
def setter(
platform_framework: PlatformFramework,
/,
*,
core_data: ConfigType | None = None,
platform_data: ConfigType | None = None,
) -> None:
platform, framework = platform_framework.value
# Set base core configuration
CORE.data[KEY_CORE] = {
KEY_TARGET_PLATFORM: platform.value,
KEY_TARGET_FRAMEWORK: framework.value,
}
# Update with any additional core data
if core_data:
CORE.data[KEY_CORE].update(core_data)
# Set platform-specific data
if platform_data:
CORE.data[platform.value] = platform_data
config.path_context.set([])
final_validate.full_config.set(Config())
yield setter
@pytest.fixture
def set_component_config() -> Callable[[str, Any], None]:
"""
Fixture to set a component configuration in the mock config.
This must be used after the core configuration has been set up.
"""
def setter(name: str, value: Any) -> None:
final_validate.full_config.get()[name] = value
return setter
@pytest.fixture @pytest.fixture
def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]: def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
"""Return a function to get absolute paths relative to the component's fixtures directory.""" """Return a function to get absolute paths relative to the component's fixtures directory."""
@ -60,7 +125,7 @@ def component_config_path(request: pytest.FixtureRequest) -> Callable[[str], Pat
@pytest.fixture @pytest.fixture
def generate_main() -> Generator[Callable[[str | Path], str]]: def generate_main() -> Generator[Callable[[str | Path], str]]:
"""Generates the C++ main.cpp file and returns it in string form.""" """Generates the C++ main.cpp from a given yaml file and returns it in string form."""
def generator(path: str | Path) -> str: def generator(path: str | Path) -> str:
CORE.config_path = str(path) CORE.config_path = str(path)
@ -69,5 +134,3 @@ def generate_main() -> Generator[Callable[[str | Path], str]]:
return CORE.cpp_main_section return CORE.cpp_main_section
yield generator yield generator
CORE.reset()

View File

View File

@ -0,0 +1,25 @@
esphome:
name: c3-7735
esp32:
board: lolin_c3_mini
spi:
mosi_pin:
number: GPIO2
ignore_strapping_warning: true
clk_pin: GPIO1
display:
- platform: mipi_spi
data_rate: 20MHz
model: st7735
cs_pin:
number: GPIO8
ignore_strapping_warning: true
dc_pin:
number: GPIO3
reset_pin:
number: GPIO4
lvgl:

View File

@ -0,0 +1,20 @@
esphome:
name: jc3636w518
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
psram:
mode: octal
spi:
id: display_qspi
type: quad
clk_pin: 9
data_pins: [11, 12, 13, 14]
display:
- platform: mipi_spi
model: jc3636w518

View File

@ -0,0 +1,387 @@
"""Tests for mpip_spi configuration validation."""
from collections.abc import Callable
from pathlib import Path
from typing import Any
import pytest
from esphome import config_validation as cv
from esphome.components.esp32 import (
KEY_BOARD,
KEY_ESP32,
KEY_VARIANT,
VARIANT_ESP32,
VARIANT_ESP32S3,
VARIANTS,
)
from esphome.components.esp32.gpio import validate_gpio_pin
from esphome.components.mipi_spi.display import (
CONF_BUS_MODE,
CONF_NATIVE_HEIGHT,
CONFIG_SCHEMA,
FINAL_VALIDATE_SCHEMA,
MODELS,
dimension_schema,
)
from esphome.const import (
CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_HEIGHT,
CONF_INIT_SEQUENCE,
CONF_WIDTH,
PlatformFramework,
)
from esphome.core import CORE
from esphome.pins import internal_gpio_pin_number
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
def run_schema_validation(config: ConfigType) -> None:
"""Run schema validation on a configuration."""
FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
@pytest.fixture
def choose_variant_with_pins() -> Callable[..., None]:
"""
Set the ESP32 variant for the given model based on pins. For ESP32 only since the other platforms
do not have variants.
"""
def chooser(*pins: int | str | None) -> None:
for v in VARIANTS:
try:
CORE.data[KEY_ESP32][KEY_VARIANT] = v
for pin in pins:
if pin is not None:
pin = internal_gpio_pin_number(pin)
validate_gpio_pin(pin)
return
except cv.Invalid:
continue
return chooser
@pytest.mark.parametrize(
("config", "error_match"),
[
pytest.param(
"a string",
"expected a dictionary",
id="invalid_string_config",
),
pytest.param(
{"id": "display_id"},
r"required key not provided @ data\['model'\]",
id="missing_model",
),
pytest.param(
{"id": "display_id", "model": "custom", "init_sequence": [[0x36, 0x01]]},
r"required key not provided @ data\['dimensions'\]",
id="missing_dimensions",
),
pytest.param(
{
"model": "custom",
"dc_pin": 18,
"dimensions": {"width": 320, "height": 240},
},
r"required key not provided @ data\['init_sequence'\]",
id="missing_init_sequence",
),
pytest.param(
{
"id": "display_id",
"model": "custom",
"dimensions": {"width": 320, "height": 240},
"draw_rounding": 13,
"init_sequence": [[0xA0, 0x01]],
},
r"value must be a power of two for dictionary value @ data\['draw_rounding'\]",
id="invalid_draw_rounding",
),
],
)
def test_basic_configuration_errors(
config: str | ConfigType,
error_match: str,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test basic configuration validation errors"""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
with pytest.raises(cv.Invalid, match=error_match):
run_schema_validation(config)
@pytest.mark.parametrize(
("rounding", "config", "error_match"),
[
pytest.param(
4,
{"width": 320},
r"required key not provided @ data\['height'\]",
id="missing_height",
),
pytest.param(
32,
{"width": 320, "height": 111},
"Dimensions and offsets must be divisible by 32",
id="dimensions_not_divisible",
),
],
)
def test_dimension_validation(
rounding: int,
config: ConfigType,
error_match: str,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test dimension-related validation errors"""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
with pytest.raises(cv.Invalid, match=error_match):
dimension_schema(rounding)(config)
@pytest.mark.parametrize(
("config", "error_match"),
[
pytest.param(
{
"model": "JC3248W535",
"transform": {"mirror_x": False, "mirror_y": True, "swap_xy": True},
},
"Axis swapping not supported by this model",
id="axis_swapping_not_supported",
),
pytest.param(
{
"model": "custom",
"dimensions": {"width": 320, "height": 240},
"transform": {"mirror_x": False, "mirror_y": True, "swap_xy": False},
"init_sequence": [[0x36, 0x01]],
},
r"transform is not supported when MADCTL \(0X36\) is in the init sequence",
id="transform_with_madctl",
),
pytest.param(
{
"model": "custom",
"dimensions": {"width": 320, "height": 240},
"init_sequence": [[0x3A, 0x01]],
},
r"PIXFMT \(0X3A\) should not be in the init sequence, it will be set automatically",
id="pixfmt_in_init_sequence",
),
],
)
def test_transform_and_init_sequence_errors(
config: ConfigType,
error_match: str,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test transform and init sequence validation errors"""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
with pytest.raises(cv.Invalid, match=error_match):
run_schema_validation(config)
@pytest.mark.parametrize(
("config", "error_match"),
[
pytest.param(
{"model": "t4-s3", "dc_pin": 18},
"DC pin is not supported in quad mode",
id="dc_pin_not_supported_quad_mode",
),
pytest.param(
{"model": "t4-s3", "color_depth": 18},
"Unknown value '18', valid options are '16', '16bit",
id="invalid_color_depth_t4_s3",
),
pytest.param(
{"model": "t-embed", "color_depth": 24},
"Unknown value '24', valid options are '16', '8",
id="invalid_color_depth_t_embed",
),
pytest.param(
{"model": "ili9488"},
"DC pin is required in single mode",
id="dc_pin_required_single_mode",
),
pytest.param(
{"model": "wt32-sc01-plus", "brightness": 128},
r"extra keys not allowed @ data\['brightness'\]",
id="brightness_not_supported",
),
pytest.param(
{"model": "T-DISPLAY-S3-PRO"},
"PSRAM is required for this display",
id="psram_required",
),
],
)
def test_esp32s3_specific_errors(
config: ConfigType,
error_match: str,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test ESP32-S3 specific configuration errors"""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3},
)
with pytest.raises(cv.Invalid, match=error_match):
run_schema_validation(config)
def test_framework_specific_errors(
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test framework-specific configuration errors"""
set_core_config(
PlatformFramework.ESP32_ARDUINO,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
with pytest.raises(
cv.Invalid,
match=r"This feature is only available with frameworks \['esp-idf'\]",
):
run_schema_validation({"model": "wt32-sc01-plus"})
def test_custom_model_with_all_options(
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test custom model configuration with all available options."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3},
)
run_schema_validation(
{
"model": "custom",
"pixel_mode": "18bit",
"color_depth": 8,
"id": "display_id",
"byte_order": "little_endian",
"bus_mode": "single",
"color_order": "rgb",
"dc_pin": 11,
"reset_pin": 12,
"enable_pin": 13,
"cs_pin": 14,
"init_sequence": [[0xA0, 0x01]],
"dimensions": {
"width": 320,
"height": 240,
"offset_width": 32,
"offset_height": 32,
},
"invert_colors": True,
"transform": {"mirror_x": True, "mirror_y": True, "swap_xy": False},
"spi_mode": "mode0",
"data_rate": "40MHz",
"use_axis_flips": True,
"draw_rounding": 4,
"spi_16": True,
"buffer_size": 0.25,
}
)
def test_all_predefined_models(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
choose_variant_with_pins: Callable[..., None],
) -> None:
"""Test all predefined display models validate successfully with appropriate defaults."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3},
)
# Enable PSRAM which is required for some models
set_component_config("psram", True)
# Test all models, providing default values where necessary
for name, model in MODELS.items():
config = {"model": name}
# Get the pins required by this model and find a compatible variant
pins = [
pin
for pin in [
model.get_default(pin, None)
for pin in ("dc_pin", "reset_pin", "cs_pin")
]
if pin is not None
]
choose_variant_with_pins(pins)
# Add required fields that don't have defaults
if (
not model.get_default(CONF_DC_PIN)
and model.get_default(CONF_BUS_MODE) != "quad"
):
config[CONF_DC_PIN] = 14
if not model.get_default(CONF_NATIVE_HEIGHT):
config[CONF_DIMENSIONS] = {CONF_HEIGHT: 240, CONF_WIDTH: 320}
if model.initsequence is None:
config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]]
run_schema_validation(config)
def test_native_generation(
generate_main: Callable[[str | Path], str],
component_fixture_path: Callable[[str], Path],
) -> None:
"""Test code generation for display."""
main_cpp = generate_main(component_fixture_path("native.yaml"))
assert (
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, display::DISPLAY_ROTATION_0_DEGREES, 1>()"
in main_cpp
)
assert "set_init_sequence({240, 1, 8, 242" in main_cpp
assert "show_test_card();" in main_cpp
assert "set_write_only(true);" in main_cpp
def test_lvgl_generation(
generate_main: Callable[[str | Path], str],
component_fixture_path: Callable[[str], Path],
) -> None:
"""Test LVGL generation configuration."""
main_cpp = generate_main(component_fixture_path("lvgl.yaml"))
assert (
"mipi_spi::MipiSpi<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_SINGLE, 128, 160, 0, 0>();"
in main_cpp
)
assert "set_init_sequence({1, 0, 10, 255, 177" in main_cpp
assert "show_test_card();" not in main_cpp
assert "set_auto_clear(false);" in main_cpp

View File

View File

View File

View File

@ -0,0 +1,21 @@
"""Type definitions for component tests."""
from __future__ import annotations
from typing import Protocol
from esphome.const import PlatformFramework
from esphome.types import ConfigType
class SetCoreConfigCallable(Protocol):
"""Protocol for the set_core_config fixture setter function."""
def __call__( # noqa: E704
self,
platform_framework: PlatformFramework,
/,
*,
core_data: ConfigType | None = None,
platform_data: ConfigType | None = None,
) -> None: ...

View File

@ -1,41 +0,0 @@
spi:
- id: quad_spi
type: quad
interface: spi3
clk_pin:
number: 47
data_pins:
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: octal_spi
type: octal
interface: hardware
clk_pin:
number: 0
data_pins:
- 36
- 37
- 38
- 39
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: spi_id_3
interface: any
clk_pin: 8
mosi_pin: 9
display:
- platform: mipi_spi
model: ESP32-2432S028

View File

@ -1,41 +0,0 @@
spi:
- id: quad_spi
type: quad
interface: spi3
clk_pin:
number: 47
data_pins:
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: octal_spi
type: octal
interface: hardware
clk_pin:
number: 0
data_pins:
- 36
- 37
- 38
- 39
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: spi_id_3
interface: any
clk_pin: 8
mosi_pin: 9
display:
- platform: mipi_spi
model: JC3248W535

View File

@ -1,19 +0,0 @@
spi:
- id: quad_spi
type: quad
interface: spi3
clk_pin:
number: 36
data_pins:
- number: 40
- number: 41
- number: 42
- number: 43
- id: spi_id_3
interface: any
clk_pin: 8
mosi_pin: 9
display:
- platform: mipi_spi
model: JC3636W518

View File

@ -0,0 +1,18 @@
substitutions:
clk_pin: GPIO16
mosi_pin: GPIO17
spi:
- id: spi_single
clk_pin:
number: ${clk_pin}
mosi_pin:
number: ${mosi_pin}
display:
- platform: mipi_spi
model: t-display-s3-pro
lvgl:
psram:

View File

@ -1,9 +0,0 @@
spi:
- id: spi_id_3
interface: any
clk_pin: 8
mosi_pin: 9
display:
- platform: mipi_spi
model: Pico-ResTouch-LCD-3.5

View File

@ -1,41 +0,0 @@
spi:
- id: quad_spi
type: quad
interface: spi3
clk_pin:
number: 47
data_pins:
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: octal_spi
type: octal
interface: hardware
clk_pin:
number: 0
data_pins:
- 36
- 37
- 38
- 39
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: spi_id_3
interface: any
clk_pin: 8
mosi_pin: 9
display:
- platform: mipi_spi
model: S3BOX

View File

@ -1,41 +0,0 @@
spi:
- id: quad_spi
type: quad
interface: spi3
clk_pin:
number: 47
data_pins:
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: octal_spi
type: octal
interface: hardware
clk_pin:
number: 0
data_pins:
- 36
- 37
- 38
- 39
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: spi_id_3
interface: any
clk_pin: 8
mosi_pin: 9
display:
- platform: mipi_spi
model: S3BOXLITE

View File

@ -1,9 +0,0 @@
spi:
- id: spi_id_3
interface: any
clk_pin: 8
mosi_pin: 9
display:
- platform: mipi_spi
model: T-DISPLAY-S3-AMOLED-PLUS

View File

@ -1,15 +0,0 @@
spi:
- id: quad_spi
type: quad
interface: spi3
clk_pin:
number: 47
data_pins:
- number: 40
- number: 41
- number: 42
- number: 43
display:
- platform: mipi_spi
model: T-DISPLAY-S3-AMOLED

View File

@ -1,9 +0,0 @@
spi:
- id: spi_id_3
interface: any
clk_pin: 8
mosi_pin: 40
display:
- platform: mipi_spi
model: T-DISPLAY-S3-PRO

View File

@ -1,37 +0,0 @@
spi:
- id: quad_spi
type: quad
interface: spi3
clk_pin:
number: 47
data_pins:
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: octal_spi
type: octal
interface: hardware
clk_pin:
number: 0
data_pins:
- 36
- 37
- 38
- 39
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
display:
- platform: mipi_spi
model: T-DISPLAY-S3

View File

@ -1,41 +0,0 @@
spi:
- id: quad_spi
type: quad
interface: spi3
clk_pin:
number: 47
data_pins:
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: octal_spi
type: octal
interface: hardware
clk_pin:
number: 0
data_pins:
- 36
- 37
- 38
- 39
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: spi_id_3
interface: any
clk_pin: 8
mosi_pin: 9
display:
- platform: mipi_spi
model: T-DISPLAY

View File

@ -1,9 +0,0 @@
spi:
- id: spi_id_3
interface: any
clk_pin: 8
mosi_pin: 40
display:
- platform: mipi_spi
model: T-EMBED

View File

@ -1,41 +0,0 @@
spi:
- id: quad_spi
type: quad
interface: spi3
clk_pin:
number: 47
data_pins:
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: octal_spi
type: octal
interface: hardware
clk_pin:
number: 0
data_pins:
- 36
- 37
- 38
- 39
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: spi_id_3
interface: any
clk_pin: 8
mosi_pin: 9
display:
- platform: mipi_spi
model: T4-S3

View File

@ -1,37 +0,0 @@
spi:
- id: quad_spi
type: quad
interface: spi3
clk_pin:
number: 47
data_pins:
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: octal_spi
type: octal
interface: hardware
clk_pin:
number: 9
data_pins:
- 36
- 37
- 38
- 39
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
display:
- platform: mipi_spi
model: WT32-SC01-PLUS