diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index d697bd47a50..445a57809d2 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -11,6 +11,9 @@ #ifdef USE_ESP8266 #include // For esp_schedule() +#elif defined(USE_RP2040) +#include // For __sev(), __wfe() +#include // For add_alarm_in_ms(), cancel_alarm() #endif namespace esphome::socket { @@ -40,6 +43,72 @@ void IRAM_ATTR socket_wake() { s_socket_woke = true; esp_schedule(); } +#elif defined(USE_RP2040) +// RP2040 (non-FreeRTOS) socket wake using hardware WFE/SEV instructions. +// +// Same pattern as ESP8266's esp_delay()/esp_schedule(): set a one-shot timer, +// then sleep with __wfe(). Wake on either: +// - Timer alarm fires → callback calls __sev() → __wfe() returns → timeout +// - Socket data arrives → LWIP callback calls socket_wake() → __sev() → __wfe() returns → early wake +// +// CYW43 WiFi chip communicates via SPI interrupts on core 0. When data arrives, +// the GPIO interrupt fires → async_context pendsv processes CYW43/LWIP → recv/accept +// callbacks call socket_wake() → __sev() wakes the main loop from __wfe() sleep. +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_socket_woke = false; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_delay_expired = false; + +static int64_t alarm_callback(alarm_id_t id, void *user_data) { + (void) id; + (void) user_data; + s_delay_expired = true; + // Wake the main loop from __wfe() sleep — timeout expired. + __sev(); + // Return 0 = don't reschedule (one-shot) + return 0; +} + +void socket_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + // If a wake was already signalled, consume it and return immediately + // instead of going to sleep. This avoids losing a wake that arrived + // between loop iterations. + if (s_socket_woke) { + s_socket_woke = false; + return; + } + s_socket_woke = false; + s_delay_expired = false; + // Set a one-shot timer to wake us after the timeout. + // add_alarm_in_ms returns >0 on success, 0 if time already passed, <0 on error. + alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback, nullptr, true); + if (alarm <= 0) { + delay(ms); + return; + } + // Sleep until woken by either the timer alarm or socket_wake(). + // __wfe() may return spuriously (stale event register, other interrupts), + // so we loop checking both flags. + while (!s_socket_woke && !s_delay_expired) { + __wfe(); + } + // Cancel timer if we woke early (socket data arrived before timeout) + if (!s_delay_expired) + cancel_alarm(alarm); +} + +// No IRAM_ATTR equivalent needed: on RP2040, CYW43 async_context runs LWIP +// callbacks via pendsv (not hard IRQ), so they execute from flash safely. +void socket_wake() { + s_socket_woke = true; + // Wake the main loop from __wfe() sleep. __sev() is a global event that + // wakes any core sleeping in __wfe(). This is ISR-safe. + __sev(); +} #endif static const char *const TAG = "socket.lwip"; @@ -371,7 +440,7 @@ err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { } else { pbuf_cat(this->rx_buf_, pb); } -#ifdef USE_ESP8266 +#if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can process the received data. socket_wake(); #endif @@ -650,7 +719,7 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { sock->init(); this->accepted_sockets_[this->accepted_socket_count_++] = std::move(sock); LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_); -#ifdef USE_ESP8266 +#if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can accept the new connection. socket_wake(); #endif diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 0884e4ba3e6..a21bd647305 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -120,13 +120,17 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po /// Format sockaddr into caller-provided buffer, returns length written (excluding null) size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span buf); -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) /// Delay that can be woken early by socket activity. -/// On ESP8266, lwip callbacks set a flag and call esp_schedule() to wake the delay. +/// On ESP8266, uses esp_delay() with a callback that checks socket activity. +/// On RP2040, uses __wfe() (Wait For Event) to truly sleep until an interrupt +/// (for example, CYW43 GPIO or a timer alarm) fires and wakes the CPU. void socket_delay(uint32_t ms); -/// 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(). +/// Signal socket/IO activity and wake the main loop early. +/// On ESP8266: sets flag + esp_schedule(). +/// On RP2040: sets flag + __sev() (Send Event) to wake from __wfe(). +/// ISR-safe on both platforms. void socket_wake(); // NOLINT(readability-redundant-declaration) #endif diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index db1c8a0c0a1..8685bff360e 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -32,7 +32,7 @@ #include "esphome/components/status_led/status_led.h" #endif -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) #include "esphome/components/socket/socket.h" #endif @@ -713,8 +713,10 @@ void Application::yield_with_select_(uint32_t delay_ms) { } // No sockets registered or select() failed - use regular delay delay(delay_ms); -#elif defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) - // No select support but can wake on socket activity via esp_schedule() +#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) + // No select support but can wake on socket activity + // ESP8266: via esp_schedule() + // RP2040: via __sev()/__wfe() hardware sleep/wake socket::socket_delay(delay_ms); #else // No select support, use regular delay diff --git a/esphome/core/application.h b/esphome/core/application.h index 49253b63244..f357c6b1a3d 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -34,7 +34,7 @@ #endif #endif #endif // USE_SOCKET_SELECT_SUPPORT -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) namespace esphome::socket { void socket_wake(); // NOLINT(readability-redundant-declaration) } // namespace esphome::socket @@ -565,8 +565,12 @@ class Application { #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. + /// 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(); } +#elif defined(USE_RP2040) && defined(USE_SOCKET_IMPL_LWIP_TCP) + /// Wake the main event loop from any context. + /// Sets the socket wake flag and calls __sev() to exit __wfe() early. + static void wake_loop_any_context() { socket::socket_wake(); } #endif protected: diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 2879d4b5ab1..cce0c7b3e04 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -322,11 +322,13 @@ 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)) || (defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)) +#if (defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)) || \ + ((defined(USE_ESP8266) || defined(USE_RP2040)) && 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. + // RP2040: sets socket wake flag and calls __sev() to exit __wfe() early. Application::wake_loop_any_context(); #endif }