# Various constants used in MIPI DBI communication # Various configuration constants for MIPI displays # Various utility functions for MIPI DBI configuration from typing import Any from esphome.components.const import CONF_COLOR_DEPTH from esphome.components.display import CONF_SHOW_TEST_CARD, display_ns import esphome.config_validation as cv from esphome.const import ( CONF_BRIGHTNESS, CONF_COLOR_ORDER, CONF_DIMENSIONS, CONF_HEIGHT, CONF_INIT_SEQUENCE, CONF_INVERT_COLORS, CONF_LAMBDA, CONF_MIRROR_X, CONF_MIRROR_Y, CONF_OFFSET_HEIGHT, CONF_OFFSET_WIDTH, CONF_PAGES, CONF_ROTATION, CONF_SWAP_XY, CONF_TRANSFORM, CONF_WIDTH, ) from esphome.core import TimePeriod LOGGER = cv.logging.getLogger(__name__) ColorOrder = display_ns.enum("ColorMode") NOP = 0x00 SWRESET = 0x01 RDDID = 0x04 RDDST = 0x09 RDMODE = 0x0A RDMADCTL = 0x0B RDPIXFMT = 0x0C RDIMGFMT = 0x0D RDSELFDIAG = 0x0F SLEEP_IN = 0x10 SLPIN = 0x10 SLEEP_OUT = 0x11 SLPOUT = 0x11 PTLON = 0x12 NORON = 0x13 INVERT_OFF = 0x20 INVOFF = 0x20 INVERT_ON = 0x21 INVON = 0x21 ALL_ON = 0x23 WRAM = 0x24 GAMMASET = 0x26 MIPI = 0x26 DISPOFF = 0x28 DISPON = 0x29 CASET = 0x2A PASET = 0x2B RASET = 0x2B RAMWR = 0x2C WDATA = 0x2C RAMRD = 0x2E PTLAR = 0x30 VSCRDEF = 0x33 TEON = 0x35 MADCTL = 0x36 MADCTL_CMD = 0x36 VSCRSADD = 0x37 IDMOFF = 0x38 IDMON = 0x39 COLMOD = 0x3A PIXFMT = 0x3A GETSCANLINE = 0x45 BRIGHTNESS = 0x51 WRDISBV = 0x51 RDDISBV = 0x52 WRCTRLD = 0x53 SWIRE1 = 0x5A SWIRE2 = 0x5B IFMODE = 0xB0 FRMCTR1 = 0xB1 FRMCTR2 = 0xB2 FRMCTR3 = 0xB3 INVCTR = 0xB4 DFUNCTR = 0xB6 ETMOD = 0xB7 PWCTR1 = 0xC0 PWCTR2 = 0xC1 PWCTR3 = 0xC2 PWCTR4 = 0xC3 PWCTR5 = 0xC4 VMCTR1 = 0xC5 IFCTR = 0xC6 VMCTR2 = 0xC7 GMCTR = 0xC8 SETEXTC = 0xC8 PWSET = 0xD0 VMCTR = 0xD1 PWSETN = 0xD2 RDID4 = 0xD3 RDINDEX = 0xD9 RDID1 = 0xDA RDID2 = 0xDB RDID3 = 0xDC RDIDX = 0xDD GMCTRP1 = 0xE0 GMCTRN1 = 0xE1 CSCON = 0xF0 PWCTR6 = 0xF6 ADJCTL3 = 0xF7 PAGESEL = 0xFE MADCTL_MY = 0x80 # Bit 7 Bottom to top MADCTL_MX = 0x40 # Bit 6 Right to left MADCTL_MV = 0x20 # Bit 5 Reverse Mode MADCTL_ML = 0x10 # Bit 4 LCD refresh Bottom to top MADCTL_RGB = 0x00 # Bit 3 Red-Green-Blue pixel order MADCTL_BGR = 0x08 # Bit 3 Blue-Green-Red pixel order MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left # These bits are used instead of the above bits on some chips, where using MX and MY results in incorrect # partial updates. MADCTL_XFLIP = 0x02 # Mirror the display horizontally MADCTL_YFLIP = 0x01 # Mirror the display vertically # Special constant for delays in command sequences DELAY_FLAG = 0xFFF # Special flag to indicate a delay CONF_PIXEL_MODE = "pixel_mode" CONF_USE_AXIS_FLIPS = "use_axis_flips" PIXEL_MODE_24BIT = "24bit" PIXEL_MODE_18BIT = "18bit" PIXEL_MODE_16BIT = "16bit" PIXEL_MODES = { PIXEL_MODE_16BIT: 0x55, PIXEL_MODE_18BIT: 0x66, PIXEL_MODE_24BIT: 0x77, } MODE_RGB = "RGB" MODE_BGR = "BGR" COLOR_ORDERS = { MODE_RGB: ColorOrder.COLOR_ORDER_RGB, MODE_BGR: ColorOrder.COLOR_ORDER_BGR, } CONF_HSYNC_BACK_PORCH = "hsync_back_porch" CONF_HSYNC_FRONT_PORCH = "hsync_front_porch" CONF_HSYNC_PULSE_WIDTH = "hsync_pulse_width" CONF_VSYNC_BACK_PORCH = "vsync_back_porch" CONF_VSYNC_FRONT_PORCH = "vsync_front_porch" CONF_VSYNC_PULSE_WIDTH = "vsync_pulse_width" CONF_PCLK_FREQUENCY = "pclk_frequency" CONF_PCLK_INVERTED = "pclk_inverted" CONF_NATIVE_HEIGHT = "native_height" CONF_NATIVE_WIDTH = "native_width" CONF_DE_PIN = "de_pin" CONF_PCLK_PIN = "pclk_pin" def power_of_two(value): value = cv.int_range(1, 128)(value) if value & (value - 1) != 0: raise cv.Invalid("value must be a power of two") return value def validate_dimension(rounding): def validator(value): value = cv.positive_int(value) if value % rounding != 0: raise cv.Invalid(f"Dimensions and offsets must be divisible by {rounding}") return value return validator def dimension_schema(rounding): return cv.Any( cv.dimensions, cv.Schema( { cv.Required(CONF_WIDTH): validate_dimension(rounding), cv.Required(CONF_HEIGHT): validate_dimension(rounding), cv.Optional(CONF_OFFSET_HEIGHT, default=0): validate_dimension( rounding ), cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension(rounding), } ), ) def map_sequence(value): """ Maps one entry in a sequence to a command and data bytes. The format is a repeated sequence of [CMD, ] where is s a sequence of bytes. The length is inferred from the length of the sequence and should not be explicit. A single integer can be provided where there are no data bytes, in which case it is treated as a command. A delay can be inserted by specifying "- delay N" where N is in ms """ if isinstance(value, str) and value.lower().startswith("delay "): value = value.lower()[6:] delay_value = cv.All( cv.positive_time_period_milliseconds, cv.Range(TimePeriod(milliseconds=1), TimePeriod(milliseconds=255)), )(value) return DELAY_FLAG, delay_value.total_milliseconds value = cv.All(cv.ensure_list(cv.int_range(0, 255)), cv.Length(1, 254))(value) return tuple(value) def delay(ms): return DELAY_FLAG, ms class DriverChip: models = {} def __init__( self, name: str, initsequence=None, **defaults, ): name = name.upper() self.name = name self.initsequence = initsequence self.defaults = defaults DriverChip.models[name] = self @classmethod def get_models(cls): """ Return the current set of models and reset the models dictionary. """ models = cls.models cls.models = {} return models def extend(self, name, **kwargs) -> "DriverChip": defaults = self.defaults.copy() if ( CONF_WIDTH in defaults and CONF_OFFSET_WIDTH in kwargs and CONF_NATIVE_WIDTH not in defaults ): defaults[CONF_NATIVE_WIDTH] = defaults[CONF_WIDTH] if ( CONF_HEIGHT in defaults and CONF_OFFSET_HEIGHT in kwargs and CONF_NATIVE_HEIGHT not in defaults ): defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT] defaults.update(kwargs) return DriverChip(name, initsequence=self.initsequence, **defaults) def get_default(self, key, fallback: Any = False) -> Any: return self.defaults.get(key, fallback) 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. """ rotation = config.get(CONF_ROTATION, 0) return rotation and ( self.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 ) def get_dimensions(self, config) -> tuple[int, int, int, int]: if CONF_DIMENSIONS in config: # Explicit dimensions, just use as is dimensions = config[CONF_DIMENSIONS] if isinstance(dimensions, dict): width = dimensions[CONF_WIDTH] height = dimensions[CONF_HEIGHT] offset_width = dimensions[CONF_OFFSET_WIDTH] offset_height = dimensions[CONF_OFFSET_HEIGHT] return width, height, offset_width, offset_height (width, height) = dimensions return width, height, 0, 0 # Default dimensions, use model defaults transform = self.get_transform(config) width = self.get_default(CONF_WIDTH) height = self.get_default(CONF_HEIGHT) offset_width = self.get_default(CONF_OFFSET_WIDTH, 0) offset_height = self.get_default(CONF_OFFSET_HEIGHT, 0) # if mirroring axes and there are offsets, also mirror the offsets to cater for situations where # the offset is asymmetric if transform[CONF_MIRROR_X]: native_width = self.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2) offset_width = native_width - width - offset_width if transform[CONF_MIRROR_Y]: native_height = self.get_default( CONF_NATIVE_HEIGHT, height + offset_height * 2 ) offset_height = native_height - height - offset_height # Swap default dimensions if swap_xy is set if transform[CONF_SWAP_XY] is True: width, height = height, width offset_height, offset_width = offset_width, offset_height return width, height, offset_width, offset_height def get_transform(self, config) -> dict[str, bool]: can_transform = self.rotation_as_transform(config) 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), }, ) # 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] elif rotation == 90: transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] else: transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] transform[CONF_TRANSFORM] = True return transform def get_sequence(self, config) -> tuple[tuple[int, ...], int]: """ Create the init sequence for the display. Use the default sequence from the model, if any, and append any custom sequence provided in the config. Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence Pixel format, color order, and orientation will be set. Returns a tuple of the init sequence and the computed MADCTL value. """ sequence = list(self.initsequence) custom_sequence = config.get(CONF_INIT_SEQUENCE, []) sequence.extend(custom_sequence) # Ensure each command is a tuple sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] # Set pixel format if not already in the custom sequence pixel_mode = config[CONF_PIXEL_MODE] if not isinstance(pixel_mode, int): pixel_mode = PIXEL_MODES[pixel_mode] sequence.append((PIXFMT, pixel_mode)) # Does the chip use the flipping bits for mirroring rather than the reverse order bits? 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)) if config[CONF_INVERT_COLORS]: sequence.append((INVON,)) else: sequence.append((INVOFF,)) if brightness := config.get(CONF_BRIGHTNESS, self.get_default(CONF_BRIGHTNESS)): sequence.append((BRIGHTNESS, brightness)) sequence.append((SLPOUT,)) sequence.append((DISPON,)) # Flatten the sequence into a list of bytes, with the length of each command # or the delay flag inserted where needed return sum( tuple( (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] for x in sequence ), (), ), madctl def requires_buffer(config) -> bool: """ Check if the display configuration requires a buffer. It will do so if any drawing methods are configured. :param config: :return: True if a buffer is required, False otherwise """ return any( config.get(key) for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD) ) def get_color_depth(config) -> int: """ Get the color depth in bits from the configuration. """ return int(config[CONF_COLOR_DEPTH].removesuffix("bit"))