From e23a6bf59f28b896b45da319d98086010e8ab683 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Apr 2026 11:23:16 -0500 Subject: [PATCH 1/3] [core] Inline micros()/millis_64() at the HAL layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the per-function ``__attribute__((optimize("O2")))`` approach in #15693 with a direct inline definition of micros() / millis_64() in hal.h for ESP32, plus inline definitions for ESP8266 micros() and RP2040 millis()/millis_64(). The original goal of #15693 was to inline micros() into the main loop so that ``call esphome::micros() → call esp_timer_get_time()`` collapses to a single ``call esp_timer_get_time``. That wrapper-collapse benefits every micros() call site, not just loop_task — so the real fix is to mark the wrapper inline, not to bump the loop's optimization level. Doing this at the HAL layer also makes runtime_stats measurements more accurate: each timing read no longer hides a wrapper call/return between the component end-time capture and the underlying clock read. Refactor: move ``micros_to_millis<>()`` from helpers.h to a new lightweight ``time_conversion.h`` so hal.h can include it without pulling the rest of helpers.h into every TU that includes hal.h. --- esphome/components/esp32/core.cpp | 17 +++++----- esphome/components/esp8266/core.cpp | 3 +- esphome/components/rp2040/core.cpp | 4 +-- esphome/core/hal.h | 43 +++++++++++++++++++++++++ esphome/core/helpers.h | 41 +++-------------------- esphome/core/time_conversion.h | 50 +++++++++++++++++++++++++++++ 6 files changed, 110 insertions(+), 48 deletions(-) create mode 100644 esphome/core/time_conversion.h diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 1c63137183..9df358f2f9 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -37,15 +37,16 @@ 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())); } +// millis_64() and micros() are defined inline in hal.h on ESP32. Both sit on +// esp_timer — a different clock from xTaskGetTickCount(). That 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. Inlining both collapses the wrapper call/return that +// runtime_stats and the main-loop hot path would otherwise incur every iteration. void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } -uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { esp_restart(); diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 159ec20e77..c08cce718f 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -19,7 +19,8 @@ void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::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(); } +// micros() is defined inline in hal.h on ESP8266 so callers collapse the +// wrapper down to a direct ::micros() call. void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { system_restart(); diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index b7a9000612..74d33ec049 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -14,8 +14,8 @@ namespace esphome { void HOT yield() { ::yield(); } -uint64_t millis_64() { return micros_to_millis(time_us_64()); } -uint32_t HOT millis() { return micros_to_millis(time_us_64()); } +// millis() and millis_64() are defined inline in hal.h on RP2040 so callers +// collapse the wrapper down to a direct time_us_64() call. 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/hal.h b/esphome/core/hal.h index e4083622b9..bd8792e0ed 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -2,6 +2,7 @@ #include #include #include "gpio.h" +#include "esphome/core/time_conversion.h" #if defined(USE_ESP32) #include @@ -70,6 +71,18 @@ extern "C" uint32_t platform_is_in_interrupt_context(void); #endif +#ifdef USE_ESP8266 +// Forward-declared from (esp8266 core) so the inline esphome::micros() +// wrapper below can call it without pulling Arduino.h into every TU. +extern "C" unsigned long micros(void); // NOLINT(google-runtime-int,readability-identifier-naming) +#endif + +#ifdef USE_RP2040 +// Forward-declared from so the inline esphome::millis()/millis_64() +// wrappers below can call it without pulling pico/time.h into every TU. +extern "C" uint64_t time_us_64(void); +#endif + namespace esphome { /// Returns true when executing inside an interrupt handler. @@ -103,9 +116,39 @@ __attribute__((always_inline)) inline bool in_isr_context() { } void yield(); +#if defined(USE_ESP32) +// Forward-declared from to avoid pulling the full header (and its +// transitive IDF deps) into every translation unit that includes hal.h. Signature +// is stable IDF public API. +extern "C" int64_t esp_timer_get_time(void); + +uint32_t millis(); +// Inlined so callers (especially the main loop via runtime_stats) collapse the +// wrapper: ``call micros → call esp_timer_get_time`` becomes a single +// ``call esp_timer_get_time``. Inlining into an IRAM_ATTR ISR is safe because +// ``esp_timer_get_time`` itself lives in IRAM (IDF marks it so). +__attribute__((always_inline)) inline uint32_t micros() { return static_cast(esp_timer_get_time()); } +__attribute__((always_inline)) inline uint64_t millis_64() { + return micros_to_millis(static_cast(esp_timer_get_time())); +} +#elif defined(USE_ESP8266) +// Arduino's ::micros() (esp8266 core) is itself in IRAM, so inlining our wrapper +// into IRAM_ATTR ISRs is safe. Forward-declared at global scope below to avoid +// pulling Arduino.h into every TU that includes hal.h. +__attribute__((always_inline)) inline uint32_t micros() { return static_cast(::micros()); } uint32_t millis(); uint64_t millis_64(); +#elif defined(USE_RP2040) uint32_t micros(); +// Pico SDK clock used by ::millis()/time_us_64() under the hood; forward-declared +// at global scope below. +__attribute__((always_inline)) inline uint32_t millis() { return micros_to_millis(::time_us_64()); } +__attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_millis(::time_us_64()); } +#else +uint32_t millis(); +uint32_t micros(); +uint64_t millis_64(); +#endif void delay(uint32_t ms); void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) void __attribute__((noreturn)) arch_restart(); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 4a91c46074..366ae6da9e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -20,6 +20,7 @@ #include #include "esphome/core/optional.h" +#include "esphome/core/time_conversion.h" // Backward compatibility re-export of heap-allocating helpers. // These functions have moved to alloc_helpers.h. External components should @@ -833,43 +834,9 @@ 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()); } -/// Convert a 64-bit microsecond count to milliseconds without calling -/// __udivdi3 (software 64-bit divide, ~1200 ns on Xtensa @ 240 MHz). -/// -/// Returns uint32_t by default (for millis()), or uint64_t when requested -/// (for millis_64()). The only difference is whether hi * Q is truncated -/// to 32 bits or widened to 64. -/// -/// On 32-bit targets, GCC does not optimize 64-bit constant division into a -/// multiply-by-reciprocal. Since 1000 = 8 * 125, we first right-shift by 3 -/// (free divide-by-8), then use the Euclidean division identity to decompose -/// the remaining 64-bit divide-by-125 into a single 32-bit division: -/// -/// floor(us / 1000) = floor(floor(us / 8) / 125) [exact for integers] -/// 2^32 = Q * 125 + R (34359738 * 125 + 46) -/// (hi * 2^32 + lo) / 125 = hi * Q + (hi * R + lo) / 125 -/// -/// GCC optimizes the remaining 32-bit "/ 125U" into a multiply-by-reciprocal -/// (mulhu + shift), so no division instruction is emitted. -/// -/// Safe for us up to ~3.2e18 (~101,700 years of microseconds). -/// -/// See: https://en.wikipedia.org/wiki/Euclidean_division -/// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html -template inline constexpr ESPHOME_ALWAYS_INLINE ReturnT micros_to_millis(uint64_t us) { - constexpr uint32_t d = 125U; - constexpr uint32_t q = static_cast((1ULL << 32) / d); // 34359738 - constexpr uint32_t r = static_cast((1ULL << 32) % d); // 46 - // 1000 = 8 * 125; divide-by-8 is a free shift - uint64_t x = us >> 3; - uint32_t lo = static_cast(x); - uint32_t hi = static_cast(x >> 32); - // Combine remainder term: hi * (2^32 % 125) + lo - uint32_t adj = hi * r + lo; - // If adj overflowed, the true value is 2^32 + adj; apply the identity again - // static_cast(hi) widens to 64-bit when ReturnT=uint64_t, preserving upper bits of hi*q - return static_cast(hi) * q + (adj < lo ? (adj + r) / d + q : adj / d); -} +// micros_to_millis<>() lives in its own lightweight header so hal.h can pull it +// in for inline millis_64() without forcing every TU that includes hal.h to +// also include the rest of helpers.h. /// Return a random 32-bit unsigned integer. /// Not thread-safe. Must only be called from the main loop. diff --git a/esphome/core/time_conversion.h b/esphome/core/time_conversion.h new file mode 100644 index 0000000000..d9bee0912b --- /dev/null +++ b/esphome/core/time_conversion.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +namespace esphome { + +/// Convert a 64-bit microsecond count to milliseconds without calling +/// __udivdi3 (software 64-bit divide, ~1200 ns on Xtensa @ 240 MHz). +/// +/// Returns uint32_t by default (for millis()), or uint64_t when requested +/// (for millis_64()). The only difference is whether hi * Q is truncated +/// to 32 bits or widened to 64. +/// +/// On 32-bit targets, GCC does not optimize 64-bit constant division into a +/// multiply-by-reciprocal. Since 1000 = 8 * 125, we first right-shift by 3 +/// (free divide-by-8), then use the Euclidean division identity to decompose +/// the remaining 64-bit divide-by-125 into a single 32-bit division: +/// +/// floor(us / 1000) = floor(floor(us / 8) / 125) [exact for integers] +/// 2^32 = Q * 125 + R (34359738 * 125 + 46) +/// (hi * 2^32 + lo) / 125 = hi * Q + (hi * R + lo) / 125 +/// +/// GCC optimizes the remaining 32-bit "/ 125U" into a multiply-by-reciprocal +/// (mulhu + shift), so no division instruction is emitted. +/// +/// Safe for us up to ~3.2e18 (~101,700 years of microseconds). +/// +/// Lives in its own header (rather than helpers.h) so hal.h can include it +/// for inline millis_64() definitions without dragging the rest of helpers.h +/// into every translation unit. +/// +/// See: https://en.wikipedia.org/wiki/Euclidean_division +/// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html +template +__attribute__((always_inline)) inline constexpr ReturnT micros_to_millis(uint64_t us) { + constexpr uint32_t d = 125U; + constexpr uint32_t q = static_cast((1ULL << 32) / d); // 34359738 + constexpr uint32_t r = static_cast((1ULL << 32) % d); // 46 + // 1000 = 8 * 125; divide-by-8 is a free shift + uint64_t x = us >> 3; + uint32_t lo = static_cast(x); + uint32_t hi = static_cast(x >> 32); + // Combine remainder term: hi * (2^32 % 125) + lo + uint32_t adj = hi * r + lo; + // If adj overflowed, the true value is 2^32 + adj; apply the identity again + // static_cast(hi) widens to 64-bit when ReturnT=uint64_t, preserving upper bits of hi*q + return static_cast(hi) * q + (adj < lo ? (adj + r) / d + q : adj / d); +} + +} // namespace esphome From 9d138e73c93b38208bf20ea755db7b54f60e8c9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Apr 2026 11:35:03 -0500 Subject: [PATCH 2/3] [core] Suppress redundant-declaration warning for ESP8266 micros() forward decl --- esphome/core/hal.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/core/hal.h b/esphome/core/hal.h index bd8792e0ed..fab7461ed1 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -73,8 +73,10 @@ extern "C" uint32_t platform_is_in_interrupt_context(void); #ifdef USE_ESP8266 // Forward-declared from (esp8266 core) so the inline esphome::micros() -// wrapper below can call it without pulling Arduino.h into every TU. -extern "C" unsigned long micros(void); // NOLINT(google-runtime-int,readability-identifier-naming) +// wrapper below can call it without pulling Arduino.h into every TU. The +// redundant-declaration NOLINT covers TUs that also include Arduino.h directly. +// NOLINTNEXTLINE(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +extern "C" unsigned long micros(void); #endif #ifdef USE_RP2040 From d3bae21d1341eeaf801c8ee1b23789548ceef8d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Apr 2026 11:51:23 -0500 Subject: [PATCH 3/3] [core] Extend HAL inlining to yield/delay/millis_64 + libretiny + rp2040 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the prior commit to cover more wrappers and platforms: - ESP32: also inline yield() and delay() - ESP8266: also inline yield(), delay(), millis(), millis_64() - LibreTiny: inline yield(), delay(), micros(), per-variant millis() fast paths, and millis_64() (via Millis64Impl::compute, now reachable from hal.h since time_64.h dropped its helpers.h dep) - RP2040: also inline yield(), delay(), micros() Consolidates the ESP8266/LibreTiny/RP2040 Arduino-flavored ::yield / ::delay / ::micros wrappers into a single shared block in hal.h. LibreTiny note: the prior IRAM_ATTR on the wrapper was decorative — ::micros(), ::yield(), ::delay() and xTaskGetTickCount all live in flash on every libretiny family (realtek-amb, beken-72xx, lightning-ln882h all checked), so an IRAM ISR call would have crashed the same way an inlined direct call does. Also drops the helpers.h include from time_64.h (only used for the ESPHOME_ALWAYS_INLINE macro, replaced with the raw attribute) so time_64.h is light enough for hal.h to include. --- esphome/components/esp32/core.cpp | 12 +--- esphome/components/esp8266/core.cpp | 7 +-- esphome/components/libretiny/core.cpp | 28 +--------- esphome/components/rp2040/core.cpp | 6 +- esphome/core/hal.h | 79 +++++++++++++++++---------- esphome/core/time_64.h | 6 +- esphome/core/time_conversion.h | 4 -- 7 files changed, 57 insertions(+), 85 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 9df358f2f9..4886745c06 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(); } +// yield(), delay(), micros(), millis_64() inlined in hal.h. // Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig), // falling back to esp_timer for non-standard rates. IRAM_ATTR is required because // Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32. @@ -37,16 +37,6 @@ uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast(esp_timer_get_time())); #endif } -// millis_64() and micros() are defined inline in hal.h on ESP32. Both sit on -// esp_timer — a different clock from xTaskGetTickCount(). That 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. Inlining both collapses the wrapper call/return that -// runtime_stats and the main-loop hot path would otherwise incur every iteration. -void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { esp_restart(); diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index c08cce718f..27f9c2393f 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -15,12 +15,7 @@ extern "C" { namespace esphome { -void HOT yield() { ::yield(); } -uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -uint64_t millis_64() { return Millis64Impl::compute(::millis()); } -void HOT delay(uint32_t ms) { ::delay(ms); } -// micros() is defined inline in hal.h on ESP8266 so callers collapse the -// wrapper down to a direct ::micros() call. +// yield(), delay(), micros(), millis(), millis_64() inlined in hal.h. void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { system_restart(); diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index ca46bcb899..f46abe3b81 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -3,7 +3,6 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#include "esphome/core/time_64.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -15,32 +14,7 @@ void loop(); namespace esphome { -void HOT yield() { ::yield(); } -// Inline the tick read so esphome::millis() matches MillisInternal::get()'s fast -// path instead of going through the Arduino core's out-of-line ::millis() wrapper. -// -// RTL87xx / LN882x (1 kHz): xTaskGetTickCount() is already ms. IRAM_ATTR + ISR -// dispatch are needed because ISR handlers (e.g. rotary_encoder) call millis(). -// -// BK72xx (500 Hz): ticks * portTICK_PERIOD_MS (== 2). IRAM_ATTR and ISR dispatch -// are both unnecessary — the SDK masks FIQ + IRQ during flash writes (see hal.h), -// so no ISR runs while flash is stalled. -#if defined(USE_RTL87XX) || defined(USE_LN882X) -uint32_t IRAM_ATTR HOT millis() { - static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick"); - return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount(); -} -#elif defined(USE_BK72XX) -uint32_t HOT millis() { - static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick"); - return xTaskGetTickCount() * portTICK_PERIOD_MS; -} -#else -uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -#endif -uint64_t millis_64() { return Millis64Impl::compute(millis()); } -uint32_t IRAM_ATTR HOT micros() { return ::micros(); } -void HOT delay(uint32_t ms) { ::delay(ms); } +// yield(), delay(), micros(), millis(), millis_64() inlined in hal.h. void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } void arch_init() { diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 74d33ec049..d3dc1cf2bb 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -13,11 +13,7 @@ namespace esphome { -void HOT yield() { ::yield(); } -// millis() and millis_64() are defined inline in hal.h on RP2040 so callers -// collapse the wrapper down to a direct time_us_64() call. -void HOT delay(uint32_t ms) { ::delay(ms); } -uint32_t HOT micros() { return ::micros(); } +// yield(), delay(), micros(), millis(), millis_64() inlined in hal.h. void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { watchdog_reboot(0, 0, 10); diff --git a/esphome/core/hal.h b/esphome/core/hal.h index fab7461ed1..c039eba19a 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -2,6 +2,8 @@ #include #include #include "gpio.h" +#include "esphome/core/defines.h" +#include "esphome/core/time_64.h" #include "esphome/core/time_conversion.h" #if defined(USE_ESP32) @@ -63,6 +65,12 @@ #include #endif +#ifdef USE_LIBRETINY +// For the inline millis() fast paths (xTaskGetTickCount, portTICK_PERIOD_MS). +#include +#include +#endif + #ifdef USE_BK72XX // Declared in the Beken FreeRTOS port (portmacro.h) and built in ARM mode so // it is callable from Thumb code via interworking. The MRS CPSR instruction @@ -71,17 +79,19 @@ extern "C" uint32_t platform_is_in_interrupt_context(void); #endif -#ifdef USE_ESP8266 -// Forward-declared from (esp8266 core) so the inline esphome::micros() -// wrapper below can call it without pulling Arduino.h into every TU. The -// redundant-declaration NOLINT covers TUs that also include Arduino.h directly. -// NOLINTNEXTLINE(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +// Forward decls from Arduino's for the inline wrappers below. +// NOLINT covers TUs that also include Arduino.h. +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) +extern "C" void yield(void); +extern "C" void delay(unsigned long ms); extern "C" unsigned long micros(void); +extern "C" unsigned long millis(void); +// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration) #endif #ifdef USE_RP2040 -// Forward-declared from so the inline esphome::millis()/millis_64() -// wrappers below can call it without pulling pico/time.h into every TU. +// Forward decl from . extern "C" uint64_t time_us_64(void); #endif @@ -117,41 +127,52 @@ __attribute__((always_inline)) inline bool in_isr_context() { #endif } -void yield(); +// yield()/delay()/micros()/millis()/millis_64() are inlined per platform to +// drop the wrapper call/return — most relevant to runtime_stats and the main +// loop. ESP8266/LibreTiny/RP2040 share Arduino's ::yield/::delay/::micros. #if defined(USE_ESP32) -// Forward-declared from to avoid pulling the full header (and its -// transitive IDF deps) into every translation unit that includes hal.h. Signature -// is stable IDF public API. +// Forward decl from . extern "C" int64_t esp_timer_get_time(void); - -uint32_t millis(); -// Inlined so callers (especially the main loop via runtime_stats) collapse the -// wrapper: ``call micros → call esp_timer_get_time`` becomes a single -// ``call esp_timer_get_time``. Inlining into an IRAM_ATTR ISR is safe because -// ``esp_timer_get_time`` itself lives in IRAM (IDF marks it so). +__attribute__((always_inline)) inline void yield() { vPortYield(); } +__attribute__((always_inline)) inline void delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } __attribute__((always_inline)) inline uint32_t micros() { return static_cast(esp_timer_get_time()); } +uint32_t millis(); __attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_millis(static_cast(esp_timer_get_time())); } -#elif defined(USE_ESP8266) -// Arduino's ::micros() (esp8266 core) is itself in IRAM, so inlining our wrapper -// into IRAM_ATTR ISRs is safe. Forward-declared at global scope below to avoid -// pulling Arduino.h into every TU that includes hal.h. +#elif defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) +__attribute__((always_inline)) inline void yield() { ::yield(); } +__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); } __attribute__((always_inline)) inline uint32_t micros() { return static_cast(::micros()); } -uint32_t millis(); -uint64_t millis_64(); -#elif defined(USE_RP2040) -uint32_t micros(); -// Pico SDK clock used by ::millis()/time_us_64() under the hood; forward-declared -// at global scope below. +#if defined(USE_ESP8266) +__attribute__((always_inline)) inline uint32_t millis() { return static_cast(::millis()); } +__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); } +#elif defined(USE_LIBRETINY) +// Per-variant millis() fast path — matches MillisInternal::get(). +#if defined(USE_RTL87XX) || defined(USE_LN882X) +static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick"); +__attribute__((always_inline)) inline uint32_t millis() { + // xTaskGetTickCountFromISR is mandatory in interrupt context per the FreeRTOS API contract. + return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount(); +} +#elif defined(USE_BK72XX) +static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick"); +__attribute__((always_inline)) inline uint32_t millis() { return xTaskGetTickCount() * portTICK_PERIOD_MS; } +#else +__attribute__((always_inline)) inline uint32_t millis() { return static_cast(::millis()); } +#endif +__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); } +#else // USE_RP2040 __attribute__((always_inline)) inline uint32_t millis() { return micros_to_millis(::time_us_64()); } __attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_millis(::time_us_64()); } +#endif #else -uint32_t millis(); +void yield(); +void delay(uint32_t ms); uint32_t micros(); +uint32_t millis(); uint64_t millis_64(); #endif -void delay(uint32_t ms); void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) void __attribute__((noreturn)) arch_restart(); void arch_init(); diff --git a/esphome/core/time_64.h b/esphome/core/time_64.h index 592e645d41..b86070cc76 100644 --- a/esphome/core/time_64.h +++ b/esphome/core/time_64.h @@ -6,8 +6,6 @@ #include #include -#include "esphome/core/helpers.h" - namespace esphome { class Scheduler; @@ -24,7 +22,9 @@ class Millis64Impl { static uint32_t last_millis_; static uint16_t millis_major_; - static inline uint64_t ESPHOME_ALWAYS_INLINE compute(uint32_t now) { + // Raw __attribute__((always_inline)) (not ESPHOME_ALWAYS_INLINE) so this + // header does not need to pull helpers.h. + static inline uint64_t __attribute__((always_inline)) 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::max() / 2; diff --git a/esphome/core/time_conversion.h b/esphome/core/time_conversion.h index d9bee0912b..e9060c0626 100644 --- a/esphome/core/time_conversion.h +++ b/esphome/core/time_conversion.h @@ -25,10 +25,6 @@ namespace esphome { /// /// Safe for us up to ~3.2e18 (~101,700 years of microseconds). /// -/// Lives in its own header (rather than helpers.h) so hal.h can include it -/// for inline millis_64() definitions without dragging the rest of helpers.h -/// into every translation unit. -/// /// See: https://en.wikipedia.org/wiki/Euclidean_division /// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html template