libretiny: fix BK72xx Thumb asm; unify IRAM_ATTR on .sram.text

Two fixes on top of the initial landing:

- BK72xx builds Thumb-mode TUs alongside its ARM FreeRTOS port, and the
  MRS CPSR instruction is ARM-only, so in_isr_context() failed to
  assemble on cb3s. Delegate to the port's own platform_is_in_interrupt_context()
  helper (declared extern "C" in portmacro.h and built in ARM mode) instead
  of embedding inline CPSR reads in Thumb code.

- Section name unified to ".sram.text" for every LibreTiny family to avoid
  the "setting incorrect section attributes for .data.*" GAS warning. The
  pre-link patcher now knows how to route that section into each family's
  RAM-resident output:
    * BK72xx: flip SRAM region (rw!x) -> (rwx) and inject
      KEEP(*(.sram.text*)) into .data : { ... }
    * LN882H: inject KEEP(*(.sram.text*)) into .flash_copysection : { ... }
    * RTL8710B: inject KEEP(*(.sram.text*)) into .image2.ram.text : { ... }
    * RTL8720C: no-op (linker already consumes *(.sram.text*))
  The injection uses a "/* esphome .sram.text */" marker so repeated runs
  are idempotent.
This commit is contained in:
J. Nick Koston
2026-04-15 13:53:35 -10:00
parent f404768fd1
commit e5b46540ee
2 changed files with 73 additions and 52 deletions
@@ -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)
+11 -23
View File
@@ -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.