From fcb21998f81ddb7db6970ed5db51e3c358038057 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 15:35:48 -1000 Subject: [PATCH 1/3] Guard portYIELD_FROM_ISR with USE_ESP32 LibreTiny's FreeRTOS port does not provide portYIELD_FROM_ISR. On ARM Cortex-M, FreeRTOS internally sets the PendSV bit when vTaskNotifyGiveFromISR wakes a higher-priority task, so the context switch happens automatically when the ISR returns. --- esphome/core/lwip_fast_select.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index a3cc6ab8fe..0722842031 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -261,7 +261,13 @@ void IRAM_ATTR esphome_lwip_wake_main_loop_any_context(void) { #endif int px_higher_priority_task_woken = 0; esphome_lwip_wake_main_loop_from_isr(&px_higher_priority_task_woken); +#ifdef USE_ESP32 + // On ESP32, explicitly request context switch if a higher-priority task was woken. + // On LibreTiny (ARM Cortex-M), portYIELD_FROM_ISR is not available — FreeRTOS + // sets the PendSV bit internally when needed, so the switch happens automatically + // when the ISR returns. portYIELD_FROM_ISR(px_higher_priority_task_woken); +#endif } else { esphome_lwip_wake_main_loop(); } From fde32555dc603ae2e9bde93dd12e0d537ff079ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 16:32:05 -1000 Subject: [PATCH 2/3] Restrict wake_loop_any_context to ESP32 only LibreTiny lacks IRAM_ATTR support needed for ISR-safe paths, and BK72xx doesn't support the mrs ipsr instruction in Thumb mode. The existing fast select functionality on LibreTiny is unaffected. --- esphome/core/application.h | 2 ++ esphome/core/component.cpp | 8 +++----- esphome/core/lwip_fast_select.c | 28 ++++------------------------ esphome/core/lwip_fast_select.h | 5 ++++- 4 files changed, 13 insertions(+), 30 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 1a3b027f25..d2345e2b0b 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -510,10 +510,12 @@ class Application { esphome_lwip_wake_main_loop_from_isr(px_higher_priority_task_woken); } +#ifdef USE_ESP32 /// Wake the main event loop from any context (ISR, thread, or main loop). /// Detects the calling context and uses the appropriate FreeRTOS API. static void IRAM_ATTR wake_loop_any_context() { esphome_lwip_wake_main_loop_any_context(); } #endif +#endif #endif protected: diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 0154bd6735..5afd901da2 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -315,7 +315,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // This method is thread and ISR-safe because: // 1. Only performs simple assignments to volatile variables (atomic on all platforms) // 2. No read-modify-write operations that could be interrupted - // 3. No memory allocation or object construction; the only call (wake_loop_any_context) is ISR-safe + // 3. No memory allocation or object construction; on ESP32 the only call (wake_loop_any_context) is ISR-safe // 4. IRAM_ATTR ensures code is in IRAM, not flash (required for ISR execution) // 5. Components are never destroyed, so no use-after-free concerns // 6. App is guaranteed to be initialized before any ISR could fire @@ -323,12 +323,10 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // 8. Race condition with main loop is handled by clearing flag before processing this->pending_enable_loop_ = true; App.has_pending_enable_loop_requests_ = true; -#ifdef USE_LWIP_FAST_SELECT +#if defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32) // Wake the main loop if sleeping in ulTaskNotifyTake(). Without this, // the main loop would not wake until the select timeout expires (~16ms). - // Uses platform-specific context detection to choose the correct notify - // API (task vs ISR), e.g. xPortInIsrContext() on ESP32 or IPSR register - // on LibreTiny — safe from any calling context. + // Uses xPortInIsrContext() to choose the correct FreeRTOS notify API. Application::wake_loop_any_context(); #endif } diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index 0722842031..989f66e9be 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -239,38 +239,18 @@ void IRAM_ATTR esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task } // Wake the main loop from any context (ISR, thread, or main loop). -// Detects ISR context and delegates to the appropriate variant: -// ESP32 (Xtensa/RISC-V): xPortInIsrContext() — checks interrupt nesting counter -// LibreTiny (ARM Cortex-M): IPSR register read via inline asm (0 = task context) -#ifdef USE_LIBRETINY -// Read the ARM Cortex-M IPSR register directly via inline asm. -// Avoids depending on CMSIS __get_IPSR() which may not be declared/available -// in all LibreTiny chip family toolchains (e.g. beken-72xx). -static inline IRAM_ATTR uint32_t esphome_get_ipsr(void) { - uint32_t ipsr = 0; - __asm volatile("mrs %0, ipsr" : "=r"(ipsr)); - return ipsr; -} -#endif - -void IRAM_ATTR esphome_lwip_wake_main_loop_any_context(void) { +// ESP32-only: uses xPortInIsrContext() to detect ISR context. +// LibreTiny is excluded because it lacks IRAM_ATTR support needed for ISR-safe paths. #ifdef USE_ESP32 +void IRAM_ATTR esphome_lwip_wake_main_loop_any_context(void) { if (xPortInIsrContext()) { -#else - if (esphome_get_ipsr() != 0) { -#endif int px_higher_priority_task_woken = 0; esphome_lwip_wake_main_loop_from_isr(&px_higher_priority_task_woken); -#ifdef USE_ESP32 - // On ESP32, explicitly request context switch if a higher-priority task was woken. - // On LibreTiny (ARM Cortex-M), portYIELD_FROM_ISR is not available — FreeRTOS - // sets the PendSV bit internally when needed, so the switch happens automatically - // when the ISR returns. portYIELD_FROM_ISR(px_higher_priority_task_woken); -#endif } else { esphome_lwip_wake_main_loop(); } } +#endif #endif // USE_LWIP_FAST_SELECT diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index ded0590359..6fce34fd76 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -34,8 +34,11 @@ void esphome_lwip_wake_main_loop(void); void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); /// Wake the main loop task from any context (ISR, thread, or main loop). -/// Detects the calling context and uses the appropriate FreeRTOS API. +/// ESP32-only: uses xPortInIsrContext() to detect ISR context. +/// LibreTiny lacks IRAM_ATTR support needed for ISR-safe paths. +#ifdef USE_ESP32 void esphome_lwip_wake_main_loop_any_context(void); +#endif #ifdef __cplusplus } From 04010664ce21a13986e008dead2a5111234995bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 15:25:23 -1000 Subject: [PATCH 3/3] [core] Add ESP8266 support to wake_loop_any_context() Wire Application::wake_loop_any_context() to socket::socket_wake() on ESP8266. This allows any component calling wake_loop_any_context() from an ISR or callback to immediately wake the main loop from esp_delay() sleep, instead of waiting up to 16ms for the timeout. Changes: - Add IRAM_ATTR to socket_wake() so it's safe to call from ISR context - Add ESP8266 wake_loop_any_context() backed by socket::socket_wake() - Extend enable_loop_soon_any_context() to wake the loop on ESP8266 --- esphome/components/socket/lwip_raw_tcp_impl.cpp | 2 +- esphome/components/socket/socket.h | 3 ++- esphome/core/application.h | 9 +++++++++ esphome/core/component.cpp | 9 +++++---- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 6e95f5bc7a..87a12cab38 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -41,7 +41,7 @@ void socket_delay(uint32_t ms) { esp_delay(ms, []() { return !s_socket_woke; }); } -void socket_wake() { +void IRAM_ATTR socket_wake() { s_socket_woke = true; esp_schedule(); } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index a771e2fe1a..13c69f5860 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -126,7 +126,8 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s /// On ESP8266, lwip callbacks set a flag and call esp_schedule() to wake the delay. void socket_delay(uint32_t ms); -/// Called by lwip callbacks to signal socket activity and wake delay. +/// Signal socket/IO activity and wake the main loop from esp_delay() early. +/// ISR-safe: uses IRAM_ATTR internally and only sets a volatile flag + esp_schedule(). void socket_wake(); #endif diff --git a/esphome/core/application.h b/esphome/core/application.h index d2345e2b0b..139bac0c6d 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -33,6 +33,9 @@ #endif #endif #endif // USE_SOCKET_SELECT_SUPPORT +#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#include "esphome/components/socket/socket.h" +#endif #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" @@ -518,6 +521,12 @@ class Application { #endif #endif +#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) + /// Wake the main event loop from any context (ISR, thread, or main loop). + /// On ESP8266: sets the socket wake flag and calls esp_schedule() to exit esp_delay() early. + static void IRAM_ATTR wake_loop_any_context() { socket::socket_wake(); } +#endif + protected: friend Component; friend class socket::Socket; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 5afd901da2..3ed158c9b1 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -323,10 +323,11 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // 8. Race condition with main loop is handled by clearing flag before processing this->pending_enable_loop_ = true; App.has_pending_enable_loop_requests_ = true; -#if defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32) - // Wake the main loop if sleeping in ulTaskNotifyTake(). Without this, - // the main loop would not wake until the select timeout expires (~16ms). - // Uses xPortInIsrContext() to choose the correct FreeRTOS notify API. +#if (defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)) || (defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)) + // Wake the main loop from sleep. Without this, the main loop would not + // wake until the select/delay timeout expires (~16ms). + // ESP32: uses xPortInIsrContext() to choose the correct FreeRTOS notify API. + // ESP8266: sets socket wake flag and calls esp_schedule() to exit esp_delay() early. Application::wake_loop_any_context(); #endif }