diff --git a/CODEOWNERS b/CODEOWNERS index c34774fce6..3f963bf960 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -299,6 +299,7 @@ esphome/components/mics_4514/* @jesserockz esphome/components/midea/* @dudanov esphome/components/midea_ir/* @dudanov esphome/components/mipi_dsi/* @clydebarrow +esphome/components/mipi_rgb/* @clydebarrow esphome/components/mipi_spi/* @clydebarrow esphome/components/mitsubishi/* @RubyBailey esphome/components/mixer/speaker/* @kahrendt diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index a9ecb9d79a..8b1ca899df 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -222,6 +222,12 @@ def delay(ms): class DriverChip: + """ + A class representing a MIPI DBI driver chip model. + The parameters supplied as defaults will be used to provide default values for the display configuration. + Setting swap_xy to cv.UNDEFINED will indicate that the model does not support swapping X and Y axes. + """ + models: dict[str, Self] = {} def __init__( @@ -232,7 +238,7 @@ class DriverChip: ): name = name.upper() self.name = name - self.initsequence = initsequence or defaults.get("init_sequence") + self.initsequence = initsequence self.defaults = defaults DriverChip.models[name] = self @@ -246,6 +252,17 @@ class DriverChip: return models def extend(self, name, **kwargs) -> "DriverChip": + """ + Extend the current model with additional parameters or a modified init sequence. + Parameters supplied here will override the defaults of the current model. + if the initsequence is not provided, the current model's initsequence will be used. + If add_init_sequence is provided, it will be appended to the current initsequence. + :param name: + :param kwargs: + :return: + """ + initsequence = list(kwargs.pop("initsequence", self.initsequence)) + initsequence.extend(kwargs.pop("add_init_sequence", ())) defaults = self.defaults.copy() if ( CONF_WIDTH in defaults @@ -260,23 +277,39 @@ class DriverChip: ): defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT] defaults.update(kwargs) - return DriverChip(name, initsequence=self.initsequence, **defaults) + return self.__class__(name, initsequence=tuple(initsequence), **defaults) def get_default(self, key, fallback: Any = False) -> Any: return self.defaults.get(key, fallback) + @property + def transforms(self) -> set[str]: + """ + Return the available transforms for this model. + """ + if self.get_default("no_transform", False): + return set() + if self.get_default(CONF_SWAP_XY) != cv.UNDEFINED: + return {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY} + return {CONF_MIRROR_X, CONF_MIRROR_Y} + def option(self, name, fallback=False) -> cv.Optional: return cv.Optional(name, default=self.get_default(name, fallback)) def rotation_as_transform(self, config) -> bool: """ Check if a rotation can be implemented in hardware using the MADCTL register. - A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. + A rotation of 180 is always possible if x and y mirroring are supported, 90 and 270 are possible if the model supports swapping X and Y. """ + transforms = self.transforms rotation = config.get(CONF_ROTATION, 0) - return rotation and ( - self.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 - ) + if rotation == 0 or not transforms: + return False + if rotation == 180: + return CONF_MIRROR_X in transforms and CONF_MIRROR_Y in transforms + if rotation == 90: + return CONF_SWAP_XY in transforms and CONF_MIRROR_X in transforms + return CONF_SWAP_XY in transforms and CONF_MIRROR_Y in transforms def get_dimensions(self, config) -> tuple[int, int, int, int]: if CONF_DIMENSIONS in config: @@ -301,10 +334,10 @@ class DriverChip: # 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]: + if transform.get(CONF_MIRROR_X): native_width = self.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2) offset_width = native_width - width - offset_width - if transform[CONF_MIRROR_Y]: + if transform.get(CONF_MIRROR_Y): native_height = self.get_default( CONF_NATIVE_HEIGHT, height + offset_height * 2 ) @@ -314,7 +347,7 @@ class DriverChip: 90, 270, ) - if transform[CONF_SWAP_XY] is True or rotated: + if transform.get(CONF_SWAP_XY) is True or rotated: width, height = height, width offset_height, offset_width = offset_width, offset_height return width, height, offset_width, offset_height @@ -324,27 +357,50 @@ class DriverChip: transform = config.get( CONF_TRANSFORM, { - CONF_MIRROR_X: self.get_default(CONF_MIRROR_X, False), - CONF_MIRROR_Y: self.get_default(CONF_MIRROR_Y, False), - CONF_SWAP_XY: self.get_default(CONF_SWAP_XY, False), + CONF_MIRROR_X: self.get_default(CONF_MIRROR_X), + CONF_MIRROR_Y: self.get_default(CONF_MIRROR_Y), + CONF_SWAP_XY: self.get_default(CONF_SWAP_XY), }, ) + # fill in defaults if not provided + mirror_x = transform.get(CONF_MIRROR_X, self.get_default(CONF_MIRROR_X)) + mirror_y = transform.get(CONF_MIRROR_Y, self.get_default(CONF_MIRROR_Y)) + swap_xy = transform.get(CONF_SWAP_XY, self.get_default(CONF_SWAP_XY)) + transform[CONF_MIRROR_X] = mirror_x + transform[CONF_MIRROR_Y] = mirror_y + transform[CONF_SWAP_XY] = swap_xy # Can we use the MADCTL register to set the rotation? if can_transform and CONF_TRANSFORM not in config: rotation = config[CONF_ROTATION] if rotation == 180: - transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] - transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + transform[CONF_MIRROR_X] = not mirror_x + transform[CONF_MIRROR_Y] = not mirror_y elif rotation == 90: - transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] - transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + transform[CONF_SWAP_XY] = not swap_xy + transform[CONF_MIRROR_X] = not mirror_x else: - transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] - transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + transform[CONF_SWAP_XY] = not swap_xy + transform[CONF_MIRROR_Y] = not mirror_y transform[CONF_TRANSFORM] = True return transform + def add_madctl(self, sequence: list, config: dict): + # Add the MADCTL command to the sequence based on the configuration. + use_flip = config.get(CONF_USE_AXIS_FLIPS) + madctl = 0 + transform = self.get_transform(config) + if transform[CONF_MIRROR_X]: + madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX + if transform[CONF_MIRROR_Y]: + madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY + if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined + madctl |= MADCTL_MV + if config[CONF_COLOR_ORDER] == MODE_BGR: + madctl |= MADCTL_BGR + sequence.append((MADCTL, madctl)) + return madctl + def get_sequence(self, config) -> tuple[tuple[int, ...], int]: """ Create the init sequence for the display. @@ -367,21 +423,9 @@ class DriverChip: pixel_mode = PIXEL_MODES[pixel_mode] sequence.append((PIXFMT, pixel_mode)) - # Does the chip use the flipping bits for mirroring rather than the reverse order bits? - use_flip = config.get(CONF_USE_AXIS_FLIPS) - madctl = 0 - transform = self.get_transform(config) if self.rotation_as_transform(config): LOGGER.info("Using hardware transform to implement rotation") - if transform.get(CONF_MIRROR_X): - madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX - if transform.get(CONF_MIRROR_Y): - madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY - if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined - madctl |= MADCTL_MV - if config[CONF_COLOR_ORDER] == MODE_BGR: - madctl |= MADCTL_BGR - sequence.append((MADCTL, madctl)) + madctl = self.add_madctl(sequence, config) if config[CONF_INVERT_COLORS]: sequence.append((INVON,)) else: diff --git a/esphome/components/mipi_rgb/__init__.py b/esphome/components/mipi_rgb/__init__.py new file mode 100644 index 0000000000..4f9972c6e0 --- /dev/null +++ b/esphome/components/mipi_rgb/__init__.py @@ -0,0 +1,2 @@ +CODEOWNERS = ["@clydebarrow"] +DOMAIN = "mipi_rgb" diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py new file mode 100644 index 0000000000..3001d33980 --- /dev/null +++ b/esphome/components/mipi_rgb/display.py @@ -0,0 +1,321 @@ +import importlib +import pkgutil + +from esphome import pins +import esphome.codegen as cg +from esphome.components import display, spi +from esphome.components.const import ( + BYTE_ORDER_BIG, + BYTE_ORDER_LITTLE, + CONF_BYTE_ORDER, + CONF_DRAW_ROUNDING, +) +from esphome.components.display import CONF_SHOW_TEST_CARD +from esphome.components.esp32 import const, only_on_variant +from esphome.components.mipi import ( + COLOR_ORDERS, + CONF_DE_PIN, + CONF_HSYNC_BACK_PORCH, + CONF_HSYNC_FRONT_PORCH, + CONF_HSYNC_PULSE_WIDTH, + CONF_PCLK_PIN, + CONF_PIXEL_MODE, + CONF_USE_AXIS_FLIPS, + CONF_VSYNC_BACK_PORCH, + CONF_VSYNC_FRONT_PORCH, + CONF_VSYNC_PULSE_WIDTH, + MODE_BGR, + PIXEL_MODE_16BIT, + PIXEL_MODE_18BIT, + DriverChip, + dimension_schema, + map_sequence, + power_of_two, + requires_buffer, +) +from esphome.components.rpi_dpi_rgb.display import ( + CONF_PCLK_FREQUENCY, + CONF_PCLK_INVERTED, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_BLUE, + CONF_COLOR_ORDER, + CONF_CS_PIN, + CONF_DATA_PINS, + CONF_DATA_RATE, + CONF_DC_PIN, + CONF_DIMENSIONS, + CONF_ENABLE_PIN, + CONF_GREEN, + CONF_HSYNC_PIN, + CONF_ID, + CONF_IGNORE_STRAPPING_WARNING, + CONF_INIT_SEQUENCE, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_MODEL, + CONF_NUMBER, + CONF_RED, + CONF_RESET_PIN, + CONF_ROTATION, + CONF_SPI_ID, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_VSYNC_PIN, + CONF_WIDTH, +) +from esphome.final_validate import full_config + +from ..spi import CONF_SPI_MODE, SPI_DATA_RATE_SCHEMA, SPI_MODE_OPTIONS, SPIComponent +from . import models + +DEPENDENCIES = ["esp32", "psram"] + +mipi_rgb_ns = cg.esphome_ns.namespace("mipi_rgb") +mipi_rgb = mipi_rgb_ns.class_("MipiRgb", display.Display, cg.Component) +mipi_rgb_spi = mipi_rgb_ns.class_( + "MipiRgbSpi", mipi_rgb, display.Display, cg.Component, spi.SPIDevice +) +ColorOrder = display.display_ns.enum("ColorMode") + +DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema + +DriverChip("CUSTOM") + +# Import all models dynamically from the models package + +for module_info in pkgutil.iter_modules(models.__path__): + importlib.import_module(f".models.{module_info.name}", package=__package__) + +MODELS = DriverChip.get_models() + + +def data_pin_validate(value): + """ + It is safe to use strapping pins as RGB output data bits, as they are outputs only, + and not initialised until after boot. + """ + if not isinstance(value, dict): + try: + return DATA_PIN_SCHEMA( + {CONF_NUMBER: value, CONF_IGNORE_STRAPPING_WARNING: True} + ) + except cv.Invalid: + pass + return DATA_PIN_SCHEMA(value) + + +def data_pin_set(length): + return cv.All( + [data_pin_validate], + cv.Length(min=length, max=length, msg=f"Exactly {length} data pins required"), + ) + + +def model_schema(config): + model = MODELS[config[CONF_MODEL].upper()] + if transforms := model.transforms: + transform = cv.Schema({cv.Required(x): cv.boolean for x in transforms}) + for x in (CONF_SWAP_XY, CONF_MIRROR_X, CONF_MIRROR_Y): + if x not in transforms: + transform = transform.extend( + {cv.Optional(x): cv.invalid(f"{x} not supported by this model")} + ) + else: + transform = cv.invalid("This model does not support transforms") + + # RPI model does not use an init sequence, indicates with empty list + if model.initsequence is None: + # Custom model requires an init sequence + iseqconf = cv.Required(CONF_INIT_SEQUENCE) + uses_spi = True + else: + iseqconf = cv.Optional(CONF_INIT_SEQUENCE) + uses_spi = CONF_INIT_SEQUENCE in config or len(model.initsequence) != 0 + swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) + + # Dimensions are optional if the model has a default width and the swap_xy transform is not overridden + cv_dimensions = ( + cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required + ) + pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_18BIT, "16", "18") + schema = display.FULL_DISPLAY_SCHEMA.extend( + { + model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + cv.GenerateID(): cv.declare_id(mipi_rgb_spi if uses_spi else mipi_rgb), + cv_dimensions(CONF_DIMENSIONS): dimension_schema( + model.get_default(CONF_DRAW_ROUNDING, 1) + ), + model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list( + pins.gpio_output_pin_schema + ), + model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(COLOR_ORDERS, upper=True), + model.option(CONF_DRAW_ROUNDING, 2): power_of_two, + model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of( + *pixel_modes, lower=True + ), + model.option(CONF_TRANSFORM, cv.UNDEFINED): transform, + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + model.option(CONF_INVERT_COLORS, False): cv.boolean, + model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean, + model.option(CONF_PCLK_FREQUENCY, "40MHz"): cv.All( + cv.frequency, cv.Range(min=4e6, max=100e6) + ), + model.option(CONF_PCLK_INVERTED, True): cv.boolean, + iseqconf: cv.ensure_list(map_sequence), + model.option(CONF_BYTE_ORDER, BYTE_ORDER_BIG): cv.one_of( + BYTE_ORDER_LITTLE, BYTE_ORDER_BIG, lower=True + ), + model.option(CONF_HSYNC_PULSE_WIDTH): cv.int_, + model.option(CONF_HSYNC_BACK_PORCH): cv.int_, + model.option(CONF_HSYNC_FRONT_PORCH): cv.int_, + model.option(CONF_VSYNC_PULSE_WIDTH): cv.int_, + model.option(CONF_VSYNC_BACK_PORCH): cv.int_, + model.option(CONF_VSYNC_FRONT_PORCH): cv.int_, + model.option(CONF_DATA_PINS): cv.Any( + data_pin_set(16), + cv.Schema( + { + cv.Required(CONF_RED): data_pin_set(5), + cv.Required(CONF_GREEN): data_pin_set(6), + cv.Required(CONF_BLUE): data_pin_set(5), + } + ), + ), + model.option( + CONF_DE_PIN, cv.UNDEFINED + ): pins.internal_gpio_output_pin_schema, + model.option(CONF_PCLK_PIN): pins.internal_gpio_output_pin_schema, + model.option(CONF_HSYNC_PIN): pins.internal_gpio_output_pin_schema, + model.option(CONF_VSYNC_PIN): pins.internal_gpio_output_pin_schema, + model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + } + ) + if uses_spi: + schema = schema.extend( + { + cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), + model.option(CONF_DC_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + model.option(CONF_DATA_RATE, "1MHz"): SPI_DATA_RATE_SCHEMA, + model.option(CONF_SPI_MODE, "MODE0"): cv.enum( + SPI_MODE_OPTIONS, upper=True + ), + model.option(CONF_CS_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + } + ) + return schema + + +def _config_schema(config): + config = cv.Schema( + { + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + }, + extra=cv.ALLOW_EXTRA, + )(config) + schema = model_schema(config) + return cv.All( + schema, + only_on_variant(supported=[const.VARIANT_ESP32S3]), + cv.only_with_esp_idf, + )(config) + + +CONFIG_SCHEMA = _config_schema + + +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 CONF_SPI_ID in config: + config = spi.final_validate_device_schema( + "mipi_rgb", require_miso=False, require_mosi=True + )(config) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + model = MODELS[config[CONF_MODEL].upper()] + width, height, _offset_width, _offset_height = model.get_dimensions(config) + var = cg.new_Pvariable(config[CONF_ID], width, height) + cg.add(var.set_model(model.name)) + 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)) + + if CONF_SPI_ID in config: + await spi.register_spi_device(var, config) + sequence, madctl = model.get_sequence(config) + cg.add(var.set_init_sequence(sequence)) + cg.add(var.set_madctl(madctl)) + + cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]])) + cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS])) + cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH])) + cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH])) + cg.add(var.set_hsync_front_porch(config[CONF_HSYNC_FRONT_PORCH])) + cg.add(var.set_vsync_pulse_width(config[CONF_VSYNC_PULSE_WIDTH])) + cg.add(var.set_vsync_back_porch(config[CONF_VSYNC_BACK_PORCH])) + cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) + cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) + cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) + index = 0 + dpins = [] + if CONF_RED in config[CONF_DATA_PINS]: + red_pins = config[CONF_DATA_PINS][CONF_RED] + green_pins = config[CONF_DATA_PINS][CONF_GREEN] + blue_pins = config[CONF_DATA_PINS][CONF_BLUE] + if config[CONF_COLOR_ORDER] == "BGR": + dpins.extend(red_pins) + dpins.extend(green_pins) + dpins.extend(blue_pins) + else: + dpins.extend(blue_pins) + dpins.extend(green_pins) + dpins.extend(red_pins) + # swap bytes to match big-endian format + dpins = dpins[8:16] + dpins[0:8] + else: + dpins = config[CONF_DATA_PINS] + for index, pin in enumerate(dpins): + data_pin = await cg.gpio_pin_expression(pin) + cg.add(var.add_data_pin(data_pin, index)) + + if dc_pin := config.get(CONF_DC_PIN): + dc = await cg.gpio_pin_expression(dc_pin) + cg.add(var.set_dc_pin(dc)) + + if reset_pin := config.get(CONF_RESET_PIN): + reset = await cg.gpio_pin_expression(reset_pin) + cg.add(var.set_reset_pin(reset)) + + if model.rotation_as_transform(config): + config[CONF_ROTATION] = 0 + + if de_pin := config.get(CONF_DE_PIN): + pin = await cg.gpio_pin_expression(de_pin) + cg.add(var.set_de_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_PCLK_PIN]) + cg.add(var.set_pclk_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_HSYNC_PIN]) + cg.add(var.set_hsync_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_VSYNC_PIN]) + cg.add(var.set_vsync_pin(pin)) + + await display.register_display(var, config) + if lamb := config.get(CONF_LAMBDA): + lambda_ = await cg.process_lambda( + lamb, [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp new file mode 100644 index 0000000000..00c9c8cbff --- /dev/null +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -0,0 +1,388 @@ +#ifdef USE_ESP32_VARIANT_ESP32S3 +#include "mipi_rgb.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include "esp_lcd_panel_rgb.h" + +namespace esphome { +namespace mipi_rgb { + +static const uint8_t DELAY_FLAG = 0xFF; +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_ML = 0x10; // Bit 4 Refresh bottom to top +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 + +void MipiRgb::setup_enables_() { + if (!this->enable_pins_.empty()) { + for (auto *pin : this->enable_pins_) { + pin->setup(); + pin->digital_write(true); + } + delay(10); + } + 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); + } +} + +#ifdef USE_SPI +void MipiRgbSpi::setup() { + this->setup_enables_(); + this->spi_setup(); + this->write_init_sequence_(); + this->common_setup_(); +} +void MipiRgbSpi::write_command_(uint8_t value) { + this->enable(); + if (this->dc_pin_ == nullptr) { + this->write(value, 9); + } else { + this->dc_pin_->digital_write(false); + this->write_byte(value); + this->dc_pin_->digital_write(true); + } + this->disable(); +} + +void MipiRgbSpi::write_data_(uint8_t value) { + this->enable(); + if (this->dc_pin_ == nullptr) { + this->write(value | 0x100, 9); + } else { + this->dc_pin_->digital_write(true); + this->write_byte(value); + } + this->disable(); +} + +/** + * this relies upon the init sequence being well-formed, which is guaranteed by the Python init code. + */ + +void MipiRgbSpi::write_init_sequence_() { + size_t index = 0; + auto &vec = this->init_sequence_; + while (index != vec.size()) { + if (vec.size() - index < 2) { + this->mark_failed("Malformed init sequence"); + 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) { + this->mark_failed("Malformed init sequence"); + return; + } + if (cmd == SLEEP_OUT) { + delay(120); // NOLINT + } + const auto *ptr = vec.data() + index; + ESP_LOGD(TAG, "Write command %02X, length %d, byte(s) %s", cmd, num_args, + format_hex_pretty(ptr, num_args, '.', false).c_str()); + index += num_args; + this->write_command_(cmd); + while (num_args-- != 0) + this->write_data_(*ptr++); + if (cmd == SLEEP_OUT) + delay(10); + } + } + // this->spi_teardown(); // SPI not needed after this + this->init_sequence_.clear(); + delay(10); +} + +void MipiRgbSpi::dump_config() { + MipiRgb::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + ESP_LOGCONFIG(TAG, + " SPI Data rate: %uMHz" + "\n Mirror X: %s" + "\n Mirror Y: %s" + "\n Swap X/Y: %s" + "\n Color Order: %s", + (unsigned) (this->data_rate_ / 1000000), YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)), + YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY | MADCTL_ML)), YESNO(this->madctl_ & MADCTL_MV), + this->madctl_ & MADCTL_BGR ? "BGR" : "RGB"); +} + +#endif // USE_SPI + +void MipiRgb::setup() { + this->setup_enables_(); + this->common_setup_(); +} + +void MipiRgb::common_setup_() { + esp_lcd_rgb_panel_config_t config{}; + config.flags.fb_in_psram = 1; + config.bounce_buffer_size_px = this->width_ * 10; + config.num_fbs = 1; + config.timings.h_res = this->width_; + config.timings.v_res = this->height_; + config.timings.hsync_pulse_width = this->hsync_pulse_width_; + config.timings.hsync_back_porch = this->hsync_back_porch_; + config.timings.hsync_front_porch = this->hsync_front_porch_; + config.timings.vsync_pulse_width = this->vsync_pulse_width_; + config.timings.vsync_back_porch = this->vsync_back_porch_; + config.timings.vsync_front_porch = this->vsync_front_porch_; + config.timings.flags.pclk_active_neg = this->pclk_inverted_; + config.timings.pclk_hz = this->pclk_frequency_; + config.clk_src = LCD_CLK_SRC_PLL160M; + size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); + for (size_t i = 0; i != data_pin_count; i++) { + config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); + } + config.data_width = data_pin_count; + config.disp_gpio_num = -1; + config.hsync_gpio_num = this->hsync_pin_->get_pin(); + config.vsync_gpio_num = this->vsync_pin_->get_pin(); + if (this->de_pin_) { + config.de_gpio_num = this->de_pin_->get_pin(); + } else { + config.de_gpio_num = -1; + } + config.pclk_gpio_num = this->pclk_pin_->get_pin(); + esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_); + if (err == ESP_OK) + err = esp_lcd_panel_reset(this->handle_); + if (err == ESP_OK) + err = esp_lcd_panel_init(this->handle_); + if (err != ESP_OK) { + auto msg = str_sprintf("lcd setup failed: %s", esp_err_to_name(err)); + this->mark_failed(msg.c_str()); + } + ESP_LOGCONFIG(TAG, "MipiRgb setup complete"); +} + +void MipiRgb::loop() { + if (this->handle_ != nullptr) + esp_lcd_rgb_panel_restart(this->handle_); +} + +void MipiRgb::update() { + if (this->is_failed()) + return; + if (this->auto_clear_enabled_) { + this->clear(); + } + if (this->show_test_card_) { + this->test_card(); + } else if (this->page_ != nullptr) { + this->page_->get_writer()(*this); + } else if (this->writer_.has_value()) { + (*this->writer_)(*this); + } else { + this->stop_poller(); + } + 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_); + 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, reinterpret_cast(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 MipiRgb::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 (w <= 0 || h <= 0 || this->is_failed()) + return; + // if color mapping is required, pass the buck. + // note that endianness is not considered here - it is assumed to match! + if (bitness != display::COLOR_BITNESS_565) { + Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); + this->write_to_display_(x_start, y_start, w, h, reinterpret_cast(this->buffer_), x_start, y_start, + this->width_ - w - x_start); + } else { + this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); + } +} + +void MipiRgb::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) { + esp_err_t err = ESP_OK; + auto stride = (x_offset + w + x_pad) * 2; + ptr += y_offset * stride + x_offset * 2; // skip to the first pixel + // x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display. + if (x_offset == 0 && x_pad == 0) { + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y_start, x_start + w, y_start + h, ptr); + } else { + // draw line by line + for (int y = 0; y != h; y++) { + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y + y_start, x_start + w, y + y_start + 1, ptr); + if (err != ESP_OK) + break; + ptr += stride; // next line + } + } + if (err != ESP_OK) + ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); +} + +bool MipiRgb::check_buffer_() { + if (this->is_failed()) + return false; + if (this->buffer_ != nullptr) + return true; + // this is dependent on the enum values. + RAMAllocator allocator; + this->buffer_ = allocator.allocate(this->height_ * this->width_); + if (this->buffer_ == nullptr) { + this->mark_failed("Could not allocate buffer for display!"); + return false; + } + return true; +} + +void MipiRgb::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y) || this->is_failed()) + return; + + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + break; + case display::DISPLAY_ROTATION_90_DEGREES: + std::swap(x, y); + x = this->width_ - x - 1; + break; + case display::DISPLAY_ROTATION_180_DEGREES: + x = this->width_ - x - 1; + y = this->height_ - y - 1; + break; + case display::DISPLAY_ROTATION_270_DEGREES: + std::swap(x, y); + y = this->height_ - y - 1; + break; + } + 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; + 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 (this->buffer_[pos] == new_color) + return; + this->buffer_[pos] = new_color; + // 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 MipiRgb::fill(Color color) { + if (!this->check_buffer_()) + return; + 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 = lo_byte | (hi_byte << 8); // little endian + std::fill_n(ptr_16, this->width_ * this->height_, new_color); +} + +int MipiRgb::get_width() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + default: + return this->get_width_internal(); + } +} + +int MipiRgb::get_height() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + default: + return this->get_width_internal(); + } +} + +static std::string get_pin_name(GPIOPin *pin) { + if (pin == nullptr) + return "None"; + return pin->dump_summary(); +} + +void MipiRgb::dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset) { + for (uint8_t i = start; i != end; i++) { + ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, this->data_pins_[i]->dump_summary().c_str()); + } +} + +void MipiRgb::dump_config() { + ESP_LOGCONFIG(TAG, + "MIPI_RGB LCD" + "\n Model: %s" + "\n Width: %u" + "\n Height: %u" + "\n Rotation: %d degrees" + "\n HSync Pulse Width: %u" + "\n HSync Back Porch: %u" + "\n HSync Front Porch: %u" + "\n VSync Pulse Width: %u" + "\n VSync Back Porch: %u" + "\n VSync Front Porch: %u" + "\n Invert Colors: %s" + "\n Pixel Clock: %dMHz" + "\n Reset Pin: %s" + "\n DE Pin: %s" + "\n PCLK Pin: %s" + "\n HSYNC Pin: %s" + "\n VSYNC Pin: %s", + this->model_, this->width_, this->height_, this->rotation_, this->hsync_pulse_width_, + this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, + this->vsync_front_porch_, YESNO(this->invert_colors_), this->pclk_frequency_ / 1000000, + get_pin_name(this->reset_pin_).c_str(), get_pin_name(this->de_pin_).c_str(), + get_pin_name(this->pclk_pin_).c_str(), get_pin_name(this->hsync_pin_).c_str(), + get_pin_name(this->vsync_pin_).c_str()); + + if (this->madctl_ & MADCTL_BGR) { + this->dump_pins_(8, 13, "Blue", 0); + this->dump_pins_(13, 16, "Green", 0); + this->dump_pins_(0, 3, "Green", 3); + this->dump_pins_(3, 8, "Red", 0); + } else { + this->dump_pins_(8, 13, "Red", 0); + this->dump_pins_(13, 16, "Green", 0); + this->dump_pins_(0, 3, "Green", 3); + this->dump_pins_(3, 8, "Blue", 0); + } +} + +} // namespace mipi_rgb +} // namespace esphome +#endif // USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/mipi_rgb/mipi_rgb.h b/esphome/components/mipi_rgb/mipi_rgb.h new file mode 100644 index 0000000000..173e23752d --- /dev/null +++ b/esphome/components/mipi_rgb/mipi_rgb.h @@ -0,0 +1,127 @@ +#pragma once + +#ifdef USE_ESP32_VARIANT_ESP32S3 +#include "esphome/core/gpio.h" +#include "esphome/components/display/display.h" +#include "esp_lcd_panel_ops.h" +#ifdef USE_SPI +#include "esphome/components/spi/spi.h" +#endif + +namespace esphome { +namespace mipi_rgb { + +constexpr static const char *const TAG = "display.mipi_rgb"; +const uint8_t SW_RESET_CMD = 0x01; +const uint8_t SLEEP_OUT = 0x11; +const uint8_t SDIR_CMD = 0xC7; +const uint8_t MADCTL_CMD = 0x36; +const uint8_t INVERT_OFF = 0x20; +const uint8_t INVERT_ON = 0x21; +const uint8_t DISPLAY_ON = 0x29; +const uint8_t CMD2_BKSEL = 0xFF; +const uint8_t CMD2_BK0[5] = {0x77, 0x01, 0x00, 0x00, 0x10}; + +class MipiRgb : public display::Display { + public: + MipiRgb(int width, int height) : width_(width), height_(height) {} + void setup() override; + void loop() override; + void update() override; + void fill(Color color); + 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_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); + bool check_buffer_(); + + display::ColorOrder get_color_mode() { return this->color_mode_; } + void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; } + void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; } + void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } + + void add_data_pin(InternalGPIOPin *data_pin, size_t index) { this->data_pins_[index] = data_pin; }; + void set_de_pin(InternalGPIOPin *de_pin) { this->de_pin_ = de_pin; } + void set_pclk_pin(InternalGPIOPin *pclk_pin) { this->pclk_pin_ = pclk_pin; } + void set_vsync_pin(InternalGPIOPin *vsync_pin) { this->vsync_pin_ = vsync_pin; } + void set_hsync_pin(InternalGPIOPin *hsync_pin) { this->hsync_pin_ = hsync_pin; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_width(uint16_t width) { this->width_ = width; } + void set_pclk_frequency(uint32_t pclk_frequency) { this->pclk_frequency_ = pclk_frequency; } + void set_pclk_inverted(bool inverted) { this->pclk_inverted_ = inverted; } + void set_model(const char *model) { this->model_ = model; } + int get_width() override; + int get_height() override; + void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; } + void set_hsync_front_porch(uint16_t hsync_front_porch) { this->hsync_front_porch_ = hsync_front_porch; } + void set_hsync_pulse_width(uint16_t hsync_pulse_width) { this->hsync_pulse_width_ = hsync_pulse_width; } + void set_vsync_pulse_width(uint16_t vsync_pulse_width) { this->vsync_pulse_width_ = vsync_pulse_width; } + void set_vsync_back_porch(uint16_t vsync_back_porch) { this->vsync_back_porch_ = vsync_back_porch; } + void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; } + void set_enable_pins(std::vector enable_pins) { this->enable_pins_ = std::move(enable_pins); } + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + int get_width_internal() override { return this->width_; } + int get_height_internal() override { return this->height_; } + void dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset); + void dump_config() override; + void draw_pixel_at(int x, int y, Color color) override; + + // this will be horribly slow. + protected: + void setup_enables_(); + void common_setup_(); + InternalGPIOPin *de_pin_{nullptr}; + InternalGPIOPin *pclk_pin_{nullptr}; + InternalGPIOPin *hsync_pin_{nullptr}; + InternalGPIOPin *vsync_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + InternalGPIOPin *data_pins_[16] = {}; + uint16_t hsync_pulse_width_ = 10; + uint16_t hsync_back_porch_ = 10; + uint16_t hsync_front_porch_ = 20; + uint16_t vsync_pulse_width_ = 10; + uint16_t vsync_back_porch_ = 10; + uint16_t vsync_front_porch_ = 10; + uint32_t pclk_frequency_ = 16 * 1000 * 1000; + bool pclk_inverted_{true}; + uint8_t madctl_{}; + const char *model_{"Unknown"}; + bool invert_colors_{}; + display::ColorOrder color_mode_{display::COLOR_ORDER_BGR}; + size_t width_; + size_t height_; + uint16_t *buffer_{nullptr}; + std::vector enable_pins_{}; + uint16_t x_low_{1}; + uint16_t y_low_{1}; + uint16_t x_high_{0}; + uint16_t y_high_{0}; + + esp_lcd_panel_handle_t handle_{}; +}; + +#ifdef USE_SPI +class MipiRgbSpi : public MipiRgb, + public spi::SPIDevice { + public: + MipiRgbSpi(int width, int height) : MipiRgb(width, height) {} + + void set_init_sequence(const std::vector &init_sequence) { this->init_sequence_ = init_sequence; } + void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } + void setup() override; + + protected: + void write_command_(uint8_t value); + void write_data_(uint8_t value); + void write_init_sequence_(); + void dump_config(); + + GPIOPin *dc_pin_{nullptr}; + std::vector init_sequence_; +}; +#endif + +} // namespace mipi_rgb +} // namespace esphome +#endif diff --git a/esphome/components/mipi_rgb/models/guition.py b/esphome/components/mipi_rgb/models/guition.py new file mode 100644 index 0000000000..da433e686e --- /dev/null +++ b/esphome/components/mipi_rgb/models/guition.py @@ -0,0 +1,24 @@ +from .st7701s import st7701s + +st7701s.extend( + "GUITION-4848S040", + width=480, + height=480, + data_rate="2MHz", + cs_pin=39, + de_pin=18, + hsync_pin=16, + vsync_pin=17, + pclk_pin=21, + pclk_frequency="12MHz", + pixel_mode="18bit", + mirror_x=True, + mirror_y=True, + data_pins={ + "red": [11, 12, 13, 14, 0], + "green": [8, 20, 3, 46, 9, 10], + "blue": [4, 5, 6, 7, 15], + }, + # Additional configuration for Guition 4848S040, 16 bit bus config + add_init_sequence=((0xCD, 0x00),), +) diff --git a/esphome/components/mipi_rgb/models/lilygo.py b/esphome/components/mipi_rgb/models/lilygo.py index e69de29bb2..109dc42af6 100644 --- a/esphome/components/mipi_rgb/models/lilygo.py +++ b/esphome/components/mipi_rgb/models/lilygo.py @@ -0,0 +1,228 @@ +from esphome.config_validation import UNDEFINED + +from .st7701s import ST7701S + +# fmt: off +ST7701S( + "T-PANEL-S3", + width=480, + height=480, + color_order="BGR", + invert_colors=False, + swap_xy=UNDEFINED, + spi_mode="MODE3", + cs_pin={"xl9535": None, "number": 17}, + reset_pin={"xl9535": None, "number": 5}, + hsync_pin=39, + vsync_pin=40, + pclk_pin=41, + data_pins={ + "red": [12, 13, 42, 46, 45], + "green": [6, 7, 8, 9, 10, 11], + "blue": [1, 2, 3, 4, 5], + }, + hsync_front_porch=20, + hsync_back_porch=0, + hsync_pulse_width=2, + vsync_front_porch=30, + vsync_back_porch=1, + vsync_pulse_width=8, + pclk_frequency="6MHz", + pclk_inverted=False, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x3B, 0x00), (0xC1, 0x0B, 0x02), (0xC2, 0x30, 0x02, 0x37), (0xCC, 0x10), + (0xB0, 0x00, 0x0F, 0x16, 0x0E, 0x11, 0x07, 0x09, 0x09, 0x08, 0x23, 0x05, 0x11, 0x0F, 0x28, 0x2D, 0x18), + (0xB1, 0x00, 0x0F, 0x16, 0x0E, 0x11, 0x07, 0x09, 0x08, 0x09, 0x23, 0x05, 0x11, 0x0F, 0x28, 0x2D, 0x18), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x4D), (0xB1, 0x33), (0xB2, 0x87), (0xB5, 0x4B), (0xB7, 0x8C), (0xB8, 0x20), (0xC1, 0x78), + (0xC2, 0x78), (0xD0, 0x88), (0xE0, 0x00, 0x00, 0x02), + (0xE1, 0x02, 0xF0, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x00, 0x44, 0x44), + (0xE2, 0x10, 0x10, 0x40, 0x40, 0xF2, 0xF0, 0x00, 0x00, 0xF2, 0xF0, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x11), (0xE4, 0x44, 0x44), + (0xE5, 0x07, 0xEF, 0xF0, 0xF0, 0x09, 0xF1, 0xF0, 0xF0, 0x03, 0xF3, 0xF0, 0xF0, 0x05, 0xED, 0xF0, 0xF0), + (0xE6, 0x00, 0x00, 0x11, 0x11), (0xE7, 0x44, 0x44), + (0xE8, 0x08, 0xF0, 0xF0, 0xF0, 0x0A, 0xF2, 0xF0, 0xF0, 0x04, 0xF4, 0xF0, 0xF0, 0x06, 0xEE, 0xF0, 0xF0), + (0xEB, 0x00, 0x00, 0xE4, 0xE4, 0x44, 0x88, 0x40), + (0xEC, 0x78, 0x00), + (0xED, 0x20, 0xF9, 0x87, 0x76, 0x65, 0x54, 0x4F, 0xFF, 0xFF, 0xF4, 0x45, 0x56, 0x67, 0x78, 0x9F, 0x02), + (0xEF, 0x10, 0x0D, 0x04, 0x08, 0x3F, 0x1F), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + ), +) + + +t_rgb = ST7701S( + "T-RGB-2.1", + width=480, + height=480, + color_order="BGR", + pixel_mode="18bit", + invert_colors=False, + swap_xy=UNDEFINED, + spi_mode="MODE3", + cs_pin={"xl9535": None, "number": 3}, + de_pin=45, + hsync_pin=47, + vsync_pin=41, + pclk_pin=42, + data_pins={ + "red": [7, 6, 5, 3, 2], + "green": [14, 13, 12, 11, 10, 9], + "blue": [21, 18, 17, 16, 15], + }, + hsync_front_porch=50, + hsync_pulse_width=1, + hsync_back_porch=30, + vsync_front_porch=20, + vsync_pulse_width=1, + vsync_back_porch=30, + pclk_frequency="12MHz", + pclk_inverted=False, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + + (0xC0, 0x3B, 0x00), + (0xC1, 0x0B, 0x02), + (0xC2, 0x07, 0x02), + (0xCC, 0x10), + (0xCD, 0x08), + + (0xB0, + 0x00, 0x11, 0x16, 0x0e, + 0x11, 0x06, 0x05, 0x09, + 0x08, 0x21, 0x06, 0x13, + 0x10, 0x29, 0x31, 0x18), + + (0xB1, + 0x00, 0x11, 0x16, 0x0e, + 0x11, 0x07, 0x05, 0x09, + 0x09, 0x21, 0x05, 0x13, + 0x11, 0x2a, 0x31, 0x18), + + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + + (0xB0, 0x6D), + (0xB1, 0x37), + (0xB2, 0x81), + (0xB3, 0x80), + (0xB5, 0x43), + (0xB7, 0x85), + (0xB8, 0x20), + + (0xC1, 0x78), + (0xC2, 0x78), + (0xC3, 0x8C), + + (0xD0, 0x88), + + (0xE0, 0x00, 0x00, 0x02), + (0xE1, + 0x03, 0xA0, 0x00, 0x00, + 0x04, 0xA0, 0x00, 0x00, + 0x00, 0x20, 0x20), + + (0xE2, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00), + + (0xE3, 0x00, 0x00, 0x11, 0x00), + (0xE4, 0x22, 0x00), + + (0xE5, + 0x05, 0xEC, 0xA0, 0xA0, + 0x07, 0xEE, 0xA0, 0xA0, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + + (0xE6, 0x00, 0x00, 0x11, 0x00), + (0xE7, 0x22, 0x00), + + (0xE8, + 0x06, 0xED, 0xA0, 0xA0, + 0x08, 0xEF, 0xA0, 0xA0, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + + (0xEB, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x10), + + (0xED, + 0xFF, 0xFF, 0xFF, 0xBA, + 0x0A, 0xBF, 0x45, 0xFF, + 0xFF, 0x54, 0xFB, 0xA0, + 0xAB, 0xFF, 0xFF, 0xFF), + + (0xEF, 0x10, 0x0D, 0x04, 0x08, 0x3F, 0x1F), + + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xEF, 0x08), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10) + ) + +) +t_rgb.extend( + "T-RGB-2.8", + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xEF, 0x08), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x3B, 0x00), + (0xC1, 0x10, 0x0C), + (0xC2, 0x07, 0x0A), + (0xC7, 0x00), + (0xC7, 0x10), + (0xCD, 0x08), + (0xB0, + 0x05, 0x12, 0x98, 0x0e, 0x0F, + 0x07, 0x07, 0x09, 0x09, 0x23, + 0x05, 0x52, 0x0F, 0x67, 0x2C, 0x11), + (0xB1, + 0x0B, 0x11, 0x97, 0x0C, 0x12, + 0x06, 0x06, 0x08, 0x08, 0x22, + 0x03, 0x51, 0x11, 0x66, 0x2B, 0x0F), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x5D), + (0xB1, 0x2D), + (0xB2, 0x81), + (0xB3, 0x80), + (0xB5, 0x4E), + (0xB7, 0x85), + (0xB8, 0x20), + (0xC1, 0x78), + (0xC2, 0x78), + (0xD0, 0x88), + (0xE0, 0x00, 0x00, 0x02), + (0xE1, + 0x06, 0x30, 0x08, 0x30, 0x05, + 0x30, 0x07, 0x30, 0x00, 0x33, + 0x33), + (0xE2, + 0x11, 0x11, 0x33, 0x33, 0xf4, + 0x00, 0x00, 0x00, 0xf4, 0x00, + 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x11), + (0xE4, 0x44, 0x44), + (0xE5, + 0x0d, 0xf5, 0x30, 0xf0, 0x0f, + 0xf7, 0x30, 0xf0, 0x09, 0xf1, + 0x30, 0xf0, 0x0b, 0xf3, 0x30, 0xf0), + (0xE6, 0x00, 0x00, 0x11, 0x11), + (0xE7, 0x44, 0x44), + (0xE8, + 0x0c, 0xf4, 0x30, 0xf0, + 0x0e, 0xf6, 0x30, 0xf0, + 0x08, 0xf0, 0x30, 0xf0, + 0x0a, 0xf2, 0x30, 0xf0), + (0xe9, 0x36), + (0xEB, 0x00, 0x01, 0xe4, 0xe4, 0x44, 0x88, 0x40), + (0xED, + 0xff, 0x10, 0xaf, 0x76, + 0x54, 0x2b, 0xcf, 0xff, + 0xff, 0xfc, 0xb2, 0x45, + 0x67, 0xfa, 0x01, 0xff), + (0xEF, 0x08, 0x08, 0x08, 0x45, 0x3f, 0x54), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + ) +) diff --git a/esphome/components/mipi_rgb/models/rpi.py b/esphome/components/mipi_rgb/models/rpi.py new file mode 100644 index 0000000000..076d96b658 --- /dev/null +++ b/esphome/components/mipi_rgb/models/rpi.py @@ -0,0 +1,9 @@ +from esphome.components.mipi import DriverChip +from esphome.config_validation import UNDEFINED + +# A driver chip for Raspberry Pi MIPI RGB displays. These require no init sequence +DriverChip( + "RPI", + swap_xy=UNDEFINED, + initsequence=(), +) diff --git a/esphome/components/mipi_rgb/models/st7701s.py b/esphome/components/mipi_rgb/models/st7701s.py new file mode 100644 index 0000000000..bfd1c9aa3f --- /dev/null +++ b/esphome/components/mipi_rgb/models/st7701s.py @@ -0,0 +1,214 @@ +from esphome.components.mipi import ( + MADCTL, + MADCTL_ML, + MADCTL_XFLIP, + MODE_BGR, + DriverChip, +) +from esphome.config_validation import UNDEFINED +from esphome.const import CONF_COLOR_ORDER, CONF_HEIGHT, CONF_MIRROR_X, CONF_MIRROR_Y + +SDIR_CMD = 0xC7 + + +class ST7701S(DriverChip): + # The ST7701s does not use the standard MADCTL bits for x/y mirroring + def add_madctl(self, sequence: list, config: dict): + transform = self.get_transform(config) + madctl = 0x00 + if config[CONF_COLOR_ORDER] == MODE_BGR: + madctl |= 0x08 + if transform.get(CONF_MIRROR_Y): + madctl |= MADCTL_ML + sequence.append((MADCTL, madctl)) + sdir = 0 + if transform.get(CONF_MIRROR_X): + sdir |= 0x04 + madctl |= MADCTL_XFLIP + sequence.append((SDIR_CMD, sdir)) + return madctl + + @property + def transforms(self) -> set[str]: + """ + The ST7701 never supports axis swapping, and mirroring the y-axis only works for full height. + """ + if self.get_default(CONF_HEIGHT) != 864: + return {CONF_MIRROR_X} + return {CONF_MIRROR_X, CONF_MIRROR_Y} + + +# fmt: off +st7701s = ST7701S( + "ST7701S", + width=480, + height=864, + swap_xy=UNDEFINED, + hsync_front_porch=20, + hsync_back_porch=10, + hsync_pulse_width=10, + vsync_front_porch=10, + vsync_back_porch=10, + vsync_pulse_width=10, + pclk_frequency="16MHz", + pclk_inverted=True, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0 + (0xC0, 0x3B, 0x00), (0xC1, 0x0D, 0x02), (0xC2, 0x31, 0x05), + (0xB0, 0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18,), + (0xB1, 0x00, 0x11, 0x19, 0x0E, 0x12, 0x07, 0x08, 0x08, 0x08, 0x22, 0x04, 0x11, 0x11, 0xA9, 0x32, 0x18,), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), # page 1 + (0xB0, 0x60), (0xB1, 0x32), (0xB2, 0x07), (0xB3, 0x80), (0xB5, 0x49), (0xB7, 0x85), (0xB8, 0x21), (0xC1, 0x78), + (0xC2, 0x78), (0xE0, 0x00, 0x1B, 0x02), + (0xE1, 0x08, 0xA0, 0x00, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x44, 0x44), + (0xE2, 0x11, 0x11, 0x44, 0x44, 0xED, 0xA0, 0x00, 0x00, 0xEC, 0xA0, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x11), + (0xE4, 0x44, 0x44), + (0xE5, 0x0A, 0xE9, 0xD8, 0xA0, 0x0C, 0xEB, 0xD8, 0xA0, 0x0E, 0xED, 0xD8, 0xA0, 0x10, 0xEF, 0xD8, 0xA0,), + (0xE6, 0x00, 0x00, 0x11, 0x11), (0xE7, 0x44, 0x44), + (0xE8, 0x09, 0xE8, 0xD8, 0xA0, 0x0B, 0xEA, 0xD8, 0xA0, 0x0D, 0xEC, 0xD8, 0xA0, 0x0F, 0xEE, 0xD8, 0xA0,), + (0xEB, 0x02, 0x00, 0xE4, 0xE4, 0x88, 0x00, 0x40), (0xEC, 0x3C, 0x00), + (0xED, 0xAB, 0x89, 0x76, 0x54, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x20, 0x45, 0x67, 0x98, 0xBA,), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), # Page 3 + (0xE5, 0xE4), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0 + (0xCD, 0x08), + ) +) + +st7701s.extend( + "MAKERFABS-4", + width=480, + height=480, + color_order="RGB", + invert_colors=True, + pixel_mode="18bit", + cs_pin=1, + de_pin={ + "number": 45, + "ignore_strapping_warning": True + }, + hsync_pin=5, + vsync_pin=4, + pclk_pin=21, + data_pins={ + "red": [39, 40, 41, 42, 2], + "green": [0, 9, 14, 47, 48, 3], + "blue": [6, 7, 15, 16, 8] + } +) + +st7701s.extend( + "SEEED-INDICATOR-D1", + width=480, + height=480, + mirror_x=True, + mirror_y=True, + invert_colors=True, + pixel_mode="18bit", + spi_mode="MODE3", + data_rate="2MHz", + hsync_front_porch=10, + hsync_pulse_width=8, + hsync_back_porch=50, + vsync_front_porch=10, + vsync_pulse_width=8, + vsync_back_porch=20, + cs_pin={"pca9554": None, "number": 4}, + de_pin=18, + hsync_pin=16, + vsync_pin=17, + pclk_pin=21, + pclk_inverted=False, + data_pins={ + "red": [4, 3, 2, 1, 0], + "green": [10, 9, 8, 7, 6, 5], + "blue": [15, 14, 13, 12, 11] + }, +) + +st7701s.extend( + "UEDX48480021-MD80ET", + width=480, + height=480, + pixel_mode="18bit", + cs_pin=18, + reset_pin=8, + de_pin=17, + vsync_pin={"number": 3, "ignore_strapping_warning": True}, + hsync_pin={"number": 46, "ignore_strapping_warning": True}, + pclk_pin=9, + data_pins={ + "red": [40, 41, 42, 2, 1], + "green": [21, 47, 48, 45, 38, 39], + "blue": [10, 11, {"number": 12, "allow_other_uses": True}, {"number": 13, "allow_other_uses": True}, 14] + }, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x3B, 0x00), (0xC1, 0x0B, 0x02), (0xC2, 0x07, 0x02), (0xC7, 0x00), (0xCC, 0x10), (0xCD, 0x08), + (0xB0, 0x00, 0x11, 0x16, 0x0E, 0x11, 0x06, 0x05, 0x09, 0x08, 0x21, 0x06, 0x13, 0x10, 0x29, 0x31, 0x18), + (0xB1, 0x00, 0x11, 0x16, 0x0E, 0x11, 0x07, 0x05, 0x09, 0x09, 0x21, 0x05, 0x13, 0x11, 0x2A, 0x31, 0x18), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x6D), (0xB1, 0x37), (0xB2, 0x8B), (0xB3, 0x80), (0xB5, 0x43), (0xB7, 0x85), + (0xB8, 0x20), (0xC0, 0x09), (0xC1, 0x78), (0xC2, 0x78), (0xD0, 0x88), + (0xE0, 0x00, 0x00, 0x02), + (0xE1, 0x03, 0xA0, 0x00, 0x00, 0x04, 0xA0, 0x00, 0x00, 0x00, 0x20, 0x20), + (0xE2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x00), + (0xE4, 0x22, 0x00), + (0xE5, 0x05, 0xEC, 0xF6, 0xCA, 0x07, 0xEE, 0xF6, 0xCA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE6, 0x00, 0x00, 0x11, 0x00), + (0xE7, 0x22, 0x00), + (0xE8, 0x06, 0xED, 0xF6, 0xCA, 0x08, 0xEF, 0xF6, 0xCA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE9, 0x36, 0x00), + (0xEB, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00), + (0xED, 0xFF, 0xFF, 0xFF, 0xBA, 0x0A, 0xFF, 0x45, 0xFF, 0xFF, 0x54, 0xFF, 0xA0, 0xAB, 0xFF, 0xFF, 0xFF), + (0xEF, 0x08, 0x08, 0x08, 0x45, 0x3F, 0x54), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xE8, 0x00, 0x0E), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00), + (0x11, 0x00), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xE8, 0x00, 0x0C), + (0xE8, 0x00, 0x00), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00) + ) +) + +st7701s.extend( + "ZX2D10GE01R-V4848", + width=480, + height=480, + pixel_mode="18bit", + cs_pin=21, + de_pin=39, + vsync_pin=48, + hsync_pin=40, + pclk_pin={"number": 45, "ignore_strapping_warning": True}, + pclk_frequency="15MHz", + pclk_inverted=True, + hsync_pulse_width=10, + hsync_back_porch=10, + hsync_front_porch=10, + vsync_pulse_width=2, + vsync_back_porch=12, + vsync_front_porch=14, + data_pins={ + "red": [10, 16, 9, 15, 46], + "green": [8, 13, 18, 12, 11, 17], + "blue": [{"number": 47, "allow_other_uses": True}, {"number": 41, "allow_other_uses": True}, 0, 42, 14] + }, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), (0xC0, 0x3B, 0x00), + (0xC1, 0x0B, 0x02), (0xC2, 0x07, 0x02), (0xCC, 0x10), (0xCD, 0x08), + (0xB0, 0x00, 0x11, 0x16, 0x0e, 0x11, 0x06, 0x05, 0x09, 0x08, 0x21, 0x06, 0x13, 0x10, 0x29, 0x31, 0x18), + (0xB1, 0x00, 0x11, 0x16, 0x0e, 0x11, 0x07, 0x05, 0x09, 0x09, 0x21, 0x05, 0x13, 0x11, 0x2a, 0x31, 0x18), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), (0xB0, 0x6d), (0xB1, 0x37), (0xB2, 0x81), (0xB3, 0x80), (0xB5, 0x43), + (0xB7, 0x85), (0xB8, 0x20), (0xC1, 0x78), (0xC2, 0x78), (0xD0, 0x88), (0xE0, 0x00, 0x00, 0x02), + (0xE1, 0x03, 0xA0, 0x00, 0x00, 0x04, 0xA0, 0x00, 0x00, 0x00, 0x20, 0x20), + (0xE2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x00), (0xE4, 0x22, 0x00), + (0xE5, 0x05, 0xEC, 0xA0, 0xA0, 0x07, 0xEE, 0xA0, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE6, 0x00, 0x00, 0x11, 0x00), (0xE7, 0x22, 0x00), + (0xE8, 0x06, 0xED, 0xA0, 0xA0, 0x08, 0xEF, 0xA0, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xEB, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00), + (0xED, 0xFF, 0xFF, 0xFF, 0xBA, 0x0A, 0xBF, 0x45, 0xFF, 0xFF, 0x54, 0xFB, 0xA0, 0xAB, 0xFF, 0xFF, 0xFF), + (0xEF, 0x10, 0x0D, 0x04, 0x08, 0x3F, 0x1F), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00) + ) +) diff --git a/esphome/components/mipi_rgb/models/waveshare.py b/esphome/components/mipi_rgb/models/waveshare.py new file mode 100644 index 0000000000..49a75da232 --- /dev/null +++ b/esphome/components/mipi_rgb/models/waveshare.py @@ -0,0 +1,64 @@ +from esphome.components.mipi import DriverChip +from esphome.config_validation import UNDEFINED + +from .st7701s import st7701s + +wave_4_3 = DriverChip( + "ESP32-S3-TOUCH-LCD-4.3", + swap_xy=UNDEFINED, + initsequence=(), + width=800, + height=480, + pclk_frequency="16MHz", + reset_pin={"ch422g": None, "number": 3}, + enable_pin={"ch422g": None, "number": 2}, + de_pin=5, + hsync_pin={"number": 46, "ignore_strapping_warning": True}, + vsync_pin={"number": 3, "ignore_strapping_warning": True}, + pclk_pin=7, + pclk_inverted=True, + hsync_front_porch=210, + hsync_pulse_width=30, + hsync_back_porch=30, + vsync_front_porch=4, + vsync_pulse_width=4, + vsync_back_porch=4, + data_pins={ + "red": [1, 2, 42, 41, 40], + "green": [39, 0, 45, 48, 47, 21], + "blue": [14, 38, 18, 17, 10], + }, +) +wave_4_3.extend( + "ESP32-S3-TOUCH-LCD-7-800X480", + enable_pin=[{"ch422g": None, "number": 2}, {"ch422g": None, "number": 6}], + hsync_back_porch=8, + hsync_front_porch=8, + hsync_pulse_width=4, + vsync_back_porch=16, + vsync_front_porch=16, + vsync_pulse_width=4, +) + +st7701s.extend( + "WAVESHARE-4-480x480", + data_rate="2MHz", + spi_mode="MODE3", + color_order="BGR", + pixel_mode="18bit", + width=480, + height=480, + invert_colors=True, + cs_pin=42, + de_pin=40, + hsync_pin=38, + vsync_pin=39, + pclk_pin=41, + pclk_frequency="12MHz", + pclk_inverted=False, + data_pins={ + "red": [46, 3, 8, 18, 17], + "green": [14, 13, 12, 11, 10, 9], + "blue": [5, 45, 48, 47, 21], + }, +) diff --git a/tests/components/mipi_rgb/test.esp32-s3-idf.yaml b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..8d0e20d6f5 --- /dev/null +++ b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml @@ -0,0 +1,67 @@ +psram: + mode: octal + +spi: + - clk_pin: + number: 47 + allow_other_uses: true + mosi_pin: + number: 41 + allow_other_uses: true + +display: + - platform: mipi_rgb + model: ZX2D10GE01R-V4848 + update_interval: 1s + color_order: BGR + draw_rounding: 2 + pixel_mode: 18bit + invert_colors: false + use_axis_flips: true + pclk_frequency: 15000000.0 + pclk_inverted: true + byte_order: big_endian + hsync_pulse_width: 10 + hsync_back_porch: 10 + hsync_front_porch: 10 + vsync_pulse_width: 2 + vsync_back_porch: 12 + vsync_front_porch: 14 + data_pins: + red: + - number: 10 + - number: 16 + - number: 9 + - number: 15 + - number: 46 + ignore_strapping_warning: true + green: + - number: 8 + - number: 13 + - number: 18 + - number: 12 + - number: 11 + - number: 17 + blue: + - number: 47 + allow_other_uses: true + - number: 41 + allow_other_uses: true + - number: 0 + ignore_strapping_warning: true + - number: 42 + - number: 14 + de_pin: + number: 39 + pclk_pin: + number: 45 + ignore_strapping_warning: true + hsync_pin: + number: 40 + vsync_pin: + number: 48 + data_rate: 1000000.0 + spi_mode: MODE0 + cs_pin: + number: 21 + show_test_card: true