Compare commits

...

7 Commits

Author SHA1 Message Date
J. Nick Koston
36250682b0 [core] Split hal.h into per-platform headers under core/hal/
Mirror the wake.{h,cpp} → wake/wake_<platform>.{h,cpp} decomposition
that PR #15978 did. After this change esphome/core/hal.h is a thin
dispatcher and each platform's HAL bits (IRAM_ATTR / PROGMEM macros,
in_isr_context(), the inline yield/delay/micros/millis/millis_64
wrappers, plus ESP8266's progmem_read_*) live in their own header
under esphome/core/hal/.

Scope is headers only — there is no esphome/core/hal.cpp today (every
out-of-line implementation lives in esphome/components/<platform>/core.cpp
alongside platform-specific concerns) so no new .cpp files are added
and no FILTER_SOURCE_FILES entries are needed in core/config.py.
recursive_sources=True on the core manifest already picks up the new
.h files automatically.

No public API moves, no symbol renames, no behavior change. Pure code
motion. The only observable difference is the dispatcher #errors when
no USE_* is set (today an unknown platform silently fell through to
the else branch with empty IRAM_ATTR/PROGMEM); this matches wake.h's
behavior.
2026-04-28 21:19:21 -05:00
J. Nick Koston
4f75647f63 Merge upstream/dev into inline-micros-esp32
Resolves conflict in esphome/components/esp8266/core.cpp per PR #15977 plan:
keep #15662's fast millis() accumulator and optimistic_yield delay() body;
drop the upstream wrappers for yield()/millis_64()/micros() since those
are now always-inlined in hal.h.
2026-04-28 20:54:39 -05:00
J. Nick Koston
9f5121e271 [core] Suppress redundant-declaration warning for ESP32 esp_timer_get_time forward decl 2026-04-24 12:33:41 -05:00
J. Nick Koston
a1e3ec7118 [core] Keep esp8266 millis/delay out-of-line for #15662 compatibility 2026-04-24 12:02:57 -05:00
J. Nick Koston
d3bae21d13 [core] Extend HAL inlining to yield/delay/millis_64 + libretiny + rp2040
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.
2026-04-24 11:51:23 -05:00
J. Nick Koston
9d138e73c9 [core] Suppress redundant-declaration warning for ESP8266 micros() forward decl 2026-04-24 11:35:03 -05:00
J. Nick Koston
e23a6bf59f [core] Inline micros()/millis_64() at the HAL layer
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.
2026-04-24 11:23:16 -05:00
14 changed files with 339 additions and 188 deletions

View File

@@ -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<uint64_t>(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<uint64_t>(static_cast<uint64_t>(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();

View File

@@ -15,7 +15,7 @@ extern "C" {
namespace esphome {
void HOT yield() { ::yield(); }
// yield(), micros(), millis_64() 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
@@ -66,7 +66,6 @@ uint32_t IRAM_ATTR HOT millis() {
xt_wsr_ps(ps);
return result;
}
uint64_t millis_64() { return Millis64Impl::compute(millis()); }
// Poll-based delay that avoids ::delay() — Arduino's __delay has an intra-object
// call to the original millis() that --wrap can't intercept, so calling ::delay()
// would keep the slow Arduino millis body alive in IRAM. optimistic_yield still
@@ -85,7 +84,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();

View File

@@ -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() {

View File

@@ -13,11 +13,7 @@
namespace esphome {
void HOT yield() { ::yield(); }
uint64_t millis_64() { return micros_to_millis<uint64_t>(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);

View File

@@ -2,111 +2,34 @@
#include <string>
#include <cstdint>
#include "gpio.h"
#include "esphome/core/defines.h"
#include "esphome/core/time_64.h"
#include "esphome/core/time_conversion.h"
// Per-platform HAL bits (IRAM_ATTR / PROGMEM macros, in_isr_context(),
// inline yield/delay/micros/millis/millis_64 wrappers, ESP8266 progmem
// helpers) live under esphome/core/hal/ and are dispatched here based on
// the active USE_* platform define. Each header guards its body with the
// matching #ifdef USE_<platform> and re-enters namespace esphome {} so it
// is safe to be re-included.
#if defined(USE_ESP32)
#include <esp_attr.h>
#ifndef PROGMEM
#define PROGMEM
#endif
#include "esphome/core/hal/hal_esp32.h"
#elif defined(USE_ESP8266)
#include <c_types.h>
#ifndef PROGMEM
#define PROGMEM ICACHE_RODATA_ATTR
#endif
#elif defined(USE_RP2040)
#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical")))
#define PROGMEM
#include "esphome/core/hal/hal_esp8266.h"
#elif defined(USE_LIBRETINY)
// IRAM_ATTR places a function in executable RAM so it is callable from an
// ISR even while flash is busy (XIP stall, OTA, logger flash write).
// Each family uses a section its stock linker already routes to RAM:
// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the
// exception: its stock linker has no matching glob, so patch_linker.py
// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link.
//
// BK72xx (all variants) are left as a no-op: their SDK wraps flash
// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for
// the duration of every write, so no ISR fires while flash is stalled and
// the race IRAM_ATTR guards against cannot occur. The trade-off is that
// interrupts are delayed (not dropped) by up to ~20 ms during a sector
// erase, but that is an SDK-level choice and cannot be changed from this
// layer.
#if defined(USE_BK72XX)
#define IRAM_ATTR
#elif defined(USE_LIBRETINY_VARIANT_RTL8710B)
// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM).
#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text")))
#include "esphome/core/hal/hal_libretiny.h"
#elif defined(USE_RP2040)
#include "esphome/core/hal/hal_rp2040.h"
#elif defined(USE_HOST)
#include "esphome/core/hal/hal_host.h"
#elif defined(USE_ZEPHYR)
#include "esphome/core/hal/hal_zephyr.h"
#else
// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text.
// LN882H: patch_linker.py.script injects *(.sram.text*) into
// .flash_copysection (> RAM0 AT> FLASH).
#define IRAM_ATTR __attribute__((noinline, section(".sram.text")))
#endif
#define PROGMEM
#else
#define IRAM_ATTR
#define PROGMEM
#endif
#ifdef USE_ESP32
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#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
// is ARM-only and user code here may be built in Thumb, so in_isr_context()
// defers to this port helper on BK72xx instead of reading CPSR inline.
extern "C" uint32_t platform_is_in_interrupt_context(void);
#error "hal.h: not implemented for this platform"
#endif
namespace esphome {
/// Returns true when executing inside an interrupt handler.
/// always_inline so callers placed in IRAM keep the detection in IRAM.
__attribute__((always_inline)) inline bool in_isr_context() {
#if defined(USE_ESP32)
return xPortInIsrContext() != 0;
#elif defined(USE_ESP8266)
// ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is
// non-zero both in a real ISR and when user code masks interrupts. The
// ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule
// which is ISR-safe) so this helper is unused on this platform.
return false;
#elif defined(USE_RP2040)
uint32_t ipsr;
__asm__ volatile("mrs %0, ipsr" : "=r"(ipsr));
return ipsr != 0;
#elif defined(USE_BK72XX)
// BK72xx is ARM968E-S (ARM9); see extern declaration above.
return platform_is_in_interrupt_context() != 0;
#elif defined(USE_LIBRETINY)
// Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number;
// non-zero means we're in a handler.
uint32_t ipsr;
__asm__ volatile("mrs %0, ipsr" : "=r"(ipsr));
return ipsr != 0;
#else
// Host and any future platform without an ISR concept.
return false;
#endif
}
void yield();
uint32_t millis();
uint64_t millis_64();
uint32_t micros();
void delay(uint32_t ms);
void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming)
void __attribute__((noreturn)) arch_restart();
void arch_init();
@@ -114,13 +37,9 @@ void arch_feed_wdt();
uint32_t arch_get_cpu_cycle_count();
uint32_t arch_get_cpu_freq_hz();
#ifdef USE_ESP8266
// ESP8266: pgm_read_* does real flash reads on Harvard architecture
uint8_t progmem_read_byte(const uint8_t *addr);
const char *progmem_read_ptr(const char *const *addr);
uint16_t progmem_read_uint16(const uint16_t *addr);
#else
// All other platforms: PROGMEM is a no-op, so these are direct dereferences
#ifndef USE_ESP8266
// All non-ESP8266 platforms: PROGMEM is a no-op, so these are direct dereferences.
// ESP8266's out-of-line declarations live in hal/hal_esp8266.h.
inline uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
inline const char *progmem_read_ptr(const char *const *addr) { return *addr; }
inline uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }

View File

@@ -0,0 +1,35 @@
#pragma once
#ifdef USE_ESP32
#include <cstdint>
#include <esp_attr.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "esphome/core/time_conversion.h"
#ifndef PROGMEM
#define PROGMEM
#endif
namespace esphome {
/// Returns true when executing inside an interrupt handler.
__attribute__((always_inline)) inline bool in_isr_context() { return xPortInIsrContext() != 0; }
// Forward decl from <esp_timer.h>.
// NOLINTNEXTLINE(readability-redundant-declaration)
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<uint32_t>(esp_timer_get_time()); }
uint32_t millis();
__attribute__((always_inline)) inline uint64_t millis_64() {
return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time()));
}
} // namespace esphome
#endif // USE_ESP32

View File

@@ -0,0 +1,44 @@
#pragma once
#ifdef USE_ESP8266
#include <c_types.h>
#include <cstdint>
#include "esphome/core/time_64.h"
#ifndef PROGMEM
#define PROGMEM ICACHE_RODATA_ATTR
#endif
// Forward decls from Arduino's <Arduino.h> for the inline wrappers below.
// 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)
namespace esphome {
/// Returns true when executing inside an interrupt handler.
/// ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is
/// non-zero both in a real ISR and when user code masks interrupts. The
/// ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule
/// which is ISR-safe) so this helper is unused on this platform.
__attribute__((always_inline)) inline bool in_isr_context() { return false; }
__attribute__((always_inline)) inline void yield() { ::yield(); }
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); }
void delay(uint32_t ms);
uint32_t millis();
__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); }
// ESP8266: pgm_read_* does real flash reads on Harvard architecture
uint8_t progmem_read_byte(const uint8_t *addr);
const char *progmem_read_ptr(const char *const *addr);
uint16_t progmem_read_uint16(const uint16_t *addr);
} // namespace esphome
#endif // USE_ESP8266

View File

@@ -0,0 +1,24 @@
#pragma once
#ifdef USE_HOST
#include <cstdint>
#define IRAM_ATTR
#define PROGMEM
namespace esphome {
/// Returns true when executing inside an interrupt handler.
/// Host has no ISR concept.
__attribute__((always_inline)) inline bool in_isr_context() { return false; }
void yield();
void delay(uint32_t ms);
uint32_t micros();
uint32_t millis();
uint64_t millis_64();
} // namespace esphome
#endif // USE_HOST

View File

@@ -0,0 +1,93 @@
#pragma once
#ifdef USE_LIBRETINY
#include <cstdint>
// For the inline millis() fast paths (xTaskGetTickCount, portTICK_PERIOD_MS).
#include <FreeRTOS.h>
#include <task.h>
#include "esphome/core/time_64.h"
// IRAM_ATTR places a function in executable RAM so it is callable from an
// ISR even while flash is busy (XIP stall, OTA, logger flash write).
// Each family uses a section its stock linker already routes to RAM:
// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the
// exception: its stock linker has no matching glob, so patch_linker.py
// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link.
//
// BK72xx (all variants) are left as a no-op: their SDK wraps flash
// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for
// the duration of every write, so no ISR fires while flash is stalled and
// the race IRAM_ATTR guards against cannot occur. The trade-off is that
// interrupts are delayed (not dropped) by up to ~20 ms during a sector
// erase, but that is an SDK-level choice and cannot be changed from this
// layer.
#if defined(USE_BK72XX)
#define IRAM_ATTR
#elif defined(USE_LIBRETINY_VARIANT_RTL8710B)
// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM).
#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text")))
#else
// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text.
// LN882H: patch_linker.py.script injects *(.sram.text*) into
// .flash_copysection (> RAM0 AT> FLASH).
#define IRAM_ATTR __attribute__((noinline, section(".sram.text")))
#endif
#define PROGMEM
#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
// is ARM-only and user code here may be built in Thumb, so in_isr_context()
// defers to this port helper on BK72xx instead of reading CPSR inline.
extern "C" uint32_t platform_is_in_interrupt_context(void);
#endif
// Forward decls from Arduino's <Arduino.h> for the inline wrappers below.
// 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)
namespace esphome {
/// Returns true when executing inside an interrupt handler.
__attribute__((always_inline)) inline bool in_isr_context() {
#if defined(USE_BK72XX)
// BK72xx is ARM968E-S (ARM9); see extern declaration above.
return platform_is_in_interrupt_context() != 0;
#else
// Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number;
// non-zero means we're in a handler.
uint32_t ipsr;
__asm__ volatile("mrs %0, ipsr" : "=r"(ipsr));
return ipsr != 0;
#endif
}
__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<uint32_t>(::micros()); }
// 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<uint32_t>(::millis()); }
#endif
__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); }
} // namespace esphome
#endif // USE_LIBRETINY

View File

@@ -0,0 +1,40 @@
#pragma once
#ifdef USE_RP2040
#include <cstdint>
#include "esphome/core/time_conversion.h"
#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical")))
#define PROGMEM
// Forward decls from Arduino's <Arduino.h> for the inline wrappers below.
// 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)
// Forward decl from <pico/time.h>.
extern "C" uint64_t time_us_64(void);
namespace esphome {
/// Returns true when executing inside an interrupt handler.
__attribute__((always_inline)) inline bool in_isr_context() {
uint32_t ipsr;
__asm__ volatile("mrs %0, ipsr" : "=r"(ipsr));
return ipsr != 0;
}
__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<uint32_t>(::micros()); }
__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<uint64_t>(::time_us_64()); }
} // namespace esphome
#endif // USE_RP2040

View File

@@ -0,0 +1,24 @@
#pragma once
#ifdef USE_ZEPHYR
#include <cstdint>
#define IRAM_ATTR
#define PROGMEM
namespace esphome {
/// Returns true when executing inside an interrupt handler.
/// Zephyr/nRF52: not currently consulted — wake path is platform-specific.
__attribute__((always_inline)) inline bool in_isr_context() { return false; }
void yield();
void delay(uint32_t ms);
uint32_t micros();
uint32_t millis();
uint64_t millis_64();
} // namespace esphome
#endif // USE_ZEPHYR

View File

@@ -20,6 +20,7 @@
#include <strings.h>
#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<std::integral T> 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<typename ReturnT = uint32_t> inline constexpr ESPHOME_ALWAYS_INLINE ReturnT micros_to_millis(uint64_t us) {
constexpr uint32_t d = 125U;
constexpr uint32_t q = static_cast<uint32_t>((1ULL << 32) / d); // 34359738
constexpr uint32_t r = static_cast<uint32_t>((1ULL << 32) % d); // 46
// 1000 = 8 * 125; divide-by-8 is a free shift
uint64_t x = us >> 3;
uint32_t lo = static_cast<uint32_t>(x);
uint32_t hi = static_cast<uint32_t>(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<ReturnT>(hi) widens to 64-bit when ReturnT=uint64_t, preserving upper bits of hi*q
return static_cast<ReturnT>(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.

View File

@@ -6,8 +6,6 @@
#include <cstdint>
#include <limits>
#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<uint32_t>::max() / 2;

View File

@@ -0,0 +1,46 @@
#pragma once
#include <cstdint>
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<typename ReturnT = uint32_t>
__attribute__((always_inline)) inline constexpr ReturnT micros_to_millis(uint64_t us) {
constexpr uint32_t d = 125U;
constexpr uint32_t q = static_cast<uint32_t>((1ULL << 32) / d); // 34359738
constexpr uint32_t r = static_cast<uint32_t>((1ULL << 32) % d); // 46
// 1000 = 8 * 125; divide-by-8 is a free shift
uint64_t x = us >> 3;
uint32_t lo = static_cast<uint32_t>(x);
uint32_t hi = static_cast<uint32_t>(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<ReturnT>(hi) widens to 64-bit when ReturnT=uint64_t, preserving upper bits of hi*q
return static_cast<ReturnT>(hi) * q + (adj < lo ? (adj + r) / d + q : adj / d);
}
} // namespace esphome