From 88378ab354301687c0aa5a93a0ed72e442bacec5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Oct 2026 21:21:28 -0500 Subject: [PATCH] [mipi_spi] Keep the init sequence in flash (#20089) --- esphome/components/mipi_spi/display.py | 3 +- esphome/components/mipi_spi/mipi_spi.h | 35 +++++++++------ tests/component_tests/mipi_spi/test_init.py | 49 ++++++++++++++++++--- 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index e8b54da5c7..b49675ca4c 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -401,7 +401,8 @@ async def to_code(config: ConfigType) -> None: init_sequence = model.get_sequence(config, add_madctl=False, add_reset=True) var_id.type, templateargs = get_instance(config) var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs)) - cg.add(var.set_init_sequence(init_sequence)) + table = cg.shared_progmem_array("mipi_spi_init_sequence", cg.uint8, init_sequence) + cg.add(var.set_init_sequence(table, len(init_sequence))) cg.add(var.set_model(config[CONF_MODEL])) if enable_pin := config.get(CONF_ENABLE_PIN): enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index e97b0e3a31..e428a1f96e 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -5,6 +5,7 @@ #include "esphome/components/spi/spi.h" #include "esphome/components/display/display.h" #include "esphome/components/display/display_color_utils.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" namespace esphome::mipi_spi { @@ -133,7 +134,10 @@ class MipiSpi : public display::Display, return HEIGHT; } - void set_init_sequence(const std::vector &sequence) { this->init_sequence_ = sequence; } + void set_init_sequence(const uint8_t *sequence, size_t len) { + this->init_sequence_ = sequence; + this->init_sequence_len_ = len; + } // reset the display, and write the init sequence void setup() override { @@ -159,15 +163,16 @@ class MipiSpi : public display::Display, // need to know when the display is ready for SLPOUT command - will be 120ms after reset auto when = millis() + 120; size_t index = 0; - auto &vec = this->init_sequence_; - while (index != vec.size()) { - if (vec.size() - index < 2) { + const uint8_t *seq = this->init_sequence_; + const size_t len = this->init_sequence_len_; + while (index != len) { + if (len - index < 2) { esph_log_e(TAG, "Malformed init sequence"); this->mark_failed(); return; } - uint8_t cmd = vec[index++]; - uint8_t x = vec[index++]; + uint8_t cmd = progmem_read_byte(seq + index++); + uint8_t x = progmem_read_byte(seq + index++); if (x == DELAY_FLAG) { if (cmd == 0) { cmd = clamp_at_least((int) (when - millis()), 0); @@ -176,19 +181,21 @@ class MipiSpi : public display::Display, delay(cmd); } else { uint8_t num_args = x & 0x7F; - if (vec.size() - index < num_args) { + if (len - index < num_args) { esph_log_e(TAG, "Malformed init sequence"); this->mark_failed(); return; } - const auto *ptr = vec.data() + index; - this->write_command_(cmd, ptr, num_args); + // The sequence is in flash, which SPI DMA (and ESP8266 byte loads) cannot read + uint8_t args[0x80]; + progmem_memcpy(args, seq + index, num_args); + this->write_command_(cmd, args, num_args); index += num_args; } } this->reset_params_(); - // init sequence no longer needed - this->init_sequence_.clear(); + // Marks init as done, so later commands log at verbose level instead of debug + this->init_sequence_len_ = 0; } // Drawing operations @@ -236,7 +243,7 @@ class MipiSpi : public display::Display, void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) { char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)]; // Don't spam the log after setup - if (this->init_sequence_.empty()) { + if (this->init_sequence_len_ == 0) { esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len)); } else { esph_log_d(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len)); @@ -486,7 +493,9 @@ class MipiSpi : public display::Display, bool invert_colors_{}; optional brightness_{}; const char *model_{"Unknown"}; - std::vector init_sequence_{}; + // Shared PROGMEM table + const uint8_t *init_sequence_{nullptr}; + size_t init_sequence_len_{0}; }; /** diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py index f29883684c..cacdc0c106 100644 --- a/tests/component_tests/mipi_spi/test_init.py +++ b/tests/component_tests/mipi_spi/test_init.py @@ -2,6 +2,7 @@ from collections.abc import Callable from pathlib import Path +import re from typing import Any import pytest @@ -33,6 +34,18 @@ from esphome.types import ConfigType from tests.component_tests.types import SetCoreConfigCallable +def _init_sequence(main_cpp: str, display_id: str = r"\w+") -> str: + """Return the shared PROGMEM init sequence table passed to a display, as rendered.""" + match = re.search(rf"{display_id}->set_init_sequence\((\w+), \d+\);", main_cpp) + assert match is not None + table = re.search( + rf"static constexpr uint8_t {match.group(1)}\[\] PROGMEM = (\{{[^;]*\}});", + main_cpp, + ) + assert table is not None + return table.group(1) + + def run_schema_validation(config: ConfigType) -> None: """Run schema validation on a configuration.""" FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config)) @@ -362,7 +375,7 @@ def test_native_generation( in main_cpp ) # A 10ms post-reset delay ({10, 255}) is prepended ahead of the model commands. - assert "set_init_sequence({10, 255, 240, 1, 8, 242" in main_cpp + assert _init_sequence(main_cpp).startswith("{10, 255, 240, 1, 8, 242") assert "show_test_card();" in main_cpp assert "set_write_only(true);" in main_cpp @@ -379,7 +392,7 @@ def test_lvgl_generation( in main_cpp ) # A 10ms post-reset delay ({10, 255}) is prepended ahead of the model commands. - assert "set_init_sequence({10, 255, 177, 3, 1, 44, 45, 178" in main_cpp + assert _init_sequence(main_cpp).startswith("{10, 255, 177, 3, 1, 44, 45, 178") assert "show_test_card();" not in main_cpp assert "set_auto_clear(false);" in main_cpp @@ -428,7 +441,9 @@ def test_swreset_prepended_without_reset_pin( # SWRESET ({1, 0}) followed by a 10ms delay ({10, 255}) is inserted ahead of # the model's own commands. - assert "swreset_display->set_init_sequence({1, 0, 10, 255, 160, 1, 1," in main_cpp + assert _init_sequence(main_cpp, "swreset_display").startswith( + "{1, 0, 10, 255, 160, 1, 1," + ) def test_swreset_not_prepended_with_reset_pin( @@ -449,5 +464,29 @@ def test_swreset_not_prepended_with_reset_pin( main_cpp = generate_main(yaml_file) # The delay ({10, 255}) is still present, but no leading SWRESET ({1, 0}). - assert "hwreset_display->set_init_sequence({10, 255, 160, 1, 1," in main_cpp - assert "hwreset_display->set_init_sequence({1, 0," not in main_cpp + assert _init_sequence(main_cpp, "hwreset_display").startswith( + "{10, 255, 160, 1, 1," + ) + + +def test_identical_init_sequences_share_one_table( + generate_main: Callable[[str | Path], str], + tmp_path: Path, +) -> None: + """Two displays with the same init sequence point at one PROGMEM table.""" + yaml_file = tmp_path / "shared.yaml" + display = _SWRESET_YAML.split("display:\n", 1)[1] + yaml_file.write_text( + _SWRESET_YAML.format(display_id="first", reset_line=" reset_pin: 5") + + display.format(display_id="second", reset_line=" reset_pin: 6") + .replace("cs_pin: 8", "cs_pin: 9") + .replace("dc_pin: 4", "dc_pin: 7") + ) + + main_cpp = generate_main(yaml_file) + + first = re.search(r"first->set_init_sequence\((\w+), \d+\);", main_cpp) + second = re.search(r"second->set_init_sequence\((\w+), \d+\);", main_cpp) + assert first is not None and second is not None + assert first.group(1) == second.group(1) + assert main_cpp.count("mipi_spi_init_sequence[] PROGMEM") == 1