[esp8266] Derive the native image header flash size from the linker script (#19961)

This commit is contained in:
J. Nick Koston
2026-10-01 00:50:40 +02:00
committed by GitHub
parent 568c1ae494
commit 6f5d7475f0
4 changed files with 106 additions and 8 deletions
+28 -3
View File
@@ -1065,10 +1065,10 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
" rspfile_content = $in_newline",
" description = LINK $out",
"rule elf2bin",
# --flash_size deliberately stays board-derived, as under
# PlatformIO (which reads upload.maximum_size, not the ldscript).
# --flash_size follows PlatformIO: ldscript filename first, then
# upload.maximum_size, so the header always matches the layout.
# -W: the framework's own elf2bin.py trips SyntaxWarning on 3.12+.
f" command = $python -W ignore::SyntaxWarning {_q(framework / 'tools' / 'elf2bin.py')} --eboot {_q(framework / 'bootloaders' / 'eboot' / 'eboot.elf')} --app $in --flash_mode {flash_mode} --flash_freq {_FLASH_FREQ_MHZ} --flash_size {_flash_size_str(BOARDS[board][KEY_FLASH_SIZE])} --path {_q(toolchain_bin)} --out $out",
f" command = $python -W ignore::SyntaxWarning {_q(framework / 'tools' / 'elf2bin.py')} --eboot {_q(framework / 'bootloaders' / 'eboot' / 'eboot.elf')} --app $in --flash_mode {flash_mode} --flash_freq {_FLASH_FREQ_MHZ} --flash_size {_elf2bin_flash_size(board, flash_ld_name)} --path {_q(toolchain_bin)} --out $out",
" description = BIN $out",
"rule copy",
" command = $python $buildtool copy $in $out",
@@ -1189,3 +1189,28 @@ def _flash_size_str(flash_size: int) -> str:
"""Flash size argument for elf2bin (e.g. ``4M``, ``512K``)."""
mb = 1024 * 1024
return f"{flash_size // mb}M" if flash_size >= mb else f"{flash_size // 1024}K"
# Same pattern PlatformIO's _get_flash_size applies to the ldscript path
_LD_FLASH_SIZE_RE = re.compile(r"\.flash\.(\d+[mk]).*\.ld")
# The framework elf2bin.py's --flash_size choices
_ELF2BIN_FLASH_SIZES = frozenset({"256K", "512K", "1M", "2M", "4M", "8M", "16M"})
def _elf2bin_flash_size(board: str, flash_ld_name: str) -> str:
"""Image-header flash size as PlatformIO derives it: ldscript filename,
else board_upload.maximum_size, else the board table. The SDK clamps the
chip to the header size at boot, so a header smaller than the linked
layout breaks OTA writes on the running device."""
if match := _LD_FLASH_SIZE_RE.search(flash_ld_name):
token = match.group(1)
base = 1024 if token[-1] == "k" else 1024 * 1024
return _flash_size_str(int(token[:-1]) * base)
if max_size := _pio_option("board_upload.maximum_size", ""):
if (
not max_size.isdigit()
or (size := _flash_size_str(int(max_size))) not in _ELF2BIN_FLASH_SIZES
):
raise EsphomeError(f"Invalid board_upload.maximum_size value {max_size!r}")
return size
return _flash_size_str(BOARDS[board][KEY_FLASH_SIZE])
+10 -4
View File
@@ -549,7 +549,9 @@ def _add_library_str(lib: str) -> None:
# platformio_options keys the native ESP8266 Arduino generator (a later PR
# in this chain) will honor; its ignored-option warning will consume the same
# list so the two cannot drift
NATIVE_ARDUINO_PIO_OPTIONS = frozenset({"board_build.f_cpu", "board_build.ldscript"})
NATIVE_ARDUINO_PIO_OPTIONS = frozenset(
{"board_build.f_cpu", "board_build.ldscript", "board_upload.maximum_size"}
)
# The full set that survives into CORE.platformio_options under the native
# arduino toolchain: lib_ignore is the only specially-translated key below
# that is stored rather than translated away. Consumed by the esp8266 native
@@ -609,9 +611,13 @@ async def _add_platformio_options(pio_options: dict[str, str | list[str]]) -> No
# through to the ignored-option warning). Other native
# toolchains have no equivalent and fall through too.
cg.add_platformio_option(key, vals[-1])
elif key != "upload_speed":
# upload_speed needs no handling: it is read from the raw
# config at upload time (upload_using_esptool)
elif key != "upload_speed" and not (
key == "board_upload.flash_size" and CORE.using_toolchain_arduino
):
# upload_speed is read from the raw config at upload time.
# board_upload.flash_size is dropped silently on arduino:
# PlatformIO's esp8266 builder never reads it either, and
# published configs (Athom) commonly set it.
_LOGGER.warning(
"esphome->platformio_options->%s is ignored when building with "
"the native '%s' toolchain",
+55 -1
View File
@@ -21,12 +21,18 @@ from esphome.arduino8266.framework import InstalledPaths, toolchain_tool
from esphome.build_gen import arduino8266
from esphome.build_gen.arduino8266 import (
_defines_flags,
_elf2bin_flash_size,
_flag_defines,
_flash_size_str,
_resolve_build_config,
get_flash_ld_path,
)
from esphome.components.esp8266.boards import BOARDS, ESP8266_BOARD_BUILD
from esphome.components.esp8266.boards import (
BOARDS,
ESP8266_BOARD_BUILD,
KEY_FLASH_SIZE,
board_ld_script,
)
from esphome.components.esp8266.build_surgery import RATETABLE_RULE
from esphome.components.esp8266.const import KEY_BOARD, KEY_ESP8266, KEY_SCANF_FLOAT
import esphome.config_validation as cv
@@ -747,6 +753,54 @@ def test_flash_size_str() -> None:
assert _flash_size_str(512 * 1024) == "512K"
def test_elf2bin_flash_size() -> None:
"""The image-header size follows the ldscript filename like PlatformIO,
falling back to board_upload.maximum_size and then the board table."""
assert _elf2bin_flash_size("esp8285", "eagle.flash.2m.ld") == "2M"
assert _elf2bin_flash_size("esp01", "eagle.flash.512k.ld") == "512K"
assert _elf2bin_flash_size("nodemcuv2", "eagle.flash.4m1m.ld") == "4M"
# The testing-mode prefix still matches (search, not match)
assert _elf2bin_flash_size("esp8285", "testing_eagle.flash.2m.ld") == "2M"
# Custom ldscript name: board_upload.maximum_size wins over the board
CORE.platformio_options["board_upload.maximum_size"] = "2097152"
assert _elf2bin_flash_size("esp8285", "custom.ld") == "2M"
del CORE.platformio_options["board_upload.maximum_size"]
assert _elf2bin_flash_size("esp8285", "custom.ld") == "1M"
@pytest.mark.parametrize("bad", ["2MB", "3145728", "-1"])
def test_elf2bin_flash_size_rejects_bad_maximum_size(bad: str) -> None:
"""A non-numeric or unsupported board_upload.maximum_size fails by name
instead of a ValueError or a late elf2bin choices error."""
CORE.platformio_options["board_upload.maximum_size"] = bad
with pytest.raises(EsphomeError, match="board_upload.maximum_size"):
_elf2bin_flash_size("esp8285", "custom.ld")
def test_elf2bin_flash_size_default_matches_board_table() -> None:
"""Without an ldscript override, every board's own ldscript parses to
the board-table size, so the emitted --flash_size is unchanged."""
for board, entry in BOARDS.items():
assert _elf2bin_flash_size(board, board_ld_script(entry)) == _flash_size_str(
entry[KEY_FLASH_SIZE]
), board
def test_write_project_flash_size_follows_ldscript_override(
tmp_path: Path,
) -> None:
"""An ldscript overriding the board's flash size drives the image header
too (the Athom shape: esp8285 with eagle.flash.2m.ld). A 1M header over
a 2M layout clamps the chip below the OTA scratch area and bricks OTA."""
paths = _make_framework(tmp_path)
(paths.framework / "variants" / "esp8285").mkdir()
CORE.data[KEY_ESP8266][KEY_BOARD] = "esp8285"
CORE.platformio_options["board_build.ldscript"] = "eagle.flash.2m.ld"
content = _write_ninja(paths)
assert "--flash_size 2M" in content
assert "eagle.flash.2m.ld" in content
def test_write_project_testing_mode(tmp_path: Path) -> None:
paths = _make_framework(tmp_path)
CORE.testing_mode = True
+13
View File
@@ -1341,6 +1341,8 @@ async def test_add_platformio_options_native_idf(
"lib_ignore": "libsodium",
"upload_speed": "115200",
"board_build.f_flash": "80000000L",
# Silently dropped on arduino only; warns here
"board_upload.flash_size": "2MB",
}
)
@@ -1351,6 +1353,9 @@ async def test_add_platformio_options_native_idf(
# nothing else lands in platformio_options on the native toolchain.
assert CORE.platformio_options == {"lib_ignore": ["libsodium"]}
assert "esphome->platformio_options->board_build.f_flash is ignored" in caplog.text
assert (
"esphome->platformio_options->board_upload.flash_size is ignored" in caplog.text
)
assert "upload_speed" not in caplog.text
# build_flags has a first-class esphome equivalent, so it is deprecated.
# lib_deps/lib_ignore are kept as valid platformio_options (no warning).
@@ -1464,13 +1469,21 @@ async def test_add_platformio_options_native_arduino(
"board_build.ldscript": ["eagle.flash.2m.ld", "eagle.flash.4m2m.ld"],
"board_build.filesystem": "littlefs",
"upload_speed": "115200",
# The Athom shape: maximum_size is the elf2bin fallback,
# flash_size is dropped silently (PlatformIO never reads it)
"board_upload.maximum_size": "2097152",
"board_upload.flash_size": "2MB",
}
)
assert CORE.platformio_options["board_build.f_cpu"] == "160000000L"
assert CORE.platformio_options["board_build.ldscript"] == "eagle.flash.4m2m.ld"
assert CORE.platformio_options["board_upload.maximum_size"] == "2097152"
assert "board_upload.flash_size" not in CORE.platformio_options
assert "board_build.f_cpu is ignored" not in caplog.text
assert "board_build.ldscript is ignored" not in caplog.text
assert "board_upload.maximum_size is ignored" not in caplog.text
assert "board_upload.flash_size is ignored" not in caplog.text
assert (
"esphome->platformio_options->board_build.filesystem is ignored" in caplog.text
)