[core] Inline HAL clock wrappers and split hal.h into per-platform headers (#15977)

This commit is contained in:
J. Nick Koston
2026-04-29 12:31:55 -05:00
committed by GitHub
parent ce61dcf387
commit 69a33d8ac0
14 changed files with 366 additions and 201 deletions
+1 -10
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();
+2 -15
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,8 +84,7 @@ 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); }
// delayMicroseconds(), arch_feed_wdt(), and progmem_read_*() are inlined in hal/hal_esp8266.h.
void arch_restart() {
system_restart();
// restart() doesn't always end execution
@@ -95,17 +93,6 @@ void arch_restart() {
}
}
void arch_init() {}
void HOT arch_feed_wdt() { system_soft_wdt_feed(); }
uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
const char *progmem_read_ptr(const char *const *addr) {
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
}
uint16_t progmem_read_uint16(const uint16_t *addr) {
return pgm_read_word(addr); // NOLINT
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return F_CPU; }
+1 -27
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() {
+1 -5
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);
+27 -104
View File
@@ -2,125 +2,48 @@
#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);
// ESP8266 inlines delayMicroseconds() and arch_feed_wdt() in hal/hal_esp8266.h;
// every other platform keeps them out-of-line in components/<platform>/core.cpp.
#ifndef USE_ESP8266
void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming)
void arch_feed_wdt();
#endif
void __attribute__((noreturn)) arch_restart();
void arch_init();
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; }
+35
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
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#ifdef USE_ESP8266
#include <c_types.h>
#include <cstdint>
#include <pgmspace.h>
#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)
// Forward decl from <user_interface.h> for arch_feed_wdt() inline below.
// NOLINTNEXTLINE(readability-redundant-declaration)
extern "C" void system_soft_wdt_feed(void);
namespace esphome {
// Forward decl from helpers.h so this header stays cheap.
// NOLINTNEXTLINE(readability-redundant-declaration)
void delay_microseconds_safe(uint32_t us);
/// 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 aligned 32-bit flash reads on Harvard architecture.
// Inline-forward to the platform macros so the wrappers themselves don't
// occupy IRAM/flash on every call site.
__attribute__((always_inline)) inline uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
__attribute__((always_inline)) inline const char *progmem_read_ptr(const char *const *addr) {
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
}
__attribute__((always_inline)) inline uint16_t progmem_read_uint16(const uint16_t *addr) {
return pgm_read_word(addr); // NOLINT
}
// NOLINTNEXTLINE(readability-identifier-naming)
__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
__attribute__((always_inline)) inline void arch_feed_wdt() { system_soft_wdt_feed(); }
} // namespace esphome
#endif // USE_ESP8266
+24
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
+93
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
+40
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
+24
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
+4 -37
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.
+3 -3
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;
+46
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