diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 1c63137183..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,15 +37,6 @@ 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(); } 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 c9bedb61be..dee810aaee 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -15,7 +15,7 @@ extern "C" { namespace esphome { -void HOT yield() { ::yield(); } +// yield() and micros() inlined in hal.h. // Fast accumulator replacement for Arduino's millis() (~3.3 μs via 4× 64-bit // multiplies on the LX106). Tracks a running ms counter from 32-bit // system_get_time() deltas using pure 32-bit ops. Installed as __wrap_millis @@ -85,7 +85,6 @@ void HOT delay(uint32_t ms) { optimistic_yield(1000); } } -uint32_t IRAM_ATTR HOT micros() { return ::micros(); } 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 b7a9000612..d3dc1cf2bb 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -13,11 +13,7 @@ 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()); } -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 e4083622b9..362f36395c 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -2,6 +2,9 @@ #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) #include @@ -62,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 @@ -70,6 +79,22 @@ extern "C" uint32_t platform_is_in_interrupt_context(void); #endif +// 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 decl from . +extern "C" uint64_t time_us_64(void); +#endif + namespace esphome { /// Returns true when executing inside an interrupt handler. @@ -102,11 +127,58 @@ __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 decl from . +extern "C" int64_t esp_timer_get_time(void); +__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) || defined(USE_LIBRETINY) || defined(USE_RP2040) +__attribute__((always_inline)) inline void yield() { ::yield(); } +__attribute__((always_inline)) inline uint32_t micros() { return static_cast(::micros()); } +#if defined(USE_ESP8266) +// delay(), millis(), millis_64() stay out-of-line on this branch (integration): +// esphome::millis() is the body of __wrap_millis (-Wl,--wrap=millis), so it must +// remain a real symbol to avoid infinite recursion. delay() has a custom +// optimistic_yield-based body that intentionally avoids Arduino's __delay path. +void delay(uint32_t ms); uint32_t millis(); uint64_t millis_64(); -uint32_t micros(); +#elif defined(USE_LIBRETINY) +__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); } +// 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 void delay(uint32_t ms) { ::delay(ms); } +__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 +void yield(); void delay(uint32_t ms); +uint32_t micros(); +uint32_t millis(); +uint64_t millis_64(); +#endif void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) void __attribute__((noreturn)) arch_restart(); void arch_init(); 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_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 new file mode 100644 index 0000000000..e9060c0626 --- /dev/null +++ b/esphome/core/time_conversion.h @@ -0,0 +1,46 @@ +#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). +/// +/// 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