mirror of
https://github.com/esphome/esphome.git
synced 2026-09-22 20:48:43 +00:00
Make IRAM_ATTR functional on LibreTiny
Previously, IRAM_ATTR was an empty no-op on every LibreTiny family, so any ISR handler (gpio binary sensor, cc1101, sx126x/sx127x, mcp23xxx, pcf8574, pca9554, pca6416a, pi4ioe5v6408, tca9555, ...) lived in flash. When the ISR fired while flash was busy (XIP stall, OTA, logger flash write), the device could deadlock or crash. - hal.h: IRAM_ATTR now routes each family into a RAM-resident section (RTL8710B .image2.ram.text, RTL8720C .sram.text, BK72xx / LN882H .data, which the SDK startup code copies from flash into SRAM before main). Also adds esphome::in_isr_context() as a portable always_inline ISR detection helper: xPortInIsrContext on ESP32, PS.INTLEVEL on ESP8266, IPSR on Cortex-M cores, CPSR mode on BK72xx ARM9. - main_task.h: both notify helpers marked always_inline so IRAM callers keep the wake path in IRAM; removes the ESP32-only notify_any_context which moves into wake.h. - wake.h / wake.cpp: LibreTiny now shares the ESP32 wake path via a new wake_main_task_any_context() helper that picks between xTaskNotifyGive and vTaskNotifyGiveFromISR using in_isr_context(). wake_loop_any_context and wake_loop_isrsafe are now IRAM_ATTR entry points on LibreTiny too. - libretiny/patch_linker.py.script: pre-link hook modelled on the ESP8266 testing_mode patcher. BK72xx linker templates declare the SRAM region as (rw!x), blocking code placement in .data. The hook flips that to (rwx) so IRAM_ATTR functions can be emitted there; the MMU already permits RAM execution (the Wi-Fi driver runs from SRAM). No-op on the other families.
This commit is contained in:
@@ -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"),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -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 <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
+13
-9
@@ -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(); }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user