[core] Move millis_64 rollover tracking out of Scheduler (#14360)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
J. Nick Koston
2026-03-02 06:56:51 -10:00
committed by GitHub
parent 77a7cbcffd
commit 1c5fd8bbd4
12 changed files with 257 additions and 226 deletions
+1
View File
@@ -1438,6 +1438,7 @@ async def to_code(config):
cg.set_cpp_standard("gnu++20")
cg.add_build_flag("-DUSE_ESP32")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.add_build_flag("-Wl,-z,noexecstack")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
variant = config[CONF_VARIANT]
+2 -2
View File
@@ -3,7 +3,7 @@
#include "core.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include "esphome/core/time_64.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
#include <Arduino.h>
@@ -17,7 +17,7 @@ namespace esphome {
void HOT yield() { ::yield(); }
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); }
uint64_t millis_64() { return Millis64Impl::compute(::millis()); }
void HOT delay(uint32_t ms) { ::delay(ms); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
+1
View File
@@ -41,6 +41,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
cg.add_build_flag("-DUSE_HOST")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
cg.add_build_flag("-std=gnu++20")
cg.add_define("ESPHOME_BOARD", "host")
+2 -2
View File
@@ -3,7 +3,7 @@
#include "core.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include "esphome/core/time_64.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
@@ -14,7 +14,7 @@ namespace esphome {
void HOT yield() { ::yield(); }
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); }
uint64_t millis_64() { return Millis64Impl::compute(::millis()); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void HOT delay(uint32_t ms) { ::delay(ms); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); }
+1
View File
@@ -169,6 +169,7 @@ async def to_code(config):
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_RP2040")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.set_cpp_standard("gnu++20")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "RP2040")
+1
View File
@@ -112,6 +112,7 @@ def add_extra_script(stage: str, filename: str, path: Path) -> None:
def zephyr_to_code(config):
cg.add_build_flag("-DUSE_ZEPHYR")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.set_cpp_standard("gnu++20")
# build is done by west so bypass board checking in platformio
cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards"))
+6
View File
@@ -650,6 +650,12 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"time_64.cpp": {
PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
# Note: lock_free_queue.h and event_pool.h are header files and don't need to be filtered
# as they are only included when needed by the preprocessor
}
+5
View File
@@ -178,6 +178,11 @@
#define USE_I2S_LEGACY
#endif
// Platforms with native 64-bit time sources (no rollover tracking needed)
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) || defined(USE_RP2040)
#define USE_NATIVE_64BIT_TIME
#endif
// ESP32-specific feature flags
#ifdef USE_ESP32
#define USE_MQTT_IDF_ENQUEUE
+3 -179
View File
@@ -9,7 +9,6 @@
#include <algorithm>
#include <cinttypes>
#include <cstring>
#include <limits>
namespace esphome {
@@ -28,10 +27,6 @@ static constexpr size_t MAX_POOL_SIZE = 5;
// Set to 5 to match the pool size - when we have as many cancelled items as our
// pool can hold, it's time to clean up and recycle them.
static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5;
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
// Half the 32-bit range - used to detect rollovers vs normal time progression
static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2;
#endif
// max delay to start an interval sequence
static constexpr uint32_t MAX_INTERVAL_DELAY = 5000;
@@ -152,9 +147,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
return;
}
// Get fresh 64-bit timestamp BEFORE taking lock
const uint64_t now_64 = millis_64();
// Take lock early to protect scheduler_item_pool_ access
LockGuard guard{this->lock_};
@@ -181,6 +173,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
} else
#endif /* not ESPHOME_THREAD_SINGLE */
{
// Only non-defer items need a timestamp for scheduling
const uint64_t now_64 = millis_64();
// Type-specific setup
if (type == SchedulerItem::INTERVAL) {
item->interval = delay;
@@ -475,19 +470,8 @@ void HOT Scheduler::call(uint32_t now) {
if (now_64 - last_print > 2000) {
last_print = now_64;
std::vector<SchedulerItemPtr> old_items;
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040) && \
defined(ESPHOME_THREAD_MULTI_ATOMICS)
const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed);
const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed);
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg);
#elif !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_);
#else
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64, this->items_.size(), this->scheduler_item_pool_.size(),
now_64);
#endif
// Cleanup before debug output
this->cleanup_();
while (!this->items_.empty()) {
@@ -715,166 +699,6 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type
return total_cancelled > 0;
}
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
uint64_t Scheduler::millis_64_impl_(uint32_t now) {
// THREAD SAFETY NOTE:
// This function has three implementations, based on the precompiler flags
// - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, RP2040, etc.)
// - ESPHOME_THREAD_MULTI_NO_ATOMICS - Runs on multi-threaded platforms without atomics (LibreTiny BK72xx)
// - ESPHOME_THREAD_MULTI_ATOMICS - Runs on multi-threaded platforms with atomics (ESP32, HOST, LibreTiny
// RTL87xx/LN882x, etc.)
//
// Make sure all changes are synchronized if you edit this function.
//
// IMPORTANT: Always pass fresh millis() values to this function. The implementation
// handles out-of-order timestamps between threads, but minimizing time differences
// helps maintain accuracy.
//
#ifdef ESPHOME_THREAD_SINGLE
// This is the single core implementation.
//
// Single-core platforms have no concurrency, so this is a simple implementation
// that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics.
uint16_t major = this->millis_major_;
uint32_t last = this->last_millis_;
// Check for rollover
if (now < last && (last - now) > HALF_MAX_UINT32) {
this->millis_major_++;
major++;
this->last_millis_ = now;
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
#endif /* ESPHOME_DEBUG_SCHEDULER */
} else if (now > last) {
// Only update if time moved forward
this->last_millis_ = now;
}
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
return now + (static_cast<uint64_t>(major) << 32);
#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
// This is the multi core no atomics implementation.
//
// 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 = this->millis_major_;
uint32_t last = this->last_millis_;
// Define a safe window around the rollover point (10 seconds)
// This covers any reasonable scheduler delays or thread preemption
static constexpr uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds
// Check if we're near the rollover boundary (close to std::numeric_limits<uint32_t>::max() or just past 0)
bool near_rollover = (last > (std::numeric_limits<uint32_t>::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW);
if (near_rollover || (now < last && (last - now) > HALF_MAX_UINT32)) {
// Near rollover or detected a rollover - need lock for safety
LockGuard guard{this->lock_};
// Re-read with lock held
last = this->last_millis_;
if (now < last && (last - now) > HALF_MAX_UINT32) {
// True rollover detected (happens every ~49.7 days)
this->millis_major_++;
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
this->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:
// 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
this->last_millis_ = now;
}
// If now <= last and we're not near rollover, don't update
// This minimizes backwards time movement
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
return now + (static_cast<uint64_t>(major) << 32);
#elif defined(ESPHOME_THREAD_MULTI_ATOMICS)
// This is the multi core with atomics implementation.
//
// Uses atomic operations with acquire/release semantics to ensure coherent
// reads of millis_major_ and last_millis_ across cores. Features:
// 1. Epoch-coherency retry loop to handle concurrent updates
// 2. Lock only taken for actual rollover detection and update
// 3. Lock-free CAS updates for normal forward time progression
// 4. Memory ordering ensures cores see consistent time values
for (;;) {
uint16_t major = this->millis_major_.load(std::memory_order_acquire);
/*
* Acquire so that if we later decide **not** to take the lock we still
* observe a `millis_major_` value coherent with the loaded `last_millis_`.
* The acquire load ensures any later read of `millis_major_` sees its
* corresponding increment.
*/
uint32_t last = this->last_millis_.load(std::memory_order_acquire);
// If we might be near a rollover (large backwards jump), take the lock for the entire operation
// This ensures rollover detection and last_millis_ update are atomic together
if (now < last && (last - now) > HALF_MAX_UINT32) {
// Potential rollover - need lock for atomic rollover detection + update
LockGuard guard{this->lock_};
// Re-read with lock held; mutex already provides ordering
last = this->last_millis_.load(std::memory_order_relaxed);
if (now < last && (last - now) > HALF_MAX_UINT32) {
// True rollover detected (happens every ~49.7 days)
this->millis_major_.fetch_add(1, std::memory_order_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 the lock to prevent races
* Publish the new low-word *after* bumping `millis_major_` (done above)
* so readers never see a mismatched pair.
*/
this->last_millis_.store(now, std::memory_order_release);
} else {
// Normal case: Try lock-free update, but only allow forward movement within same epoch
// This prevents accidentally moving backwards across a rollover boundary
while (now > last && (now - last) < HALF_MAX_UINT32) {
if (this->last_millis_.compare_exchange_weak(last, now,
std::memory_order_release, // success
std::memory_order_relaxed)) { // failure
break;
}
// CAS failure means no data was published; relaxed is fine
// last is automatically updated by compare_exchange_weak if it fails
}
}
uint16_t major_end = this->millis_major_.load(std::memory_order_relaxed);
if (major_end == major)
return now + (static_cast<uint64_t>(major) << 32);
}
// Unreachable - the loop always returns when major_end == major
__builtin_unreachable();
#else
#error \
"No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined."
#endif
}
#endif // !USE_ESP32 && !USE_HOST && !USE_ZEPHYR && !USE_RP2040
bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) {
// High bits are almost always equal (change only on 32-bit rollover ~49 days)
// Optimize for common case: check low bits first when high bits are equal
+4 -43
View File
@@ -12,6 +12,7 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/time_64.h"
namespace esphome {
@@ -284,23 +285,16 @@ class Scheduler {
bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id);
// Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now.
// On ESP32, Host, Zephyr, and RP2040, ignores now and uses the native 64-bit time source via millis_64().
// On platforms with native 64-bit time, ignores now and uses millis_64() directly.
// On other platforms, extends now to 64-bit using rollover tracking.
uint64_t millis_64_from_(uint32_t now) {
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_ZEPHYR) || defined(USE_RP2040)
#ifdef USE_NATIVE_64BIT_TIME
(void) now;
return millis_64();
#else
return this->millis_64_impl_(now);
return Millis64Impl::compute(now);
#endif
}
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
// On platforms without native 64-bit time, millis_64() HAL function delegates to this
// method which tracks 32-bit millis() rollover using millis_major_ and last_millis_.
friend uint64_t millis_64();
uint64_t millis_64_impl_(uint32_t now);
#endif
// Cleanup logically deleted items from the scheduler
// Returns the number of items remaining after cleanup
// IMPORTANT: This method should only be called from the main thread (loop task).
@@ -566,39 +560,6 @@ class Scheduler {
// can stall the entire system, causing timing issues and dropped events for any components that need
// to synchronize between tasks (see https://github.com/esphome/backlog/issues/52)
std::vector<SchedulerItemPtr> scheduler_item_pool_;
#if !defined(USE_ESP32) && !defined(USE_HOST) && !defined(USE_ZEPHYR) && !defined(USE_RP2040)
// On platforms with native 64-bit time (ESP32, Host, Zephyr, RP2040), no rollover tracking needed.
// On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_().
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
/*
* Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates
*
* MEMORY-ORDERING NOTE
* --------------------
* `last_millis_` and `millis_major_` form a single 64-bit timestamp split in half.
* Writers publish `last_millis_` with memory_order_release and readers use
* memory_order_acquire. This ensures that once a reader sees the new low word,
* it also observes the corresponding increment of `millis_major_`.
*/
std::atomic<uint32_t> last_millis_{0};
#else /* not ESPHOME_THREAD_MULTI_ATOMICS */
// Platforms without atomic support or single-threaded platforms
uint32_t last_millis_{0};
#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */
/*
* Upper 16 bits of the 64-bit millis counter. Incremented only while holding
* `lock_`; read concurrently. Atomic (relaxed) avoids a formal data race.
* Ordering relative to `last_millis_` is provided by its release store and the
* corresponding acquire loads.
*/
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
std::atomic<uint16_t> millis_major_{0};
#else /* not ESPHOME_THREAD_MULTI_ATOMICS */
uint16_t millis_major_{0};
#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */
#endif /* !USE_ESP32 && !USE_HOST && !USE_ZEPHYR && !USE_RP2040 */
};
} // namespace esphome
+207
View File
@@ -0,0 +1,207 @@
#include "esphome/core/defines.h"
#ifndef USE_NATIVE_64BIT_TIME
#include "time_64.h"
#include "esphome/core/helpers.h"
#ifdef ESPHOME_DEBUG_SCHEDULER
#include "esphome/core/log.h"
#include <cinttypes>
#endif
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
#include <atomic>
#endif
#include <limits>
namespace esphome {
#ifdef ESPHOME_DEBUG_SCHEDULER
static const char *const TAG = "time_64";
#endif
uint64_t Millis64Impl::compute(uint32_t now) {
// Half the 32-bit range - used to detect rollovers vs normal time progression
static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2;
// State variables for rollover tracking - static to persist across calls
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// Mutex for rollover serialization (taken only every ~49.7 days).
// A spinlock would be smaller (~1 byte vs ~80-100 bytes) but is unsafe on
// preemptive single-core RTOS platforms due to priority inversion: a high-priority
// task spinning would prevent the lock holder from running to release it.
static Mutex lock;
/*
* Multi-threaded platforms with atomic support: last_millis needs atomic for lock-free updates.
* Writers publish last_millis with memory_order_release and readers use memory_order_acquire.
* This ensures that once a reader sees the new low word, it also observes the corresponding
* increment of millis_major.
*/
static std::atomic<uint32_t> last_millis{0};
/*
* Upper 16 bits of the 64-bit millis counter. Incremented only while holding lock;
* read concurrently. Atomic (relaxed) avoids a formal data race. Ordering relative
* to last_millis is provided by its release store and the corresponding acquire loads.
*/
static std::atomic<uint16_t> millis_major{0};
#elif !defined(ESPHOME_THREAD_SINGLE) /* ESPHOME_THREAD_MULTI_NO_ATOMICS */
static Mutex lock;
static uint32_t last_millis{0};
static uint16_t millis_major{0};
#else /* ESPHOME_THREAD_SINGLE */
static uint32_t last_millis{0};
static uint16_t millis_major{0};
#endif
// THREAD SAFETY NOTE:
// This function has three implementations, based on the precompiler flags
// - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, etc.)
// - ESPHOME_THREAD_MULTI_NO_ATOMICS - Runs on multi-threaded platforms without atomics (LibreTiny BK72xx)
// - ESPHOME_THREAD_MULTI_ATOMICS - Runs on multi-threaded platforms with atomics (LibreTiny RTL87xx/LN882x, etc.)
//
// Make sure all changes are synchronized if you edit this function.
//
// IMPORTANT: Always pass fresh millis() values to this function. The implementation
// handles out-of-order timestamps between threads, but minimizing time differences
// helps maintain accuracy.
#ifdef ESPHOME_THREAD_SINGLE
// Single-core platforms have no concurrency, so this is a simple implementation
// that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics.
uint16_t major = millis_major;
uint32_t last = last_millis;
// Check for rollover
if (now < last && (last - now) > HALF_MAX_UINT32) {
millis_major++;
major++;
last_millis = now;
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
#endif /* ESPHOME_DEBUG_SCHEDULER */
} else if (now > last) {
// Only update if time moved forward
last_millis = now;
}
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
return now + (static_cast<uint64_t>(major) << 32);
#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
// 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;
// Define a safe window around the rollover point (10 seconds)
// This covers any reasonable scheduler delays or thread preemption
static constexpr uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds
// Check if we're near the rollover boundary (close to std::numeric_limits<uint32_t>::max() or just past 0)
bool near_rollover = (last > (std::numeric_limits<uint32_t>::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW);
if (near_rollover || (now < last && (last - now) > HALF_MAX_UINT32)) {
// Near rollover or detected a rollover - need lock for safety
LockGuard guard{lock};
// Re-read with lock held
last = last_millis;
if (now < last && (last - now) > HALF_MAX_UINT32) {
// True rollover detected (happens every ~49.7 days)
millis_major++;
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;
} 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:
// 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;
}
// If now <= last and we're not near rollover, don't update
// This minimizes backwards time movement
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
return now + (static_cast<uint64_t>(major) << 32);
#elif defined(ESPHOME_THREAD_MULTI_ATOMICS)
// Uses atomic operations with acquire/release semantics to ensure coherent
// reads of millis_major and last_millis across cores. Features:
// 1. Epoch-coherency retry loop to handle concurrent updates
// 2. Lock only taken for actual rollover detection and update
// 3. Lock-free CAS updates for normal forward time progression
// 4. Memory ordering ensures cores see consistent time values
for (;;) {
uint16_t major = millis_major.load(std::memory_order_acquire);
/*
* Acquire so that if we later decide **not** to take the lock we still
* observe a millis_major value coherent with the loaded last_millis.
* The acquire load ensures any later read of millis_major sees its
* corresponding increment.
*/
uint32_t last = last_millis.load(std::memory_order_acquire);
// If we might be near a rollover (large backwards jump), take the lock
// This ensures rollover detection and last_millis update are atomic together
if (now < last && (last - now) > HALF_MAX_UINT32) {
// Potential rollover - need lock for atomic rollover detection + update
LockGuard guard{lock};
// Re-read with lock held; mutex already provides ordering
last = last_millis.load(std::memory_order_relaxed);
if (now < last && (last - now) > HALF_MAX_UINT32) {
// True rollover detected (happens every ~49.7 days)
millis_major.fetch_add(1, std::memory_order_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 the lock to prevent races.
* Publish the new low-word *after* bumping millis_major (done above)
* so readers never see a mismatched pair.
*/
last_millis.store(now, std::memory_order_release);
} else {
// Normal case: Try lock-free update, but only allow forward movement within same epoch
// This prevents accidentally moving backwards across a rollover boundary
while (now > last && (now - last) < HALF_MAX_UINT32) {
if (last_millis.compare_exchange_weak(last, now,
std::memory_order_release, // success
std::memory_order_relaxed)) { // failure
break;
}
// CAS failure means no data was published; relaxed is fine
// last is automatically updated by compare_exchange_weak if it fails
}
}
uint16_t major_end = millis_major.load(std::memory_order_relaxed);
if (major_end == major)
return now + (static_cast<uint64_t>(major) << 32);
}
// Unreachable - the loop always returns when major_end == major
__builtin_unreachable();
#else
#error \
"No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined."
#endif
}
} // namespace esphome
#endif // !USE_NATIVE_64BIT_TIME
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "esphome/core/defines.h"
#ifndef USE_NATIVE_64BIT_TIME
#include <cstdint>
namespace esphome {
class Scheduler;
/// Extends 32-bit millis() to 64-bit using rollover tracking.
/// Access restricted to platform HAL (millis_64()) and Scheduler.
/// All other code should call millis_64() from hal.h instead.
class Millis64Impl {
friend uint64_t millis_64();
friend class Scheduler;
static uint64_t compute(uint32_t now);
};
} // namespace esphome
#endif // !USE_NATIVE_64BIT_TIME