Merge branch 'isr-wake-enable-loop-soon' into integration

This commit is contained in:
J. Nick Koston
2026-02-28 10:57:42 -10:00
6 changed files with 53 additions and 99 deletions
@@ -2,7 +2,6 @@
#include "uart_component_esp_idf.h"
#include <cinttypes>
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -10,6 +9,10 @@
#include "driver/gpio.h"
#include "soc/gpio_num.h"
#ifdef USE_UART_WAKE_LOOP_ON_RX
#include "esphome/core/application.h"
#endif
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
@@ -107,12 +110,6 @@ void IDFUARTComponent::load_settings(bool dump_config) {
esp_err_t err;
if (uart_is_driver_installed(this->uart_num_)) {
#ifdef USE_UART_WAKE_LOOP_ON_RX
if (this->rx_event_task_handle_ != nullptr) {
vTaskDelete(this->rx_event_task_handle_);
this->rx_event_task_handle_ = nullptr;
}
#endif
err = uart_driver_delete(this->uart_num_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err));
@@ -120,20 +117,13 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return;
}
}
#ifdef USE_UART_WAKE_LOOP_ON_RX
constexpr int event_queue_size = 20;
QueueHandle_t *event_queue_ptr = &this->uart_event_queue_;
#else
constexpr int event_queue_size = 0;
QueueHandle_t *event_queue_ptr = nullptr;
#endif
err = uart_driver_install(this->uart_num_, // UART number
this->rx_buffer_size_, // RX ring buffer size
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
// block task until all data has been sent out
event_queue_size, // event queue size/depth
event_queue_ptr, // event queue
0 // Flags used to allocate the interrupt
0, // event queue size/depth
nullptr, // event queue
0 // Flags used to allocate the interrupt
);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
@@ -213,8 +203,10 @@ void IDFUARTComponent::load_settings(bool dump_config) {
}
#ifdef USE_UART_WAKE_LOOP_ON_RX
// Start the RX event task to enable low-latency data notifications
this->start_rx_event_task_();
// Register ISR callback to wake the main loop when UART data arrives.
// The callback runs in ISR context and uses vTaskNotifyGiveFromISR() to
// wake the main loop task directly — no queue or FreeRTOS task needed.
uart_set_select_notif_callback(this->uart_num_, IDFUARTComponent::uart_rx_isr_callback);
#endif // USE_UART_WAKE_LOOP_ON_RX
if (dump_config) {
@@ -345,71 +337,12 @@ void IDFUARTComponent::flush() {
void IDFUARTComponent::check_logger_conflict() {}
#ifdef USE_UART_WAKE_LOOP_ON_RX
void IDFUARTComponent::start_rx_event_task_() {
// Create FreeRTOS task to monitor UART events
BaseType_t result = xTaskCreate(rx_event_task_func, // Task function
"uart_rx_evt", // Task name (max 16 chars)
2240, // Stack size in bytes (~2.2KB); increase if needed for logging
this, // Task parameter (this pointer)
tskIDLE_PRIORITY + 1, // Priority (low, just above idle)
&this->rx_event_task_handle_ // Task handle
);
if (result != pdPASS) {
ESP_LOGE(TAG, "Failed to create RX event task");
return;
}
ESP_LOGV(TAG, "RX event task started");
}
// FreeRTOS task that relays UART ISR events to the main loop.
// This task exists because wake_loop_threadsafe() is not ISR-safe (it uses a
// UDP loopback socket), so we need a task as an ISR-to-main-loop trampoline.
// IMPORTANT: This task must NOT call any UART wrapper methods (read_array,
// write_array, peek_byte, etc.) or touch has_peek_/peek_byte_ — all reading
// is done by the main loop. This task only reads from the event queue and
// calls App.wake_loop_threadsafe().
void IDFUARTComponent::rx_event_task_func(void *param) {
auto *self = static_cast<IDFUARTComponent *>(param);
uart_event_t event;
ESP_LOGV(TAG, "RX event task running");
// Run forever - task lifecycle matches component lifecycle
while (true) {
// Wait for UART events (blocks efficiently)
if (xQueueReceive(self->uart_event_queue_, &event, portMAX_DELAY) == pdTRUE) {
switch (event.type) {
case UART_DATA:
// Data available in UART RX buffer - wake the main loop
ESP_LOGVV(TAG, "Data event: %d bytes", event.size);
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
break;
case UART_FIFO_OVF:
case UART_BUFFER_FULL:
// Don't call uart_flush_input() here — this task does not own the read side.
// ESP-IDF examples flush on overflow because the same task handles both events
// and reads, so flush and read are serialized. Here, reads happen on the main
// loop, so flushing from this task races with read_array() and can destroy data
// mid-read. The driver self-heals without an explicit flush: uart_read_bytes()
// calls uart_check_buf_full() after each chunk, which moves stashed FIFO bytes
// into the ring buffer and re-enables RX interrupts once space is freed.
ESP_LOGW(TAG, "FIFO overflow or ring buffer full");
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
break;
default:
// Ignore other event types
ESP_LOGVV(TAG, "Event type: %d", event.type);
break;
}
}
// ISR callback invoked by the ESP-IDF UART driver when data arrives.
// Wakes the main loop directly via vTaskNotifyGiveFromISR() — no queue or task needed.
void IRAM_ATTR IDFUARTComponent::uart_rx_isr_callback(uart_port_t uart_num, uart_select_notif_t uart_select_notif,
BaseType_t *task_woken) {
if (uart_select_notif == UART_SELECT_READ_NOTIF) {
Application::wake_loop_isrsafe(task_woken);
}
}
#endif // USE_UART_WAKE_LOOP_ON_RX
@@ -3,6 +3,9 @@
#ifdef USE_ESP32
#include <driver/uart.h>
#ifdef USE_UART_WAKE_LOOP_ON_RX
#include <driver/uart_select.h>
#endif
#include "esphome/core/component.h"
#include "uart_component.h"
@@ -12,9 +15,7 @@ namespace esphome::uart {
///
/// Thread safety: All public methods must only be called from the main loop.
/// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's
/// peek byte state (has_peek_/peek_byte_) is not synchronized. The rx_event_task
/// (when enabled) must not call any of these methods — it communicates with the
/// main loop exclusively via App.wake_loop_threadsafe().
/// peek byte state (has_peek_/peek_byte_) is not synchronized.
class IDFUARTComponent : public UARTComponent, public Component {
public:
void setup() override;
@@ -33,9 +34,6 @@ class IDFUARTComponent : public UARTComponent, public Component {
void flush() override;
uint8_t get_hw_serial_number() { return this->uart_num_; }
#ifdef USE_UART_WAKE_LOOP_ON_RX
QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; }
#endif
/**
* Load the UART with the current settings.
@@ -61,15 +59,8 @@ class IDFUARTComponent : public UARTComponent, public Component {
uint8_t peek_byte_;
#ifdef USE_UART_WAKE_LOOP_ON_RX
// RX notification support — runs on a separate FreeRTOS task.
// IMPORTANT: rx_event_task_func must NOT call any UART wrapper methods (read_array,
// write_array, etc.) or touch has_peek_/peek_byte_. It must only read from the
// event queue and call App.wake_loop_threadsafe().
void start_rx_event_task_();
static void rx_event_task_func(void *param);
QueueHandle_t uart_event_queue_;
TaskHandle_t rx_event_task_handle_{nullptr};
// ISR callback for UART RX data notification — wakes the main loop directly.
static void uart_rx_isr_callback(uart_port_t uart_num, uart_select_notif_t uart_select_notif, BaseType_t *task_woken);
#endif // USE_UART_WAKE_LOOP_ON_RX
};
+10
View File
@@ -617,6 +617,16 @@ class Application {
/// On other platforms: uses UDP loopback socket
void wake_loop_threadsafe();
#endif
#ifdef USE_LWIP_FAST_SELECT
/// Wake the main event loop from an ISR.
/// Uses vTaskNotifyGiveFromISR() — <1 us, ISR-safe.
/// Only available on platforms with fast select (ESP32, LibreTiny).
/// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed.
static void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) {
esphome_lwip_wake_main_loop_from_isr(px_higher_priority_task_woken);
}
#endif
#endif
protected:
+7
View File
@@ -336,6 +336,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;
#ifdef USE_LWIP_FAST_SELECT
// Wake the main loop if sleeping in ulTaskNotifyTake(). Without this,
// the main loop would not wake until the select timeout expires (~16ms).
// vTaskNotifyGiveFromISR(NULL) is safe here — it skips the yield flag
// and the context switch happens at the next scheduler tick (<1ms).
Application::wake_loop_isrsafe(nullptr);
#endif
}
void Component::reset_to_construction_state() {
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
+8
View File
@@ -225,4 +225,12 @@ void esphome_lwip_wake_main_loop(void) {
}
}
// Wake the main loop from an ISR. ISR-safe variant.
void IRAM_ATTR esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken) {
TaskHandle_t task = s_main_loop_task;
if (task != NULL) {
vTaskNotifyGiveFromISR(task, (BaseType_t *) px_higher_priority_task_woken);
}
}
#endif // defined(USE_ESP32) || defined(USE_LIBRETINY)
+5
View File
@@ -28,6 +28,11 @@ void esphome_lwip_hook_socket(int fd);
/// NOT ISR-safe — must only be called from task context.
void esphome_lwip_wake_main_loop(void);
/// Wake the main loop task from an ISR — costs <1 us.
/// ISR-safe variant using vTaskNotifyGiveFromISR().
/// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed.
void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken);
#ifdef __cplusplus
}
#endif