[wifi] Use queue abstraction for LibreTiny WiFi events (#15343)

This commit is contained in:
J. Nick Koston
2026-04-22 06:24:09 +02:00
committed by GitHub
parent bb81c91d0c
commit edcf96d057
9 changed files with 227 additions and 53 deletions

View File

@@ -443,6 +443,13 @@ async def component_to_code(config):
# 4-8KB flash). Even if linked, it would use locks, so explicit FreeRTOS # 4-8KB flash). Even if linked, it would use locks, so explicit FreeRTOS
# mutexes are simpler and equivalent. # mutexes are simpler and equivalent.
cg.add_define(ThreadModel.MULTI_NO_ATOMICS) cg.add_define(ThreadModel.MULTI_NO_ATOMICS)
# Enable FreeRTOS static allocation so FreeRTOSQueue can use
# xQueueCreateStatic (queue storage in BSS, no heap allocation).
# Also moves FreeRTOS internal structures (timer command queue) to BSS.
# BK72xx's FreeRTOSConfig.h doesn't define this, defaulting to 0.
# The -D wins over the #ifndef default in FreeRTOS.h.
# Not enabled on RTL87xx/LN882x — costs more heap than it saves there.
cg.add_build_flag("-DconfigSUPPORT_STATIC_ALLOCATION=1")
# RTL8710B needs FreeRTOS 8.2.3+ for xTaskNotifyGive/ulTaskNotifyTake # RTL8710B needs FreeRTOS 8.2.3+ for xTaskNotifyGive/ulTaskNotifyTake
# required by AsyncTCP 3.4.3+ (https://github.com/esphome/esphome/issues/10220) # required by AsyncTCP 3.4.3+ (https://github.com/esphome/esphome/issues/10220)

View File

@@ -0,0 +1,52 @@
/*
* FreeRTOS static allocation callbacks for LibreTiny platforms.
*
* Required when configSUPPORT_STATIC_ALLOCATION is enabled. These callbacks
* provide memory for the idle and timer tasks. Following ESP-IDF's approach,
* we allocate from the FreeRTOS heap (pvPortMalloc) rather than using truly
* static buffers, to avoid assumptions about memory layout.
*
* This enables xQueueCreateStatic, xTaskCreateStatic, etc. throughout ESPHome,
* allowing queue storage to live in BSS with zero runtime heap allocation.
*/
#ifdef USE_BK72XX
#include <FreeRTOS.h>
#include <task.h>
#if (configSUPPORT_STATIC_ALLOCATION == 1)
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize) {
/* Stack grows down on ARM — allocate stack first, then TCB,
* so the stack does not grow into the TCB. */
StackType_t *stack = (StackType_t *) pvPortMalloc(configMINIMAL_STACK_SIZE * sizeof(StackType_t));
StaticTask_t *tcb = (StaticTask_t *) pvPortMalloc(sizeof(StaticTask_t));
configASSERT(stack != NULL);
configASSERT(tcb != NULL);
*ppxIdleTaskTCBBuffer = tcb;
*ppxIdleTaskStackBuffer = stack;
*pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}
#if (configUSE_TIMERS == 1)
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer, StackType_t **ppxTimerTaskStackBuffer,
uint32_t *pulTimerTaskStackSize) {
StackType_t *stack = (StackType_t *) pvPortMalloc(configTIMER_TASK_STACK_DEPTH * sizeof(StackType_t));
StaticTask_t *tcb = (StaticTask_t *) pvPortMalloc(sizeof(StaticTask_t));
configASSERT(stack != NULL);
configASSERT(tcb != NULL);
*ppxTimerTaskTCBBuffer = tcb;
*ppxTimerTaskStackBuffer = stack;
*pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH;
}
#endif /* configUSE_TIMERS */
#endif /* configSUPPORT_STATIC_ALLOCATION */
#endif /* USE_BK72XX */

View File

