Compare commits

...
Author SHA1 Message Date
J. Nick Koston 43c18c9e1c Skip the table for an empty init sequence; test it 2026-10-03 05:52:13 -05:00
J. Nick Koston 96df32024d [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:29 -05:00
J. Nick Koston 83b38d9cdd [st7701s] Keep the init sequence in a shared PROGMEM table 2026-10-02 20:55:04 -05:00
J. Nick Koston ebef8390aa [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:20 -05:00
J. Nick Koston ad1e5ef9d0 [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:20 -05:00
8 changed files with 154 additions and 6 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,
+3 -1
View File
@@ -181,7 +181,9 @@ async def to_code(config: ConfigType) -> None:
sequence = []
for seq in config[CONF_INIT_SEQUENCE]:
sequence.extend(seq)
cg.add(var.set_init_sequence(sequence))
if sequence:
table = cg.shared_progmem_array("st7701s_init_sequence", cg.uint8, sequence)
cg.add(var.set_init_sequence(table, len(sequence)))
cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]]))
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH]))
+2 -2
View File
@@ -144,14 +144,14 @@ void ST7701S::write_sequence_(uint8_t cmd, size_t len, const uint8_t *bytes) {
}
void ST7701S::write_init_sequence_() {
for (size_t i = 0; i != this->init_sequence_.size();) {
for (size_t i = 0; i != this->init_sequence_len_;) {
uint8_t cmd = this->init_sequence_[i++];
size_t len = this->init_sequence_[i++];
if (len == ST7701S_DELAY_FLAG) {
ESP_LOGV(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
this->write_sequence_(cmd, len, &this->init_sequence_[i]);
this->write_sequence_(cmd, len, this->init_sequence_ + i);
i += len;
ESP_LOGV(TAG, "Command %X, %d bytes", cmd, len);
if (cmd == SW_RESET_CMD)
+7 -2
View File
@@ -62,7 +62,10 @@ class ST7701S final : public display::Display,
void set_vsync_pulse_width(uint16_t vsync_pulse_width) { this->vsync_pulse_width_ = vsync_pulse_width; }
void set_vsync_back_porch(uint16_t vsync_back_porch) { this->vsync_back_porch_ = vsync_back_porch; }
void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; }
void set_init_sequence(const std::vector<uint8_t> &init_sequence) { this->init_sequence_ = init_sequence; }
void set_init_sequence(const uint8_t *init_sequence, size_t len) {
this->init_sequence_ = init_sequence;
this->init_sequence_len_ = len;
}
void set_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; }
void set_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; }
void set_offsets(int16_t offset_x, int16_t offset_y) {
@@ -95,7 +98,9 @@ class ST7701S final : public display::Display,
uint16_t vsync_pulse_width_ = 10;
uint16_t vsync_back_porch_ = 10;
uint16_t vsync_front_porch_ = 10;
std::vector<uint8_t> init_sequence_;
// Shared PROGMEM table
const uint8_t *init_sequence_{nullptr};
size_t init_sequence_len_{0};
uint32_t pclk_frequency_ = 16 * 1000 * 1000;
bool pclk_inverted_{true};
+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,68 @@
"""Tests for the st7701s init sequence codegen."""
from collections.abc import Callable
from pathlib import Path
import re
_YAML = """
esphome:
name: st7701s-test
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
psram:
mode: octal
spi:
clk_pin: 10
mosi_pin: 11
display:
- platform: st7701s
id: panel
dimensions:
width: 480
height: 480
cs_pin: 44
de_pin: 18
hsync_pin: 16
vsync_pin: 17
pclk_pin: 21
init_sequence: {init_sequence}
data_pins: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 14, 15, 38, 39]
"""
def test_init_sequence_is_progmem_table(
generate_main: Callable[[str | Path], str],
tmp_path: Path,
) -> None:
"""The init sequence is a PROGMEM table instead of a vector copied at boot."""
yaml_file = tmp_path / "st7701s.yaml"
yaml_file.write_text(
_YAML.format(init_sequence="[1, [0x23, 0xA, 0xB], delay 20ms]")
)
main_cpp = generate_main(yaml_file)
call = re.search(r"panel->set_init_sequence\((\w+), (\d+)\);", main_cpp)
assert call is not None
table = re.search(
rf"static constexpr uint8_t {call.group(1)}\[\] PROGMEM = \{{([^;]*)\}};",
main_cpp,
)
assert table is not None
assert len(table.group(1).split(",")) == int(call.group(2))
def test_empty_init_sequence_emits_no_table(
generate_main: Callable[[str | Path], str],
tmp_path: Path,
) -> None:
"""An empty init sequence leaves the display without a table instead of a zero length array."""
yaml_file = tmp_path / "st7701s.yaml"
yaml_file.write_text(_YAML.format(init_sequence="[]"))
main_cpp = generate_main(yaml_file)
assert "st7701s_init_sequence" not in main_cpp
assert "set_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"