[libretiny] Make IRAM_ATTR functional on RTL87xx and LN882H (#15766)
CI / Create common environment (push) Has been cancelled
CI / Check pylint (push) Has been cancelled
CI / Run script/ci-custom (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.11) (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.14) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.13) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.14) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.11) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.14) (push) Has been cancelled
CI / Determine which jobs to run (push) Has been cancelled
CI / Run integration tests (push) Has been cancelled
CI / Run C++ unit tests (push) Has been cancelled
CI / Run CodSpeed benchmarks (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF (push) Has been cancelled
CI / Run script/clang-tidy for ESP8266 (push) Has been cancelled
CI / Run script/clang-tidy for ZEPHYR (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Has been cancelled
CI / Test components batch (${{ matrix.components }}) (push) Has been cancelled
CI / pre-commit.ci lite (push) Has been cancelled
CI / Build target branch for memory impact (push) Has been cancelled
CI / Build PR branch for memory impact (push) Has been cancelled
CI / Comment memory impact (push) Has been cancelled
CI / CI Status (push) Has been cancelled
Stale / stale (push) Has been cancelled
Lock closed issues and PRs / lock (push) Has been cancelled
Publish Release / Initialize build (push) Has been cancelled
Publish Release / Build and publish to PyPi (push) Has been cancelled
Publish Release / Build ESPHome amd64 (push) Has been cancelled
Publish Release / Build ESPHome arm64 (push) Has been cancelled
Publish Release / Publish ESPHome docker to dockerhub (push) Has been cancelled
Publish Release / Publish ESPHome docker to ghcr (push) Has been cancelled
Publish Release / Publish ESPHome ha-addon to dockerhub (push) Has been cancelled
Publish Release / Publish ESPHome ha-addon to ghcr (push) Has been cancelled
Publish Release / deploy-ha-addon-repo (push) Has been cancelled
Publish Release / deploy-esphome-schema (push) Has been cancelled
Publish Release / version-notifier (push) Has been cancelled

This commit is contained in:
J. Nick Koston
2026-04-16 09:38:49 -10:00
committed by GitHub
parent 6bb90a1268
commit 627e440bd6
11 changed files with 308 additions and 29 deletions
+5
View File
@@ -65,3 +65,8 @@ async def to_code(config):
@pins.PIN_SCHEMA_REGISTRY.register("bk72xx", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()
+17
View File
@@ -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,11 @@ 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 is a no-op on BK72xx (SDK masks FIQ+IRQ around flash ops).
# On other families, patch_linker.py routes .sram.text into the right
# RAM-executable output section and prints a post-link placement summary.
if FAMILY_COMPONENT[config[CONF_FAMILY]] != COMPONENT_BK72XX:
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 +556,13 @@ async def component_to_code(config):
_configure_lwip(config)
await cg.register_component(var, config)
# Called by writer.py
def copy_files() -> None:
script_dir = Path(__file__).parent
patch_linker_file = script_dir / "patch_linker.py.script"
copy_file_if_changed(
patch_linker_file,
CORE.relative_build_path("patch_linker.py"),
)
@@ -79,6 +79,11 @@ async def to_code(config):
@pins.PIN_SCHEMA_REGISTRY.register("{COMPONENT_LOWER}", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()
'''
BASE_CODE_BOARDS = '''
@@ -0,0 +1,171 @@
# pylint: disable=E0602
Import("env") # noqa
import os
import re
import subprocess
# ESPHome marks ISR code IRAM_ATTR, which on LibreTiny maps to a per-family
# section routed into RAM-executable memory (see esphome/core/hal.h).
#
# This script is NOT loaded on BK72xx (IRAM_ATTR is a no-op there; the SDK
# masks FIQ+IRQ around flash writes). On the remaining families:
# - RTL8710B: hal.h uses section(".image2.ram.text"); stock linker consumes it.
# - RTL8720C: hal.h uses section(".sram.text"); stock linker consumes it.
# - LN882H: stock linker has no glob for ".sram.text", so we inject
# KEEP(*(.sram.text*)) into ".flash_copysection" (> RAM0 AT> FLASH).
#
# All families also get a post-link summary showing where IRAM_ATTR landed.
_MARKER = "/* esphome .sram.text */"
# Strong assignments (not PROVIDE) so the symbols are always emitted in the
# ELF; PROVIDE symbols with no references can be garbage-collected.
_KEEP_LINE = (
" __esphome_sram_text_start = .; "
"KEEP(*(.sram.text*)) "
"__esphome_sram_text_end = .; "
+ _MARKER + "\n"
)
_LN_COPY = re.compile(r"(\.flash_copysection\s*:\s*\{\s*\n)")
def _detect(env):
prefix = "USE_LIBRETINY_VARIANT_"
# CPPDEFINES may hold strings or (name, value) tuples; BUILD_FLAGS holds
# the raw "-DNAME" strings. PlatformIO populates both, but the exact order
# vs. extra_scripts varies, so check both to be robust.
for token in env.get("CPPDEFINES", []):
if isinstance(token, (list, tuple)):
token = token[0]
if isinstance(token, str) and token.startswith(prefix):
return token[len(prefix):]
for flag in env.get("BUILD_FLAGS", []):
if isinstance(flag, str) and "-D" + prefix in flag:
name = flag.split("-D", 1)[1].split("=", 1)[0].strip()
if name.startswith(prefix):
return name[len(prefix):]
return None
KNOWN_VARIANTS = frozenset({
"LN882H",
"RTL8710B",
"RTL8720C",
})
def _inject_keep(host_section):
"""Return a patcher that injects _KEEP_LINE at the top of `host_section`."""
def patch(content):
if _MARKER in content:
return content
return host_section.sub(r"\1" + _KEEP_LINE, content, count=1)
return patch
# Variants not listed here intentionally have no .ld patcher:
# - RTL8710B: hal.h uses section(".image2.ram.text") which the stock linker
# already routes into .ram_image2.text (> BD_RAM).
# - RTL8720C: stock linker already consumes *(.sram.text*).
# - BK72xx (all): SDK masks FIQ+IRQ around flash writes, IRAM_ATTR is no-op.
_PATCHERS_BY_VARIANT = {
"LN882H": (_inject_keep(_LN_COPY),),
}
def _patchers_for(variant):
return _PATCHERS_BY_VARIANT.get(variant, ())
def _pre_link(target, source, env):
build_dir = env.subst("$BUILD_DIR")
ld_files = [f for f in os.listdir(build_dir) if f.endswith(".ld")]
patched = 0
for name in ld_files:
path = os.path.join(build_dir, name)
with open(path, "r", encoding="utf-8") as fh:
original = fh.read()
if _MARKER in original:
patched += 1
continue
content = original
for fn in _patchers:
content = fn(content)
if content != original:
with open(path, "w", encoding="utf-8") as fh:
fh.write(content)
print("ESPHome: patched {} for IRAM_ATTR placement".format(name))
patched += 1
if not patched:
raise RuntimeError(
"ESPHome: no .ld in {} was patched for IRAM_ATTR. Update the "
"regex in patch_linker.py.script (_PATCHERS_BY_VARIANT).".format(
build_dir
)
)
# Substrings matched against demangled names as a fallback on RTL8720C,
# where we cannot inject __esphome_sram_text_start/end markers.
_FALLBACK_SUBSTRINGS = ("wake_loop_any_context", "wake_loop_isrsafe",
"enable_loop_soon_any_context")
def _post_link(target, source, env):
"""Print where IRAM_ATTR ended up so users can confirm at a glance."""
elf = env.subst("$BUILD_DIR/${PROGNAME}.elf")
if not os.path.isfile(elf):
return
nm = env.subst("$NM")
try:
out = subprocess.check_output(
[nm, "--defined-only", "--demangle", elf], text=True
)
except (OSError, subprocess.CalledProcessError) as exc:
print("ESPHome: IRAM_ATTR summary unavailable (nm failed: {})".format(exc))
return
start = end = None
fallback = []
for line in out.splitlines():
parts = line.split(maxsplit=2)
if len(parts) != 3:
continue
addr_str, _kind, name = parts
if name == "__esphome_sram_text_start":
start = int(addr_str, 16)
elif name == "__esphome_sram_text_end":
end = int(addr_str, 16)
elif "veneer" not in name and any(s in name for s in _FALLBACK_SUBSTRINGS):
fallback.append(int(addr_str, 16))
print("ESPHome: IRAM_ATTR placement summary ({}):".format(_variant))
if start is not None and end is not None:
print(" .sram.text: {} bytes at 0x{:08x} - 0x{:08x}".format(end - start, start, end))
elif fallback:
lo, hi = min(fallback), max(fallback)
print(" IRAM symbols at 0x{:08x} - 0x{:08x} (approx {} bytes)".format(lo, hi, hi - lo))
else:
print(" no IRAM_ATTR symbols found")
if (_variant := _detect(env)) is None:
raise RuntimeError(
"ESPHome: could not determine LibreTiny variant from build flags. "
"patch_linker.py needs USE_LIBRETINY_VARIANT_* to route IRAM_ATTR "
"into SRAM; without it, ISR handlers would silently end up in flash."
)
if _variant not in KNOWN_VARIANTS:
raise RuntimeError(
"ESPHome: unknown LibreTiny variant {!r}; patch_linker.py does not "
"know how to route IRAM_ATTR into SRAM for this family. Update "
"patch_linker.py.script before shipping firmware.".format(_variant)
)
if _patchers := _patchers_for(_variant):
# 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", _pre_link)
# Post-link summary for every family that reaches this script.
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", _post_link)
+5
View File
@@ -65,3 +65,8 @@ async def to_code(config):
@pins.PIN_SCHEMA_REGISTRY.register("ln882x", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()
+5
View File
@@ -65,3 +65,8 @@ async def to_code(config):
@pins.PIN_SCHEMA_REGISTRY.register("rtl87xx", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()
+2 -2
View File
@@ -337,8 +337,8 @@ class Application {
/// @see esphome::wake_loop_threadsafe() in wake.h for platform details.
void wake_loop_threadsafe() { esphome::wake_loop_threadsafe(); }
#ifdef USE_ESP32
/// Wake from ISR (ESP32 only).
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
/// Wake from ISR (ESP32 and LibreTiny).
static void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px) { esphome::wake_loop_isrsafe(px); }
#endif
+72
View File
@@ -21,6 +21,35 @@
#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical")))
#define PROGMEM
#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).
// Each family uses a section its stock linker already routes to RAM:
// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the
// exception: its stock linker has no matching glob, so patch_linker.py
// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link.
//
// BK72xx (all variants) are left as a no-op: their SDK wraps flash
// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for
// the duration of every write, so no ISR fires while flash is stalled and
// the race IRAM_ATTR guards against cannot occur. The trade-off is that
// interrupts are delayed (not dropped) by up to ~20 ms during a sector
// erase, but that is an SDK-level choice and cannot be changed from this
// layer.
#if defined(USE_BK72XX)
#define IRAM_ATTR
#elif defined(USE_LIBRETINY_VARIANT_RTL8710B)
// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM).
#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text")))
#else
// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text.
// LN882H: patch_linker.py.script injects *(.sram.text*) into
// .flash_copysection (> RAM0 AT> FLASH).
#define IRAM_ATTR __attribute__((noinline, section(".sram.text")))
#endif
#define PROGMEM
#else
#define IRAM_ATTR
@@ -28,8 +57,51 @@
#endif
#ifdef USE_ESP32
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#endif
#ifdef USE_BK72XX
// Declared in the Beken FreeRTOS port (portmacro.h) and built in ARM mode so
// it is callable from Thumb code via interworking. The MRS CPSR instruction
// is ARM-only and user code here may be built in Thumb, so in_isr_context()
// defers to this port helper on BK72xx instead of reading CPSR inline.
extern "C" uint32_t platform_is_in_interrupt_context(void);
#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)
// ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is
// non-zero both in a real ISR and when user code masks interrupts. The
// ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule
// which is ISR-safe) so this helper is unused on this platform.
return false;
#elif defined(USE_RP2040)
uint32_t ipsr;
__asm__ volatile("mrs %0, ipsr" : "=r"(ipsr));
return ipsr != 0;
#elif defined(USE_BK72XX)
// BK72xx is ARM968E-S (ARM9); see extern declaration above.
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.
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();
+4 -15
View File
@@ -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
+3 -3
View File
@@ -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 ===
+19 -9
View File
@@ -28,17 +28,27 @@ 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.
void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken);
/// IRAM_ATTR entry point — defined in wake.cpp.
void wake_loop_any_context();
/// 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);
#ifdef portYIELD_FROM_ISR
portYIELD_FROM_ISR(px_higher_priority_task_woken);
#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(); }
// ARM9 FreeRTOS port (BK72xx) does not define portYIELD_FROM_ISR; the IRQ
// exit sequence performs the context switch if one was requested.
(void) px_higher_priority_task_woken;
#endif
} else {
esphome_main_task_notify();
}
}
/// IRAM_ATTR entry points — defined in wake.cpp.
void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken);
void wake_loop_any_context();
inline void wake_loop_threadsafe() { esphome_main_task_notify(); }