From 22ed9b3c1e821e55b2fc4163290fd075705c68ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Apr 2026 06:06:02 -0500 Subject: [PATCH 1/6] [scheduler] Replace volatile with __atomic_{load,store}_n on NO_ATOMICS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review on #15947 flagged that `volatile uint32_t` is not a well-defined concurrent access in the C++ memory model — it prevents the compiler caching/eliding the read, but does not turn a plain cross-thread read/write pair into a defined access. Technically still a formal data race even though aligned 32-bit LDR/STR on ARMv5TE is atomic at the hardware level. Switch the NO_ATOMICS counter reads and writes to GCC's atomic builtins with __ATOMIC_RELAXED: - Readers: __atomic_load_n(&counter, __ATOMIC_RELAXED) - Writers (under lock_): __atomic_store_n(&counter, new_value, __ATOMIC_RELAXED) - Increment/decrement (under lock_): explicit load + compute + __atomic_store_n. Lock_ serialises the load-modify-store against other writers; the atomic ops make the write visible to concurrent readers in the memory model. On ARMv5TE these builtins compile to plain LDR/STR — same codegen as the previous volatile approach, and no libatomic dependency (only RMW builtins like __atomic_fetch_add would need the lib). ATOMICS and SINGLE paths are unchanged. --- esphome/core/scheduler.h | 65 +++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 946ec8ca78..a6ee079c3a 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -525,40 +525,41 @@ class Scheduler { #ifndef ESPHOME_THREAD_SINGLE // Fast-path counter for process_to_add() to skip taking the lock when there - // is nothing to add. std::atomic on ATOMICS; volatile uint32_t on NO_ATOMICS - // (aligned 32-bit reads are atomic on ARMv5TE — BK72xx — and volatile - // prevents the compiler caching/eliding the read). On NO_ATOMICS, callers - // must hold lock_ for any RMW mutation. Not needed on SINGLE. + // is nothing to add. std::atomic on ATOMICS; plain uint32_t on NO_ATOMICS + // (BK72xx — ARMv5TE single-core, lacks LDREX/STREX so std::atomic RMW would + // require libatomic). Reads use __atomic_load_n(__ATOMIC_RELAXED) on + // NO_ATOMICS — compiles to a plain LDR (aligned 32-bit load is naturally + // atomic on ARMv5TE) but expresses the concurrent-access intent in the C++ + // memory model. Writes live behind *_locked_ helpers and must hold lock_. #ifdef ESPHOME_THREAD_MULTI_ATOMICS std::atomic to_add_count_{0}; #else - volatile uint32_t to_add_count_{0}; + uint32_t to_add_count_{0}; #endif #endif /* ESPHOME_THREAD_SINGLE */ // Fast-path helper for process_to_add() to decide if it can skip the lock. - // - SINGLE: direct container check (no concurrent writers). - // - ATOMICS: lock-free load of to_add_count_. - // - NO_ATOMICS: volatile read. A stale 0 is benign — next call() iteration - // observes the update; RMW mutation is still under lock_. bool to_add_empty_() const { #ifdef ESPHOME_THREAD_SINGLE return this->to_add_.empty(); #elif defined(ESPHOME_THREAD_MULTI_ATOMICS) return this->to_add_count_.load(std::memory_order_relaxed) == 0; #else - return this->to_add_count_ == 0; + return __atomic_load_n(&this->to_add_count_, __ATOMIC_RELAXED) == 0; #endif } - // Increment to_add_count_ (no-op on single-threaded platforms) + // Increment to_add_count_ (no-op on single-threaded platforms). + // On NO_ATOMICS the caller must hold lock_ to serialise the load-modify-store + // against other writers; the __atomic_store_n makes the write visible to + // concurrent readers in the C++ memory model. void to_add_count_increment_locked_() { #ifdef ESPHOME_THREAD_SINGLE // No counter needed — to_add_empty_() checks the vector directly #elif defined(ESPHOME_THREAD_MULTI_ATOMICS) this->to_add_count_.fetch_add(1, std::memory_order_relaxed); #else - this->to_add_count_++; + __atomic_store_n(&this->to_add_count_, __atomic_load_n(&this->to_add_count_, __ATOMIC_RELAXED) + 1, __ATOMIC_RELAXED); #endif } @@ -569,7 +570,7 @@ class Scheduler { #elif defined(ESPHOME_THREAD_MULTI_ATOMICS) this->to_add_count_.store(0, std::memory_order_relaxed); #else - this->to_add_count_ = 0; + __atomic_store_n(&this->to_add_count_, 0, __ATOMIC_RELAXED); #endif } @@ -581,11 +582,11 @@ class Scheduler { size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items) // Fast-path counter for process_defer_queue_() to skip lock when nothing to - // process. See to_add_count_ above for the volatile rationale on NO_ATOMICS. + // process. See to_add_count_ above for the NO_ATOMICS rationale. #ifdef ESPHOME_THREAD_MULTI_ATOMICS std::atomic defer_count_{0}; #else - volatile uint32_t defer_count_{0}; + uint32_t defer_count_{0}; #endif bool defer_empty_() const { @@ -593,7 +594,7 @@ class Scheduler { #ifdef ESPHOME_THREAD_MULTI_ATOMICS return this->defer_count_.load(std::memory_order_relaxed) == 0; #else - return this->defer_count_ == 0; + return __atomic_load_n(&this->defer_count_, __ATOMIC_RELAXED) == 0; #endif } @@ -601,7 +602,7 @@ class Scheduler { #ifdef ESPHOME_THREAD_MULTI_ATOMICS this->defer_count_.fetch_add(1, std::memory_order_relaxed); #else - this->defer_count_++; + __atomic_store_n(&this->defer_count_, __atomic_load_n(&this->defer_count_, __ATOMIC_RELAXED) + 1, __ATOMIC_RELAXED); #endif } @@ -609,61 +610,69 @@ class Scheduler { #ifdef ESPHOME_THREAD_MULTI_ATOMICS this->defer_count_.store(0, std::memory_order_relaxed); #else - this->defer_count_ = 0; + __atomic_store_n(&this->defer_count_, 0, __ATOMIC_RELAXED); #endif } #endif /* ESPHOME_THREAD_SINGLE */ // Counter for items marked for removal. Incremented cross-thread in - // cancel_item_locked_(). See to_add_count_ above for the volatile rationale - // on NO_ATOMICS. + // cancel_item_locked_(). See to_add_count_ above for the NO_ATOMICS + // rationale. #ifdef ESPHOME_THREAD_MULTI_ATOMICS std::atomic to_remove_{0}; -#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) - volatile uint32_t to_remove_{0}; #else -uint32_t to_remove_{0}; + uint32_t to_remove_{0}; #endif // Lock-free check if there are items to remove (for fast-path in cleanup_) bool to_remove_empty_() const { #ifdef ESPHOME_THREAD_MULTI_ATOMICS return this->to_remove_.load(std::memory_order_relaxed) == 0; +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + return __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED) == 0; #else - return this->to_remove_ == 0; + return this->to_remove_ == 0; #endif } void to_remove_add_locked_(uint32_t count) { #ifdef ESPHOME_THREAD_MULTI_ATOMICS this->to_remove_.fetch_add(count, std::memory_order_relaxed); +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + __atomic_store_n(&this->to_remove_, __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED) + count, __ATOMIC_RELAXED); #else - this->to_remove_ += count; + this->to_remove_ += count; #endif } void to_remove_decrement_locked_() { #ifdef ESPHOME_THREAD_MULTI_ATOMICS this->to_remove_.fetch_sub(1, std::memory_order_relaxed); +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + __atomic_store_n(&this->to_remove_, __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED) - 1, __ATOMIC_RELAXED); #else - this->to_remove_--; + this->to_remove_--; #endif } void to_remove_clear_locked_() { #ifdef ESPHOME_THREAD_MULTI_ATOMICS this->to_remove_.store(0, std::memory_order_relaxed); +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + __atomic_store_n(&this->to_remove_, 0, __ATOMIC_RELAXED); #else - this->to_remove_ = 0; + this->to_remove_ = 0; #endif } uint32_t to_remove_count_() const { #ifdef ESPHOME_THREAD_MULTI_ATOMICS return this->to_remove_.load(std::memory_order_relaxed); +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + return __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED); #else - return this->to_remove_; + return this->to_remove_; #endif } From 202bcf5b101dd8497f147e1163e6ad7a262fb600 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Apr 2026 06:07:51 -0500 Subject: [PATCH 2/6] [scheduler] Use __atomic_load_n only on reader fast-path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Walk back the __atomic_store_n on the writer paths — the mutators already hold lock_, so plain counter_++/=/+=/-- is sufficient to serialise against other writers. The reader fast-path still uses __atomic_load_n(&counter, __ATOMIC_RELAXED) to express concurrent- read intent in the C++ memory model and keep the compiler from caching/eliding the read. On ARMv5TE it compiles to a plain LDR — same codegen as before. --- esphome/core/scheduler.h | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index a6ee079c3a..d5d1a0385c 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -550,16 +550,15 @@ class Scheduler { } // Increment to_add_count_ (no-op on single-threaded platforms). - // On NO_ATOMICS the caller must hold lock_ to serialise the load-modify-store - // against other writers; the __atomic_store_n makes the write visible to - // concurrent readers in the C++ memory model. + // On NO_ATOMICS the caller must hold lock_ to serialise RMW against + // other writers; reader fast-path uses __atomic_load_n. void to_add_count_increment_locked_() { #ifdef ESPHOME_THREAD_SINGLE // No counter needed — to_add_empty_() checks the vector directly #elif defined(ESPHOME_THREAD_MULTI_ATOMICS) this->to_add_count_.fetch_add(1, std::memory_order_relaxed); #else - __atomic_store_n(&this->to_add_count_, __atomic_load_n(&this->to_add_count_, __ATOMIC_RELAXED) + 1, __ATOMIC_RELAXED); + this->to_add_count_++; #endif } @@ -570,7 +569,7 @@ class Scheduler { #elif defined(ESPHOME_THREAD_MULTI_ATOMICS) this->to_add_count_.store(0, std::memory_order_relaxed); #else - __atomic_store_n(&this->to_add_count_, 0, __ATOMIC_RELAXED); + this->to_add_count_ = 0; #endif } @@ -602,7 +601,7 @@ class Scheduler { #ifdef ESPHOME_THREAD_MULTI_ATOMICS this->defer_count_.fetch_add(1, std::memory_order_relaxed); #else - __atomic_store_n(&this->defer_count_, __atomic_load_n(&this->defer_count_, __ATOMIC_RELAXED) + 1, __ATOMIC_RELAXED); + this->defer_count_++; #endif } @@ -610,7 +609,7 @@ class Scheduler { #ifdef ESPHOME_THREAD_MULTI_ATOMICS this->defer_count_.store(0, std::memory_order_relaxed); #else - __atomic_store_n(&this->defer_count_, 0, __ATOMIC_RELAXED); + this->defer_count_ = 0; #endif } @@ -639,30 +638,24 @@ class Scheduler { void to_remove_add_locked_(uint32_t count) { #ifdef ESPHOME_THREAD_MULTI_ATOMICS this->to_remove_.fetch_add(count, std::memory_order_relaxed); -#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) - __atomic_store_n(&this->to_remove_, __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED) + count, __ATOMIC_RELAXED); #else - this->to_remove_ += count; + this->to_remove_ += count; #endif } void to_remove_decrement_locked_() { #ifdef ESPHOME_THREAD_MULTI_ATOMICS this->to_remove_.fetch_sub(1, std::memory_order_relaxed); -#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) - __atomic_store_n(&this->to_remove_, __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED) - 1, __ATOMIC_RELAXED); #else - this->to_remove_--; + this->to_remove_--; #endif } void to_remove_clear_locked_() { #ifdef ESPHOME_THREAD_MULTI_ATOMICS this->to_remove_.store(0, std::memory_order_relaxed); -#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) - __atomic_store_n(&this->to_remove_, 0, __ATOMIC_RELAXED); #else - this->to_remove_ = 0; + this->to_remove_ = 0; #endif } From 3c2396ab86cea654f9a17543d34f9093767fb2d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Apr 2026 06:11:03 -0500 Subject: [PATCH 3/6] [core] Apply __atomic_load_n/store_n pattern to Millis64 NO_ATOMICS path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same treatment as the scheduler counters (#15947): - Unlocked reads of millis_major / last_millis at the top of Millis64Impl::compute(): switch from plain reads to __atomic_load_n(&..., __ATOMIC_RELAXED). - Unlocked write of last_millis in the "normal forward progression" branch: switch from plain assignment to __atomic_store_n(..., __ATOMIC_RELAXED). This is the one write that happens without the lock, so it needs to be formally atomic to pair cleanly with the unlocked atomic reader in the C++ memory model. - Writes under `lock` stay plain (millis_major++, last_millis = now inside the near-rollover branch). The lock serialises them against other writers. On ARMv5TE the builtins compile to plain LDR/STR — same codegen, no libatomic dependency. Updates the "accepting minor races" comment to describe the formally-defined version of the race. --- esphome/core/time_64.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/esphome/core/time_64.cpp b/esphome/core/time_64.cpp index b8a299ff7e..0d381dc322 100644 --- a/esphome/core/time_64.cpp +++ b/esphome/core/time_64.cpp @@ -72,10 +72,14 @@ uint64_t Millis64Impl::compute(uint32_t now) { // Without atomics, this implementation uses locks more aggressively: // 1. Always locks when near the rollover boundary (within 10 seconds) // 2. Always locks when detecting a large backwards jump - // 3. Updates without lock in normal forward progression (accepting minor races) - // This is less efficient but necessary without atomic operations. - uint16_t major = millis_major; - uint32_t last = last_millis; + // 3. Updates without lock in normal forward progression. + // Concurrent reads/writes use __atomic_load_n / __atomic_store_n with + // __ATOMIC_RELAXED so the cross-thread accesses are well-defined in the + // C++ memory model. On ARMv5TE these compile to plain LDR/STR. Writers + // holding `lock` use plain assignments; the lock serialises them against + // other writers. + uint16_t major = __atomic_load_n(&millis_major, __ATOMIC_RELAXED); + uint32_t last = __atomic_load_n(&last_millis, __ATOMIC_RELAXED); // Define a safe window around the rollover point (10 seconds) // This covers any reasonable scheduler delays or thread preemption @@ -101,13 +105,14 @@ uint64_t Millis64Impl::compute(uint32_t now) { // Update last_millis while holding lock last_millis = now; } else if (now > last) { - // Normal case: Not near rollover and time moved forward - // Update without lock. While this may cause minor races (microseconds of - // backwards time movement), they're acceptable because: + // Normal case: Not near rollover and time moved forward. Publish the new + // low word without taking the lock. A concurrent writer under lock may + // overwrite this with a slightly-different value, which can produce a + // few microseconds of backwards time movement — acceptable because: // 1. The scheduler operates at millisecond resolution, not microsecond // 2. We've already prevented the critical rollover race condition // 3. Any backwards movement is orders of magnitude smaller than scheduler delays - last_millis = now; + __atomic_store_n(&last_millis, now, __ATOMIC_RELAXED); } // If now <= last and we're not near rollover, don't update // This minimizes backwards time movement From b3f93a4da78798b0d8bd008717e8d937c17f202d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Apr 2026 06:12:10 -0500 Subject: [PATCH 4/6] [core] Restore original Millis64 NO_ATOMICS comments The preceding commit needlessly rewrote comments that were still accurate. Revert the prose-only changes; keep only the two line-level code changes (__atomic_load_n on the unlocked reads, __atomic_store_n on the unlocked write). --- esphome/core/time_64.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/esphome/core/time_64.cpp b/esphome/core/time_64.cpp index 0d381dc322..70ad209f02 100644 --- a/esphome/core/time_64.cpp +++ b/esphome/core/time_64.cpp @@ -72,12 +72,8 @@ uint64_t Millis64Impl::compute(uint32_t now) { // Without atomics, this implementation uses locks more aggressively: // 1. Always locks when near the rollover boundary (within 10 seconds) // 2. Always locks when detecting a large backwards jump - // 3. Updates without lock in normal forward progression. - // Concurrent reads/writes use __atomic_load_n / __atomic_store_n with - // __ATOMIC_RELAXED so the cross-thread accesses are well-defined in the - // C++ memory model. On ARMv5TE these compile to plain LDR/STR. Writers - // holding `lock` use plain assignments; the lock serialises them against - // other writers. + // 3. Updates without lock in normal forward progression (accepting minor races) + // This is less efficient but necessary without atomic operations. uint16_t major = __atomic_load_n(&millis_major, __ATOMIC_RELAXED); uint32_t last = __atomic_load_n(&last_millis, __ATOMIC_RELAXED); @@ -105,10 +101,9 @@ uint64_t Millis64Impl::compute(uint32_t now) { // Update last_millis while holding lock last_millis = now; } else if (now > last) { - // Normal case: Not near rollover and time moved forward. Publish the new - // low word without taking the lock. A concurrent writer under lock may - // overwrite this with a slightly-different value, which can produce a - // few microseconds of backwards time movement — acceptable because: + // Normal case: Not near rollover and time moved forward + // Update without lock. While this may cause minor races (microseconds of + // backwards time movement), they're acceptable because: // 1. The scheduler operates at millisecond resolution, not microsecond // 2. We've already prevented the critical rollover race condition // 3. Any backwards movement is orders of magnitude smaller than scheduler delays From 98e88ac02c0f33385049e4e52232022372062282 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Apr 2026 06:14:48 -0500 Subject: [PATCH 5/6] [scheduler] Normalise to_remove_empty_/to_remove_count_ preprocessor form Use `#if defined(X)` / `#elif defined(Y)` / `#else` for the three-way ATOMICS / NO_ATOMICS / SINGLE split. Also fix the SINGLE-branch body indentation to match the other branches. --- esphome/core/scheduler.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index d5d1a0385c..aaa8f7fc5c 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -626,7 +626,7 @@ class Scheduler { // Lock-free check if there are items to remove (for fast-path in cleanup_) bool to_remove_empty_() const { -#ifdef ESPHOME_THREAD_MULTI_ATOMICS +#if defined(ESPHOME_THREAD_MULTI_ATOMICS) return this->to_remove_.load(std::memory_order_relaxed) == 0; #elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) return __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED) == 0; @@ -660,7 +660,7 @@ class Scheduler { } uint32_t to_remove_count_() const { -#ifdef ESPHOME_THREAD_MULTI_ATOMICS +#if defined(ESPHOME_THREAD_MULTI_ATOMICS) return this->to_remove_.load(std::memory_order_relaxed); #elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) return __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED); From 867fae3bb813ae042ca27b7c7b73c9a3e3ae926c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Apr 2026 06:19:28 -0500 Subject: [PATCH 6/6] [scheduler,core] Make NO_ATOMICS writer paths fully memory-model-clean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch writer-side plain stores under lock_ to __atomic_store_n with __ATOMIC_RELAXED on NO_ATOMICS. The input value for RMW is read plainly (safe — only writers mutate, serialised by the lock; readers only atomic-load so two reads don't race). Closes the formal C++ memory-model hole where plain-store vs atomic-load was a data race in the standard even though aligned 32-bit STR/LDR on ARMv5TE is atomic in practice. Applies to scheduler.h counter mutators and the under-lock writes to last_millis / millis_major in time_64.cpp's near-rollover branch. Same ARMv5TE codegen (plain STR). ATOMICS / SINGLE paths unchanged. --- esphome/core/scheduler.h | 36 ++++++++++++++++++++++-------------- esphome/core/time_64.cpp | 4 ++-- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index aaa8f7fc5c..5b3eb46ee5 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -550,26 +550,28 @@ class Scheduler { } // Increment to_add_count_ (no-op on single-threaded platforms). - // On NO_ATOMICS the caller must hold lock_ to serialise RMW against - // other writers; reader fast-path uses __atomic_load_n. + // On NO_ATOMICS the caller must hold lock_; the atomic store pairs with + // the reader's __atomic_load_n in to_add_empty_(). The input-value read is + // plain — safe because only writers (serialised by lock_) modify the + // counter, and concurrent readers only atomic-load (no conflicting write). void to_add_count_increment_locked_() { -#ifdef ESPHOME_THREAD_SINGLE +#if defined(ESPHOME_THREAD_SINGLE) // No counter needed — to_add_empty_() checks the vector directly #elif defined(ESPHOME_THREAD_MULTI_ATOMICS) this->to_add_count_.fetch_add(1, std::memory_order_relaxed); #else - this->to_add_count_++; + __atomic_store_n(&this->to_add_count_, this->to_add_count_ + 1, __ATOMIC_RELAXED); #endif } // Reset to_add_count_ (no-op on single-threaded platforms) void to_add_count_clear_locked_() { -#ifdef ESPHOME_THREAD_SINGLE +#if defined(ESPHOME_THREAD_SINGLE) // No counter needed — to_add_empty_() checks the vector directly #elif defined(ESPHOME_THREAD_MULTI_ATOMICS) this->to_add_count_.store(0, std::memory_order_relaxed); #else - this->to_add_count_ = 0; + __atomic_store_n(&this->to_add_count_, 0, __ATOMIC_RELAXED); #endif } @@ -601,7 +603,7 @@ class Scheduler { #ifdef ESPHOME_THREAD_MULTI_ATOMICS this->defer_count_.fetch_add(1, std::memory_order_relaxed); #else - this->defer_count_++; + __atomic_store_n(&this->defer_count_, this->defer_count_ + 1, __ATOMIC_RELAXED); #endif } @@ -609,7 +611,7 @@ class Scheduler { #ifdef ESPHOME_THREAD_MULTI_ATOMICS this->defer_count_.store(0, std::memory_order_relaxed); #else - this->defer_count_ = 0; + __atomic_store_n(&this->defer_count_, 0, __ATOMIC_RELAXED); #endif } @@ -636,26 +638,32 @@ class Scheduler { } void to_remove_add_locked_(uint32_t count) { -#ifdef ESPHOME_THREAD_MULTI_ATOMICS +#if defined(ESPHOME_THREAD_MULTI_ATOMICS) this->to_remove_.fetch_add(count, std::memory_order_relaxed); +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + __atomic_store_n(&this->to_remove_, this->to_remove_ + count, __ATOMIC_RELAXED); #else - this->to_remove_ += count; + this->to_remove_ += count; #endif } void to_remove_decrement_locked_() { -#ifdef ESPHOME_THREAD_MULTI_ATOMICS +#if defined(ESPHOME_THREAD_MULTI_ATOMICS) this->to_remove_.fetch_sub(1, std::memory_order_relaxed); +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + __atomic_store_n(&this->to_remove_, this->to_remove_ - 1, __ATOMIC_RELAXED); #else - this->to_remove_--; + this->to_remove_--; #endif } void to_remove_clear_locked_() { -#ifdef ESPHOME_THREAD_MULTI_ATOMICS +#if defined(ESPHOME_THREAD_MULTI_ATOMICS) this->to_remove_.store(0, std::memory_order_relaxed); +#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS) + __atomic_store_n(&this->to_remove_, 0, __ATOMIC_RELAXED); #else - this->to_remove_ = 0; + this->to_remove_ = 0; #endif } diff --git a/esphome/core/time_64.cpp b/esphome/core/time_64.cpp index 70ad209f02..d9a12aa477 100644 --- a/esphome/core/time_64.cpp +++ b/esphome/core/time_64.cpp @@ -92,14 +92,14 @@ uint64_t Millis64Impl::compute(uint32_t now) { if (now < last && (last - now) > HALF_MAX_UINT32) { // True rollover detected (happens every ~49.7 days) - millis_major++; + __atomic_store_n(&millis_major, static_cast(millis_major + 1), __ATOMIC_RELAXED); major++; #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); #endif /* ESPHOME_DEBUG_SCHEDULER */ } // Update last_millis while holding lock - last_millis = now; + __atomic_store_n(&last_millis, now, __ATOMIC_RELAXED); } else if (now > last) { // Normal case: Not near rollover and time moved forward // Update without lock. While this may cause minor races (microseconds of