Compare commits

...
Author SHA1 Message Date
pre-commit-ci-lite[bot] f91b023422 apply automatic formatting fixes 2026-10-03 03:49:03 +00:00
J. Nick Koston ed639a9378 [core] Let shared_progmem_array emit an unshared array
Identical lambdas can carry separate static state, so callers need a way
to keep such tables apart while still getting a collision free name.
2026-10-02 21:24:50 -05:00
J. Nick Koston bac7799d9b [qspi_dbi] Keep the init sequence in a shared PROGMEM table 2026-10-02 20:58:51 -05:00
J. Nick Koston 40a4f6640a [core] Emit shared PROGMEM arrays at namespace scope
A setup() local table could only be reused from the scope of its first
use. The arrays are now globals. A lambda in a table only references
variables that codegen has already declared, so writing the global
straight away keeps the order valid.
2026-10-02 20:45:22 -05:00
J. Nick Koston 77ddc89b79 [core] Add cg.shared_progmem_array for deduplicated PROGMEM tables
Emits one PROGMEM array per distinct type and contents and returns the
same variable to later callers. The array name is made unique against
every config id and registered variable, so a generated table can never
collide with a user id.
2026-10-02 20:45:21 -05:00
8 changed files with 182 additions and 38 deletions
+1
View File
@@ -40,6 +40,7 @@ from esphome.cpp_generator import ( # noqa: F401
progmem_array,
safe_exp,
set_cpp_standard,
shared_progmem_array,
statement,
static_const_array,
static_function,
+6 -7
View File
@@ -173,13 +173,12 @@ async def to_code(config: ConfigType) -> None:
await spi.register_spi_device(var, config, write_only=True)
chip = DriverChip.chips[config[CONF_MODEL]]
if chip.initsequence:
cg.add(var.add_init_sequence(chip.initsequence))
if init_sequences := config.get(CONF_INIT_SEQUENCE):
sequence = []
for seq in init_sequences:
sequence.extend(seq)
cg.add(var.add_init_sequence(sequence))
sequence = list(chip.initsequence)
for seq in config.get(CONF_INIT_SEQUENCE, []):
sequence.extend(seq)
if sequence:
table = cg.shared_progmem_array("qspi_dbi_init_sequence", cg.uint8, sequence)
cg.add(var.set_init_sequence(table, len(sequence)))
cg.add(var.set_color_mode(config[CONF_COLOR_ORDER]))
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
+25 -27
View File
@@ -110,8 +110,31 @@ void QspiDbi::reset_params_(bool ready) {
}
void QspiDbi::write_init_sequence_() {
for (const auto &seq : this->init_sequences_) {
this->write_sequence_(seq);
const uint8_t *seq = this->init_sequence_;
const size_t len = this->init_sequence_len_;
size_t index = 0;
while (index != len) {
if (len - index < 2) {
ESP_LOGE(TAG, "Malformed init sequence");
break;
}
uint8_t cmd = seq[index++];
uint8_t x = seq[index++];
if (x == DELAY_FLAG) {
ESP_LOGV(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (len - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence");
break;
}
// The sequence is in flash, which SPI DMA cannot read
uint8_t args[0x80];
memcpy(args, seq + index, num_args);
this->write_command_(cmd, args, num_args);
index += num_args;
}
}
this->reset_params_(true);
this->setup_complete_ = true;
@@ -187,31 +210,6 @@ void QspiDbi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
this->disable();
}
void QspiDbi::write_sequence_(const std::vector<uint8_t> &vec) {
size_t index = 0;
while (index != vec.size()) {
if (vec.size() - index < 2) {
ESP_LOGE(TAG, "Malformed init sequence");
return;
}
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
ESP_LOGV(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence");
return;
}
const auto *ptr = vec.data() + index;
this->write_command_(cmd, ptr, num_args);
index += num_args;
}
}
}
void QspiDbi::dump_config() {
ESP_LOGCONFIG("", "QSPI_DBI Display");
ESP_LOGCONFIG("", "Model: %s", this->model_);
+7 -3
View File
@@ -101,7 +101,10 @@ class QspiDbi final : public display::DisplayBuffer,
int get_width_internal() override { return this->width_; }
int get_height_internal() override { return this->height_; }
bool can_proceed() override { return this->setup_complete_; }
void add_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequences_.push_back(sequence); }
void set_init_sequence(const uint8_t *sequence, size_t len) {
this->init_sequence_ = sequence;
this->init_sequence_len_ = len;
}
void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; }
protected:
@@ -109,7 +112,6 @@ class QspiDbi final : public display::DisplayBuffer,
if (this->buffer_ == nullptr)
this->init_internal_(this->width_ * this->height_ * 2);
}
void write_sequence_(const std::vector<uint8_t> &vec);
void draw_absolute_pixel_internal(int x, int y, Color color) override;
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;
@@ -162,7 +164,9 @@ class QspiDbi final : public display::DisplayBuffer,
unsigned draw_rounding_{2};
uint8_t brightness_{0xD0};
const char *model_{"Unknown"};
std::vector<std::vector<uint8_t>> init_sequences_{};
// Shared PROGMEM table: the model's sequence followed by any custom one
const uint8_t *init_sequence_{nullptr};
size_t init_sequence_len_{0};
esp_lcd_panel_handle_t handle_{};
};
+36 -1
View File
@@ -19,7 +19,12 @@ from esphome.core import (
TimePeriodNanoseconds,
TimePeriodSeconds,
)
from esphome.helpers import cpp_string_escape, indent, indent_all_but_first_and_last
from esphome.helpers import (
cpp_string_escape,
ensure_unique_string,
indent,
indent_all_but_first_and_last,
)
from esphome.types import Expression, SafeExpType, TemplateArgsType
from esphome.util import OrderedDict
from esphome.yaml_util import ESPHomeDataBase
@@ -463,6 +468,36 @@ def progmem_array(id_, rhs) -> "MockObj":
return obj
def shared_progmem_array(
name: str, type_: "MockObjClass", rhs: SafeExpType, *, share: bool = True
) -> "MockObj":
"""Emit a global PROGMEM array once per distinct type and contents; later calls reuse it.
The array is ``static constexpr``, so elements must be constant expressions and lambdas
must be captureless. Its name is made unique against every config id and variable.
``share=False`` always emits a new array, e.g. for lambdas that may keep static state.
"""
from esphome.config import iter_ids
from esphome.config_validation import RESERVED_IDS
arrays: dict[str, MockObj] = CORE.data.setdefault("shared_progmem_array", {})
rhs = safe_exp(rhs)
key = f"{type_} {rhs}"
if share and (array := arrays.get(key)) is not None:
return array
used = {str(i) for i, _ in iter_ids(CORE.config)}
used |= {str(i) for i in CORE.variables}
used |= set(RESERVED_IDS) | CORE.loaded_integrations
id_ = ID(ensure_unique_string(name, used), is_declaration=True, type=type_)
# Global, so any scope can use it; anything a lambda references is already declared.
CORE.add_global(ProgmemAssignmentExpression(type_, id_, rhs))
array = MockObj(id_, ".")
CORE.register_variable(id_, array)
if share:
arrays[key] = array
return array
def static_const_array(id_, rhs) -> "MockObj":
rhs = safe_exp(rhs)
obj = MockObj(id_, ".")
@@ -0,0 +1,70 @@
"""Tests for the qspi_dbi init sequence codegen."""
from collections.abc import Callable
from pathlib import Path
import re
_YAML = """
esphome:
name: qspi-dbi-test
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
psram:
mode: octal
spi:
type: quad
clk_pin: 47
data_pins: [18, 7, 48, 5]
display:
- platform: qspi_dbi
id: builtin
model: RM690B0
dimensions:
width: 450
height: 600
cs_pin: 11
- platform: qspi_dbi
id: builtin_too
model: RM690B0
dimensions:
width: 450
height: 600
cs_pin: 12
- platform: qspi_dbi
id: custom
model: CUSTOM
dimensions:
width: 536
height: 240
cs_pin: 6
init_sequence:
- [0x3A, 0x66]
- delay 120ms
"""
def test_init_sequence_is_shared_progmem_table(
generate_main: Callable[[str | Path], str],
tmp_path: Path,
) -> None:
"""Model and custom commands form one PROGMEM table; identical displays share it."""
yaml_file = tmp_path / "qspi_dbi.yaml"
yaml_file.write_text(_YAML)
main_cpp = generate_main(yaml_file)
calls = {
m[0]: (m[1], int(m[2]))
for m in re.findall(r"(\w+)->set_init_sequence\((\w+), (\d+)\);", main_cpp)
}
assert calls["builtin"] == calls["builtin_too"]
assert calls["custom"][0] != calls["builtin"][0]
custom = re.search(
rf"static constexpr uint8_t {calls['custom'][0]}\[\] PROGMEM = \{{([^;]*)\}};",
main_cpp,
)
assert custom is not None
assert custom.group(1).replace(" ", "") == "58,1,102,120,255"
assert "add_init_sequence" not in main_cpp
+37
View File
@@ -4,6 +4,7 @@ import math
import pytest
from esphome import cpp_generator as cg, cpp_types as ct
from esphome.core import CORE, ID
class TestExpressions:
@@ -808,3 +809,39 @@ async def test_templatable__lambda_with_std_string() -> None:
result = await cg.templatable(lambda_obj, [], ct.std_string)
assert isinstance(result, cg.LambdaExpression)
class TestSharedProgmemArray:
def test_identical_contents_share_one_array(self) -> None:
CORE.config = {}
a = cg.shared_progmem_array("table", ct.uint8, [1, 2, 3])
b = cg.shared_progmem_array("table", ct.uint8, [1, 2, 3])
c = cg.shared_progmem_array("table", ct.uint8, [4])
assert a is b
assert str(a) != str(c)
assert sum("PROGMEM" in str(st) for st in CORE.global_statements) == 2
assert not any("PROGMEM" in str(st) for st in CORE.main_statements)
def test_share_false_always_emits_a_new_array(self) -> None:
CORE.config = {}
a = cg.shared_progmem_array("table", ct.uint8, [1], share=False)
b = cg.shared_progmem_array("table", ct.uint8, [1], share=False)
assert str(a) != str(b)
assert sum("PROGMEM" in str(st) for st in CORE.global_statements) == 2
def test_same_contents_different_type_are_separate(self) -> None:
CORE.config = {}
a = cg.shared_progmem_array("table", ct.uint8, [1])
b = cg.shared_progmem_array("table", ct.uint16, [1])
assert str(a) != str(b)
def test_name_avoids_config_ids(self) -> None:
CORE.config = {"sensor": [{"id": ID("table", is_declaration=True)}]}
array = cg.shared_progmem_array("table", ct.uint8, [1])
assert str(array) == "table_2"
def test_name_avoids_registered_variables(self) -> None:
CORE.config = {}
CORE.register_variable(ID("table", is_declaration=True), cg.MockObj("table"))
array = cg.shared_progmem_array("table", ct.uint8, [1])
assert str(array) == "table_2"