From 0249c30cbfc6a789cfc1a0616eed33e0ab4676d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Apr 2026 09:48:23 -1000 Subject: [PATCH 1/2] [esp32] Document why millis_64() uses a different clock than millis() millis() uses xTaskGetTickCount (tick clock) while millis_64() uses esp_timer_get_time (hardware timer). Safe because they are never cross-compared: Scheduler::millis_64_from_() on ESP32 discards the 32-bit millis parameter and calls millis_64() directly, keeping all 64-bit scheduling on the esp_timer clock. --- esphome/components/esp32/core.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index c97e4a5eb1..0284328ad9 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -37,6 +37,12 @@ uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast(esp_timer_get_time())); #endif } +// millis_64() stays on esp_timer — a different clock from xTaskGetTickCount(). This is +// safe because the two are never cross-compared: millis() values are only used for +// millis()-vs-millis() deltas (feed_wdt, warn_blocking, component start time), while +// millis_64() is used by the Scheduler and uptime sensors. On ESP32 (USE_NATIVE_64BIT_TIME), +// Scheduler::millis_64_from_(now) discards the 32-bit now and calls millis_64() directly, +// so the Scheduler is internally consistent on the esp_timer clock. uint64_t HOT millis_64() { return micros_to_millis(static_cast(esp_timer_get_time())); } void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } From e5a0246c8003de4cc0205f22ba7d7bfde83bec2b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Apr 2026 09:49:28 -1000 Subject: [PATCH 2/2] [core] Document clock divergence between millis() and millis_64_from_() On ESP32, millis() uses xTaskGetTickCount (tick clock) but millis_64_from_() discards the 32-bit value and calls millis_64() (esp_timer clock). Safe because scheduling only compares millis_64 against millis_64. On ESP8266, both use the same accumulator clock. --- esphome/core/scheduler.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 43a3ec7049..5d86736c16 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -284,8 +284,12 @@ 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 platforms with native 64-bit time, ignores now and uses millis_64() directly. - // On other platforms, extends now to 64-bit using rollover tracking. + // On platforms with native 64-bit time (ESP32), ignores now and uses millis_64() directly. + // This means the Scheduler uses the esp_timer clock (via millis_64()) for all scheduling, + // even though the caller's 32-bit now came from xTaskGetTickCount (via millis()). Safe + // because scheduling only compares millis_64 against millis_64 — never against millis(). + // On other platforms (ESP8266), extends now to 64-bit using rollover tracking, so both + // millis() and scheduling use the same clock. uint64_t millis_64_from_(uint32_t now) { #ifdef USE_NATIVE_64BIT_TIME (void) now;