mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	[mipi_rgb] Unified driver for MIPI RGB displays (#9892)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
							
								
								
									
										2
									
								
								esphome/components/mipi_rgb/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								esphome/components/mipi_rgb/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| CODEOWNERS = ["@clydebarrow"] | ||||
| DOMAIN = "mipi_rgb" | ||||
							
								
								
									
										321
									
								
								esphome/components/mipi_rgb/display.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										321
									
								
								esphome/components/mipi_rgb/display.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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_)) | ||||
							
								
								
									
										388
									
								
								esphome/components/mipi_rgb/mipi_rgb.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										388
									
								
								esphome/components/mipi_rgb/mipi_rgb.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<const uint8_t *>(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<const uint8_t *>(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<uint16_t> 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<uint8_t>(color.r & 0xF8) | (color.g >> 5); | ||||
|   uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3); | ||||
|   uint16_t new_color = hi_byte | (lo_byte << 8);  // big endian | ||||
|   if (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<uint16_t *>(this->buffer_); | ||||
|   uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5); | ||||
|   uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3); | ||||
|   uint16_t new_color = 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 | ||||
							
								
								
									
										127
									
								
								esphome/components/mipi_rgb/mipi_rgb.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								esphome/components/mipi_rgb/mipi_rgb.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<GPIOPin *> 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<GPIOPin *> 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<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, | ||||
|                                          spi::DATA_RATE_1MHZ> { | ||||
|  public: | ||||
|   MipiRgbSpi(int width, int height) : MipiRgb(width, height) {} | ||||
|  | ||||
|   void set_init_sequence(const std::vector<uint8_t> &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<uint8_t> init_sequence_; | ||||
| }; | ||||
| #endif | ||||
|  | ||||
| }  // namespace mipi_rgb | ||||
| }  // namespace esphome | ||||
| #endif | ||||
							
								
								
									
										24
									
								
								esphome/components/mipi_rgb/models/guition.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								esphome/components/mipi_rgb/models/guition.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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),), | ||||
| ) | ||||
| @@ -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), | ||||
|     ) | ||||
| ) | ||||
|   | ||||
							
								
								
									
										9
									
								
								esphome/components/mipi_rgb/models/rpi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								esphome/components/mipi_rgb/models/rpi.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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=(), | ||||
| ) | ||||
							
								
								
									
										214
									
								
								esphome/components/mipi_rgb/models/st7701s.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								esphome/components/mipi_rgb/models/st7701s.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
|     ) | ||||
| ) | ||||
							
								
								
									
										64
									
								
								esphome/components/mipi_rgb/models/waveshare.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								esphome/components/mipi_rgb/models/waveshare.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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], | ||||
|     }, | ||||
| ) | ||||
							
								
								
									
										67
									
								
								tests/components/mipi_rgb/test.esp32-s3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								tests/components/mipi_rgb/test.esp32-s3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
		Reference in New Issue
	
	Block a user