[esp32] Add a flash chip option that drops the unused flash vendor drivers (#19217)

This commit is contained in:
J. Nick Koston
2026-09-17 08:40:30 -05:00
committed by GitHub
parent 7d17efc15f
commit 8ac3dba11a
8 changed files with 177 additions and 2 deletions
+42
View File
@@ -111,6 +111,7 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample"
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_FLASH_CHIP = "flash_chip"
CONF_KEY_ID = "key_id"
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
CONF_NVS_ENCRYPTION = "nvs_encryption"
@@ -464,6 +465,20 @@ ESP32_CHIP_REVISIONS = {
"3.1": "CONFIG_ESP32_REV_MIN_3_1",
}
# Flash vendor drivers ESP-IDF can link; each costs IRAM plus a 124 B table in DRAM
# and only the one matching the flash ID is ever used
ESP32_FLASH_CHIPS = {
"gd": "CONFIG_SPI_FLASH_SUPPORT_GD_CHIP",
"issi": "CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP",
"mxic": "CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP",
"winbond": "CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP",
"boya": "CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP",
"th": "CONFIG_SPI_FLASH_SUPPORT_TH_CHIP",
"mxic_opi": "CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP",
}
FLASH_CHIP_GENERIC = "generic"
FLASH_CHIP_OPI = "mxic_opi" # the octal driver, ESP32-S3 only
# Socket limit configuration for ESP-IDF
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default
@@ -1533,6 +1548,25 @@ def final_validate(config) -> None:
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_SRAM1_AS_IRAM],
)
)
if (flash_chip := advanced.get(CONF_FLASH_CHIP)) is not None:
opi = flash_chip == FLASH_CHIP_OPI
if opi and config[CONF_VARIANT] != VARIANT_ESP32S3:
errs.append(
cv.Invalid(
f"'{CONF_FLASH_CHIP}: {flash_chip}' is only supported on {VARIANT_ESP32S3}",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP],
)
)
elif opi != (config.get(CONF_FLASH_MODE) == "opi"):
errs.append(
cv.Invalid(
f"'{CONF_FLASH_CHIP}: {flash_chip}' requires '{CONF_FLASH_MODE}: opi'"
if opi
else f"'{CONF_FLASH_CHIP}: {flash_chip}' does not match "
f"'{CONF_FLASH_MODE}: opi'; octal flash uses {FLASH_CHIP_OPI}",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP],
)
)
if (
config[CONF_VARIANT] != VARIANT_ESP32P4
and config.get(CONF_ENGINEERING_SAMPLE) is not None
@@ -1971,6 +2005,9 @@ FRAMEWORK_SCHEMA = cv.Schema(
*ESP32_CHIP_REVISIONS, string=True
),
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
cv.Optional(CONF_FLASH_CHIP): cv.one_of(
FLASH_CHIP_GENERIC, *ESP32_FLASH_CHIPS, lower=True
),
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
# it will handle disabling DHCP server when AP is not configured.
# Default to false (disabled) when WiFi is not used.
@@ -2765,6 +2802,11 @@ async def to_code(config):
add_idf_sdkconfig_option(flag, rev == min_rev)
cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET")
# Keep only the flash vendor driver the board needs; the boot log names it
if (flash_chip := conf[CONF_ADVANCED].get(CONF_FLASH_CHIP)) is not None:
for chip, flag in ESP32_FLASH_CHIPS.items():
add_idf_sdkconfig_option(flag, chip == flash_chip)
# Use SRAM1 region as IRAM on ESP32 (original) variant
# This provides an additional 40KB of IRAM by using SRAM1 memory that was previously
# reserved for bootloader DRAM. Requires a bootloader from ESP-IDF v5.1 or later.
+32 -2
View File
@@ -11,6 +11,19 @@
#include <esp_chip_info.h>
#include <esp_ota_ops.h>
#include <esp_bootloader_desc.h>
#include <esp_flash.h>
#if __has_include(<esp_flash_chips/spi_flash_chip_generic.h>)
#include <esp_flash_chips/spi_flash_chip_generic.h> // ESP-IDF 6
#include <esp_flash_chips/spi_flash_chip_mxic.h>
#else
#include <spi_flash_chip_generic.h>
#include <spi_flash_chip_mxic.h>
#endif
// Vendor flash drivers linked next to the generic one; sdkconfig defines each as 1 or not at all
#define ESPHOME_FLASH_VENDOR_DRIVERS \
(CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP + CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP + CONFIG_SPI_FLASH_SUPPORT_GD_CHIP + \
CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP + CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP + CONFIG_SPI_FLASH_SUPPORT_TH_CHIP + \
CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP)
#endif
#include "esphome/core/version.h"
#include "esphome/core/hal.h"
@@ -157,8 +170,25 @@ void Application::process_dump_config_() {
esp_chip_info(&chip_info);
ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100,
chip_info.revision % 100, chip_info.cores);
#if defined(USE_ESP32_VARIANT_ESP32) && (!defined(USE_ESP32_MIN_CHIP_REVISION_SET) || !defined(USE_ESP32_SRAM1_AS_IRAM))
static const char *const ESP32_ADVANCED_PATH = "under esp32 > framework > advanced";
[[maybe_unused]] static const char *const ESP32_ADVANCED_PATH = "under esp32 > framework > advanced";
#if ESPHOME_FLASH_VENDOR_DRIVERS > 0
{
// Only the driver in use earns its IRAM; with several linked at least one is idle
const spi_flash_chip_t *flash_driver = esp_flash_default_chip->chip_drv;
#if ESPHOME_FLASH_VENDOR_DRIVERS > 1
constexpr bool idle_driver = true;
#else
const bool idle_driver = flash_driver == &esp_flash_chip_generic;
#endif
if (idle_driver) {
const char *value = flash_driver->name;
#ifdef CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP
if (flash_driver == &esp_flash_chip_mxic_opi)
value = "mxic_opi";
#endif
ESP_LOGW(TAG, "Set flash_chip: %s %s to save IRAM", value, ESP32_ADVANCED_PATH);
}
}
#endif
#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET)
{
@@ -0,0 +1,9 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
advanced:
flash_chip: gd
@@ -0,0 +1,9 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
advanced:
flash_chip: generic
@@ -0,0 +1,10 @@
esphome:
name: test
esp32:
variant: esp32s3
flash_mode: opi
framework:
type: esp-idf
advanced:
flash_chip: mxic_opi
+73
View File
@@ -10,6 +10,7 @@ from typing import Any
import pytest
from esphome.components.esp32 import (
ESP32_FLASH_CHIPS,
KEY_FATFS_REQUIRED,
KEY_MBEDTLS_TLS_EXTRAS_REQUIRED,
KEY_MBEDTLS_TLS_SERVER_REQUIRED,
@@ -252,6 +253,41 @@ def test_esp32_rejects_unsupported_cli_toolchain(
r"value must be at most 5 .* @ data\['framework'\]\['advanced'\]\['nvs_encryption'\]\['key_id'\]",
id="nvs_encryption_key_id_out_of_range",
),
pytest.param(
{
"variant": "esp32",
"board": "esp32dev",
"framework": {
"type": "esp-idf",
"advanced": {"flash_chip": "mxic_opi"},
},
},
r"'flash_chip: mxic_opi' is only supported on ESP32S3 @ data\['framework'\]\['advanced'\]\['flash_chip'\]",
id="flash_chip_mxic_opi_only_on_s3",
),
pytest.param(
{
"variant": "esp32s3",
"flash_mode": "opi",
"framework": {
"type": "esp-idf",
"advanced": {"flash_chip": "gd"},
},
},
r"'flash_chip: gd' does not match 'flash_mode: opi'; octal flash uses mxic_opi @ data\['framework'\]\['advanced'\]\['flash_chip'\]",
id="flash_chip_must_match_opi_mode",
),
pytest.param(
{
"variant": "esp32s3",
"framework": {
"type": "esp-idf",
"advanced": {"flash_chip": "mxic_opi"},
},
},
r"'flash_chip: mxic_opi' requires 'flash_mode: opi' @ data\['framework'\]\['advanced'\]\['flash_chip'\]",
id="flash_chip_mxic_opi_requires_opi_mode",
),
pytest.param(
{
"variant": "esp32",
@@ -719,6 +755,43 @@ def test_flash_mode_sets_sdkconfig_and_pio_option(
assert CORE.platformio_options.get("board_build.f_flash") == "80000000L"
@pytest.mark.parametrize(
("config_file", "enabled"),
[
pytest.param("flash_chip_gd.yaml", "CONFIG_SPI_FLASH_SUPPORT_GD_CHIP", id="gd"),
pytest.param("flash_chip_generic.yaml", None, id="generic"),
pytest.param(
"flash_chip_mxic_opi_s3.yaml",
"CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP",
id="mxic_opi_s3",
),
],
)
def test_flash_chip_keeps_one_vendor_driver(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
config_file: str,
enabled: str | None,
) -> None:
"""flash_chip enables only the chosen vendor driver."""
generate_main(component_config_path(config_file))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
vendors = {
k: v for k, v in sdkconfig.items() if k.startswith("CONFIG_SPI_FLASH_SUPPORT_")
}
assert vendors == {flag: flag == enabled for flag in ESP32_FLASH_CHIPS.values()}
def test_flash_chip_unset_keeps_idf_defaults(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Without flash_chip every vendor driver stays at its ESP-IDF default."""
generate_main(component_config_path("flash_mode_default.yaml"))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert not any(key.startswith("CONFIG_SPI_FLASH_SUPPORT_") for key in sdkconfig)
def test_flash_mode_opi_enables_octal_flash(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
@@ -22,6 +22,7 @@ esp32:
disable_regi2c_in_iram: true
disable_fatfs: true
sram1_as_iram: true
flash_chip: gd
watchdog_timeout: 7s
wifi:
@@ -9,6 +9,7 @@ esp32:
type: esp-idf
advanced:
execute_from_psram: true
flash_chip: gd
disable_libc_locks_in_iram: true # Test default RAM optimization enabled
disable_debug_stubs: true
disable_ocd_aware: true