From bc672d358bed980ae1915cc94865b44b9b9787bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 15 Apr 2026 14:34:16 -1000 Subject: [PATCH] libretiny: scope BK72xx IRAM_ATTR to BK7231N; T/Q/7251 stay no-op BK7231T / BK7231Q / BK7251 share a LibreTiny linker template that only declares flash + ram, no executable RAM region. Their SDK wraps flash writes in GLOBAL_INT_DISABLE() which masks both FIQ and IRQ, so no ISR fires while flash is stalled and the "code in flash while flash is busy" scenario IRAM_ATTR is guarding against does not occur on those variants. Map IRAM_ATTR to nothing on them rather than orphan-linking .sram.text into flash and drop the T/Q/7251 entries from _PATCHERS_BY_VARIANT so the pre-link hook is not registered for them. BK7231N still gets the full fix (grown .itcm.code) because its SDK takes the defense-in-depth route of also placing flash/ISR/FreeRTOS critical code in ITCM, so our IRAM_ATTR functions need to live there too. Also make _grow_bk72xx_itcm fail loudly if the tcm/itcm declarations in the BK7231N template ever change shape, matching the fail-hard stance of the rest of the script. --- esphome/components/libretiny/__init__.py | 14 ++++++----- .../libretiny/patch_linker.py.script | 25 +++++++++++++------ esphome/core/hal.h | 21 +++++++++++----- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index b8833e5a55..10dc0fe984 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -467,15 +467,17 @@ async def component_to_code(config): # it for project source files only. GCC uses the last -O flag. build_src_flags += " -Os" cg.add_platformio_option("build_src_flags", build_src_flags) - # IRAM_ATTR on LibreTiny expands to section(".sram.text") (see - # esphome/core/hal.h). This pre-link hook rewrites the processed .ld - # files in the build dir so that section lands in an executable RAM - # output section on each family: - # - BK72xx: inject KEEP(*(.sram.text*)) into .itcm.code (the only - # rwx RAM region; main SRAM is rw!x on ARM968E-S). + # IRAM_ATTR on LibreTiny expands to section(".sram.text") on families + # where executable RAM is available (see esphome/core/hal.h). This pre- + # link hook rewrites the processed .ld files in the build dir so that + # section lands in each family's executable RAM output section: + # - BK7231N: grow .itcm from 4.5 kB to 8.5 kB (steal from .tcm) and + # inject KEEP(*(.sram.text*)) into .itcm.code. # - LN882H: inject KEEP(*(.sram.text*)) into .flash_copysection. # - RTL8710B: inject KEEP(*(.sram.text*)) into .image2.ram.text. # - RTL8720C: no-op (stock linker already consumes *(.sram.text*)). + # BK7231T/Q/7251 have no executable RAM region; their SDK disables IRQ + # + FIQ around flash writes, so IRAM_ATTR is left a no-op on them. cg.add_platformio_option("extra_scripts", ["pre:patch_linker.py"]) # dummy version code cg.add_define("USE_ARDUINO_VERSION_CODE", cg.RawExpression("VERSION_CODE(0, 0, 0)")) diff --git a/esphome/components/libretiny/patch_linker.py.script b/esphome/components/libretiny/patch_linker.py.script index 2ae7702c2c..ee6d3124f9 100644 --- a/esphome/components/libretiny/patch_linker.py.script +++ b/esphome/components/libretiny/patch_linker.py.script @@ -52,9 +52,20 @@ _BK_ITCM_REGION = re.compile( def _grow_bk72xx_itcm(content): # Shrink tcm by 4 kB: 60k - 512 -> 56k - 512. - new_content = _BK_TCM_LEN.sub(r"\g<1>56k\g<2>", content) + new_content, tcm_count = _BK_TCM_LEN.subn(r"\g<1>56k\g<2>", content) # Shift itcm origin back 4 kB and grow length: 4k + 512 -> 8k + 512. - new_content = _BK_ITCM_REGION.sub(r"\g<1>0x003FDE00\g<2>8k\g<3>", new_content) + new_content, itcm_count = _BK_ITCM_REGION.subn( + r"\g<1>0x003FDE00\g<2>8k\g<3>", new_content + ) + if tcm_count != 1 or itcm_count != 1: + raise RuntimeError( + "ESPHome: BK72xx linker script did not match the expected " + "tcm/itcm declarations (tcm matches: {}, itcm matches: {}); " + "refusing to link because IRAM_ATTR placement cannot be verified. " + "LibreTiny probably changed the bk7231*_bsp.template.ld layout, " + "update _BK_TCM_LEN / _BK_ITCM_REGION in patch_linker.py.script " + "before shipping firmware.".format(tcm_count, itcm_count) + ) return new_content @@ -96,13 +107,13 @@ def _inject_keep(host_section): return patch -# RTL8720C is absent: its stock linker already consumes *(.sram.text*), so -# no .ld patch is needed; the summary falls back to reading symbol addresses. +# Variants not listed here intentionally have no .ld patcher: +# - RTL8720C: stock linker already consumes *(.sram.text*). +# - BK7231T / BK7231Q / BK7251: SDK wraps flash ops in GLOBAL_INT_DISABLE() +# (FIQ + IRQ masked), so no ISR fires during a flash stall and IRAM_ATTR +# is a no-op on those variants (see esphome/core/hal.h). _PATCHERS_BY_VARIANT = { "BK7231N": (_grow_bk72xx_itcm, _inject_keep(_BK_ITCM)), - "BK7231T": (_grow_bk72xx_itcm, _inject_keep(_BK_ITCM)), - "BK7231Q": (_grow_bk72xx_itcm, _inject_keep(_BK_ITCM)), - "BK7251": (_grow_bk72xx_itcm, _inject_keep(_BK_ITCM)), "LN882H": (_inject_keep(_LN_COPY),), "RTL8710B": (_inject_keep(_RTL8710B_IMAGE2),), } diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 35ee55076a..f96cb48598 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -24,13 +24,22 @@ #elif defined(USE_LIBRETINY) // IRAM_ATTR places a function in executable RAM so it is callable from an -// ISR even while flash is busy (XIP stall, OTA, logger flash write). All -// LibreTiny families use ".sram.text"; patch_linker.py.script routes it -// into each family's RAM-executable output section (.itcm.code on BK72xx, -// .image2.ram.text on RTL8710B, .flash_copysection on LN882H). RTL8720C's -// stock linker already consumes *(.sram.text*) via its .ram.code_text -// output. +// ISR even while flash is busy (XIP stall, OTA, logger flash write). +// patch_linker.py.script routes ".sram.text" into each family's RAM- +// executable output section: .itcm.code on BK7231N, .image2.ram.text on +// RTL8710B, .flash_copysection on LN882H, stock *(.sram.text*) glob on +// RTL8720C. +// +// BK7231T/Q/7251 are left as a no-op: their SDK wraps flash operations in +// GLOBAL_INT_DISABLE() which masks FIQ + IRQ for the duration of the +// write, so no ISR fires while flash is stalled and the scenario +// IRAM_ATTR guards against does not occur there. +#if defined(USE_LIBRETINY_VARIANT_BK7231T) || defined(USE_LIBRETINY_VARIANT_BK7231Q) || \ + defined(USE_LIBRETINY_VARIANT_BK7251) +#define IRAM_ATTR +#else #define IRAM_ATTR __attribute__((noinline, section(".sram.text"))) +#endif #define PROGMEM #else