diff --git a/esphome/components/libretiny/patch_linker.py.script b/esphome/components/libretiny/patch_linker.py.script index b62ec1a5fb2..08b95462f08 100644 --- a/esphome/components/libretiny/patch_linker.py.script +++ b/esphome/components/libretiny/patch_linker.py.script @@ -4,38 +4,69 @@ Import("env") # noqa import os import re -# BK72xx linker templates declare the SRAM region as "(rw!x)" — read/write but -# not executable. That prevents the linker from emitting functions we mark -# IRAM_ATTR (defined as section(".data.iram_text") on BK72xx in -# esphome/core/hal.h; folded into .data by the linker's "*(.data.*)" glob) -# into the .data output section, which is the only section the SDK startup -# code copies from flash into SRAM before main() runs. +# ESPHome marks ISR code IRAM_ATTR, which on LibreTiny expands to +# section(".sram.text") (see esphome/core/hal.h). Each family's linker script +# needs that section routed into RAM-resident code so the function is callable +# while flash is busy (XIP stall, OTA, logger flash write): # -# LibreTiny's own Wi-Fi driver already runs code from SRAM at runtime, so the -# BK72xx MMU permits execution from that region; the "!x" is a stale hint in -# the linker template. Flipping it to "(rwx)" lets the linker place the -# function body in .data, where it is copied alongside initialized globals and -# becomes callable from an ISR even while flash is busy. -# -# LN882H already declares RAM0 as "(rwx)" and Realtek families use dedicated -# .image2.ram.text / .sram.text output sections — nothing to patch there. +# - RTL8720C (AmebaZ2): stock linker already consumes "*(.sram.text*)", no-op. +# - RTL8710B (AmebaZ): stock linker has ".image2.ram.text" — inject +# "*(.sram.text*)" into it. +# - BK72xx: stock linker has a (rw!x) SRAM region that blocks executable +# placement; flip to (rwx) and inject "KEEP(*(.sram.text*))" into the ".data" +# output (which is already flash-to-SRAM copied at startup by the SDK). +# - LN882H: stock linker has ".flash_copysection" which is flash-to-RAM0 +# copied at startup; inject "KEEP(*(.sram.text*))" there. -def _is_bk72xx(defines): +_MARKER = "/* esphome .sram.text */" +_KEEP_LINE = " KEEP(*(.sram.text*)) " + _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)") + + +def _detect(defines): prefix = "USE_LIBRETINY_VARIANT_" - bk = ("BK7231N", "BK7231T", "BK7231Q", "BK7251") for token in defines: if isinstance(token, tuple): token = token[0] if isinstance(token, str) and token.startswith(prefix): - return token[len(prefix):] in bk - return False + return token[len(prefix):] + return None -_RW_NO_X = re.compile(r"(\bram\s*\()\s*rw\s*!\s*x(\s*\))") +def _patch_bk72xx(content): + new_content = _BK_RW_NO_X.sub(r"\1rwx\2", content) + if _MARKER not in new_content: + new_content = _BK_DATA.sub(r"\1" + _KEEP_LINE, new_content, count=1) + return new_content -def _patch_directory(build_dir): +def _patch_ln882h(content): + if _MARKER in content: + return content + return _LN_COPY.sub(r"\1" + _KEEP_LINE, content, count=1) + + +def _patch_rtl8710b(content): + if _MARKER in content: + return content + return _RTL8710B_IMAGE2.sub(r"\1" + _KEEP_LINE, content, count=1) + + +def _patchers_for(variant): + if variant in ("BK7231N", "BK7231T", "BK7231Q", "BK7251"): + return (_patch_bk72xx,) + if variant == "LN882H": + return (_patch_ln882h,) + if variant == "RTL8710B": + return (_patch_rtl8710b,) + return () + + +def _patch_build_dir(patchers, build_dir): if not os.path.isdir(build_dir): return for name in os.listdir(build_dir): @@ -44,22 +75,24 @@ def _patch_directory(build_dir): path = os.path.join(build_dir, name) with open(path, "r", encoding="utf-8") as fh: content = fh.read() - patched = _RW_NO_X.sub(r"\1rwx\2", content) + patched = content + for patch in patchers: + patched = patch(patched) if patched != content: with open(path, "w", encoding="utf-8") as fh: fh.write(patched) - print( - "ESPHome: patched BK72xx linker script {} (rw!x -> rwx) so " - "IRAM_ATTR functions can be placed in .data".format(name) - ) + print("ESPHome: patched linker script {} for IRAM_ATTR placement".format(name)) -def _patch_ld_before_link(target, source, env): - _patch_directory(env.subst("$BUILD_DIR")) +def _pre_link(target, source, env): + _patch_build_dir(_patchers, env.subst("$BUILD_DIR")) -if _is_bk72xx(env.get("CPPDEFINES", [])): +_variant = _detect(env.get("CPPDEFINES", [])) +_patchers = _patchers_for(_variant) if _variant else () + +if _patchers: # LibreTiny writes the processed .ld templates into $BUILD_DIR during its # 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", _patch_ld_before_link) + env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", _pre_link) diff --git a/esphome/core/hal.h b/esphome/core/hal.h index aa36a1f03dd..466f864dc17 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -24,23 +24,13 @@ #elif defined(USE_LIBRETINY) // IRAM_ATTR places a function in SRAM so it is callable from an ISR even -// while flash is busy (XIP stall, OTA, logger flash write). The section used -// varies per family, based on what each linker script already supports: -// - RTL8710B (AmebaZ): ".image2.ram.text" output section exists. -// - RTL8720C (AmebaZ2): "*(.sram.text*)" is already consumed. -// - BK72xx / LN882H: the stock linker script has no RAM text section, so we -// use ".data.iram_text" which the linker's "*(.data.*)" glob folds into -// the .data output section. The SDK startup code copies .data from flash -// into SRAM before main() runs, so the function body ends up in executable -// RAM. Using ".data.*" instead of plain ".data" keeps the assembler happy -// (no section-attribute collision) while still landing in the right place. -#if defined(USE_LIBRETINY_VARIANT_RTL8710B) -#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text"))) -#elif defined(USE_LIBRETINY_VARIANT_RTL8720C) +// while flash is busy (XIP stall, OTA, logger flash write). All LibreTiny +// families use ".sram.text". RTL8720C (AmebaZ2) already consumes that +// section; RTL8710B (AmebaZ), BK72xx, and LN882H get it routed into their +// RAM-resident output section via patch_linker.py.script. Using a custom +// name avoids the assembler "setting incorrect section attributes" warning +// that ".data.*" triggers when we place executable code there. #define IRAM_ATTR __attribute__((noinline, section(".sram.text"))) -#else -#define IRAM_ATTR __attribute__((noinline, section(".data.iram_text"))) -#endif #define PROGMEM #else @@ -73,13 +63,11 @@ __attribute__((always_inline)) inline bool in_isr_context() { return ipsr != 0; #elif defined(USE_LIBRETINY_VARIANT_BK7231N) || defined(USE_LIBRETINY_VARIANT_BK7231T) || \ defined(USE_LIBRETINY_VARIANT_BK7231Q) || defined(USE_LIBRETINY_VARIANT_BK7251) - // BK72xx is ARM968E-S (ARM9). CPSR mode bits [4:0]: - // 0x10 USR, 0x13 SVC, 0x1F SYS are normal; others (IRQ=0x12, FIQ=0x11, - // ABT=0x17, UND=0x1B) are exception modes. - uint32_t cpsr; - __asm__ volatile("mrs %0, cpsr" : "=r"(cpsr)); - uint32_t mode = cpsr & 0x1Fu; - return mode != 0x10u && mode != 0x13u && mode != 0x1Fu; + // BK72xx is ARM968E-S (ARM9). The MRS CPSR instruction is ARM-only, and + // user code here may be built in Thumb mode. Defer to the FreeRTOS port + // helper (compiled in ARM mode by the SDK) which reads CPSR internally. + extern "C" uint32_t platform_is_in_interrupt_context(void); + return platform_is_in_interrupt_context() != 0; #elif defined(USE_LIBRETINY) // Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number; // non-zero means we're in a handler.