From 92f93e128fd4be7691105e807c143f8dd372ee57 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Apr 2026 14:47:52 -1000 Subject: [PATCH] [esphome.ota] Drop unnecessary IRAM_ATTR from wake hook The wake hook is called only from: - esphome_socket_event_callback (lwip fast select, LwIP TCP/IP task context) - LWIPRawListenImpl::accept_fn_ (raw TCP accept callback) - Application::yield_with_select_ (host select fallback, main thread) None of those contexts require IRAM-resident code. The LwIP fast-select event callback itself is not IRAM_ATTR; the raw-TCP accept callback runs from a low-priority user IRQ on RP2040 where IRAM_ATTR is a no-op anyway (it's an ESP32-specific section attribute for code that must run while flash cache is disabled). This is not a real ISR path the way enable_loop_soon_any_context() is (which is called from GPIO ISRs and genuinely does need IRAM). Removing IRAM_ATTR frees scarce IRAM on ESP32. --- esphome/core/application.cpp | 2 +- esphome/core/application.h | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 8373c672ed..9b7091f59a 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -453,7 +453,7 @@ void Application::enable_pending_loops_() { // Called from the LwIP TCP/IP task via esphome_socket_event_callback() on NETCONN_EVT_RCVPLUS. // Only marks the OTA component as pending loop-enable; the fast-select callback itself has // already woken the main task via xTaskNotifyGive(). -extern "C" void IRAM_ATTR esphome_wake_ota_component_any_context() { App.wake_ota_component_any_context(); } +extern "C" void esphome_wake_ota_component_any_context() { App.wake_ota_component_any_context(); } #endif #ifdef USE_LWIP_FAST_SELECT diff --git a/esphome/core/application.h b/esphome/core/application.h index 100ff3440d..67ef1d666f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -564,13 +564,14 @@ class Application { /// a new connection arrives on the listening socket. OTA disables its own /// loop while idle to avoid per-tick dispatch overhead. void set_ota_wake_component(Component *component) { this->ota_wake_component_ = component; } - /// Mark the registered OTA component (if any) for loop re-enable from any - /// context. Intentionally does NOT call wake_loop_any_context() — every - /// caller (lwip fast-select callback, raw-tcp accept callback, host - /// select() return path) has already woken the main loop, so a second wake - /// here would be redundant. Application is a friend of Component, so we - /// set the pending-enable flags directly. - void IRAM_ATTR wake_ota_component_any_context() { + /// Mark the registered OTA component (if any) for loop re-enable. Intentionally + /// does NOT call wake_loop_any_context() — every caller (lwip fast-select + /// callback, raw-tcp accept callback, host select() return path) has already + /// woken the main loop, so a second wake here would be redundant. Application + /// is a friend of Component, so we set the pending-enable flags directly. + /// Not IRAM_ATTR: all callers run in task / user-IRQ context, not a real ISR, + /// and the LwIP event callbacks that invoke this are not IRAM-resident either. + void wake_ota_component_any_context() { if (this->ota_wake_component_ != nullptr) { this->ota_wake_component_->pending_enable_loop_ = true; this->has_pending_enable_loop_requests_ = true;