[core] Wake the main loop on its own task notification index on ESP32 (#20326)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Keith Burzinski
2026-10-07 21:17:16 -05:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 68a5e33182
commit 6a6d263c0d
7 changed files with 46 additions and 15 deletions
+4
View File
@@ -3074,6 +3074,10 @@ async def to_code(config):
# Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms
add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000)
# Main loop wakes use notification index 1; index 0 stays free for ESP-IDF waits
# such as pthread_join(), which a stray main-loop wake would otherwise end early.
add_idf_sdkconfig_option("CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES", 2)
# Place non-ISR FreeRTOS functions into flash instead of IRAM
# This saves up to 8KB of IRAM. ISR-safe functions (FromISR variants) stay in IRAM.
# In ESP-IDF 6.0 this becomes the default and CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH
+2 -2
View File
@@ -673,8 +673,8 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
#if defined(USE_LWIP_FAST_SELECT) && defined(ESPHOME_THREAD_MULTI_ATOMICS)
// Pairs with the TCP/IP thread's SYS_ARCH_UNPROTECT release on rcvevent so
// subsequent Socket::ready() checks in this iter observe the published state
// without a per-call memw. Wake is independent (xTaskNotifyGive/
// ulTaskNotifyTake), so non-losing. Skipped on MULTI_NO_ATOMICS (e.g.
// without a per-call memw. Wake is independent (esphome_main_task_notify/
// esphome_main_task_wait), so non-losing. Skipped on MULTI_NO_ATOMICS (e.g.
// BK72xx) — that path keeps `volatile` in esphome_lwip_socket_has_data()
// instead.
std::atomic_thread_fence(std::memory_order_acquire);
+5 -5
View File
@@ -99,10 +99,10 @@
// ESP32-C3/C6/H2) cross-core visibility is not an issue.
//
// FreeRTOS task notification value:
// Written by TCP/IP thread (xTaskNotifyGive in callback) and background tasks
// (xTaskNotifyGive in wake_main_loop). Read by main loop (ulTaskNotifyTake).
// Written by TCP/IP thread (esphome_main_task_notify in callback) and background tasks
// (esphome_main_task_notify in wake_main_loop). Read by main loop (esphome_main_task_wait).
// Safe: FreeRTOS notification APIs are thread-safe by design (use internal
// critical sections). Multiple concurrent xTaskNotifyGive calls are safe —
// critical sections). Multiple concurrent notify calls are safe —
// the notification count simply increments.
// USE_LWIP_FAST_SELECT is set via -D build flag (not cg.add_define) so it is
@@ -178,7 +178,7 @@ static void esphome_socket_event_callback(struct netconn *conn, enum netconn_evt
// s_original_callback is always valid here: hook_socket() sets it before swapping
// the callback pointer, so this wrapper cannot run until it's initialized.
s_original_callback(conn, evt, len);
// Wake the main loop task if sleeping in ulTaskNotifyTake().
// Wake the main loop task if sleeping in esphome_main_task_wait().
// Only notify on receive events to avoid spurious wakeups from send-ready events.
// NETCONN_EVT_ERROR is deliberately omitted: LwIP signals errors via RCVPLUS
// (rcvevent++ with a NULL pbuf or error in recvmbox), so error conditions
@@ -186,7 +186,7 @@ static void esphome_socket_event_callback(struct netconn *conn, enum netconn_evt
if (evt == NETCONN_EVT_RCVPLUS) {
#ifdef USE_OTA_PLATFORM_ESPHOME
// Mark OTA pending-enable only for events on its listen socket. MUST happen
// before xTaskNotifyGive so the flags are visible when the main task wakes.
// before the main task is notified so the flags are visible when it wakes.
if (conn == s_ota_listener_conn) {
esphome_wake_ota_component_any_context();
}
+4 -4
View File
@@ -33,9 +33,9 @@ struct lwip_sock *esphome_lwip_get_sock(int fd);
/// per-call `memw` that volatile would emit on Xtensa under default
/// -mserialize-volatile. Without atomics (e.g. BK72xx), the fence is skipped
/// and the volatile load provides ordering on its own.
/// Stale reads are harmless either way: the hooked event_callback
/// xTaskNotifyGives on RCVPLUS, so the next iteration re-snapshots and
/// ulTaskNotifyTake never loses a wake.
/// Stale reads are harmless either way: the hooked event_callback calls
/// esphome_main_task_notify() on RCVPLUS, so the next iteration re-snapshots and
/// esphome_main_task_wait() never loses a wake.
/// The offset and size are verified at compile time in lwip_fast_select.c.
static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) {
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
@@ -46,7 +46,7 @@ static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) {
}
/// Hook a socket's netconn callback to notify the main loop task on receive events.
/// Wraps the original event_callback with one that also calls xTaskNotifyGive().
/// Wraps the original event_callback with one that also calls esphome_main_task_notify().
/// Must be called from the main loop after socket creation.
/// The sock pointer must have been obtained from esphome_lwip_get_sock().
void esphome_lwip_hook_socket(struct lwip_sock *sock);
+26
View File
@@ -19,12 +19,25 @@ extern "C" {
extern TaskHandle_t esphome_main_task_handle;
#ifdef USE_ESP32
/// ESP-IDF waits on index 0 from the main task (e.g. pthread_join() uses an unfiltered
/// xTaskNotifyWait), so main-loop wakes on index 0 would end those waits early.
enum { ESPHOME_MAIN_TASK_NOTIFY_INDEX = 1 };
#if configTASK_NOTIFICATION_ARRAY_ENTRIES < 2
#error "CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES must be at least 2"
#endif
#endif
/// Wake the main loop task from another FreeRTOS task. NOT ISR-safe.
/// always_inline so callers placed in IRAM do not reference a flash-resident copy.
__attribute__((always_inline)) static inline void esphome_main_task_notify() {
TaskHandle_t task = esphome_main_task_handle;
if (task != NULL) {
#ifdef USE_ESP32
xTaskNotifyGiveIndexed(task, ESPHOME_MAIN_TASK_NOTIFY_INDEX);
#else
xTaskNotifyGive(task);
#endif
}
}
@@ -33,10 +46,23 @@ __attribute__((always_inline)) static inline void esphome_main_task_notify_from_
BaseType_t *px_higher_priority_task_woken) {
TaskHandle_t task = esphome_main_task_handle;
if (task != NULL) {
#ifdef USE_ESP32
vTaskNotifyGiveIndexedFromISR(task, ESPHOME_MAIN_TASK_NOTIFY_INDEX, px_higher_priority_task_woken);
#else
vTaskNotifyGiveFromISR(task, px_higher_priority_task_woken);
#endif
}
}
/// Block the main loop task until it is woken or the timeout expires.
__attribute__((always_inline)) static inline void esphome_main_task_wait(TickType_t ticks) {
#ifdef USE_ESP32
ulTaskNotifyTakeIndexed(ESPHOME_MAIN_TASK_NOTIFY_INDEX, pdTRUE, ticks);
#else
ulTaskNotifyTake(pdTRUE, ticks);
#endif
}
#ifdef __cplusplus
}
#endif
+4 -4
View File
@@ -43,15 +43,15 @@ namespace internal {
inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) {
// Fast path (with USE_LWIP_FAST_SELECT): FreeRTOS task notifications posted by the lwip
// event_callback wrapper (see lwip_fast_select.c) are the single source of truth for
// socket wake-ups. Every NETCONN_EVT_RCVPLUS posts an xTaskNotifyGive, so any notification
// that lands between wakes keeps the counter non-zero (next ulTaskNotifyTake returns
// immediately) or wakes a blocked Take directly. Additional wake sources:
// socket wake-ups. Every NETCONN_EVT_RCVPLUS calls esphome_main_task_notify(), so any notification
// that lands between wakes keeps the counter non-zero (next esphome_main_task_wait() returns
// immediately) or wakes a blocked wait directly. Additional wake sources:
// wake_loop_threadsafe() from background tasks, and the ms timeout.
if (ms == 0) [[unlikely]] {
yield();
return;
}
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms));
esphome_main_task_wait(pdMS_TO_TICKS(ms));
}
} // namespace internal
+1
View File
@@ -13,6 +13,7 @@ CONFIG_LIBC_NEWLIB=y
# esp32
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_FREERTOS_HZ=1000
CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2
CONFIG_ESP_TASK_WDT_INIT=y
CONFIG_ESP_TASK_WDT_PANIC=y
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n