From 8b81cf3fda52db985a428b9f7962ad12903e1e9f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 20:39:21 -1000 Subject: [PATCH 01/17] [wifi] Use queue abstraction for LibreTiny WiFi events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace static FreeRTOS queue globals in the LibreTiny WiFi component with the same queue abstraction used by ESP32: - ESPHOME_THREAD_MULTI_ATOMICS (RTL87xx, LN882x): LockFreeQueue - ESPHOME_THREAD_MULTI_NO_ATOMICS (BK72xx): new FreeRTOSQueue wrapper Add FreeRTOSQueue in freertos_queue.h — an xQueue wrapper providing the same API as LockFreeQueue (push, pop, get_and_reset_dropped_count) for platforms without hardware atomic instructions. The event queue is now a class member instead of a static global, matching the ESP32 pattern. --- esphome/components/wifi/wifi_component.h | 16 ++++ .../wifi/wifi_component_libretiny.cpp | 47 ++--------- esphome/core/freertos_queue.h | 82 +++++++++++++++++++ 3 files changed, 107 insertions(+), 38 deletions(-) create mode 100644 esphome/core/freertos_queue.h diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 665dec37d5a..4f5ed0bc9ff 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -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 @@ -882,6 +887,17 @@ class WiFiComponent final : public Component { LockFreeQueue 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). +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + LockFreeQueue event_queue_; +#else + FreeRTOSQueue 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. diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 9565ffa7473..479030d4424 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -10,9 +10,6 @@ #include "lwip/err.h" #include "lwip/dns.h" -#include -#include - #ifdef USE_BK72XX extern "C" { #include @@ -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) } diff --git a/esphome/core/freertos_queue.h b/esphome/core/freertos_queue.h new file mode 100644 index 00000000000..e65d2fbbb12 --- /dev/null +++ b/esphome/core/freertos_queue.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include + +#include +#include + +/* + * 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. + * + * @tparam T The type of elements stored in the queue (stored as pointers) + * @tparam SIZE The maximum number of elements + */ + +namespace esphome { + +template class FreeRTOSQueue { + public: + FreeRTOSQueue() : dropped_count_(0) { this->handle_ = xQueueCreate(SIZE, sizeof(T *)); } + + bool push(T *element) { + if (element == nullptr || this->handle_ == nullptr) + return false; + + if (xQueueSend(this->handle_, &element, 0) != pdPASS) { + this->dropped_count_++; + return false; + } + return true; + } + + T *pop() { + if (this->handle_ == nullptr) + return nullptr; + + T *element; + if (xQueueReceive(this->handle_, &element, 0) != pdTRUE) { + return nullptr; + } + return element; + } + + uint16_t get_and_reset_dropped_count() { + uint16_t count = this->dropped_count_; + if (count == 0) + return 0; + this->dropped_count_ = 0; + return count; + } + + void increment_dropped_count() { this->dropped_count_++; } + + bool empty() const { + if (this->handle_ == nullptr) + return true; + return uxQueueMessagesWaiting(this->handle_) == 0; + } + + bool full() const { + if (this->handle_ == nullptr) + return true; + return uxQueueSpacesAvailable(this->handle_) == 0; + } + + size_t size() const { + if (this->handle_ == nullptr) + return 0; + return uxQueueMessagesWaiting(this->handle_); + } + + protected: + QueueHandle_t handle_; + volatile uint16_t dropped_count_; +}; + +} // namespace esphome From 42c2f1906f2a9bef13a19b4aa5e256a96b5a66c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 20:39:43 -1000 Subject: [PATCH 02/17] Use constexpr for queue size with +1 for LockFreeQueue ring buffer --- esphome/components/wifi/wifi_component.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 4f5ed0bc9ff..a44a9bd6176 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -891,10 +891,12 @@ class WiFiComponent final : public Component { // 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 - LockFreeQueue event_queue_; + // Ring buffer reserves one slot, so +1 for 16 usable slots + LockFreeQueue event_queue_; #else - FreeRTOSQueue event_queue_; + FreeRTOSQueue event_queue_; #endif #endif From 9db5c8ac2dac1c7a46b349ea4088ce5b0172d7c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 20:42:13 -1000 Subject: [PATCH 03/17] Use xQueueCreateStatic to avoid heap allocation Storage buffer and control block are now class members, matching LockFreeQueue's static buffer approach. Removes all null handle checks. --- esphome/core/freertos_queue.h | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/esphome/core/freertos_queue.h b/esphome/core/freertos_queue.h index e65d2fbbb12..073b47eff68 100644 --- a/esphome/core/freertos_queue.h +++ b/esphome/core/freertos_queue.h @@ -14,6 +14,9 @@ * empty, full, size) but uses xQueue internally, which synchronizes via * FreeRTOS critical sections. * + * Uses xQueueCreateStatic so the queue storage lives in the object itself + * with no heap allocation, matching LockFreeQueue's static buffer approach. + * * @tparam T The type of elements stored in the queue (stored as pointers) * @tparam SIZE The maximum number of elements */ @@ -22,10 +25,12 @@ namespace esphome { template class FreeRTOSQueue { public: - FreeRTOSQueue() : dropped_count_(0) { this->handle_ = xQueueCreate(SIZE, sizeof(T *)); } + FreeRTOSQueue() : dropped_count_(0) { + this->handle_ = xQueueCreateStatic(SIZE, sizeof(T *), this->storage_, &this->queue_buf_); + } bool push(T *element) { - if (element == nullptr || this->handle_ == nullptr) + if (element == nullptr) return false; if (xQueueSend(this->handle_, &element, 0) != pdPASS) { @@ -36,9 +41,6 @@ template class FreeRTOSQueue { } T *pop() { - if (this->handle_ == nullptr) - return nullptr; - T *element; if (xQueueReceive(this->handle_, &element, 0) != pdTRUE) { return nullptr; @@ -56,25 +58,16 @@ template class FreeRTOSQueue { void increment_dropped_count() { this->dropped_count_++; } - bool empty() const { - if (this->handle_ == nullptr) - return true; - return uxQueueMessagesWaiting(this->handle_) == 0; - } + bool empty() const { return uxQueueMessagesWaiting(this->handle_) == 0; } - bool full() const { - if (this->handle_ == nullptr) - return true; - return uxQueueSpacesAvailable(this->handle_) == 0; - } + bool full() const { return uxQueueSpacesAvailable(this->handle_) == 0; } - size_t size() const { - if (this->handle_ == nullptr) - return 0; - return uxQueueMessagesWaiting(this->handle_); - } + size_t size() const { return uxQueueMessagesWaiting(this->handle_); } protected: + // Static storage for the queue - sized for SIZE pointer-sized items + uint8_t storage_[SIZE * sizeof(T *)]; + StaticQueue_t queue_buf_; QueueHandle_t handle_; volatile uint16_t dropped_count_; }; From 948449502f031d10f226e4653926dec7229c2497 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 20:44:43 -1000 Subject: [PATCH 04/17] Guard freertos_queue.h with ESPHOME_THREAD_MULTI_NO_ATOMICS Prevents compilation failure on host/native builds where FreeRTOS headers are not available. --- esphome/core/freertos_queue.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/core/freertos_queue.h b/esphome/core/freertos_queue.h index 073b47eff68..ccf99523d8f 100644 --- a/esphome/core/freertos_queue.h +++ b/esphome/core/freertos_queue.h @@ -1,5 +1,9 @@ #pragma once +#include "esphome/core/defines.h" + +#ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS + #include #include @@ -73,3 +77,5 @@ template class FreeRTOSQueue { }; } // namespace esphome + +#endif // ESPHOME_THREAD_MULTI_NO_ATOMICS From 586e37c8644b71d7b7b79700a0ebc9370bb3d82d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 20:47:47 -1000 Subject: [PATCH 05/17] =?UTF-8?q?Use=20xQueueCreate=20=E2=80=94=20BK72xx?= =?UTF-8?q?=20FreeRTOS=20lacks=20static=20allocation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BK72xx's FreeRTOS does not enable configSUPPORT_STATIC_ALLOCATION, so xQueueCreateStatic is unavailable. Fall back to xQueueCreate which is a one-time heap allocation during setup. --- esphome/core/freertos_queue.h | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/esphome/core/freertos_queue.h b/esphome/core/freertos_queue.h index ccf99523d8f..d264489663c 100644 --- a/esphome/core/freertos_queue.h +++ b/esphome/core/freertos_queue.h @@ -18,9 +18,6 @@ * empty, full, size) but uses xQueue internally, which synchronizes via * FreeRTOS critical sections. * - * Uses xQueueCreateStatic so the queue storage lives in the object itself - * with no heap allocation, matching LockFreeQueue's static buffer approach. - * * @tparam T The type of elements stored in the queue (stored as pointers) * @tparam SIZE The maximum number of elements */ @@ -29,9 +26,7 @@ namespace esphome { template class FreeRTOSQueue { public: - FreeRTOSQueue() : dropped_count_(0) { - this->handle_ = xQueueCreateStatic(SIZE, sizeof(T *), this->storage_, &this->queue_buf_); - } + FreeRTOSQueue() : dropped_count_(0) { this->handle_ = xQueueCreate(SIZE, sizeof(T *)); } bool push(T *element) { if (element == nullptr) @@ -69,9 +64,6 @@ template class FreeRTOSQueue { size_t size() const { return uxQueueMessagesWaiting(this->handle_); } protected: - // Static storage for the queue - sized for SIZE pointer-sized items - uint8_t storage_[SIZE * sizeof(T *)]; - StaticQueue_t queue_buf_; QueueHandle_t handle_; volatile uint16_t dropped_count_; }; From 4f33faf89dfcb3f4724626beaec5d85cfd828110 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 20:48:33 -1000 Subject: [PATCH 06/17] Restore nullptr checks for xQueueCreate failure --- esphome/core/freertos_queue.h | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/esphome/core/freertos_queue.h b/esphome/core/freertos_queue.h index d264489663c..9ce4fe35439 100644 --- a/esphome/core/freertos_queue.h +++ b/esphome/core/freertos_queue.h @@ -29,7 +29,7 @@ template class FreeRTOSQueue { FreeRTOSQueue() : dropped_count_(0) { this->handle_ = xQueueCreate(SIZE, sizeof(T *)); } bool push(T *element) { - if (element == nullptr) + if (element == nullptr || this->handle_ == nullptr) return false; if (xQueueSend(this->handle_, &element, 0) != pdPASS) { @@ -40,6 +40,9 @@ template class FreeRTOSQueue { } T *pop() { + if (this->handle_ == nullptr) + return nullptr; + T *element; if (xQueueReceive(this->handle_, &element, 0) != pdTRUE) { return nullptr; @@ -57,11 +60,23 @@ template class FreeRTOSQueue { void increment_dropped_count() { this->dropped_count_++; } - bool empty() const { return uxQueueMessagesWaiting(this->handle_) == 0; } + bool empty() const { + if (this->handle_ == nullptr) + return true; + return uxQueueMessagesWaiting(this->handle_) == 0; + } - bool full() const { return uxQueueSpacesAvailable(this->handle_) == 0; } + bool full() const { + if (this->handle_ == nullptr) + return true; + return uxQueueSpacesAvailable(this->handle_) == 0; + } - size_t size() const { return uxQueueMessagesWaiting(this->handle_); } + size_t size() const { + if (this->handle_ == nullptr) + return 0; + return uxQueueMessagesWaiting(this->handle_); + } protected: QueueHandle_t handle_; From bb2991bfdae6de1ec4cf70b8fcfc982215691e76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 21:31:41 -1000 Subject: [PATCH 07/17] Address review: critical sections for dropped_count_, delete copy/move - Protect dropped_count_ increment and get+reset with portENTER_CRITICAL/portEXIT_CRITICAL since std::atomic is unavailable on NO_ATOMICS platforms - Delete copy/move constructors and assignment operators - Remove volatile qualifier (critical sections provide proper synchronization) --- esphome/core/freertos_queue.h | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/esphome/core/freertos_queue.h b/esphome/core/freertos_queue.h index 9ce4fe35439..5778c117973 100644 --- a/esphome/core/freertos_queue.h +++ b/esphome/core/freertos_queue.h @@ -28,12 +28,20 @@ template class FreeRTOSQueue { public: FreeRTOSQueue() : dropped_count_(0) { this->handle_ = xQueueCreate(SIZE, sizeof(T *)); } + // 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 || this->handle_ == nullptr) return false; if (xQueueSend(this->handle_, &element, 0) != pdPASS) { + portENTER_CRITICAL(); this->dropped_count_++; + portEXIT_CRITICAL(); return false; } return true; @@ -51,14 +59,18 @@ template class FreeRTOSQueue { } uint16_t get_and_reset_dropped_count() { + portENTER_CRITICAL(); uint16_t count = this->dropped_count_; - if (count == 0) - return 0; this->dropped_count_ = 0; + portEXIT_CRITICAL(); return count; } - void increment_dropped_count() { this->dropped_count_++; } + void increment_dropped_count() { + portENTER_CRITICAL(); + this->dropped_count_++; + portEXIT_CRITICAL(); + } bool empty() const { if (this->handle_ == nullptr) @@ -80,7 +92,7 @@ template class FreeRTOSQueue { protected: QueueHandle_t handle_; - volatile uint16_t dropped_count_; + uint16_t dropped_count_; }; } // namespace esphome From 1e156d46b6be25c2adf2be4cab8b1abee35a78a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 21:32:26 -1000 Subject: [PATCH 08/17] Fast path for get_and_reset_dropped_count to avoid critical section Plain read of aligned uint16_t is safe on ARM. Since drops are rare, almost every call returns 0 without entering a critical section. --- esphome/core/freertos_queue.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/core/freertos_queue.h b/esphome/core/freertos_queue.h index 5778c117973..0548ebfc710 100644 --- a/esphome/core/freertos_queue.h +++ b/esphome/core/freertos_queue.h @@ -59,6 +59,10 @@ template class FreeRTOSQueue { } uint16_t get_and_reset_dropped_count() { + // Fast path: plain read is safe for aligned uint16_t on ARM. + // Drops are rare so almost always returns 0 without entering a critical section. + if (this->dropped_count_ == 0) + return 0; portENTER_CRITICAL(); uint16_t count = this->dropped_count_; this->dropped_count_ = 0; From b829bbee954e47a438a553f0191cdcccd5353317 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 21:33:57 -1000 Subject: [PATCH 09/17] Use increment_dropped_count() internally in push() --- esphome/core/freertos_queue.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/core/freertos_queue.h b/esphome/core/freertos_queue.h index 0548ebfc710..4209472cc21 100644 --- a/esphome/core/freertos_queue.h +++ b/esphome/core/freertos_queue.h @@ -39,9 +39,7 @@ template class FreeRTOSQueue { return false; if (xQueueSend(this->handle_, &element, 0) != pdPASS) { - portENTER_CRITICAL(); - this->dropped_count_++; - portEXIT_CRITICAL(); + this->increment_dropped_count(); return false; } return true; From b980978f05626baecbbd68e81180e88d886cce8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 21:42:59 -1000 Subject: [PATCH 10/17] Enable configSUPPORT_STATIC_ALLOCATION on BK72xx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add build flag and required callbacks (following ESP-IDF's pvPortMalloc approach) so FreeRTOSQueue can use xQueueCreateStatic — queue storage lives in BSS with zero runtime heap allocation. --- esphome/components/libretiny/__init__.py | 5 ++ .../libretiny/freertos_static_alloc.c | 48 +++++++++++++++++++ esphome/core/freertos_queue.h | 33 +++++-------- 3 files changed, 65 insertions(+), 21 deletions(-) create mode 100644 esphome/components/libretiny/freertos_static_alloc.c diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 8f991246040..91fdf002305 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -438,6 +438,11 @@ 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). + # BK72xx's FreeRTOSConfig.h doesn't define this, defaulting to 0. + # The -D wins over the #ifndef default in FreeRTOS.h. + 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) diff --git a/esphome/components/libretiny/freertos_static_alloc.c b/esphome/components/libretiny/freertos_static_alloc.c new file mode 100644 index 00000000000..cd919e86a9e --- /dev/null +++ b/esphome/components/libretiny/freertos_static_alloc.c @@ -0,0 +1,48 @@ +/* + * 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 +#include + +#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)); + + *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)); + + *ppxTimerTaskTCBBuffer = tcb; + *ppxTimerTaskStackBuffer = stack; + *pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH; +} + +#endif /* configUSE_TIMERS */ + +#endif /* configSUPPORT_STATIC_ALLOCATION */ + +#endif /* USE_BK72XX */ diff --git a/esphome/core/freertos_queue.h b/esphome/core/freertos_queue.h index 4209472cc21..38e764137e2 100644 --- a/esphome/core/freertos_queue.h +++ b/esphome/core/freertos_queue.h @@ -16,7 +16,8 @@ * * 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. + * 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 @@ -26,7 +27,9 @@ namespace esphome { template class FreeRTOSQueue { public: - FreeRTOSQueue() : dropped_count_(0) { this->handle_ = xQueueCreate(SIZE, sizeof(T *)); } + FreeRTOSQueue() : dropped_count_(0) { + this->handle_ = xQueueCreateStatic(SIZE, sizeof(T *), this->storage_, &this->queue_buf_); + } // Non-copyable, non-movable — queue handle is not transferable FreeRTOSQueue(const FreeRTOSQueue &) = delete; @@ -35,7 +38,7 @@ template class FreeRTOSQueue { FreeRTOSQueue &operator=(FreeRTOSQueue &&) = delete; bool push(T *element) { - if (element == nullptr || this->handle_ == nullptr) + if (element == nullptr) return false; if (xQueueSend(this->handle_, &element, 0) != pdPASS) { @@ -46,9 +49,6 @@ template class FreeRTOSQueue { } T *pop() { - if (this->handle_ == nullptr) - return nullptr; - T *element; if (xQueueReceive(this->handle_, &element, 0) != pdTRUE) { return nullptr; @@ -74,25 +74,16 @@ template class FreeRTOSQueue { portEXIT_CRITICAL(); } - bool empty() const { - if (this->handle_ == nullptr) - return true; - return uxQueueMessagesWaiting(this->handle_) == 0; - } + bool empty() const { return uxQueueMessagesWaiting(this->handle_) == 0; } - bool full() const { - if (this->handle_ == nullptr) - return true; - return uxQueueSpacesAvailable(this->handle_) == 0; - } + bool full() const { return uxQueueSpacesAvailable(this->handle_) == 0; } - size_t size() const { - if (this->handle_ == nullptr) - return 0; - return uxQueueMessagesWaiting(this->handle_); - } + 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_; }; From 6e9ebf6b1bedacb410f1435f72d4a89463a03fbb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 21:43:21 -1000 Subject: [PATCH 11/17] Fix scoping: declare count outside portENTER_CRITICAL block BK72xx portENTER_CRITICAL expands to something that introduces a scope, so variables declared between ENTER/EXIT are not visible after. --- esphome/core/freertos_queue.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/core/freertos_queue.h b/esphome/core/freertos_queue.h index 38e764137e2..98756062102 100644 --- a/esphome/core/freertos_queue.h +++ b/esphome/core/freertos_queue.h @@ -61,8 +61,10 @@ template class FreeRTOSQueue { // Drops are rare so almost always returns 0 without entering a critical section. if (this->dropped_count_ == 0) return 0; + // Declare outside critical section — BK72xx portENTER_CRITICAL may introduce a scope + uint16_t count; portENTER_CRITICAL(); - uint16_t count = this->dropped_count_; + count = this->dropped_count_; this->dropped_count_ = 0; portEXIT_CRITICAL(); return count; From d3545c960fdf8db9442290707d509e529dea61c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 21:45:15 -1000 Subject: [PATCH 12/17] Document intentional no-destructor and fast-path read rationale --- esphome/core/freertos_queue.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/core/freertos_queue.h b/esphome/core/freertos_queue.h index 98756062102..2f3faf818a3 100644 --- a/esphome/core/freertos_queue.h +++ b/esphome/core/freertos_queue.h @@ -31,6 +31,9 @@ template class FreeRTOSQueue { 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; @@ -57,8 +60,9 @@ template class FreeRTOSQueue { } uint16_t get_and_reset_dropped_count() { - // Fast path: plain read is safe for aligned uint16_t on ARM. - // Drops are rare so almost always returns 0 without entering a critical section. + // 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 From bdb96189386c65b9ba24634f58f220278768bd04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 22:00:22 -1000 Subject: [PATCH 13/17] Enable configSUPPORT_STATIC_ALLOCATION for all LibreTiny platforms The FreeRTOS timer command queue moves from heap to BSS on all LibreTiny targets, not just BK72xx. RTL87xx and LN882x benefit from the same heap savings. --- esphome/components/libretiny/__init__.py | 12 +++++++----- esphome/components/libretiny/freertos_static_alloc.c | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 91fdf002305..2eeb8809976 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -438,11 +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). - # BK72xx's FreeRTOSConfig.h doesn't define this, defaulting to 0. - # The -D wins over the #ifndef default in FreeRTOS.h. - cg.add_build_flag("-DconfigSUPPORT_STATIC_ALLOCATION=1") + + # Enable FreeRTOS static allocation APIs (xQueueCreateStatic, etc.). + # Moves FreeRTOS internal structures (timer command queue) from heap to BSS, + # and allows FreeRTOSQueue to use xQueueCreateStatic on BK72xx. + # LibreTiny's FreeRTOSConfig.h doesn't define this, defaulting to 0. + # The -D wins over the #ifndef default in FreeRTOS.h. + 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) diff --git a/esphome/components/libretiny/freertos_static_alloc.c b/esphome/components/libretiny/freertos_static_alloc.c index cd919e86a9e..dc95eda7854 100644 --- a/esphome/components/libretiny/freertos_static_alloc.c +++ b/esphome/components/libretiny/freertos_static_alloc.c @@ -10,7 +10,7 @@ * allowing queue storage to live in BSS with zero runtime heap allocation. */ -#ifdef USE_BK72XX +#ifdef USE_LIBRETINY #include #include @@ -45,4 +45,4 @@ void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer, StackT #endif /* configSUPPORT_STATIC_ALLOCATION */ -#endif /* USE_BK72XX */ +#endif /* USE_LIBRETINY */ From 444ee47ecbab602fe2f981a5881f07532ab72b4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 22:04:14 -1000 Subject: [PATCH 14/17] Revert: only enable configSUPPORT_STATIC_ALLOCATION on BK72xx RTL87xx loses 472 bytes heap with it enabled due to different FreeRTOS internals. BK72xx gains 712 bytes, so keep it there only. --- esphome/components/libretiny/__init__.py | 15 ++++++++------- .../components/libretiny/freertos_static_alloc.c | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 2eeb8809976..418a9991a42 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -438,13 +438,14 @@ 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 APIs (xQueueCreateStatic, etc.). - # Moves FreeRTOS internal structures (timer command queue) from heap to BSS, - # and allows FreeRTOSQueue to use xQueueCreateStatic on BK72xx. - # LibreTiny's FreeRTOSConfig.h doesn't define this, defaulting to 0. - # The -D wins over the #ifndef default in FreeRTOS.h. - cg.add_build_flag("-DconfigSUPPORT_STATIC_ALLOCATION=1") + # 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. + # Only enabled on BK72xx — RTL87xx loses heap with it enabled due to + # different FreeRTOS internals. + 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) diff --git a/esphome/components/libretiny/freertos_static_alloc.c b/esphome/components/libretiny/freertos_static_alloc.c index dc95eda7854..cd919e86a9e 100644 --- a/esphome/components/libretiny/freertos_static_alloc.c +++ b/esphome/components/libretiny/freertos_static_alloc.c @@ -10,7 +10,7 @@ * allowing queue storage to live in BSS with zero runtime heap allocation. */ -#ifdef USE_LIBRETINY +#ifdef USE_BK72XX #include #include @@ -45,4 +45,4 @@ void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer, StackT #endif /* configSUPPORT_STATIC_ALLOCATION */ -#endif /* USE_LIBRETINY */ +#endif /* USE_BK72XX */ From 08eccb081cbcffa36a114c6d850fd52a322ef4ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 22:04:44 -1000 Subject: [PATCH 15/17] Revert "Revert: only enable configSUPPORT_STATIC_ALLOCATION on BK72xx" This reverts commit 444ee47ecbab602fe2f981a5881f07532ab72b4a. --- esphome/components/libretiny/__init__.py | 15 +++++++-------- .../components/libretiny/freertos_static_alloc.c | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 418a9991a42..2eeb8809976 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -438,14 +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. - # Only enabled on BK72xx — RTL87xx loses heap with it enabled due to - # different FreeRTOS internals. - cg.add_build_flag("-DconfigSUPPORT_STATIC_ALLOCATION=1") + + # Enable FreeRTOS static allocation APIs (xQueueCreateStatic, etc.). + # Moves FreeRTOS internal structures (timer command queue) from heap to BSS, + # and allows FreeRTOSQueue to use xQueueCreateStatic on BK72xx. + # LibreTiny's FreeRTOSConfig.h doesn't define this, defaulting to 0. + # The -D wins over the #ifndef default in FreeRTOS.h. + 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) diff --git a/esphome/components/libretiny/freertos_static_alloc.c b/esphome/components/libretiny/freertos_static_alloc.c index cd919e86a9e..dc95eda7854 100644 --- a/esphome/components/libretiny/freertos_static_alloc.c +++ b/esphome/components/libretiny/freertos_static_alloc.c @@ -10,7 +10,7 @@ * allowing queue storage to live in BSS with zero runtime heap allocation. */ -#ifdef USE_BK72XX +#ifdef USE_LIBRETINY #include #include @@ -45,4 +45,4 @@ void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer, StackT #endif /* configSUPPORT_STATIC_ALLOCATION */ -#endif /* USE_BK72XX */ +#endif /* USE_LIBRETINY */ From b726d108f8fa4e8016150153b4ee3bf78bec4822 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 22:06:35 -1000 Subject: [PATCH 16/17] Only enable configSUPPORT_STATIC_ALLOCATION on BK72xx RTL87xx loses 280 bytes heap with it enabled. BK72xx gains 712 bytes. Keep it BK72xx-only where it's a clear win. --- esphome/components/libretiny/__init__.py | 14 +++++++------- .../components/libretiny/freertos_static_alloc.c | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 2eeb8809976..8588cf5418d 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -438,13 +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 APIs (xQueueCreateStatic, etc.). - # Moves FreeRTOS internal structures (timer command queue) from heap to BSS, - # and allows FreeRTOSQueue to use xQueueCreateStatic on BK72xx. - # LibreTiny's FreeRTOSConfig.h doesn't define this, defaulting to 0. - # The -D wins over the #ifndef default in FreeRTOS.h. - cg.add_build_flag("-DconfigSUPPORT_STATIC_ALLOCATION=1") + # 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) diff --git a/esphome/components/libretiny/freertos_static_alloc.c b/esphome/components/libretiny/freertos_static_alloc.c index dc95eda7854..cd919e86a9e 100644 --- a/esphome/components/libretiny/freertos_static_alloc.c +++ b/esphome/components/libretiny/freertos_static_alloc.c @@ -10,7 +10,7 @@ * allowing queue storage to live in BSS with zero runtime heap allocation. */ -#ifdef USE_LIBRETINY +#ifdef USE_BK72XX #include #include @@ -45,4 +45,4 @@ void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer, StackT #endif /* configSUPPORT_STATIC_ALLOCATION */ -#endif /* USE_LIBRETINY */ +#endif /* USE_BK72XX */ From 249a4bdcf039bc735102b73ebad4f094f1b323d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2026 22:27:19 -1000 Subject: [PATCH 17/17] Add configASSERT checks for pvPortMalloc failures Matches ESP-IDF's assert pattern for the static allocation callbacks. --- esphome/components/libretiny/freertos_static_alloc.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/libretiny/freertos_static_alloc.c b/esphome/components/libretiny/freertos_static_alloc.c index cd919e86a9e..62b0524230a 100644 --- a/esphome/components/libretiny/freertos_static_alloc.c +++ b/esphome/components/libretiny/freertos_static_alloc.c @@ -23,6 +23,8 @@ void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer, StackTyp * 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; @@ -35,6 +37,8 @@ void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer, StackT 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;