diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 656eee6d7bf..0635e5b7884 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -1,5 +1,6 @@ import json import logging +from pathlib import Path import esphome.codegen as cg import esphome.config_validation as cv @@ -24,6 +25,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.core.config import BOARD_MAX_LENGTH +from esphome.helpers import copy_file_if_changed from esphome.storage_json import StorageJSON from . import gpio # noqa @@ -465,6 +467,10 @@ 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) + # Patch the linker script to add a ".sram.text" output section so IRAM_ATTR + # (defined in esphome/core/hal.h) places code in SRAM on BK72xx and LN882H. + # No-op on RTL8710B/RTL8720C whose stock linker scripts already provide it. + 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)")) # decrease web server stack size (16k words -> 4k words) @@ -549,3 +555,13 @@ async def component_to_code(config): _configure_lwip(config) await cg.register_component(var, config) + + +# Called by writer.py +def copy_files() -> None: + dir = Path(__file__).parent + patch_linker_file = dir / "patch_linker.py.script" + copy_file_if_changed( + patch_linker_file, + CORE.relative_build_path("patch_linker.py"), + ) diff --git a/esphome/components/libretiny/patch_linker.py.script b/esphome/components/libretiny/patch_linker.py.script new file mode 100644 index 00000000000..56a42db0ee0 --- /dev/null +++ b/esphome/components/libretiny/patch_linker.py.script @@ -0,0 +1,64 @@ +# pylint: disable=E0602 +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") on BK72xx in esphome/core/hal.h) into +# the .data output section, which is the only section the SDK startup code +# copies from flash into SRAM before main() runs. +# +# 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. + + +def _is_bk72xx(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 + + +_RW_NO_X = re.compile(r"(\bram\s*\()\s*rw\s*!\s*x(\s*\))") + + +def _patch_directory(build_dir): + if not os.path.isdir(build_dir): + return + for name in os.listdir(build_dir): + if not name.endswith(".ld"): + continue + 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) + 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) + ) + + +def _patch_ld_before_link(target, source, env): + _patch_directory(env.subst("$BUILD_DIR")) + + +if _is_bk72xx(env.get("CPPDEFINES", [])): + # 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) diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 03a30b7459f..4009950ea04 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -21,6 +21,25 @@ #define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical"))) #define PROGMEM +#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: stock linker script has no RAM text section, so we +// piggyback on ".data" — the SDK startup code copies .data from flash into +// SRAM before main() runs, so the function body ends up in executable RAM. +#if defined(USE_LIBRETINY_VARIANT_RTL8710B) +#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text"))) +#elif defined(USE_LIBRETINY_VARIANT_RTL8720C) +#define IRAM_ATTR __attribute__((noinline, section(".sram.text"))) +#else +#define IRAM_ATTR __attribute__((noinline, section(".data"))) +#endif +#define PROGMEM + #else #define IRAM_ATTR @@ -28,8 +47,48 @@ #endif +#ifdef USE_ESP32 +#include +#include +#endif + namespace esphome { +/// Returns true when executing inside an interrupt handler. +/// always_inline so callers placed in IRAM keep the detection in IRAM. +__attribute__((always_inline)) inline bool in_isr_context() { +#if defined(USE_ESP32) + return xPortInIsrContext() != 0; +#elif defined(USE_ESP8266) + // Xtensa LX106 PS.INTLEVEL[3:0]. Non-zero indicates interrupt in progress. + uint32_t ps; + __asm__ volatile("rsr.ps %0" : "=r"(ps)); + return (ps & 0xF) != 0; +#elif defined(USE_RP2040) + uint32_t ipsr; + __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); + 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; +#elif defined(USE_LIBRETINY) + // Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number; + // non-zero means we're in a handler. + uint32_t ipsr; + __asm__ volatile("mrs %0, ipsr" : "=r"(ipsr)); + return ipsr != 0; +#else + // Host and any future platform without an ISR concept. + return false; +#endif +} + void yield(); uint32_t millis(); uint64_t millis_64(); diff --git a/esphome/core/main_task.h b/esphome/core/main_task.h index ed2885d2e25..3aa8669e445 100644 --- a/esphome/core/main_task.h +++ b/esphome/core/main_task.h @@ -20,7 +20,8 @@ extern "C" { extern TaskHandle_t esphome_main_task_handle; /// Wake the main loop task from another FreeRTOS task. NOT ISR-safe. -static inline void esphome_main_task_notify() { +/// always_inline so callers placed in IRAM do not reference a flash-resident copy. +__attribute__((always_inline)) static inline void esphome_main_task_notify() { TaskHandle_t task = esphome_main_task_handle; if (task != NULL) { xTaskNotifyGive(task); @@ -28,26 +29,14 @@ static inline void esphome_main_task_notify() { } /// Wake the main loop task from an ISR. ISR-safe. -static inline void esphome_main_task_notify_from_isr(BaseType_t *px_higher_priority_task_woken) { +__attribute__((always_inline)) static inline void esphome_main_task_notify_from_isr( + BaseType_t *px_higher_priority_task_woken) { TaskHandle_t task = esphome_main_task_handle; if (task != NULL) { vTaskNotifyGiveFromISR(task, px_higher_priority_task_woken); } } -#ifdef USE_ESP32 -/// Wake the main loop from any context (ISR or task). ESP32-only (needs xPortInIsrContext). -static inline void esphome_main_task_notify_any_context() { - if (xPortInIsrContext()) { - int px_higher_priority_task_woken = 0; - esphome_main_task_notify_from_isr(&px_higher_priority_task_woken); - portYIELD_FROM_ISR(px_higher_priority_task_woken); - } else { - esphome_main_task_notify(); - } -} -#endif - #ifdef __cplusplus } #endif diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index b6b59b59909..3709fa88ac7 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -12,12 +12,12 @@ namespace esphome { -// === ESP32 — IRAM_ATTR entry points === -#ifdef USE_ESP32 +// === ESP32 / LibreTiny — IRAM_ATTR entry points === +#if defined(USE_ESP32) || defined(USE_LIBRETINY) void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken) { esphome_main_task_notify_from_isr(px_higher_priority_task_woken); } -void IRAM_ATTR wake_loop_any_context() { esphome_main_task_notify_any_context(); } +void IRAM_ATTR wake_loop_any_context() { wake_main_task_any_context(); } #endif // === ESP8266 / RP2040 === diff --git a/esphome/core/wake.h b/esphome/core/wake.h index a8c9b7ad08b..f0a2d34b502 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -28,17 +28,21 @@ extern volatile bool g_main_loop_woke; // === ESP32 / LibreTiny (FreeRTOS) === #if defined(USE_ESP32) || defined(USE_LIBRETINY) -#ifdef USE_ESP32 -/// IRAM_ATTR entry point — defined in wake.cpp. +/// Wake the main loop from any context (ISR or task). +/// always_inline so callers placed in IRAM keep the whole wake path in IRAM. +__attribute__((always_inline)) inline void wake_main_task_any_context() { + if (in_isr_context()) { + BaseType_t px_higher_priority_task_woken = pdFALSE; + esphome_main_task_notify_from_isr(&px_higher_priority_task_woken); + portYIELD_FROM_ISR(px_higher_priority_task_woken); + } else { + esphome_main_task_notify(); + } +} + +/// IRAM_ATTR entry points — defined in wake.cpp. void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken); -/// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_any_context(); -#else -/// LibreTiny: IRAM_ATTR is not functional and the FreeRTOS port does not -/// provide vTaskNotifyGiveFromISR/portYIELD_FROM_ISR, so ISR-safe wake -/// is not possible. xTaskNotifyGive is used as the best available option. -inline void wake_loop_any_context() { esphome_main_task_notify(); } -#endif inline void wake_loop_threadsafe() { esphome_main_task_notify(); }