[mipi_spi] Keep the init sequence in flash (#20089)

This commit is contained in:
J. Nick Koston
2026-10-04 21:21:28 -05:00
committed by GitHub
parent f91ac5ed1d
commit 88378ab354
3 changed files with 68 additions and 19 deletions
+2 -1
View File
@@ -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]
+22 -13
View File
@@ -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<uint8_t> &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<uint8_t> brightness_{};
const char *model_{"Unknown"};
std::vector<uint8_t> init_sequence_{};
// Shared PROGMEM table
const uint8_t *init_sequence_{nullptr};
size_t init_sequence_len_{0};
};
/**
+44 -5
View File
@@ -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