Merge remote-tracking branch 'upstream/libretiny-wifi-queue-abstraction' into integration

This commit is contained in:
J. Nick Koston
2026-03-31 23:25:15 -10:00
5 changed files with 185 additions and 38 deletions
+7
View File
@@ -438,6 +438,13 @@ async def component_to_code(config):
# 4-8KB flash). Even if linked, it would use locks, so explicit FreeRTOS
# mutexes are simpler and equivalent.
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
# required by AsyncTCP 3.4.3+ (https://github.com/esphome/esphome/issues/10220)
@@ -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 */
+18
View File
@@ -9,6 +9,11 @@
#ifdef USE_ESP32
#include "esphome/core/lock_free_queue.h"
#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 <span>
@@ -882,6 +887,19 @@ class WiFiComponent final : public Component {
LockFreeQueue<IDFWiFiEvent, 17> event_queue_;
#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:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.
@@ -10,9 +10,6 @@
#include "lwip/err.h"
#include "lwip/dns.h"
#include <FreeRTOS.h>
#include <queue.h>
#ifdef USE_BK72XX
extern "C" {
#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:
// - The main loop may never see state changes (values cached in registers)
// - 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.
// This is the same approach used by ESP32 IDF's wifi_process_event_().
// 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
static QueueHandle_t s_event_queue = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static volatile uint32_t s_event_queue_overflow_count =
0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
//
// On platforms with hardware atomics (RTL87xx, LN882x): LockFreeQueue (SPSC ring buffer)
// On platforms without (BK72xx): FreeRTOSQueue (xQueue wrapper with critical sections)
// Event structure for queued WiFi events - contains a copy of event data
// 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
// 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) {
if (s_event_queue == nullptr) {
return;
}
// Allocate on heap and fill directly to avoid extra memcpy
auto *to_send = new LTWiFiEvent{}; // NOLINT(cppcoreguidelines-owning-memory)
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)
if (xQueueSend(s_event_queue, &to_send, 0) != pdPASS) {
if (!this->event_queue_.push(to_send)) {
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_() {
// 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(
[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
@@ -797,24 +778,14 @@ network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask(
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; }
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; }
void WiFiComponent::wifi_loop_() {
// Process all pending events from the queue
if (s_event_queue == nullptr) {
return;
}
// Check for dropped events due to queue overflow
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;
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);
}
while (true) {
LTWiFiEvent *event;
if (xQueueReceive(s_event_queue, &event, 0) != pdTRUE) {
// No more events
break;
}
LTWiFiEvent *event;
while ((event = this->event_queue_.pop()) != nullptr) {
wifi_process_event_(event);
delete event; // NOLINT(cppcoreguidelines-owning-memory)
}
+99
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