[esp32] Add config option to execute from PSRAM (#9907)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Clyde Stubbs 2025-08-01 16:07:11 +10:00 committed by GitHub
parent f761404bf6
commit 940a8b43fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 144 additions and 33 deletions

View File

@ -1,6 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import sensor from esphome.components import sensor
from esphome.components.esp32 import CONF_CPU_FREQUENCY from esphome.components.esp32 import CONF_CPU_FREQUENCY
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BLOCK, CONF_BLOCK,
@ -54,7 +55,7 @@ CONFIG_SCHEMA = {
), ),
cv.Optional(CONF_PSRAM): cv.All( cv.Optional(CONF_PSRAM): cv.All(
cv.only_on_esp32, cv.only_on_esp32,
cv.requires_component("psram"), cv.requires_component(PSRAM_DOMAIN),
sensor.sensor_schema( sensor.sensor_schema(
unit_of_measurement=UNIT_BYTES, unit_of_measurement=UNIT_BYTES,
icon=ICON_COUNTER, icon=ICON_COUNTER,

View File

@ -76,6 +76,7 @@ CONF_ASSERTION_LEVEL = "assertion_level"
CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_COMPILER_OPTIMIZATION = "compiler_optimization"
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_RELEASE = "release" CONF_RELEASE = "release"
ASSERTION_LEVELS = { ASSERTION_LEVELS = {
@ -519,32 +520,59 @@ def _detect_variant(value):
def final_validate(config): def final_validate(config):
if not ( # Imported locally to avoid circular import issues
pio_options := fv.full_config.get()[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS) from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
):
# Not specified or empty
return config
errs = []
full_config = fv.full_config.get()
if pio_options := full_config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS):
pio_flash_size_key = "board_upload.flash_size" pio_flash_size_key = "board_upload.flash_size"
pio_partitions_key = "board_build.partitions" pio_partitions_key = "board_build.partitions"
if CONF_PARTITIONS in config and pio_partitions_key in pio_options: if CONF_PARTITIONS in config and pio_partitions_key in pio_options:
raise cv.Invalid( errs.append(
cv.Invalid(
f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32" f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
) )
)
if pio_flash_size_key in pio_options: if pio_flash_size_key in pio_options:
raise cv.Invalid( errs.append(
cv.Invalid(
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
) )
)
if ( if (
config[CONF_VARIANT] != VARIANT_ESP32 config[CONF_VARIANT] != VARIANT_ESP32
and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK]) and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK])
and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED] and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED]
): ):
raise cv.Invalid( errs.append(
f"{CONF_IGNORE_EFUSE_MAC_CRC} is not supported on {config[CONF_VARIANT]}" cv.Invalid(
f"'{CONF_IGNORE_EFUSE_MAC_CRC}' is not supported on {config[CONF_VARIANT]}",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC],
) )
)
if (
config.get(CONF_FRAMEWORK, {})
.get(CONF_ADVANCED, {})
.get(CONF_EXECUTE_FROM_PSRAM)
):
if config[CONF_VARIANT] != VARIANT_ESP32S3:
errs.append(
cv.Invalid(
f"'{CONF_EXECUTE_FROM_PSRAM}' is only supported on {VARIANT_ESP32S3} variant",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM],
)
)
if PSRAM_DOMAIN not in full_config:
errs.append(
cv.Invalid(
f"'{CONF_EXECUTE_FROM_PSRAM}' requires PSRAM to be configured",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM],
)
)
if errs:
raise cv.MultipleInvalid(errs)
return config return config
@ -627,6 +655,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
cv.Optional( cv.Optional(
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
): cv.boolean, ): cv.boolean,
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
} }
), ),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
@ -792,6 +821,9 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
# Apply LWIP core locking for better socket performance # Apply LWIP core locking for better socket performance
# This is already enabled by default in Arduino framework, where it provides # This is already enabled by default in Arduino framework, where it provides

View File

