From 6486147da1fdf005f6155c219bf8dbc5dd918347 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:05:27 +1000 Subject: [PATCH] [mipi_spi] Template code, partial buffer support (#9314) Co-authored-by: J. Nick Koston Co-authored-by: Keith Burzinski --- esphome/components/const/__init__.py | 1 + esphome/components/mipi_spi/__init__.py | 2 - esphome/components/mipi_spi/display.py | 376 +++++++--- esphome/components/mipi_spi/mipi_spi.cpp | 486 +------------ esphome/components/mipi_spi/mipi_spi.h | 668 +++++++++++++++--- .../components/mipi_spi/models/adafruit.py | 30 + esphome/components/mipi_spi/models/amoled.py | 10 +- .../components/mipi_spi/models/waveshare.py | 3 + esphome/components/spi/__init__.py | 34 +- tests/__init__.py | 1 + tests/component_tests/__init__.py | 0 .../component_tests/binary_sensor/__init__.py | 0 tests/component_tests/button/__init__.py | 0 tests/component_tests/conftest.py | 71 +- tests/component_tests/deep_sleep/__init__.py | 0 tests/component_tests/image/__init__.py | 0 tests/component_tests/mipi_spi/__init__.py | 0 .../mipi_spi/fixtures/lvgl.yaml | 25 + .../mipi_spi/fixtures/native.yaml | 20 + tests/component_tests/mipi_spi/test_init.py | 387 ++++++++++ tests/component_tests/ota/__init__.py | 0 tests/component_tests/packages/__init__.py | 0 tests/component_tests/sensor/__init__.py | 0 tests/component_tests/text/__init__.py | 0 tests/component_tests/text_sensor/__init__.py | 0 tests/component_tests/types.py | 21 + tests/component_tests/web_server/__init__.py | 0 .../test-esp32-2432s028.esp32-s3-idf.yaml | 41 -- .../test-jc3248w535.esp32-s3-idf.yaml | 41 -- .../test-jc3636w518.esp32-s3-idf.yaml | 19 - .../mipi_spi/test-lvgl.esp32-s3-idf.yaml | 18 + ...est-pico-restouch-lcd-35.esp32-s3-idf.yaml | 9 - .../mipi_spi/test-s3box.esp32-s3-idf.yaml | 41 -- .../mipi_spi/test-s3boxlite.esp32-s3-idf.yaml | 41 -- ...t-display-s3-amoled-plus.esp32-s3-idf.yaml | 9 - ...test-t-display-s3-amoled.esp32-s3-idf.yaml | 15 - .../test-t-display-s3-pro.esp32-s3-idf.yaml | 9 - .../test-t-display-s3.esp32-s3-idf.yaml | 37 - .../mipi_spi/test-t-display.esp32-s3-idf.yaml | 41 -- .../mipi_spi/test-t-embed.esp32-s3-idf.yaml | 9 - .../mipi_spi/test-t4-s3.esp32-s3-idf.yaml | 41 -- .../test-wt32-sc01-plus.esp32-s3-idf.yaml | 37 - 42 files changed, 1430 insertions(+), 1113 deletions(-) create mode 100644 esphome/components/mipi_spi/models/adafruit.py create mode 100644 tests/__init__.py create mode 100644 tests/component_tests/__init__.py create mode 100644 tests/component_tests/binary_sensor/__init__.py create mode 100644 tests/component_tests/button/__init__.py create mode 100644 tests/component_tests/deep_sleep/__init__.py create mode 100644 tests/component_tests/image/__init__.py create mode 100644 tests/component_tests/mipi_spi/__init__.py create mode 100644 tests/component_tests/mipi_spi/fixtures/lvgl.yaml create mode 100644 tests/component_tests/mipi_spi/fixtures/native.yaml create mode 100644 tests/component_tests/mipi_spi/test_init.py create mode 100644 tests/component_tests/ota/__init__.py create mode 100644 tests/component_tests/packages/__init__.py create mode 100644 tests/component_tests/sensor/__init__.py create mode 100644 tests/component_tests/text/__init__.py create mode 100644 tests/component_tests/text_sensor/__init__.py create mode 100644 tests/component_tests/types.py create mode 100644 tests/component_tests/web_server/__init__.py delete mode 100644 tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml delete mode 100644 tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml delete mode 100644 tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml delete mode 100644 tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml delete mode 100644 tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml delete mode 100644 tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml delete mode 100644 tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml delete mode 100644 tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml delete mode 100644 tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml delete mode 100644 tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml delete mode 100644 tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml delete mode 100644 tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml delete mode 100644 tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml delete mode 100644 tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index b084622f4c..5b40545d89 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -3,6 +3,7 @@ CODEOWNERS = ["@esphome/core"] CONF_BYTE_ORDER = "byte_order" +CONF_COLOR_DEPTH = "color_depth" CONF_DRAW_ROUNDING = "draw_rounding" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" diff --git a/esphome/components/mipi_spi/__init__.py b/esphome/components/mipi_spi/__init__.py index 46b0206a1f..879efda619 100644 --- a/esphome/components/mipi_spi/__init__.py +++ b/esphome/components/mipi_spi/__init__.py @@ -2,10 +2,8 @@ CODEOWNERS = ["@clydebarrow"] DOMAIN = "mipi_spi" -CONF_DRAW_FROM_ORIGIN = "draw_from_origin" CONF_SPI_16 = "spi_16" CONF_PIXEL_MODE = "pixel_mode" -CONF_COLOR_DEPTH = "color_depth" CONF_BUS_MODE = "bus_mode" CONF_USE_AXIS_FLIPS = "use_axis_flips" CONF_NATIVE_WIDTH = "native_width" diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 061257e859..d25dfd8539 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -3,11 +3,18 @@ import logging from esphome import pins import esphome.codegen as cg 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 import esphome.config_validation as cv from esphome.config_validation import ALLOW_EXTRA from esphome.const import ( CONF_BRIGHTNESS, + CONF_BUFFER_SIZE, CONF_COLOR_ORDER, CONF_CS_PIN, CONF_DATA_RATE, @@ -24,19 +31,19 @@ from esphome.const import ( CONF_MODEL, CONF_OFFSET_HEIGHT, CONF_OFFSET_WIDTH, + CONF_PAGES, CONF_RESET_PIN, CONF_ROTATION, CONF_SWAP_XY, CONF_TRANSFORM, 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 ( CONF_BUS_MODE, - CONF_DRAW_FROM_ORIGIN, CONF_NATIVE_HEIGHT, CONF_NATIVE_WIDTH, CONF_PIXEL_MODE, @@ -55,6 +62,7 @@ from .models import ( MADCTL_XFLIP, MADCTL_YFLIP, DriverChip, + adafruit, amoled, cyd, ili, @@ -69,43 +77,112 @@ DEPENDENCIES = ["spi"] LOGGER = logging.getLogger(DOMAIN) mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi") -MipiSpi = mipi_spi_ns.class_( - "MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice +MipiSpi = mipi_spi_ns.class_("MipiSpi", display.Display, cg.Component, spi.SPIDevice) +MipiSpiBuffer = mipi_spi_ns.class_( + "MipiSpiBuffer", MipiSpi, display.Display, cg.Component, spi.SPIDevice ) ColorOrder = display.display_ns.enum("ColorMode") ColorBitness = display.display_ns.enum("ColorBitness") Model = mipi_spi_ns.enum("Model") +PixelMode = mipi_spi_ns.enum("PixelMode") +BusType = mipi_spi_ns.enum("BusType") + COLOR_ORDERS = { MODE_RGB: ColorOrder.COLOR_ORDER_RGB, MODE_BGR: ColorOrder.COLOR_ORDER_BGR, } COLOR_DEPTHS = { - 8: ColorBitness.COLOR_BITNESS_332, - 16: ColorBitness.COLOR_BITNESS_565, + 8: PixelMode.PIXEL_MODE_8, + 16: PixelMode.PIXEL_MODE_16, + 18: PixelMode.PIXEL_MODE_18, } + 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 -# These statements are noops, but serve to suppress linting of side-effect-only imports -for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare): +# This loop is a noop, but suppresses linting of side-effect-only imports +for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare, adafruit): pass -PixelMode = mipi_spi_ns.enum("PixelMode") -PIXEL_MODE_18BIT = "18bit" -PIXEL_MODE_16BIT = "16bit" +DISPLAY_18BIT = "18bit" +DISPLAY_16BIT = "16bit" -PIXEL_MODES = { - PIXEL_MODE_16BIT: 0x55, - PIXEL_MODE_18BIT: 0x66, +DISPLAY_PIXEL_MODES = { + DISPLAY_16BIT: (0x55, PixelMode.PIXEL_MODE_16), + 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 validator(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( { cv.Required(CONF_MIRROR_X): 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 iseqconf = ( cv.Required(CONF_INIT_SEQUENCE) if model.initsequence is None 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.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 = ( ("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 = ( display.FULL_DISPLAY_SCHEMA.extend( 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( 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_DRAW_ROUNDING, 2): power_of_two, - model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any( - cv.one_of(*pixel_modes, lower=True), - cv.int_range(0, 255, min_included=True, max_included=True), + model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of( + *pixel_modes, lower=True ), cv.Optional(CONF_TRANSFORM): transform, 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), iseqconf: cv.ensure_list(map_sequence), + cv.Optional(CONF_BUFFER_SIZE): cv.All( + cv.percentage, cv.Range(0.12, 1.0) + ), } ) - .extend( - { - model.option(x): cv.boolean - for x in [ - CONF_DRAW_FROM_ORIGIN, - CONF_SPI_16, - CONF_INVERT_COLORS, - CONF_USE_AXIS_FLIPS, - ] - } - ) + .extend({model.option(x): cv.boolean for x in other_options}) ) if brightness := model.get_default(CONF_BRIGHTNESS): schema = schema.extend( @@ -259,18 +340,25 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): 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. 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) return rotation and ( 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 config = cv.Schema( { @@ -288,29 +376,94 @@ def config_schema(config): extra=ALLOW_EXTRA, )(config) bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) - swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True - config = model_schema(bus_mode, model, swapsies)(config) + config = model_schema(config)(config) # Check for invalid combinations of MADCTL config 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( 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: 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: raise cv.Invalid(f"DC pin is required in {bus_mode} mode") + denominator(config) return config -CONFIG_SCHEMA = config_schema +CONFIG_SCHEMA = customise_schema -def get_transform(model, config): - can_transform = rotation_as_transform(model, config) +def requires_buffer(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( CONF_TRANSFORM, { @@ -350,16 +503,13 @@ def get_sequence(model, config): sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] commands = [x[0] for x in sequence] # Set pixel format if not already in the custom sequence - if PIXFMT not in commands: - pixel_mode = config[CONF_PIXEL_MODE] - if not isinstance(pixel_mode, int): - pixel_mode = PIXEL_MODES[pixel_mode] - sequence.append((PIXFMT, pixel_mode)) + pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]] + sequence.append((PIXFMT, pixel_mode[0])) # Does the chip use the flipping bits for mirroring rather than the reverse order bits? use_flip = config[CONF_USE_AXIS_FLIPS] if MADCTL not in commands: madctl = 0 - transform = get_transform(model, config) + transform = get_transform(config) if transform.get(CONF_TRANSFORM): LOGGER.info("Using hardware transform to implement rotation") 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): model = MODELS[config[CONF_MODEL]] - transform = get_transform(model, 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] - 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 - ) + var_id = config[CONF_ID] + var_id.type, templateargs = get_instance(config) + var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs)) 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: LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") else: config[CONF_ROTATION] = 0 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_spi_16(config[CONF_SPI_16])) if enable_pin := config.get(CONF_ENABLE_PIN): enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] cg.add(var.set_enable_pins(enable)) @@ -472,4 +621,5 @@ async def to_code(config): cg.add(var.set_writer(lambda_)) await display.register_display(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)) diff --git a/esphome/components/mipi_spi/mipi_spi.cpp b/esphome/components/mipi_spi/mipi_spi.cpp index 962575477d..272915b4e1 100644 --- a/esphome/components/mipi_spi/mipi_spi.cpp +++ b/esphome/components/mipi_spi/mipi_spi.cpp @@ -2,489 +2,5 @@ #include "esphome/core/log.h" namespace esphome { -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(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(this->buffer_); - uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); - uint8_t lo_byte = static_cast((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(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(this->data_rate_ / 1000000), this->bus_width_); -} - -} // namespace mipi_spi +namespace mipi_spi {} // namespace mipi_spi } // namespace esphome diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 052ebe3a6b..cdba5a3235 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -4,40 +4,39 @@ #include "esphome/components/spi/spi.h" #include "esphome/components/display/display.h" -#include "esphome/components/display/display_buffer.h" #include "esphome/components/display/display_color_utils.h" namespace esphome { namespace mipi_spi { constexpr static const char *const TAG = "display.mipi_spi"; -static const uint8_t SW_RESET_CMD = 0x01; -static const uint8_t SLEEP_OUT = 0x11; -static const uint8_t NORON = 0x13; -static const uint8_t INVERT_OFF = 0x20; -static const uint8_t INVERT_ON = 0x21; -static const uint8_t ALL_ON = 0x23; -static const uint8_t WRAM = 0x24; -static const uint8_t MIPI = 0x26; -static const uint8_t DISPLAY_ON = 0x29; -static const uint8_t RASET = 0x2B; -static const uint8_t CASET = 0x2A; -static const uint8_t WDATA = 0x2C; -static const uint8_t TEON = 0x35; -static const uint8_t MADCTL_CMD = 0x36; -static const uint8_t PIXFMT = 0x3A; -static const uint8_t BRIGHTNESS = 0x51; -static const uint8_t SWIRE1 = 0x5A; -static const uint8_t SWIRE2 = 0x5B; -static const uint8_t PAGESEL = 0xFE; +static constexpr uint8_t SW_RESET_CMD = 0x01; +static constexpr uint8_t SLEEP_OUT = 0x11; +static constexpr uint8_t NORON = 0x13; +static constexpr uint8_t INVERT_OFF = 0x20; +static constexpr uint8_t INVERT_ON = 0x21; +static constexpr uint8_t ALL_ON = 0x23; +static constexpr uint8_t WRAM = 0x24; +static constexpr uint8_t MIPI = 0x26; +static constexpr uint8_t DISPLAY_ON = 0x29; +static constexpr uint8_t RASET = 0x2B; +static constexpr uint8_t CASET = 0x2A; +static constexpr uint8_t WDATA = 0x2C; +static constexpr uint8_t TEON = 0x35; +static constexpr uint8_t MADCTL_CMD = 0x36; +static constexpr uint8_t PIXFMT = 0x3A; +static constexpr uint8_t BRIGHTNESS = 0x51; +static constexpr uint8_t SWIRE1 = 0x5A; +static constexpr uint8_t SWIRE2 = 0x5B; +static constexpr uint8_t PAGESEL = 0xFE; -static const uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top -static const uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left -static const uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes -static const 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 const 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_MY = 0x80; // Bit 7 Bottom to top +static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left +static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes +static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order +static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order +static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally +static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically static const uint8_t DELAY_FLAG = 0xFF; // 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; } +// Buffer mode, conveniently also the number of bytes in a pixel enum PixelMode { - PIXEL_MODE_16, - PIXEL_MODE_18, + PIXEL_MODE_8 = 1, + 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 +class MipiSpi : public display::Display, public spi::SPIDevice { public: - MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth) - : width_(width), - height_(height), - offset_width_(offset_width), - offset_height_(offset_height), - color_depth_(color_depth) {} + MipiSpi() {} + void update() override { this->stop_poller(); } + void draw_pixel_at(int x, int y, Color color) override {} 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_enable_pins(std::vector enable_pins) { this->enable_pins_ = std::move(enable_pins); } 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->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; } - void dump_config() override; - int get_width_internal() override { return this->width_; } - int get_height_internal() override { return this->height_; } - bool can_proceed() override { return this->setup_complete_; } + int get_width_internal() override { return WIDTH; } + int get_height_internal() override { return HEIGHT; } void set_init_sequence(const std::vector &sequence) { this->init_sequence_ = sequence; } 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(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(this->data_rate_ / 1000000), BUS_TYPE); + } protected: - bool check_buffer_() { - if (this->is_failed()) - 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); - + /* METHODS */ + // convenience functions to write commands with or without data 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 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(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}; std::vector enable_pins_{}; 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_{}; - 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}; optional brightness_{}; const char *model_{"Unknown"}; std::vector 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 +class MipiSpiBuffer : public MipiSpi { + public: + MipiSpiBuffer() { this->rotation_ = ROTATION; } + + void dump_config() override { + MipiSpi::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::setup(); + RAMAllocator 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(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 esphome diff --git a/esphome/components/mipi_spi/models/adafruit.py b/esphome/components/mipi_spi/models/adafruit.py new file mode 100644 index 0000000000..0e91107bee --- /dev/null +++ b/esphome/components/mipi_spi/models/adafruit.py @@ -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 = {} diff --git a/esphome/components/mipi_spi/models/amoled.py b/esphome/components/mipi_spi/models/amoled.py index 14277b243f..882d19db30 100644 --- a/esphome/components/mipi_spi/models/amoled.py +++ b/esphome/components/mipi_spi/models/amoled.py @@ -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 = {} diff --git a/esphome/components/mipi_spi/models/waveshare.py b/esphome/components/mipi_spi/models/waveshare.py index 6d14f56fc6..726718aaf6 100644 --- a/esphome/components/mipi_spi/models/waveshare.py +++ b/esphome/components/mipi_spi/models/waveshare.py @@ -1,3 +1,5 @@ +import esphome.config_validation as cv + from . import DriverChip from .ili import ILI9488_A @@ -128,6 +130,7 @@ DriverChip( ILI9488_A.extend( "PICO-RESTOUCH-LCD-3.5", + swap_xy=cv.UNDEFINED, spi_16=True, pixel_mode="16bit", mirror_x=True, diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 58bfc3f411..065ccc2668 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -1,4 +1,5 @@ import re +from typing import Any from esphome import pins import esphome.codegen as cg @@ -139,6 +140,27 @@ def get_hw_interface_list(): 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 def get_spi_index(name): 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( "force_sw is deprecated - use interface: software" ), - cv.Optional(CONF_INTERFACE, default="any"): cv.one_of( - *sum(get_hw_interface_list(), ["software", "hardware", "any"]), - lower=True, + cv.Optional(CONF_INTERFACE, default="any"): one_of_interface_validator( + ["software", "hardware", "any"] ), cv.Optional(CONF_DATA_PINS): cv.invalid( "'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.Length(min=pin_count, max=pin_count), ), - cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of( - *sum(get_hw_interface_list(), ["hardware"]), - lower=True, - ), + cv.Optional( + CONF_INTERFACE, default="hardware" + ): one_of_interface_validator(["hardware"]), cv.Optional(CONF_MISO_PIN): cv.invalid( f"'miso_pin' should not be used with {mode} SPI" ), diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..d49aac4bab --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""ESPHome tests package.""" diff --git a/tests/component_tests/__init__.py b/tests/component_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/binary_sensor/__init__.py b/tests/component_tests/binary_sensor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/button/__init__.py b/tests/component_tests/button/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py index b1e0eaa200..b269e23cd6 100644 --- a/tests/component_tests/conftest.py +++ b/tests/component_tests/conftest.py @@ -5,18 +5,30 @@ from __future__ import annotations from collections.abc import Callable, Generator from pathlib import Path import sys +from typing import Any 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 here = Path(__file__).parent package_root = here.parent.parent sys.path.insert(0, package_root.as_posix()) 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 .types import SetCoreConfigCallable # noqa: E402 + @pytest.fixture(autouse=True) 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 +@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 def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]: """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 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: CORE.config_path = str(path) @@ -69,5 +134,3 @@ def generate_main() -> Generator[Callable[[str | Path], str]]: return CORE.cpp_main_section yield generator - - CORE.reset() diff --git a/tests/component_tests/deep_sleep/__init__.py b/tests/component_tests/deep_sleep/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/image/__init__.py b/tests/component_tests/image/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/mipi_spi/__init__.py b/tests/component_tests/mipi_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/mipi_spi/fixtures/lvgl.yaml b/tests/component_tests/mipi_spi/fixtures/lvgl.yaml new file mode 100644 index 0000000000..bc800e1090 --- /dev/null +++ b/tests/component_tests/mipi_spi/fixtures/lvgl.yaml @@ -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: diff --git a/tests/component_tests/mipi_spi/fixtures/native.yaml b/tests/component_tests/mipi_spi/fixtures/native.yaml new file mode 100644 index 0000000000..6962ac25c7 --- /dev/null +++ b/tests/component_tests/mipi_spi/fixtures/native.yaml @@ -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 diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py new file mode 100644 index 0000000000..96abad02ad --- /dev/null +++ b/tests/component_tests/mipi_spi/test_init.py @@ -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()" + 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();" + 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 diff --git a/tests/component_tests/ota/__init__.py b/tests/component_tests/ota/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/packages/__init__.py b/tests/component_tests/packages/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/sensor/__init__.py b/tests/component_tests/sensor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/text/__init__.py b/tests/component_tests/text/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/text_sensor/__init__.py b/tests/component_tests/text_sensor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/types.py b/tests/component_tests/types.py new file mode 100644 index 0000000000..72b8be4503 --- /dev/null +++ b/tests/component_tests/types.py @@ -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: ... diff --git a/tests/component_tests/web_server/__init__.py b/tests/component_tests/web_server/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml deleted file mode 100644 index a28776798c..0000000000 --- a/tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml +++ /dev/null @@ -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 diff --git a/tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml deleted file mode 100644 index 02b8f78d58..0000000000 --- a/tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml +++ /dev/null @@ -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 diff --git a/tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml deleted file mode 100644 index 147d4833ac..0000000000 --- a/tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml +++ /dev/null @@ -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 diff --git a/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml new file mode 100644 index 0000000000..e0f65a3a6a --- /dev/null +++ b/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml @@ -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: diff --git a/tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml deleted file mode 100644 index 8d96f31fd5..0000000000 --- a/tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml +++ /dev/null @@ -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 diff --git a/tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml deleted file mode 100644 index 98f6955bf3..0000000000 --- a/tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml +++ /dev/null @@ -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 diff --git a/tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml deleted file mode 100644 index 11ad869d54..0000000000 --- a/tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml +++ /dev/null @@ -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 diff --git a/tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml deleted file mode 100644 index dc328f950c..0000000000 --- a/tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml +++ /dev/null @@ -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 diff --git a/tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml deleted file mode 100644 index f0432270dc..0000000000 --- a/tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml +++ /dev/null @@ -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 diff --git a/tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml deleted file mode 100644 index 5cda38e096..0000000000 --- a/tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml +++ /dev/null @@ -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 diff --git a/tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml deleted file mode 100644 index 144bde8366..0000000000 --- a/tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml +++ /dev/null @@ -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 diff --git a/tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml deleted file mode 100644 index 39339b5ae2..0000000000 --- a/tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml +++ /dev/null @@ -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 diff --git a/tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml deleted file mode 100644 index 6c9edb25b3..0000000000 --- a/tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -spi: - - id: spi_id_3 - interface: any - clk_pin: 8 - mosi_pin: 40 - -display: - - platform: mipi_spi - model: T-EMBED diff --git a/tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml deleted file mode 100644 index 46eaedb7cb..0000000000 --- a/tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml +++ /dev/null @@ -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 diff --git a/tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml deleted file mode 100644 index 3efb05ec89..0000000000 --- a/tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml +++ /dev/null @@ -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