From 17a10bf303d02353c4a2b339749f50c850ca4da2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 23:21:08 -1000 Subject: [PATCH 1/5] [core] Derive fast_div1000_32 constants from expressions --- esphome/core/helpers.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 8a55359840..484e410634 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -546,9 +546,9 @@ inline uint32_t fnv1a_hash(const std::string &str) { return fnv1a_hash(str.c_str /// See: https://en.wikipedia.org/wiki/Euclidean_division /// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html inline ESPHOME_ALWAYS_INLINE uint32_t fast_div1000_32(uint64_t us) { - static constexpr uint32_t D = 1000U; // divisor (microseconds per millisecond) - static constexpr uint32_t Q = 4294967U; // 2^32 / 1000 - static constexpr uint32_t R = 296U; // 2^32 % 1000 + static constexpr uint32_t D = 1000U; + static constexpr uint32_t Q = static_cast((1ULL << 32) / D); // 4294967 + static constexpr uint32_t R = static_cast((1ULL << 32) % D); // 296 uint32_t lo = static_cast(us); uint32_t hi = static_cast(us >> 32); // Combine remainder term: hi * (2^32 % 1000) + lo From cc3a51562d337d9692521b8d702277cbeee09f40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 23:23:34 -1000 Subject: [PATCH 2/5] [core] Document safe range for fast_div1000_32 intermediate --- esphome/core/helpers.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 484e410634..2713b67849 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -552,6 +552,7 @@ inline ESPHOME_ALWAYS_INLINE uint32_t fast_div1000_32(uint64_t us) { uint32_t lo = static_cast(us); uint32_t hi = static_cast(us >> 32); // Combine remainder term: hi * (2^32 % 1000) + lo + // Note: hi * R fits in uint32_t for all practical uptimes (hi < 14.5M, i.e. < 584,942 years) uint32_t adj = hi * R + lo; // If adj overflowed, the true value is 2^32 + adj; apply the identity again return hi * Q + (adj < lo ? (adj + R) / D + Q : adj / D); From 263d6c1085f2e2b626b1daab27d171f5fe2f3bef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 23:24:08 -1000 Subject: [PATCH 3/5] [core] Fix comment to describe input range, not uptime --- esphome/core/helpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 2713b67849..fc0b9cd99f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -552,7 +552,7 @@ inline ESPHOME_ALWAYS_INLINE uint32_t fast_div1000_32(uint64_t us) { uint32_t lo = static_cast(us); uint32_t hi = static_cast(us >> 32); // Combine remainder term: hi * (2^32 % 1000) + lo - // Note: hi * R fits in uint32_t for all practical uptimes (hi < 14.5M, i.e. < 584,942 years) + // Note: hi * R fits in uint32_t when hi < UINT32_MAX / R (~14.5M), i.e. us < ~62 quadrillion uint32_t adj = hi * R + lo; // If adj overflowed, the true value is 2^32 + adj; apply the identity again return hi * Q + (adj < lo ? (adj + R) / D + Q : adj / D); From bf8990dcb02749d5d9e5915f5449ad91ad49ae2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 23:27:38 -1000 Subject: [PATCH 4/5] [core] Document exact safe input range for fast_div1000_32 --- esphome/core/helpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index fc0b9cd99f..4f3930465d 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -552,7 +552,7 @@ inline ESPHOME_ALWAYS_INLINE uint32_t fast_div1000_32(uint64_t us) { uint32_t lo = static_cast(us); uint32_t hi = static_cast(us >> 32); // Combine remainder term: hi * (2^32 % 1000) + lo - // Note: hi * R fits in uint32_t when hi < UINT32_MAX / R (~14.5M), i.e. us < ~62 quadrillion + // Safe for us < (14510024 << 32), i.e. ~6.2e16 (~1,975 years of microseconds) uint32_t adj = hi * R + lo; // If adj overflowed, the true value is 2^32 + adj; apply the identity again return hi * Q + (adj < lo ? (adj + R) / D + Q : adj / D); From 7c175dda17c40596ad47a3cc6565168ec17141ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Mar 2026 23:30:06 -1000 Subject: [PATCH 5/5] [core] Rename fast_div1000_32 to micros_to_millis --- esphome/components/esp32/core.cpp | 2 +- esphome/components/rp2040/core.cpp | 2 +- esphome/core/helpers.h | 16 ++++++---------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 0d7ab52281..6423b61f64 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -22,7 +22,7 @@ extern "C" __attribute__((weak)) void initArduino() {} namespace esphome { void HOT yield() { vPortYield(); } -uint32_t IRAM_ATTR HOT millis() { return fast_div1000_32(static_cast(esp_timer_get_time())); } +uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast(esp_timer_get_time())); } uint64_t HOT millis_64() { return static_cast(esp_timer_get_time()) / 1000ULL; } void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 2958d93a02..a4ec17608d 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -12,7 +12,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint64_t millis_64() { return time_us_64() / 1000ULL; } -uint32_t HOT millis() { return fast_div1000_32(time_us_64()); } +uint32_t HOT millis() { return micros_to_millis(time_us_64()); } void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t HOT micros() { return ::micros(); } void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 4f3930465d..1cfc13b9ef 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -527,32 +527,28 @@ template constexpr uint32_t fnv1a_hash_extend(uint32_t hash, T constexpr uint32_t fnv1a_hash(const char *str) { return fnv1a_hash_extend(FNV1_OFFSET_BASIS, str); } inline uint32_t fnv1a_hash(const std::string &str) { return fnv1a_hash(str.c_str()); } -/// Divide a uint64_t by 1000 and return the low 32 bits of the quotient. +/// Convert a 64-bit microsecond count to a 32-bit millisecond count without +/// calling __udivdi3 (software 64-bit divide, ~1200 ns on Xtensa @ 240 MHz). /// /// On 32-bit targets, GCC does not optimize 64-bit constant division into a -/// multiply-by-reciprocal and instead calls __udivdi3 (software 64-bit divide, -/// ~1200 ns on Xtensa @ 240 MHz). This uses the Euclidean division identity to +/// multiply-by-reciprocal. This uses the Euclidean division identity to /// decompose the 64-bit division into a single 32-bit division: /// -/// 2^32 = 4294967 * 1000 + 296 (Q * D + R) -/// -/// Therefore: (hi * 2^32 + lo) / 1000 = hi * Q + (hi * R + lo) / 1000 +/// 2^32 = Q * 1000 + R (4294967 * 1000 + 296) +/// (hi * 2^32 + lo) / 1000 = hi * Q + (hi * R + lo) / 1000 /// /// GCC optimizes the remaining 32-bit "/ 1000U" into a multiply-by-reciprocal /// (mulhu + shift), so no division instruction is emitted. /// -/// Only the low 32 bits of the quotient are returned (same 49.7-day wrap as millis()). -/// /// See: https://en.wikipedia.org/wiki/Euclidean_division /// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html -inline ESPHOME_ALWAYS_INLINE uint32_t fast_div1000_32(uint64_t us) { +inline ESPHOME_ALWAYS_INLINE uint32_t micros_to_millis(uint64_t us) { static constexpr uint32_t D = 1000U; static constexpr uint32_t Q = static_cast((1ULL << 32) / D); // 4294967 static constexpr uint32_t R = static_cast((1ULL << 32) % D); // 296 uint32_t lo = static_cast(us); uint32_t hi = static_cast(us >> 32); // Combine remainder term: hi * (2^32 % 1000) + lo - // Safe for us < (14510024 << 32), i.e. ~6.2e16 (~1,975 years of microseconds) uint32_t adj = hi * R + lo; // If adj overflowed, the true value is 2^32 + adj; apply the identity again return hi * Q + (adj < lo ? (adj + R) / D + Q : adj / D);