@ -4,6 +4,7 @@ from esphome.automation import build_automation, register_action, validate_autom
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING
from esphome.components.display import Display from esphome.components.display import Display
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_AUTO_CLEAR_ENABLED, CONF_AUTO_CLEAR_ENABLED,
@ -219,7 +220,7 @@ def final_validation(configs):
draw_rounding, config[CONF_DRAW_ROUNDING] draw_rounding, config[CONF_DRAW_ROUNDING]
) )
buffer_frac = config[CONF_BUFFER_SIZE] buffer_frac = config[CONF_BUFFER_SIZE]
if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config: if CORE.is_esp32 and buffer_frac > 0.5 and PSRAM_DOMAIN not in global_config:
LOGGER.warning("buffer_size: may need to be reduced without PSRAM") LOGGER.warning("buffer_size: may need to be reduced without PSRAM")
for image_id in lv_images_used: for image_id in lv_images_used:
path = global_config.get_path_for_id(image_id)[:-1] path = global_config.get_path_for_id(image_id)[:-1]

View File

@ -25,6 +25,7 @@ from esphome.components.mipi import (
power_of_two, power_of_two,
requires_buffer, requires_buffer,
) )
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.config_validation import ALLOW_EXTRA from esphome.config_validation import ALLOW_EXTRA
@ -292,7 +293,7 @@ def _final_validate(config):
# If no drawing methods are configured, and LVGL is not enabled, show a test card # If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True config[CONF_SHOW_TEST_CARD] = True
if "psram" not in global_config and CONF_BUFFER_SIZE not in config: if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config:
if not requires_buffer(config): if not requires_buffer(config):
return config # No buffer needed, so no need to set a buffer size return config # No buffer needed, so no need to set a buffer size
# If PSRAM is not enabled, choose a small buffer size by default # If PSRAM is not enabled, choose a small buffer size by default

View File

@ -28,12 +28,13 @@ from esphome.core import CORE
import esphome.final_validate as fv import esphome.final_validate as fv
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
DOMAIN = "psram"
DEPENDENCIES = [PLATFORM_ESP32] DEPENDENCIES = [PLATFORM_ESP32]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
psram_ns = cg.esphome_ns.namespace("psram") psram_ns = cg.esphome_ns.namespace(DOMAIN)
PsramComponent = psram_ns.class_("PsramComponent", cg.Component) PsramComponent = psram_ns.class_("PsramComponent", cg.Component)
TYPE_QUAD = "quad" TYPE_QUAD = "quad"

View File

@ -329,6 +329,28 @@ class ConfigValidationStep(abc.ABC):
def run(self, result: Config) -> None: ... # noqa: E704 def run(self, result: Config) -> None: ... # noqa: E704
class LoadTargetPlatformValidationStep(ConfigValidationStep):
"""Load target platform step."""
def __init__(self, domain: str, conf: ConfigType):
self.domain = domain
self.conf = conf
def run(self, result: Config) -> None:
if self.conf is None:
result[self.domain] = self.conf = {}
result.add_output_path([self.domain], self.domain)
component = get_component(self.domain)
result[self.domain] = self.conf
path = [self.domain]
CORE.loaded_integrations.add(self.domain)
result.add_validation_step(
SchemaValidationStep(self.domain, path, self.conf, component)
)
class LoadValidationStep(ConfigValidationStep): class LoadValidationStep(ConfigValidationStep):
"""Load step, this step is called once for each domain config fragment. """Load step, this step is called once for each domain config fragment.
@ -582,16 +604,18 @@ class MetadataValidationStep(ConfigValidationStep):
) )
return return
for i, part_conf in enumerate(self.conf): for i, part_conf in enumerate(self.conf):
path = self.path + [i]
result.add_validation_step( result.add_validation_step(
SchemaValidationStep( SchemaValidationStep(self.domain, path, part_conf, self.comp)
self.domain, self.path + [i], part_conf, self.comp
)
) )
result.add_validation_step(FinalValidateValidationStep(path, self.comp))
return return
result.add_validation_step( result.add_validation_step(
SchemaValidationStep(self.domain, self.path, self.conf, self.comp) SchemaValidationStep(self.domain, self.path, self.conf, self.comp)
) )
result.add_validation_step(FinalValidateValidationStep(self.path, self.comp))
class SchemaValidationStep(ConfigValidationStep): class SchemaValidationStep(ConfigValidationStep):
@ -628,7 +652,6 @@ class SchemaValidationStep(ConfigValidationStep):
result.set_by_path(self.path, validated) result.set_by_path(self.path, validated)
path_context.reset(token) path_context.reset(token)
result.add_validation_step(FinalValidateValidationStep(self.path, self.comp))
class IDPassValidationStep(ConfigValidationStep): class IDPassValidationStep(ConfigValidationStep):
@ -909,7 +932,7 @@ def validate_config(
# First run platform validation steps # First run platform validation steps
result.add_validation_step( result.add_validation_step(
LoadValidationStep(target_platform, config[target_platform]) LoadTargetPlatformValidationStep(target_platform, config[target_platform])
) )
result.run_validation_steps() result.run_validation_steps()

View File

@ -65,6 +65,7 @@ def set_core_config() -> Generator[SetCoreConfigCallable]:
*, *,
core_data: ConfigType | None = None, core_data: ConfigType | None = None,
platform_data: ConfigType | None = None, platform_data: ConfigType | None = None,
full_config: dict[str, ConfigType] | None = None,
) -> None: ) -> None:
platform, framework = platform_framework.value platform, framework = platform_framework.value
@ -83,7 +84,7 @@ def set_core_config() -> Generator[SetCoreConfigCallable]:
CORE.data[platform.value] = platform_data CORE.data[platform.value] = platform_data
config.path_context.set([]) config.path_context.set([])
final_validate.full_config.set(Config()) final_validate.full_config.set(full_config or Config())
yield setter yield setter

