Add the ESP8266 linker-script surgery and board build metadata

This commit is contained in:
J. Nick Koston
2026-08-24 18:46:47 -05:00
committed by J. Nick Koston
parent b3d9aa2d84
commit cdd8a16cb9
4 changed files with 293 additions and 0 deletions
+111
View File
@@ -360,3 +360,114 @@ BOARDS = {
"flash_size": FLASH_SIZE_4_MB,
},
}
"""
ESP8266_BOARD_BUILD generate with:
git clone https://github.com/platformio/platform-espressif8266
python3 - <<'EOF'
import json, glob, os
for f in sorted(glob.glob("platform-espressif8266/boards/*.json")):
b = json.load(open(f))["build"]
extra = b["extra_flags"]
extra = extra.split() if isinstance(extra, str) else extra
defines = [
e[2:] for e in extra if e not in ("-DESP8266", "-DARDUINO_ARCH_ESP8266")
]
entries = ", ".join(f'"{d}"' for d in defines) + ("," if len(defines) == 1 else "")
board = os.path.splitext(os.path.basename(f))[0]
print(f' "{board}": {{"variant": "{b["variant"]}", "defines": ({entries})}},')
EOF
"""
# Per-board Arduino core build metadata for the native (PlatformIO-free)
# toolchain: the variant directory (supplies pins_arduino.h) and the
# board-identity defines the PlatformIO builder passes via build.extra_flags.
# -DESP8266 and -DARDUINO_ARCH_ESP8266 are shared by every board and added by
# the generator; only the per-board defines are listed here.
ESP8266_BOARD_BUILD = {
"agruminolemon": {
"variant": "agruminolemonv4",
"defines": ("ARDUINO_ESP8266_AGRUMINO_LEMON_V4",),
},
"d1": {"variant": "d1", "defines": ("ARDUINO_ESP8266_WEMOS_D1R1",)},
"d1_mini": {"variant": "d1_mini", "defines": ("ARDUINO_ESP8266_WEMOS_D1MINI",)},
"d1_mini_lite": {
"variant": "d1_mini",
"defines": ("ARDUINO_ESP8266_WEMOS_D1MINILITE",),
},
"d1_mini_pro": {
"variant": "d1_mini",
"defines": ("ARDUINO_ESP8266_WEMOS_D1MINIPRO",),
},
"d1_wroom_02": {
"variant": "d1_mini",
"defines": ("ARDUINO_ESP8266_WEMOS_D1WROOM02",),
},
"eduinowifi": {
"variant": "eduinowifi",
"defines": ("ARDUINO_ESP8266_SCHIRMILABS_EDUINO_WIFI",),
},
"esp01": {"variant": "generic", "defines": ("ARDUINO_ESP8266_ESP01",)},
"esp01_1m": {"variant": "generic", "defines": ("ARDUINO_ESP8266_ESP01",)},
"esp07": {"variant": "generic", "defines": ("ARDUINO_ESP8266_ESP07",)},
"esp07s": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_ESP07",)},
"esp12e": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_ESP12",)},
"esp210": {"variant": "generic", "defines": ("ARDUINO_ESP8266_ESP210",)},
"esp8285": {"variant": "esp8285", "defines": ("ARDUINO_ESP8266_ESP01",)},
"esp_wroom_02": {
"variant": "nodemcu",
"defines": ("ARDUINO_ESP8266_ESP_WROOM_02",),
},
"espduino": {"variant": "ESPDuino", "defines": ("ARDUINO_ESP8266_ESP13",)},
"espectro": {"variant": "espectro", "defines": ("ARDUINO_ESP8266_ESPECTRO_CORE",)},
"espino": {"variant": "espino", "defines": ("ARDUINO_ESP8266_ESP12",)},
"espinotee": {"variant": "espinotee", "defines": ("ARDUINO_ESP8266_ESP13",)},
"espmxdevkit": {
"variant": "esp8285",
"defines": ("ARDUINO_ESP8266_ESP01", "LED_BUILTIN=16"),
},
"espresso_lite_v1": {
"variant": "espresso_lite_v1",
"defines": ("ARDUINO_ESP8266_ESPRESSO_LITE_V1",),
},
"espresso_lite_v2": {
"variant": "espresso_lite_v2",
"defines": ("ARDUINO_ESP8266_ESPRESSO_LITE_V2",),
},
"gen4iod": {"variant": "generic", "defines": ("ARDUINO_GEN4_IOD",)},
"heltec_wifi_kit_8": {
"variant": "wifi_kit_8",
"defines": ("ARDUINO_wifi_kit_8",),
},
"huzzah": {"variant": "adafruit", "defines": ("ARDUINO_ESP8266_ADAFRUIT_HUZZAH",)},
"inventone": {"variant": "inventone", "defines": ("ARDUINO_ESP8266_INVENT_ONE",)},
"modwifi": {"variant": "generic", "defines": ("ARDUINO_MOD_WIFI_ESP8266",)},
"nodemcu": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_NODEMCU",)},
"nodemcuv2": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_NODEMCU_ESP12E",)},
"oak": {"variant": "oak", "defines": ("ARDUINO_ESP8266_OAK",)},
"phoenix_v1": {
"variant": "phoenix_v1",
"defines": ("ARDUINO_ESP8266_PHOENIX_V1",),
},
"phoenix_v2": {
"variant": "phoenix_v2",
"defines": ("ARDUINO_ESP8266_PHOENIX_V2",),
},
"sonoff_basic": {"variant": "itead", "defines": ("ARDUINO_ESP8266_SONOFF_BASIC",)},
"sonoff_s20": {"variant": "itead", "defines": ("ARDUINO_ESP8266_SONOFF_S20",)},
"sonoff_sv": {"variant": "itead", "defines": ("ARDUINO_ESP8266_SONOFF_SV",)},
"sonoff_th": {"variant": "itead", "defines": ("ARDUINO_ESP8266_SONOFF_TH",)},
"sparkfunBlynk": {"variant": "thing", "defines": ("ARDUINO_ESP8266_THING",)},
"thing": {"variant": "thing", "defines": ("ARDUINO_ESP8266_THING",)},
"thingdev": {"variant": "thing", "defines": ("ARDUINO_ESP8266_THING_DEV",)},
"wifi_slot": {"variant": "wifi_slot", "defines": ("ARDUINO_AMPERKA_WIFI_SLOT",)},
"wifiduino": {"variant": "wifiduino", "defines": ("ARDUINO_WIFIDUINO_ESP8266",)},
"wifinfo": {"variant": "wifinfo", "defines": ("ARDUINO_WIFINFO",)},
"wio_link": {"variant": "wiolink", "defines": ("ARDUINO_ESP8266_WIO_LINK",)},
"wio_node": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_ESP_WROOM_02",)},
"xinabox_cw01": {
"variant": "xinabox",
"defines": ("ARDUINO_ESP8266_XINABOX_CW01",),
},
}
@@ -0,0 +1,92 @@
"""Linker-script surgery shared with the native (PlatformIO-free) toolchain.
These mirror the PlatformIO extra scripts in this directory
(``relocate_ratetable.py.script`` and ``testing_mode.py.script``), which run
inside SCons and must stay self-contained. The native build generator applies
the same patches to the linker scripts it generates, so the logic lives here
as plain functions. Keep both in sync when changing either.
"""
from __future__ import annotations
from collections.abc import Collection
import re
# Move the NONOS SDK wifi rate tables from flash to DRAM; see
# relocate_ratetable.py.script for the full background (NONOS SDK issue 320).
RATETABLE_RULE = "*libnet80211.a:ieee80211_phy.o(.irom.text .irom.text.*)"
# Match the whole line: "_data_start" is also a substring of the
# "_dport0_data_start" line in the earlier .dport0.data section
_RATETABLE_ANCHOR = re.compile(r"^\s*_data_start = ABSOLUTE\(\.\);", re.MULTILINE)
# Memory sizes for testing mode (allow larger builds for CI component grouping)
TESTING_IRAM_SIZE = "0x200000" # 2MB
TESTING_DRAM_SIZE = "0x200000" # 2MB
TESTING_FLASH_SIZE = "0x2000000" # 32MB
def relocate_ratetable(content: str) -> str:
"""Insert the rate-table DRAM rule into a generated common linker script."""
if RATETABLE_RULE in content:
return content
match = _RATETABLE_ANCHOR.search(content)
if match is None:
raise RuntimeError(
"'_data_start' anchor not found in the generated linker script; "
"cannot apply wifi rate table DRAM relocation "
"(has the Arduino core linker script changed?)"
)
insert_pos = match.end()
return (
content[:insert_pos]
+ "\n /* ESPHome: wifi rate tables must live in DRAM, see NONOS SDK issue 320 */"
+ f"\n {RATETABLE_RULE}"
+ content[insert_pos:]
)
_TESTING_SEGMENT_SIZES = (
("iram1_0_seg", TESTING_IRAM_SIZE),
("dram0_0_seg", TESTING_DRAM_SIZE),
("irom0_0_seg", TESTING_FLASH_SIZE),
)
def _patch_segment_size(content: str, segment_name: str, new_size: str) -> str:
pattern = (
rf"({segment_name}\s*:\s*org\s*=\s*0x[0-9a-fA-F]+\s*,\s*len\s*=\s*)"
r"0x[0-9a-fA-F]+"
)
return re.sub(pattern, rf"\g<1>{new_size}", content)
def apply_testing_memory_patches(content: str, require: Collection[str]) -> str:
"""Enlarge IRAM/DRAM/flash segments so grouped CI test builds can link.
``require`` names the segments this file must define; a silently
unpatched segment would keep the real memory limits and fail grouped
builds far from the cause. The segments are split across the two linker
scripts (iram1_0_seg in the generated common one, dram0_0_seg and
irom0_0_seg in the flash one), so each caller requires only its own.
"""
missing = set(require)
for segment, size in _TESTING_SEGMENT_SIZES:
patched = _patch_segment_size(content, segment, size)
if patched != content:
missing.discard(segment)
content = patched
if missing:
raise RuntimeError(
f"Testing-mode memory patch failed: segment(s) {', '.join(sorted(missing))} "
"not found (has the Arduino core linker script changed?)"
)
return content
def segment_length(content: str, segment_name: str) -> int | None:
"""Read a memory segment's length from linker script content."""
match = re.search(
rf"{segment_name}\s*:.+len\s*=\s*(0x[\da-fA-F]+)",
content,
)
return int(match.group(1), 16) if match else None
+3
View File
@@ -15,6 +15,9 @@ CONF_ENABLE_SERIAL1 = "enable_serial1"
KEY_WAVEFORM_REQUIRED = "waveform_required"
KEY_SERIAL_REQUIRED = "serial_required"
KEY_SERIAL1_REQUIRED = "serial1_required"
# Set for the native (non-PlatformIO) toolchain's build generator
KEY_FLASH_MODE = "flash_mode"
KEY_SCANF_FLOAT = "scanf_float"
# esp8266 namespace is already defined by arduino, manually prefix esphome
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
@@ -0,0 +1,87 @@
"""Tests for the linker-script surgery shared with the native toolchain."""
from __future__ import annotations
import pytest
from esphome.components.esp8266.build_surgery import (
RATETABLE_RULE,
apply_testing_memory_patches,
relocate_ratetable,
)
_COMMON_LD_SNIPPET = """\
.dport0.data : ALIGN(4)
{
_dport0_data_start = ABSOLUTE(.);
} >dport0_0_seg :dport0_0_phdr
.data : ALIGN(4)
{
_data_start = ABSOLUTE(.);
*(.data)
} >dram0_0_seg :dram0_0_phdr
"""
_FLASH_LD_SNIPPET = """\
MEMORY
{
dport0_0_seg : org = 0x3FF00000, len = 0x10
dram0_0_seg : org = 0x3FFE8000, len = 0x14000
iram1_0_seg : org = 0x40100000, len = 0x8000
irom0_0_seg : org = 0x40201010, len = 0xfeff0
}
"""
def test_relocate_ratetable_inserts_after_data_start() -> None:
patched = relocate_ratetable(_COMMON_LD_SNIPPET)
assert RATETABLE_RULE in patched
# Inserted after the .data section's anchor, not the .dport0.data one
assert patched.index("_data_start = ABSOLUTE(.);") < patched.index(RATETABLE_RULE)
assert patched.index(RATETABLE_RULE) < patched.index("*(.data)")
# Idempotent on an already-patched script
assert relocate_ratetable(patched) == patched
def test_relocate_ratetable_requires_anchor() -> None:
with pytest.raises(RuntimeError, match="_data_start"):
relocate_ratetable("SECTIONS { }")
def test_testing_memory_patches_enlarge_segments() -> None:
patched = apply_testing_memory_patches(_FLASH_LD_SNIPPET, require=())
assert (
"iram1_0_seg : org = 0x40100000, len = 0x200000"
in patched
)
assert (
"dram0_0_seg : org = 0x3FFE8000, len = 0x200000"
in patched
)
assert (
"irom0_0_seg : org = 0x40201010, len = 0x2000000"
in patched
)
def test_segment_length() -> None:
from esphome.components.esp8266.build_surgery import segment_length
assert segment_length(_FLASH_LD_SNIPPET, "irom0_0_seg") == 0xFEFF0
assert segment_length(_FLASH_LD_SNIPPET, "missing_seg") is None
def test_testing_memory_patches_require() -> None:
"""A required segment the patch could not find raises instead of
silently keeping the real memory limits."""
patched = apply_testing_memory_patches(
_FLASH_LD_SNIPPET, require=("dram0_0_seg", "irom0_0_seg")
)
assert "0x2000000" in patched
with pytest.raises(RuntimeError, match="dram0_0_seg, irom0_0_seg"):
apply_testing_memory_patches(
"MEMORY { }", require=("dram0_0_seg", "irom0_0_seg")
)
# Segments a file does not require are patched opportunistically only
# With nothing required, unmatched content passes through unchanged
assert apply_testing_memory_patches("MEMORY { }", require=()) == "MEMORY { }"