[psram] Consolidate task stack in PSRAM handling (#16628)

This commit is contained in:
Kevin Ahrendt
2026-05-25 10:15:51 -04:00
committed by GitHub
parent cf1fabe6d4
commit 7c494fd3ef
9 changed files with 54 additions and 65 deletions

View File

@@ -1,7 +1,5 @@
from typing import Any
import esphome.codegen as cg
from esphome.components import audio, esp32, media_source, psram
from esphome.components import audio, media_source, psram
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM
from esphome.types import ConfigType
@@ -21,19 +19,13 @@ def _request_micro_decoder(config: ConfigType) -> ConfigType:
return config
def _validate_task_stack_in_psram(value: Any) -> bool:
if value := cv.boolean(value):
return cv.requires_component(psram.DOMAIN)(value)
return value
CONFIG_SCHEMA = cv.All(
media_source.media_source_schema(
AudioFileMediaSource,
)
.extend(
{
cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram,
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
}
)
.extend(cv.COMPONENT_SCHEMA),
@@ -49,6 +41,4 @@ async def to_code(config: ConfigType) -> None:
if config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(True))
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
psram.request_external_task_stack()

View File

@@ -1,7 +1,5 @@
from typing import Any
import esphome.codegen as cg
from esphome.components import audio, esp32, media_source, psram
from esphome.components import audio, media_source, psram
import esphome.config_validation as cv
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TASK_STACK_IN_PSRAM
from esphome.types import ConfigType
@@ -20,14 +18,6 @@ def _request_micro_decoder(config: ConfigType) -> ConfigType:
return config
def _validate_task_stack_in_psram(value: Any) -> bool:
# Only require the psram component when actually enabling PSRAM stacks; validating
# the boolean first means `false` doesn't trigger the requires_component check.
if value := cv.boolean(value):
return cv.requires_component(psram.DOMAIN)(value)
return value
CONFIG_SCHEMA = cv.All(
media_source.media_source_schema(
AudioHTTPMediaSource,
@@ -37,7 +27,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_BUFFER_SIZE, default=50000): cv.int_range(
min=5000, max=1000000
),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram,
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
}
)
.extend(cv.COMPONENT_SCHEMA),
@@ -53,7 +43,5 @@ async def to_code(config: ConfigType) -> None:
if config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(True))
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
psram.request_external_task_stack()
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))

View File

@@ -1,6 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import audio, esp32, speaker
from esphome.components import audio, psram, speaker
import esphome.config_validation as cv
from esphome.const import (
CONF_BITS_PER_SAMPLE,
@@ -93,7 +93,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_BITS_PER_SAMPLE): cv.one_of(8, 16, 24, 32, int=True),
cv.Optional(CONF_NUM_CHANNELS): cv.int_range(min=1, max=2),
cv.Optional(CONF_QUEUE_MODE, default=False): cv.boolean,
cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean,
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
}
),
cv.only_on([PLATFORM_ESP32]),
@@ -123,12 +123,9 @@ async def to_code(config):
cg.add(var.set_output_speaker(spkr))
cg.add(var.set_queue_mode(config[CONF_QUEUE_MODE]))
if task_stack_in_psram := config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(task_stack_in_psram))
if task_stack_in_psram and config[CONF_TASK_STACK_IN_PSRAM]:
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
if config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(True))
psram.request_external_task_stack()
# Initialize FixedVector with exact count of source speakers
cg.add(var.init_source_speakers(len(config[CONF_SOURCE_SPEAKERS])))

View File

