Compare commits

...
Author SHA1 Message Date
J. Nick Koston 081d8d492a Note that the custom init sequence must end with a 0 command 2026-10-02 21:37:05 -05:00
J. Nick Koston e18c1bb39b [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:25:12 -05:00
J. Nick Koston 70ee367c28 [ili9xxx] Keep the custom init sequence in a shared PROGMEM table
The table carries the 0 terminator init_lcd_() stops at; the vector it
replaces had none, so the loop read past its end.
2026-10-02 21:00:59 -05:00
J. Nick Koston 5c9c233568 [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:23 -05:00
J. Nick Koston c58a26dce2 [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:23 -05:00
8 changed files with 131 additions and 5 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,
+4 -1
View File
@@ -234,7 +234,10 @@ async def to_code(config: ConfigType) -> None:
sequence = []
for seq in init_sequences:
sequence.extend(seq)
cg.add(var.add_init_sequence(sequence))
# init_lcd_() stops at a 0 command
sequence.append(0)
table = cg.shared_progmem_array("ili9xxx_init_sequence", cg.uint8, sequence)
cg.add(var.add_init_sequence(table))
if pixel_mode := config.get(CONF_PIXEL_MODE):
cg.add(var.set_pixel_mode(pixel_mode))
@@ -32,7 +32,7 @@ void ILI9XXXDisplay::set_madctl() {
void ILI9XXXDisplay::setup() {
this->setup_pins_();
this->init_lcd_(this->init_sequence_);
this->init_lcd_(this->extra_init_sequence_.data());
this->init_lcd_(this->extra_init_sequence_);
switch (this->pixel_mode_) {
case PIXEL_MODE_16:
if (this->is_18bitdisplay_) {
+4 -2
View File
@@ -58,7 +58,8 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
}
}
void add_init_sequence(const std::vector<uint8_t> &sequence) { this->extra_init_sequence_ = sequence; }
// `sequence` is a PROGMEM table that must end with a 0 command.
void add_init_sequence(const uint8_t *sequence) { this->extra_init_sequence_ = sequence; }
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
float get_setup_priority() const override;
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
@@ -114,7 +115,8 @@ class ILI9XXXDisplay : public display::DisplayBuffer,
void reset_();
uint8_t const *init_sequence_{};
std::vector<uint8_t> extra_init_sequence_;
// Shared PROGMEM table ending in a 0 command, like the built-in sequences
const uint8_t *extra_init_sequence_{nullptr};
int16_t width_{0}; ///< Display width as modified by current rotation
int16_t height_{0}; ///< Display height as modified by current rotation
int16_t offset_x_{0};
+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,31 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: arduino
spi:
clk_pin: GPIO18
mosi_pin: GPIO23
display:
- platform: ili9xxx
id: first
model: ST7789V
cs_pin: GPIO5
dc_pin: GPIO17
invert_colors: false
init_sequence:
- [0x36, 0x48]
- [0x3A, 0x55]
- platform: ili9xxx
id: second
model: ST7789V
cs_pin: GPIO4
dc_pin: GPIO16
invert_colors: false
init_sequence:
- [0x36, 0x48]
- [0x3A, 0x55]
@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import re
def test_ili9xxx_placement_new_uses_model_subclass(
@@ -29,3 +30,19 @@ def test_ili9xxx_placement_new_uses_model_subclass(
assert "new(tft_display) ili9xxx::ILI9XXXST7789V()" in main_cpp
# Base-class default constructor must NOT be used.
assert "new(tft_display) ili9xxx::ILI9XXXDisplay()" not in main_cpp
def test_extra_init_sequence_is_terminated_progmem_table(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""A custom init sequence is a 0 terminated PROGMEM table shared by equal displays."""
main_cpp = generate_main(component_config_path("init_sequence.yaml"))
tables = re.findall(r"(\w+)->add_init_sequence\((\w+)\);", main_cpp)
assert {t[0] for t in tables} == {"first", "second"}
assert len({t[1] for t in tables}) == 1
assert (
f"static constexpr uint8_t {tables[0][1]}[] PROGMEM = {{54, 1, 72, 58, 1, 85, 0}};"
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"