From 7fb18f395f447d427516994332ba23e73393da93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Aug 2026 20:25:23 -0500 Subject: [PATCH 1/3] Honor board_build.f_cpu under the native ESP8266 Arduino toolchain Dropping it was a real regression: many published ESP8266 configs pin board_build.f_cpu: 160000000L for timing-sensitive integrations (MHI-AC-Ctrl documents the 160 MHz requirement in its example), and the warn-and-drop left those devices at 80 MHz. The option now routes into CORE.platformio_options under toolchain: arduino for the generator to consume; other native toolchains keep the warning. --- esphome/core/config.py | 7 +++++++ tests/unit_tests/core/test_config.py | 11 ++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/esphome/core/config.py b/esphome/core/config.py index 1c481894b2..71f2ebfc94 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -593,6 +593,13 @@ async def _add_platformio_options(pio_options: dict[str, str | list[str]]) -> No # platformio/library.py); filters top-level libraries and # discovered dependencies cg.add_platformio_option(key, vals) + elif key == "board_build.f_cpu" and CORE.using_toolchain_arduino: + # A real-world overclock knob (many published ESP8266 configs + # pin 160000000L for timing-sensitive integrations); the + # esp8266 native generator reads it for -DF_CPU. Other native + # toolchains have no equivalent and fall through to the + # warning. + cg.add_platformio_option(key, val) elif key != "upload_speed": # upload_speed needs no handling: it is read from the raw # config at upload time (upload_using_esptool) diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index f322c1d4c9..0a143c466c 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -1397,8 +1397,8 @@ def test_esphome_build_internals_are_yaml_only() -> None: async def test_add_platformio_options_native_arduino( caplog: pytest.LogCaptureFixture, ) -> None: - """The native ESP8266 Arduino toolchain warns about ignored options the - same way the native IDF toolchain does.""" + """The native ESP8266 Arduino toolchain honors board_build.f_cpu (a + real-world overclock knob) and warns about the rest like native IDF.""" CORE.toolchain = Toolchain.ARDUINO CORE.data[KEY_CORE] = { KEY_TARGET_PLATFORM: "esp8266", @@ -1408,11 +1408,16 @@ async def test_add_platformio_options_native_arduino( await config._add_platformio_options( { "board_build.f_cpu": "160000000L", + "board_build.filesystem": "littlefs", "upload_speed": "115200", } ) - assert "esphome->platformio_options->board_build.f_cpu is ignored" in caplog.text + assert CORE.platformio_options["board_build.f_cpu"] == "160000000L" + assert "board_build.f_cpu is ignored" not in caplog.text + assert ( + "esphome->platformio_options->board_build.filesystem is ignored" in caplog.text + ) assert "'arduino' toolchain" in caplog.text assert "upload_speed" not in caplog.text From d8c02221cc4d6c2374e17ebd852ae21c5f33d571 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Aug 2026 20:27:33 -0500 Subject: [PATCH 2/3] Route board_build.ldscript through to the native ESP8266 generator too Published configs override it to reserve a filesystem region (SmartIntercom's eagle.flash.4m2m.ld) or to correct a board's assumed flash size (2 MB ESP8285 plugs pinning eagle.flash.2m.ld); dropping it changes the flash layout under them. --- esphome/core/config.py | 16 ++++++++++------ tests/unit_tests/core/test_config.py | 3 +++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/esphome/core/config.py b/esphome/core/config.py index 71f2ebfc94..c6463beebb 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -593,12 +593,16 @@ async def _add_platformio_options(pio_options: dict[str, str | list[str]]) -> No # platformio/library.py); filters top-level libraries and # discovered dependencies cg.add_platformio_option(key, vals) - elif key == "board_build.f_cpu" and CORE.using_toolchain_arduino: - # A real-world overclock knob (many published ESP8266 configs - # pin 160000000L for timing-sensitive integrations); the - # esp8266 native generator reads it for -DF_CPU. Other native - # toolchains have no equivalent and fall through to the - # warning. + elif ( + key in ("board_build.f_cpu", "board_build.ldscript") + and CORE.using_toolchain_arduino + ): + # Real-world knobs many published ESP8266 configs rely on: + # f_cpu 160000000L for timing-sensitive integrations, and a + # custom ldscript to reserve a filesystem region or correct + # a board's flash size. The esp8266 native generator reads + # both; other native toolchains have no equivalent and fall + # through to the warning. cg.add_platformio_option(key, val) elif key != "upload_speed": # upload_speed needs no handling: it is read from the raw diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 0a143c466c..063da34556 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -1408,13 +1408,16 @@ async def test_add_platformio_options_native_arduino( await config._add_platformio_options( { "board_build.f_cpu": "160000000L", + "board_build.ldscript": "eagle.flash.4m2m.ld", "board_build.filesystem": "littlefs", "upload_speed": "115200", } ) assert CORE.platformio_options["board_build.f_cpu"] == "160000000L" + assert CORE.platformio_options["board_build.ldscript"] == "eagle.flash.4m2m.ld" assert "board_build.f_cpu is ignored" not in caplog.text + assert "board_build.ldscript is ignored" not in caplog.text assert ( "esphome->platformio_options->board_build.filesystem is ignored" in caplog.text ) From accfd12199cecc9343ec34e675acf0b13a691e4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Aug 2026 20:28:27 -0500 Subject: [PATCH 3/3] Honor board_build.f_cpu on the native compile line The routed override (published configs pin 160000000L for timing-sensitive integrations) reaches -DF_CPU via a small _pio_option reader; the default stays the audited 80 MHz all 45 boards ship. --- esphome/build_gen/arduino8266.py | 22 +++++++++++++++---- .../unit_tests/build_gen/test_arduino8266.py | 17 ++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index 21356f343f..abbaa3f8bf 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -298,6 +298,19 @@ def _resolve_build_config(defines: dict[str, str]) -> _BuildConfig: ) +def _pio_option(key: str, default: str) -> str: + """A platformio_options value the native build honors (str-normalized). + + Routed into ``CORE.platformio_options`` by core/config.py under the + arduino toolchain; a repeated option accumulates as a list, where the + last value wins like a later platformio.ini line. + """ + value = CORE.platformio_options.get(key) + if isinstance(value, list): + value = value[-1] if value else None + return default if value is None else str(value) + + def _defines_flags( config: _BuildConfig, flash_mode: str, board: str, board_defines: tuple[str, ...] ) -> list[str]: @@ -310,10 +323,11 @@ def _defines_flags( return [ f"-D{d}" for d in ( - # Upstream reads this from the board manifest (build.f_cpu); all - # 45 supported boards ship 80000000L, so the value is hardcoded - # here rather than drift (same rationale as _MMU_DEFAULT) - "F_CPU=80000000L", + # Upstream reads this from the board manifest (build.f_cpu), + # where all 45 supported boards ship 80000000L, overridable via + # board_build.f_cpu; published configs pin 160000000L for + # timing-sensitive integrations, so the override is honored + f"F_CPU={_pio_option('board_build.f_cpu', '80000000L')}", "__ets__", "ICACHE_FLASH", "_GNU_SOURCE", diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index 0f0aab72f5..f52a9fa617 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -887,3 +887,20 @@ def test_generate_ld_scripts_gcc_change_invalidates_stamp(tmp_path: Path) -> Non with patch.object(arduino8266.subprocess, "run", return_value=result) as mock_run: _run_generate_ld_scripts(paths) mock_run.assert_called_once() + + +def test_defines_flags_honors_f_cpu_override() -> None: + """board_build.f_cpu (a published-config overclock knob) reaches the + compile line; the default stays 80 MHz.""" + _set_flags() + config = _resolve_build_config(_flag_defines(set(), [])) + board_build = ESP8266_BOARD_BUILD["nodemcuv2"] + defines = _defines_flags(config, "dout", "nodemcuv2", board_build["defines"]) + assert "-DF_CPU=80000000L" in defines + CORE.platformio_options = {"board_build.f_cpu": "160000000L"} + defines = _defines_flags(config, "dout", "nodemcuv2", board_build["defines"]) + assert "-DF_CPU=160000000L" in defines + # A repeated option accumulates as a list; the last value wins + CORE.platformio_options = {"board_build.f_cpu": ["80000000L", "160000000L"]} + defines = _defines_flags(config, "dout", "nodemcuv2", board_build["defines"]) + assert "-DF_CPU=160000000L" in defines