diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0c5b9c5df6a..748be9ae4cf 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -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. diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 38d3503c2c3..50d1c619595 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -11,6 +11,19 @@ #include #include #include +#include +#if __has_include() +#include // ESP-IDF 6 +#include +#else +#include +#include +#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) { diff --git a/tests/component_tests/esp32/config/flash_chip_gd.yaml b/tests/component_tests/esp32/config/flash_chip_gd.yaml new file mode 100644 index 00000000000..6d564135c00 --- /dev/null +++ b/tests/component_tests/esp32/config/flash_chip_gd.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + advanced: + flash_chip: gd diff --git a/tests/component_tests/esp32/config/flash_chip_generic.yaml b/tests/component_tests/esp32/config/flash_chip_generic.yaml new file mode 100644 index 00000000000..8c7bcf61664 --- /dev/null +++ b/tests/component_tests/esp32/config/flash_chip_generic.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + advanced: + flash_chip: generic diff --git a/tests/component_tests/esp32/config/flash_chip_mxic_opi_s3.yaml b/tests/component_tests/esp32/config/flash_chip_mxic_opi_s3.yaml new file mode 100644 index 00000000000..1531e749f20 --- /dev/null +++ b/tests/component_tests/esp32/config/flash_chip_mxic_opi_s3.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + flash_mode: opi + framework: + type: esp-idf + advanced: + flash_chip: mxic_opi diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index d5d0acfb2ad..a42d244ac8f 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -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], diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index 7f31fe59c6f..5c7cb1d61b8 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -22,6 +22,7 @@ esp32: disable_regi2c_in_iram: true disable_fatfs: true sram1_as_iram: true + flash_chip: gd watchdog_timeout: 7s wifi: diff --git a/tests/components/esp32/test.esp32-s3-idf.yaml b/tests/components/esp32/test.esp32-s3-idf.yaml index b9a3b804a8d..5bdf94e8e1d 100644 --- a/tests/components/esp32/test.esp32-s3-idf.yaml +++ b/tests/components/esp32/test.esp32-s3-idf.yaml @@ -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