@@ -1,5 +1,6 @@
import logging
import textwrap
from typing import Any
import esphome.codegen as cg
from esphome.components.const import CONF_IGNORE_NOT_FOUND
@@ -94,6 +95,27 @@ def is_guaranteed() -> bool:
return CORE.data.get(KEY_PSRAM_GUARANTEED, False)
def request_external_task_stack() -> None:
"""Allow FreeRTOS task stacks to be allocated in external RAM (PSRAM).
Components that expose a ``task_stack_in_psram`` option should call this from their
``to_code`` when the option is enabled. The sdkconfig option only permits external
stacks; it does not move any stack into PSRAM on its own, so it stays opt-in per task.
"""
add_idf_sdkconfig_option("CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True)
def validate_task_stack_in_psram(value: Any) -> bool:
"""Validate a ``task_stack_in_psram`` boolean, requiring the psram component only when enabled.
Validating the boolean first means an explicit ``false`` does not pull in the psram
requirement, so the option can still be set to false on devices without PSRAM.
"""
if value := cv.boolean(value):
return cv.requires_component(DOMAIN)(value)
return value
def validate_psram_mode(config):
esp32_config = fv.full_config.get()[PLATFORM_ESP32]
if config[CONF_SPEED] == "120MHZ":

View File

@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import audio, esp32, speaker
from esphome.components import audio, psram, speaker
import esphome.config_validation as cv
from esphome.const import (
CONF_BITS_PER_SAMPLE,
@@ -63,7 +63,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(
CONF_BUFFER_DURATION, default="100ms"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean,
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
cv.Optional(CONF_FILTERS, default=16): cv.int_range(min=2, max=1024),
cv.Optional(CONF_TAPS, default=16): _validate_taps,
}
@@ -88,9 +88,7 @@ async def to_code(config):
if config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(True))
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
psram.request_external_task_stack()
cg.add(var.set_target_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
cg.add(var.set_target_sample_rate(config[CONF_SAMPLE_RATE]))

View File

@@ -121,13 +121,6 @@ def register_player_config(config: ConfigType) -> None:
data.player_config = config
def _validate_task_stack_in_psram(value):
value = cv.boolean(value)
if value:
return cv.requires_component(psram.DOMAIN)(value)
return value
def _request_high_performance_networking(config: ConfigType) -> ConfigType:
"""Request high performance networking for Sendspin streaming.
@@ -152,7 +145,7 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SendspinHub),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram,
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
}
),
cv.only_on_esp32,
@@ -201,9 +194,7 @@ async def to_code(config: ConfigType) -> None:
if config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(True))
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
psram.request_external_task_stack()
# sendspin-cpp library
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.6.1")
@@ -261,9 +252,7 @@ async def to_code(config: ConfigType) -> None:
psram_stack = player_cfg.get(CONF_TASK_STACK_IN_PSRAM, False)
if psram_stack:
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
psram.request_external_task_stack()
# Library defaults: priority 18 (one above httpd_priority 17 so the decoder is not
# starved by the HTTP server during the initial encoded-audio burst at stream start),

View File

@@ -1,6 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import media_source
from esphome.components import media_source, psram
import esphome.config_validation as cv
from esphome.const import (
CONF_BUFFER_SIZE,
@@ -19,7 +19,6 @@ from .. import (
CONF_SENDSPIN_ID,
MEMORY_LOCATIONS,
SendspinHub,
_validate_task_stack_in_psram,
register_player_config,
request_controller_support,
sendspin_ns,
@@ -71,7 +70,7 @@ CONFIG_SCHEMA = cv.All(
).extend(
{
cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram,
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range(min=25000),
cv.Optional(CONF_INITIAL_STATIC_DELAY, default="0ms"): cv.All(
cv.positive_time_period_milliseconds,

View File

@@ -7,7 +7,6 @@ import esphome.codegen as cg
from esphome.components import (
audio,
audio_file,
esp32,
media_player,
network,
ota,
@@ -155,9 +154,7 @@ CONFIG_SCHEMA = cv.All(
# Remove before 2026.10.0
cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.Any(cv.boolean, cv.string),
cv.Optional(CONF_FILES): audio_file.audio_files_schema(),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All(
cv.boolean, cv.requires_component(psram.DOMAIN)
),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage,
cv.Optional(CONF_VOLUME_INITIAL, default=0.5): cv.percentage,
cv.Optional(CONF_VOLUME_MAX, default=1.0): cv.percentage,
@@ -198,9 +195,7 @@ async def to_code(config):
if config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(True))
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
psram.request_external_task_stack()
cg.add(var.set_volume_increment(config[CONF_VOLUME_INCREMENT]))
cg.add(var.set_volume_initial(config[CONF_VOLUME_INITIAL]))

View File

@@ -0,0 +1,11 @@
audio_file:
- id: test_audio
file:
type: local
path: $component_dir/test.wav
media_source:
- platform: audio_file
id: audio_file_source
# task_stack_in_psram: false must validate without a psram: component
task_stack_in_psram: false