mirror of
https://github.com/esphome/esphome.git
synced 2026-05-20 17:52:00 +08:00
[socket] Add socket wake support for RP2040 (#14498)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user