libretiny: print IRAM_ATTR placement summary after link

After linking, walk the ELF symbol table and print one of:

  ESPHome: IRAM_ATTR placement summary (BK7231N):
    .sram.text:  312 bytes at 0x004001d0 - 0x00400308

  ESPHome: IRAM_ATTR placement summary (RTL8720C):
    IRAM symbols at 0x10002f6c - 0x10002fa8 (approx 60 bytes)

On the three families whose linker scripts we already patch (BK72xx, LN882H,
RTL8710B) the PROVIDE(__esphome_sram_text_start/end = .) markers injected
alongside KEEP(*(.sram.text*)) give an exact byte count for the ESPHome
IRAM_ATTR contribution. On RTL8720C we cannot patch the linker script (it
is loaded directly from the framework package) so the summary falls back to
reading the addresses of the three core IRAM_ATTR symbols. Either way the
address range is visible proof that the functions ended up in SRAM rather
than flash.
This commit is contained in:
J. Nick Koston
2026-04-15 14:05:48 -10:00
parent 33118e0e57
commit ab382c1e52
@@ -20,11 +20,19 @@ import re
_MARKER = "/* esphome .sram.text */"
_KEEP_LINE = " KEEP(*(.sram.text*)) " + _MARKER + "\n"
_KEEP_LINE = (
" PROVIDE(__esphome_sram_text_start = .); "
"KEEP(*(.sram.text*)) "
"PROVIDE(__esphome_sram_text_end = .); "
+ _MARKER + "\n"
)
_BK_DATA = re.compile(r"(\.data\s*:\s*\{\s*\n)")
_BK_RW_NO_X = re.compile(r"(\bram\s*\()\s*rw\s*!\s*x(\s*\))")
_LN_COPY = re.compile(r"(\.flash_copysection\s*:\s*\{\s*\n)")
_RTL8710B_IMAGE2 = re.compile(r"(\.image2\.ram\.text\s*:\s*\{\s*\n)")
# RTL8720C loads its linker script directly from the framework package so we
# cannot inject PROVIDE markers for it; the summary falls back to reading
# addresses of known IRAM_ATTR symbols instead of measuring a bracketed span.
def _detect(env):
@@ -82,6 +90,8 @@ def _patchers_for(variant):
return (_patch_ln882h,)
if variant == "RTL8710B":
return (_patch_rtl8710b,)
# RTL8720C: stock linker already consumes *(.sram.text*), no .ld patch
# needed; summary falls back to reading symbol addresses.
return ()
@@ -118,6 +128,49 @@ def _pre_link(target, source, env):
_patch_build_dir(_patchers, env.subst("$BUILD_DIR"))
def _post_link(target, source, env):
"""Print where IRAM_ATTR ended up so users can confirm at a glance."""
nm = env.subst("$NM") or "arm-none-eabi-nm"
elf = env.subst("$BUILD_DIR/${PROGNAME}.elf")
if not os.path.isfile(elf):
return
try:
import subprocess
out = subprocess.check_output([nm, "--defined-only", elf], text=True)
except (OSError, subprocess.CalledProcessError):
return
start = end = None
sample = []
for line in out.splitlines():
parts = line.split(maxsplit=2)
if len(parts) != 3:
continue
addr, _kind, name = parts
if name == "__esphome_sram_text_start":
start = int(addr, 16)
elif name == "__esphome_sram_text_end":
end = int(addr, 16)
elif name in (
"_ZN7esphome21wake_loop_any_contextEv",
"_ZN7esphome17wake_loop_isrsafeEPl",
"_ZN7esphome9Component28enable_loop_soon_any_contextEv",
):
sample.append((int(addr, 16), name))
header = "ESPHome: IRAM_ATTR placement summary ({}):".format(_variant)
if start is not None and end is not None:
print(header)
print(" .sram.text: {} bytes at 0x{:08x} - 0x{:08x}".format(end - start, start, end))
elif sample:
print(header)
sample.sort()
lo = sample[0][0]
hi = sample[-1][0]
print(" IRAM symbols at 0x{:08x} - 0x{:08x} (approx {} bytes)".format(lo, hi, hi - lo))
else:
print(header)
print(" no IRAM_ATTR symbols found in the ELF")
_variant = _detect(env)
if _variant is None:
@@ -139,3 +192,6 @@ if _patchers:
# own builder setup, which may run after this script. Register the patch
# as a pre-link action so it executes once the linker scripts exist.
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", _pre_link)
# Post-link summary runs for every LibreTiny family.
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", _post_link)