diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 7e7b127814..d703e22e46 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1615,8 +1615,14 @@ FLASH_SIZES = [ ] CONF_FLASH_SIZE = "flash_size" +CONF_FLASH_MODE = "flash_mode" +CONF_FLASH_FREQUENCY = "flash_frequency" CONF_CPU_FREQUENCY = "cpu_frequency" CONF_PARTITIONS = "partitions" +FLASH_MODES = ["qio", "qout", "dio", "dout", "opi"] +FLASH_FREQUENCIES = [ + f"{freq}MHZ" for freq in (120, 80, 64, 60, 48, 40, 32, 30, 26, 24, 20, 16) +] CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -1630,6 +1636,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( *FLASH_SIZES, upper=True ), + cv.Optional(CONF_FLASH_MODE): cv.one_of(*FLASH_MODES, lower=True), + cv.Optional(CONF_FLASH_FREQUENCY): cv.one_of( + *FLASH_FREQUENCIES, upper=True + ), cv.Optional(CONF_PARTITIONS): cv.Any( cv.file_, cv.ensure_list( @@ -1866,6 +1876,12 @@ async def to_code(config): "board_upload.maximum_size", int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024, ) + if flash_mode := config.get(CONF_FLASH_MODE): + cg.add_platformio_option("board_build.flash_mode", flash_mode) + if flash_frequency := config.get(CONF_FLASH_FREQUENCY): + cg.add_platformio_option( + "board_build.f_flash", f"{flash_frequency[:-3]}000000L" + ) if CONF_SOURCE in conf: cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]]) @@ -2016,6 +2032,14 @@ async def to_code(config): add_idf_sdkconfig_option( f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True ) + if flash_mode := config.get(CONF_FLASH_MODE): + add_idf_sdkconfig_option( + f"CONFIG_ESPTOOLPY_FLASHMODE_{flash_mode.upper()}", True + ) + if flash_frequency := config.get(CONF_FLASH_FREQUENCY): + add_idf_sdkconfig_option( + f"CONFIG_ESPTOOLPY_FLASHFREQ_{flash_frequency[:-3]}M", True + ) # ESP32-P4: ESP-IDF 5.5.3 changed the default of ESP32P4_SELECTS_REV_LESS_V3 # from y to n. PlatformIO uses sections.ld.in (for rev <3) or diff --git a/tests/component_tests/esp32/config/flash_mode_default.yaml b/tests/component_tests/esp32/config/flash_mode_default.yaml new file mode 100644 index 0000000000..0d05142099 --- /dev/null +++ b/tests/component_tests/esp32/config/flash_mode_default.yaml @@ -0,0 +1,7 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf diff --git a/tests/component_tests/esp32/config/flash_mode_idf.yaml b/tests/component_tests/esp32/config/flash_mode_idf.yaml new file mode 100644 index 0000000000..7c7f50a439 --- /dev/null +++ b/tests/component_tests/esp32/config/flash_mode_idf.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +esp32: + board: esp32dev + flash_mode: qio + flash_frequency: 80MHz + framework: + type: esp-idf diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index e9fa9446d4..a8b5720a80 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -285,3 +285,29 @@ def test_native_idf_enables_reproducible_build( sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] assert sdkconfig.get("CONFIG_APP_REPRODUCIBLE_BUILD") is True + + +def test_flash_mode_sets_sdkconfig_and_pio_option( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """flash_mode/flash_frequency select the esptool flash parameters on both backends.""" + generate_main(component_config_path("flash_mode_idf.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert sdkconfig.get("CONFIG_ESPTOOLPY_FLASHMODE_QIO") is True + assert sdkconfig.get("CONFIG_ESPTOOLPY_FLASHFREQ_80M") is True + assert CORE.platformio_options.get("board_build.flash_mode") == "qio" + assert CORE.platformio_options.get("board_build.f_flash") == "80000000L" + + +def test_flash_mode_unset_leaves_defaults( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Without flash_mode the board/sdkconfig defaults stay untouched.""" + generate_main(component_config_path("flash_mode_default.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert not any(key.startswith("CONFIG_ESPTOOLPY_FLASHMODE_") for key in sdkconfig) + assert not any(key.startswith("CONFIG_ESPTOOLPY_FLASHFREQ_") for key in sdkconfig) + assert "board_build.flash_mode" not in CORE.platformio_options + assert "board_build.f_flash" not in CORE.platformio_options