[socket] Add socket wake support for RP2040

Enable instant wake on socket activity for RP2040 using ARM WFE/SEV
hardware instructions, matching ESP8266's esp_delay/esp_schedule pattern.

Previously, RP2040 used plain delay() in the main loop which could not
be interrupted by incoming socket data. Now LWIP recv/accept callbacks
call socket_wake() which sets a flag and sends a hardware event (__sev)
to wake the main loop from __wfe() sleep immediately.

The implementation uses a one-shot pico-sdk timer alarm for timeout
(same pattern as ESP8266's os_timer_arm) combined with __wfe() for
true hardware sleep between events.
This commit is contained in:
J. Nick Koston
2026-03-04 22:28:04 -10:00
parent 0e2a10c5f0
commit 6af723e87d
3 changed files with 72 additions and 9 deletions
@@ -13,6 +13,11 @@
#include <coredecls.h> // For esp_schedule()
#endif
#ifdef USE_RP2040
#include <hardware/sync.h> // For __sev(), __wfe()
#include <pico/time.h> // For add_alarm_in_ms(), cancel_alarm()
#endif
namespace esphome::socket {
#ifdef USE_ESP8266
@@ -42,6 +47,58 @@ void IRAM_ATTR socket_wake() {
}
#endif
#ifdef 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)
return;
s_socket_woke = false;
s_delay_expired = false;
// Set a one-shot timer to wake us after the timeout
alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback_, nullptr, true);
// 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 (alarm > 0 && !s_delay_expired)
cancel_alarm(alarm);
}
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";
// set to 1 to enable verbose lwip logging
@@ -371,7 +428,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 +707,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
/// (CYW43 GPIO, timer alarm) fires, then processes pending async_context work.
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