[socket] Add socket wake support for RP2040 (#14498)

This commit is contained in:
J. Nick Koston
2026-03-08 15:11:24 -10:00
committed by GitHub
parent d0285cdc41
commit c681dc8872
5 changed files with 93 additions and 12 deletions
@@ -11,6 +11,9 @@
#ifdef USE_ESP8266
#include <coredecls.h> // For esp_schedule()
#elif defined(USE_RP2040)
#include <hardware/sync.h> // For __sev(), __wfe()
#include <pico/time.h> // 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
+8 -4
View File
@@ -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<char, SOCKADDR_STR_LEN> 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
+5 -3
View File
@@ -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
+6 -2
View File
@@ -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:
+3 -1
View File
@@ -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
}