View File

@ -8,10 +8,13 @@ import pytest
from esphome.components.esp32 import VARIANTS from esphome.components.esp32 import VARIANTS
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import PlatformFramework from esphome.const import CONF_ESPHOME, PlatformFramework
from tests.component_tests.types import SetCoreConfigCallable
def test_esp32_config(set_core_config) -> None: def test_esp32_config(
set_core_config: SetCoreConfigCallable,
) -> None:
set_core_config(PlatformFramework.ESP32_IDF) set_core_config(PlatformFramework.ESP32_IDF)
from esphome.components.esp32 import CONFIG_SCHEMA from esphome.components.esp32 import CONFIG_SCHEMA
@ -60,14 +63,49 @@ def test_esp32_config(set_core_config) -> None:
r"Option 'variant' does not match selected board. @ data\['variant'\]", r"Option 'variant' does not match selected board. @ data\['variant'\]",
id="mismatched_board_variant_config", id="mismatched_board_variant_config",
), ),
pytest.param(
{
"variant": "esp32s2",
"framework": {
"type": "esp-idf",
"advanced": {"execute_from_psram": True},
},
},
r"'execute_from_psram' is only supported on ESP32S3 variant @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]",
id="execute_from_psram_invalid_for_variant_config",
),
pytest.param(
{
"variant": "esp32s3",
"framework": {
"type": "esp-idf",
"advanced": {"execute_from_psram": True},
},
},
r"'execute_from_psram' requires PSRAM to be configured @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]",
id="execute_from_psram_requires_psram_config",
),
pytest.param(
{
"variant": "esp32s3",
"framework": {
"type": "esp-idf",
"advanced": {"ignore_efuse_mac_crc": True},
},
},
r"'ignore_efuse_mac_crc' is not supported on ESP32S3 @ data\['framework'\]\['advanced'\]\['ignore_efuse_mac_crc'\]",
id="ignore_efuse_mac_crc_only_on_esp32",
),
], ],
) )
def test_esp32_configuration_errors( def test_esp32_configuration_errors(
config: Any, config: Any,
error_match: str, error_match: str,
set_core_config: SetCoreConfigCallable,
) -> None: ) -> None:
set_core_config(PlatformFramework.ESP32_IDF, full_config={CONF_ESPHOME: {}})
"""Test detection of invalid configuration.""" """Test detection of invalid configuration."""
from esphome.components.esp32 import CONFIG_SCHEMA from esphome.components.esp32 import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
with pytest.raises(cv.Invalid, match=error_match): with pytest.raises(cv.Invalid, match=error_match):
CONFIG_SCHEMA(config) FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))

View File

@ -18,4 +18,5 @@ class SetCoreConfigCallable(Protocol):
*, *,
core_data: ConfigType | None = None, core_data: ConfigType | None = None,
platform_data: ConfigType | None = None, platform_data: ConfigType | None = None,
full_config: dict[str, ConfigType] | None = None,
) -> None: ... ) -> None: ...

View File

@ -0,0 +1,12 @@
esp32:
variant: esp32s3
framework:
type: esp-idf
advanced:
execute_from_psram: true
psram:
mode: octal
speed: 80MHz
logger: