From d3bae21d1341eeaf801c8ee1b23789548ceef8d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Apr 2026 11:51:23 -0500 Subject: [PATCH] [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