@@ -732,9 +732,16 @@ void WiFiComponent::restart_adapter() {
} }
void WiFiComponent::loop() { void WiFiComponent::loop() {
this->wifi_loop_(); bool events_processed = this->wifi_loop_();
const uint32_t now = App.get_loop_component_start_time(); const uint32_t now = App.get_loop_component_start_time();
this->update_connected_state_(); // Connection state can only change when events are processed (ESP-IDF/LibreTiny)
// or polled (ESP8266/Pico W). Skip the expensive wifi_sta_connect_status_() call
// when no events arrived and we're already in steady state.
// Must also run when connected_ is false — after state transitions to STA_CONNECTED,
// connected_ won't be set until update_connected_state_() runs.
if (events_processed || !this->connected_) {
this->update_connected_state_();
}
if (this->has_sta()) { if (this->has_sta()) {
#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) #if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER)

View File

@@ -9,6 +9,11 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "esphome/core/lock_free_queue.h" #include "esphome/core/lock_free_queue.h"
#endif #endif
#if defined(USE_LIBRETINY) && defined(ESPHOME_THREAD_MULTI_ATOMICS)
#include "esphome/core/lock_free_queue.h"
#elif defined(USE_LIBRETINY) && defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
#include "esphome/core/freertos_queue.h"
#endif
#include "esphome/core/string_ref.h" #include "esphome/core/string_ref.h"
#include <span> #include <span>
@@ -657,7 +662,7 @@ class WiFiComponent final : public Component {
void connect_soon_(); void connect_soon_();
void wifi_loop_(); bool wifi_loop_();
#ifdef USE_ESP8266 #ifdef USE_ESP8266
void process_pending_callbacks_(); void process_pending_callbacks_();
#endif #endif
@@ -882,6 +887,19 @@ class WiFiComponent final : public Component {
LockFreeQueue<IDFWiFiEvent, 17> event_queue_; LockFreeQueue<IDFWiFiEvent, 17> event_queue_;
#endif #endif
#ifdef USE_LIBRETINY
// Thread-safe queue for WiFi events from LibreTiny callback thread.
// LockFreeQueue on platforms with hardware atomics (RTL87xx, LN882x),
// FreeRTOSQueue on platforms without (BK72xx).
static constexpr uint8_t LT_EVENT_QUEUE_SIZE = 16;
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// Ring buffer reserves one slot, so +1 for 16 usable slots
LockFreeQueue<LTWiFiEvent, LT_EVENT_QUEUE_SIZE + 1> event_queue_;
#else
FreeRTOSQueue<LTWiFiEvent, LT_EVENT_QUEUE_SIZE> event_queue_;
#endif
#endif
private: private:
// Stores a pointer to a string literal (static storage duration). // Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings. // ONLY set from Python-generated code with string literals - never dynamic strings.

View File

@@ -938,7 +938,10 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() {
return network::IPAddress(&ip.gw); return network::IPAddress(&ip.gw);
} }
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); }
void WiFiComponent::wifi_loop_() { this->process_pending_callbacks_(); } bool WiFiComponent::wifi_loop_() {
this->process_pending_callbacks_();
return true;
}
void WiFiComponent::process_pending_callbacks_() { void WiFiComponent::process_pending_callbacks_() {
// Process callbacks deferred from ESP8266 SDK system context (~2KB stack) // Process callbacks deferred from ESP8266 SDK system context (~2KB stack)

View File

@@ -715,17 +715,25 @@ const char *get_disconnect_reason_str(uint8_t reason) {
} }
} }
void WiFiComponent::wifi_loop_() { bool WiFiComponent::wifi_loop_() {
// Use pop() directly instead of empty() — pop() costs 1 memw (acquire on tail_),
// while empty() costs 2 memw (acquire on both head_ and tail_) on Xtensa.
IDFWiFiEvent *data = this->event_queue_.pop();
if (data == nullptr)
return false;
do {
wifi_process_event_(data);
delete data; // NOLINT(cppcoreguidelines-owning-memory)
} while ((data = this->event_queue_.pop()) != nullptr);
// Drops only occur when the queue is full, and only this loop drains it,
// so if pop() returned nullptr above we can skip this check.
uint16_t dropped = this->event_queue_.get_and_reset_dropped_count(); uint16_t dropped = this->event_queue_.get_and_reset_dropped_count();
if (dropped > 0) { if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %u WiFi events due to buffer overflow", dropped); ESP_LOGW(TAG, "Dropped %u WiFi events due to buffer overflow", dropped);
} }
return true;
IDFWiFiEvent *data;
while ((data = this->event_queue_.pop()) != nullptr) {
wifi_process_event_(data);
delete data; // NOLINT(cppcoreguidelines-owning-memory)
}
} }
// Events are processed from queue in main loop context, but listener notifications // Events are processed from queue in main loop context, but listener notifications
// must be deferred until after the state machine transitions (in check_connecting_finished) // must be deferred until after the state machine transitions (in check_connecting_finished)

View File

@@ -10,9 +10,6 @@
#include "lwip/err.h" #include "lwip/err.h"
#include "lwip/dns.h" #include "lwip/dns.h"
#include <FreeRTOS.h>
#include <queue.h>
#ifdef USE_BK72XX #ifdef USE_BK72XX
extern "C" { extern "C" {
#include <wlan_ui_pub.h> #include <wlan_ui_pub.h>
@@ -43,16 +40,13 @@ static const char *const TAG = "wifi_lt";
// (like connection status flags) from the callback causes race conditions: // (like connection status flags) from the callback causes race conditions:
// - The main loop may never see state changes (values cached in registers) // - The main loop may never see state changes (values cached in registers)
// - State changes may be visible in inconsistent order // - State changes may be visible in inconsistent order
// - LibreTiny targets (BK7231, RTL8720) lack atomic instructions (no LDREX/STREX)
// //
// Solution: Queue events in the callback and process them in the main loop. // Solution: Queue events in the callback and process them in the main loop.
// This is the same approach used by ESP32 IDF's wifi_process_event_(). // This is the same approach used by ESP32 IDF's wifi_process_event_().
// All state modifications happen in the main loop context, eliminating races. // All state modifications happen in the main loop context, eliminating races.
//
static constexpr size_t EVENT_QUEUE_SIZE = 16; // Max pending WiFi events before overflow // On platforms with hardware atomics (RTL87xx, LN882x): LockFreeQueue (SPSC ring buffer)
static QueueHandle_t s_event_queue = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) // On platforms without (BK72xx): FreeRTOSQueue (xQueue wrapper with critical sections)
static volatile uint32_t s_event_queue_overflow_count =
0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Event structure for queued WiFi events - contains a copy of event data // Event structure for queued WiFi events - contains a copy of event data
// to avoid lifetime issues with the original event data from the callback // to avoid lifetime issues with the original event data from the callback
@@ -352,10 +346,6 @@ using esphome_wifi_event_info_t = arduino_event_info_t;
// Event callback - runs in WiFi driver thread context // Event callback - runs in WiFi driver thread context
// Only queues events for processing in main loop, no logging or state changes here // Only queues events for processing in main loop, no logging or state changes here
void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) { void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) {
if (s_event_queue == nullptr) {
return;
}
// Allocate on heap and fill directly to avoid extra memcpy // Allocate on heap and fill directly to avoid extra memcpy
auto *to_send = new LTWiFiEvent{}; // NOLINT(cppcoreguidelines-owning-memory) auto *to_send = new LTWiFiEvent{}; // NOLINT(cppcoreguidelines-owning-memory)
to_send->event_id = event; to_send->event_id = event;
@@ -428,9 +418,8 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
} }
// Queue event (don't block if queue is full) // Queue event (don't block if queue is full)
if (xQueueSend(s_event_queue, &to_send, 0) != pdPASS) { if (!this->event_queue_.push(to_send)) {
delete to_send; // NOLINT(cppcoreguidelines-owning-memory) delete to_send; // NOLINT(cppcoreguidelines-owning-memory)
s_event_queue_overflow_count++;
} }
} }
@@ -620,14 +609,6 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
} }
} }
void WiFiComponent::wifi_pre_setup_() { void WiFiComponent::wifi_pre_setup_() {
// Create event queue for thread-safe event handling
// Events are pushed from WiFi callback thread and processed in main loop
s_event_queue = xQueueCreate(EVENT_QUEUE_SIZE, sizeof(LTWiFiEvent *));
if (s_event_queue == nullptr) {
ESP_LOGE(TAG, "Failed to create event queue");
return;
}
WiFi.onEvent( WiFi.onEvent(
[this](arduino_event_id_t event, arduino_event_info_t info) { this->wifi_event_callback_(event, info); }); [this](arduino_event_id_t event, arduino_event_info_t info) { this->wifi_event_callback_(event, info); });
// Make sure WiFi is in clean state before anything starts // Make sure WiFi is in clean state before anything starts
@@ -796,28 +777,26 @@ int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; }
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; }
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; }
void WiFiComponent::wifi_loop_() { bool WiFiComponent::wifi_loop_() {
// Process all pending events from the queue // Use pop() directly instead of empty() — avoids redundant synchronization.
if (s_event_queue == nullptr) { // LockFreeQueue: pop() costs 1 memw vs empty()'s 2 memw on Xtensa.
return; // FreeRTOSQueue: pop() is 1 critical section vs empty() + pop() = 2.
} LTWiFiEvent *event = this->event_queue_.pop();
if (event == nullptr)
// Check for dropped events due to queue overflow return false;
if (s_event_queue_overflow_count > 0) {
ESP_LOGW(TAG, "Event queue overflow, %" PRIu32 " events dropped", s_event_queue_overflow_count);
s_event_queue_overflow_count = 0;
}
while (true) {
LTWiFiEvent *event;
if (xQueueReceive(s_event_queue, &event, 0) != pdTRUE) {
// No more events
break;
}
do {
wifi_process_event_(event); wifi_process_event_(event);
delete event; // NOLINT(cppcoreguidelines-owning-memory) delete event; // NOLINT(cppcoreguidelines-owning-memory)
} while ((event = this->event_queue_.pop()) != nullptr);
// Drops only occur when the queue is full, and only this loop drains it,
// so if pop() returned nullptr above we can skip this check.
uint16_t dropped = this->event_queue_.get_and_reset_dropped_count();
if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %" PRIu16 " WiFi events due to buffer overflow", dropped);
} }
return true;
} }
} // namespace esphome::wifi } // namespace esphome::wifi

