[esp32] Honor sdkconfig CPU frequency options (#20158)

Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Rupayon Haldar
2026-10-10 09:13:05 -10:00
committed by GitHub
co-authored by J. Nick Koston
parent 865863abf5
commit ff96530e4d
2 changed files with 326 additions and 3 deletions
+87 -3
View File
@@ -551,9 +551,88 @@ assert all(variant in CPU_FREQUENCIES for variant in VARIANTS)
FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values()))
_SDKCONFIG_CPU_FREQUENCY_OPTION = "CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ"
_SDKCONFIG_CPU_FREQUENCY_PREFIX = f"{_SDKCONFIG_CPU_FREQUENCY_OPTION}_"
# Older IDF versions named the CPU frequency options after the chip
_LEGACY_SDKCONFIG_CPU_FREQUENCY_VARIANTS = frozenset(
{VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3}
)
def _get_sdkconfig_cpu_frequencies(
sdkconfig_options: dict[str, str], variant: str
) -> tuple[set[int], set[int]]:
"""Return the CPU frequencies in MHz that sdkconfig_options selects and disables."""
# (integer option, choice option prefix) pairs that can carry a frequency
keys = [(_SDKCONFIG_CPU_FREQUENCY_OPTION, _SDKCONFIG_CPU_FREQUENCY_PREFIX)]
if variant in _LEGACY_SDKCONFIG_CPU_FREQUENCY_VARIANTS:
keys.append(
(
f"CONFIG_{variant}_DEFAULT_CPU_FREQ_MHZ",
f"CONFIG_{variant}_DEFAULT_CPU_FREQ_",
)
)
selected: set[int] = set()
disabled: set[int] = set()
for integer_key, choice_prefix in keys:
if (value := sdkconfig_options.get(integer_key)) is not None:
try:
selected.add(int(value))
except ValueError as err:
raise cv.Invalid(
f"{integer_key} must be an integer MHz value",
path=[CONF_FRAMEWORK, CONF_SDKCONFIG_OPTIONS],
) from err
for name, value in sdkconfig_options.items():
mhz = name.removeprefix(choice_prefix)
if mhz == name or not mhz.isdigit():
continue
if value.lower() == "y":
selected.add(int(mhz))
elif value.lower() == "n":
disabled.add(int(mhz))
else:
raise cv.Invalid(
f"{name} must be y or n",
path=[CONF_FRAMEWORK, CONF_SDKCONFIG_OPTIONS],
)
return selected, disabled
def set_core_data(config):
cpu_frequency = config.get(CONF_CPU_FREQUENCY, None)
cpu_frequency = config.get(CONF_CPU_FREQUENCY)
variant = config[CONF_VARIANT]
sdkconfig_path = [CONF_FRAMEWORK, CONF_SDKCONFIG_OPTIONS]
selected, disabled = _get_sdkconfig_cpu_frequencies(
config[CONF_FRAMEWORK][CONF_SDKCONFIG_OPTIONS], variant
)
if len(selected) > 1:
conflicting = ", ".join(f"{mhz}MHz" for mhz in sorted(selected))
raise cv.Invalid(
f"sdkconfig_options contains conflicting CPU frequencies ({conflicting})",
path=sdkconfig_path,
)
sdkconfig_cpu_frequency = f"{selected.pop()}MHZ" if selected else None
if cpu_frequency is None and sdkconfig_cpu_frequency is not None:
if sdkconfig_cpu_frequency not in CPU_FREQUENCIES[variant]:
raise cv.Invalid(
f"sdkconfig_options selects {sdkconfig_cpu_frequency}, which {variant} does not support",
path=sdkconfig_path,
)
_LOGGER.warning(
"sdkconfig_options contains a CPU frequency setting; using %s. "
"Set 'esp32.cpu_frequency' to configure it directly.",
sdkconfig_cpu_frequency,
)
cpu_frequency = sdkconfig_cpu_frequency
elif (
sdkconfig_cpu_frequency is not None and sdkconfig_cpu_frequency != cpu_frequency
):
raise cv.Invalid(
f"esp32.cpu_frequency ({cpu_frequency}) conflicts with sdkconfig_options "
f"({sdkconfig_cpu_frequency})",
path=sdkconfig_path,
)
# if not specified in config, default to the maximum supported frequency
# (ESP32-P4 engineering samples are limited to 360MHz, non-engineering can do 400MHz)
if cpu_frequency is None:
@@ -562,12 +641,17 @@ def set_core_data(config):
cpu_frequency = "360MHZ"
else:
cpu_frequency = choices[-1]
config[CONF_CPU_FREQUENCY] = cpu_frequency
elif cpu_frequency not in CPU_FREQUENCIES[variant]:
raise cv.Invalid(
f"Invalid CPU frequency '{cpu_frequency}' for {config[CONF_VARIANT]}",
path=[CONF_CPU_FREQUENCY],
)
if int(cpu_frequency[:-3]) in disabled:
raise cv.Invalid(
f"sdkconfig_options disables the selected CPU frequency ({cpu_frequency})",
path=sdkconfig_path,
)
config[CONF_CPU_FREQUENCY] = cpu_frequency
if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ":
_LOGGER.warning(
@@ -3138,7 +3222,7 @@ async def to_code(config):
# Set default CPU frequency
add_idf_sdkconfig_option(
f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{config[CONF_CPU_FREQUENCY][:-3]}", True
f"{_SDKCONFIG_CPU_FREQUENCY_PREFIX}{config[CONF_CPU_FREQUENCY][:-3]}", True
)
# Apply LWIP optimization settings
+239
View File
@@ -111,6 +111,245 @@ def test_esp32_config(
assert VARIANT_FRIENDLY[variant].lower() in config["board"]
@pytest.mark.parametrize(
(
"sdkconfig_options",
"cpu_frequency",
"expected_frequency",
"warns",
"variant",
"framework_type",
),
[
pytest.param(
{
"CONFIG_ESP32_DEFAULT_CPU_FREQ_160": "y",
"CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ": "160",
},
None,
"160MHZ",
True,
VARIANT_ESP32,
"esp-idf",
id="legacy-frequency-options-with-default",
),
pytest.param(
{"CONFIG_ESP32_DEFAULT_CPU_FREQ_160": "y"},
"160MHz",
"160MHZ",
False,
VARIANT_ESP32,
"esp-idf",
id="explicit-cpu-frequency",
),
pytest.param(
{"CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ": "160"},
None,
"160MHZ",
True,
VARIANT_ESP32,
"esp-idf",
id="legacy-scalar-frequency",
),
pytest.param(
{"CONFIG_ESP32_DEFAULT_CPU_FREQ_160": "y"},
None,
"160MHZ",
True,
VARIANT_ESP32,
"esp-idf",
id="legacy-frequency-choice",
),
pytest.param(
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160": "y"},
None,
"160MHZ",
True,
VARIANT_ESP32,
"esp-idf",
id="current-idf-frequency-choice",
),
pytest.param(
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160": "n"},
None,
"240MHZ",
False,
VARIANT_ESP32,
"esp-idf",
id="disabled-frequency-choice",
),
pytest.param(
{"CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ_240": "n"},
"240MHz",
"240MHZ",
False,
VARIANT_ESP32,
"esp-idf",
id="ignore-unrecognized-legacy-disabled-choice",
),
pytest.param(
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ": "160"},
None,
"160MHZ",
True,
VARIANT_ESP32C3,
"esp-idf",
id="current-scalar-frequency",
),
pytest.param(
{"CONFIG_ESP32S2_DEFAULT_CPU_FREQ_MHZ": "160"},
None,
"160MHZ",
True,
VARIANT_ESP32S2,
"esp-idf",
id="s2-legacy-scalar-frequency",
),
pytest.param(
{"CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160": "y"},
None,
"160MHZ",
True,
VARIANT_ESP32S3,
"esp-idf",
id="s3-legacy-choice-frequency",
),
pytest.param(
{"CONFIG_ESP32C3_DEFAULT_CPU_FREQ_80": "y"},
None,
"80MHZ",
True,
VARIANT_ESP32C3,
"esp-idf",
id="c3-legacy-choice-frequency",
),
pytest.param(
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ": "160"},
None,
"160MHZ",
True,
VARIANT_ESP32,
"arduino",
id="arduino-framework-current-scalar-frequency",
),
],
)
def test_sdkconfig_cpu_frequency_is_honored(
set_core_config: SetCoreConfigCallable,
caplog: pytest.LogCaptureFixture,
sdkconfig_options: dict[str, str],
cpu_frequency: str | None,
expected_frequency: str,
warns: bool,
variant: str,
framework_type: str,
) -> None:
"""SDK config frequency settings are honored and recommend the native config key."""
platform_framework = (
PlatformFramework.ESP32_ARDUINO
if framework_type == "arduino"
else PlatformFramework.ESP32_IDF
)
set_core_config(platform_framework)
from esphome.components.esp32 import CONFIG_SCHEMA
config: dict[str, Any] = {
"variant": variant,
"framework": {
"type": framework_type,
"sdkconfig_options": sdkconfig_options,
},
}
if cpu_frequency is not None:
config["cpu_frequency"] = cpu_frequency
with caplog.at_level(logging.WARNING):
config = CONFIG_SCHEMA(config)
assert ("CPU frequency setting" in caplog.text) is warns
assert config["cpu_frequency"] == expected_frequency
@pytest.mark.parametrize(
("variant", "cpu_frequency", "sdkconfig_options", "error_match"),
[
pytest.param(
VARIANT_ESP32C3,
None,
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ": "fast"},
"must be an integer MHz value",
id="integer-option-not-a-number",
),
pytest.param(
VARIANT_ESP32C3,
None,
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240": "y"},
"ESP32C3 does not support",
id="unsupported-frequency",
),
pytest.param(
VARIANT_ESP32C3,
None,
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160": "yes"},
"must be y or n",
id="choice-option-not-y-or-n",
),
pytest.param(
VARIANT_ESP32C3,
None,
{
"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160": "y",
"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240": "y",
},
r"conflicting CPU frequencies \(160MHz, 240MHz\)",
id="two-frequencies-selected",
),
pytest.param(
VARIANT_ESP32,
"160MHz",
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240": "y"},
"conflicts with sdkconfig_options",
id="conflicts-with-explicit-value",
),
pytest.param(
VARIANT_ESP32,
None,
{"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240": "n"},
"disables the selected CPU frequency",
id="default-frequency-disabled",
),
pytest.param(
VARIANT_ESP32,
"160MHz",
{"CONFIG_ESP32_DEFAULT_CPU_FREQ_160": "n"},
"disables the selected CPU frequency",
id="explicit-frequency-disabled-by-legacy-option",
),
],
)
def test_sdkconfig_cpu_frequency_errors(
set_core_config: SetCoreConfigCallable,
variant: str,
cpu_frequency: str | None,
sdkconfig_options: dict[str, str],
error_match: str,
) -> None:
"""Bad, conflicting or disabled sdkconfig frequencies fail at validation."""
set_core_config(PlatformFramework.ESP32_IDF)
from esphome.components.esp32 import CONFIG_SCHEMA
config: dict[str, Any] = {
"variant": variant,
"framework": {"type": "esp-idf", "sdkconfig_options": sdkconfig_options},
}
if cpu_frequency is not None:
config["cpu_frequency"] = cpu_frequency
with pytest.raises(cv.Invalid, match=error_match) as exc_info:
CONFIG_SCHEMA(config)
assert exc_info.value.path == ["framework", "sdkconfig_options"]
@pytest.mark.parametrize(
("config_toolchain", "expected"),
[