View File

@@ -303,7 +303,7 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
// Connect state listener notifications are deferred until after the state machine // Connect state listener notifications are deferred until after the state machine
// transitions (in check_connecting_finished) so that conditions like wifi.connected // transitions (in check_connecting_finished) so that conditions like wifi.connected
// return correct values in automations. // return correct values in automations.
void WiFiComponent::wifi_loop_() { bool WiFiComponent::wifi_loop_() {
// Handle scan completion // Handle scan completion
if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) {
this->scan_done_ = true; this->scan_done_ = true;
@@ -365,6 +365,7 @@ void WiFiComponent::wifi_loop_() {
#endif #endif
} }
} }
return true;
} }
void WiFiComponent::wifi_pre_setup_() {} void WiFiComponent::wifi_pre_setup_() {}

View File

@@ -0,0 +1,99 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS
#include <cstddef>
#include <cstdint>
#include <FreeRTOS.h>
#include <queue.h>
/*
* FreeRTOS queue wrapper for single-producer single-consumer scenarios on
* platforms without hardware atomic support (e.g. BK72xx ARM968E-S).
*
* Provides the same API as LockFreeQueue (push, pop, get_and_reset_dropped_count,
* empty, full, size) but uses xQueue internally, which synchronizes via
* FreeRTOS critical sections. Uses xQueueCreateStatic so the queue storage
* lives in BSS with zero runtime heap allocation.
*
* @tparam T The type of elements stored in the queue (stored as pointers)
* @tparam SIZE The maximum number of elements
*/
namespace esphome {
template<class T, uint8_t SIZE> class FreeRTOSQueue {
public:
FreeRTOSQueue() : dropped_count_(0) {
this->handle_ = xQueueCreateStatic(SIZE, sizeof(T *), this->storage_, &this->queue_buf_);
}
// No destructor — ESPHome components are never destroyed. Intentionally
// omitted to avoid pulling in vQueueDelete code on resource-constrained targets.
// Non-copyable, non-movable — queue handle is not transferable
FreeRTOSQueue(const FreeRTOSQueue &) = delete;
FreeRTOSQueue &operator=(const FreeRTOSQueue &) = delete;
FreeRTOSQueue(FreeRTOSQueue &&) = delete;
FreeRTOSQueue &operator=(FreeRTOSQueue &&) = delete;
bool push(T *element) {
if (element == nullptr)
return false;
if (xQueueSend(this->handle_, &element, 0) != pdPASS) {
this->increment_dropped_count();
return false;
}
return true;
}
T *pop() {
T *element;
if (xQueueReceive(this->handle_, &element, 0) != pdTRUE) {
return nullptr;
}
return element;
}
uint16_t get_and_reset_dropped_count() {
// Fast path: plain read of aligned uint16_t is a single ARM load instruction.
// Worst case is reading a stale zero and reporting drops one iteration later.
// Avoids critical section overhead on every loop() call since drops are rare.
if (this->dropped_count_ == 0)
return 0;
// Declare outside critical section — BK72xx portENTER_CRITICAL may introduce a scope
uint16_t count;
portENTER_CRITICAL();
count = this->dropped_count_;
this->dropped_count_ = 0;
portEXIT_CRITICAL();
return count;
}
void increment_dropped_count() {
portENTER_CRITICAL();
this->dropped_count_++;
portEXIT_CRITICAL();
}
bool empty() const { return uxQueueMessagesWaiting(this->handle_) == 0; }
bool full() const { return uxQueueSpacesAvailable(this->handle_) == 0; }
size_t size() const { return uxQueueMessagesWaiting(this->handle_); }
protected:
// Static storage for the queue — lives in BSS, no heap allocation
uint8_t storage_[SIZE * sizeof(T *)];
StaticQueue_t queue_buf_;
QueueHandle_t handle_;
uint16_t dropped_count_;
};
} // namespace esphome
#endif // ESPHOME_THREAD_MULTI_NO_ATOMICS