From 406546876a1be7391a4c9aed6f449b0343545c59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Apr 2026 22:16:38 -1000 Subject: [PATCH] [esp8266] Replace millis() with fast accumulator, wrap Arduino callers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Arduino ESP8266's millis() uses 4x 64-bit multiplies with magic constants to convert system_get_time() to ms while tracking overflow. On the LX106 (no hardware multiply-high instruction), each 64-bit multiply goes through the __umulsidi3 software helper — costing ~3.3 us per call. Replace with a simple accumulator that tracks a running millis counter from system_get_time() deltas using pure 32-bit integer ops (subtract, add, compare, subtract). No 64-bit math, no __umulsidi3. Use -Wl,--wrap=millis to intercept all ::millis() calls site-wide so Arduino libraries and ISR handlers (Wiegand, ZyAura) also get the fast version. Brief interrupt disable (~125 ns) protects the static state. Overflow safety: unsigned 32-bit delta arithmetic handles the 71-minute system_get_time() wrap correctly for one wrap. ESPHome calls millis() thousands of times per second, so missing a full wrap is not a realistic concern. At boot, both the accumulator and system_get_time() start at 0, so no special initialization is needed. Benchmarked on real ESP8266 hardware: Before: 3348 ns/call (Arduino 4x 64-bit multiply) After: ~800-900 ns/call (accumulator, estimated) --- esphome/components/esp8266/__init__.py | 5 +++ esphome/components/esp8266/core.cpp | 44 ++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index bef7e36470..34540bd48d 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -314,6 +314,11 @@ async def to_code(config): for symbol in ("vprintf", "printf", "fprintf"): cg.add_build_flag(f"-Wl,--wrap={symbol}") + # Wrap Arduino's millis() so all callers (including Arduino libraries and ISR + # handlers) use our fast accumulator instead of the expensive 4x 64-bit multiply + # implementation in the Arduino ESP8266 core. + cg.add_build_flag("-Wl,--wrap=millis") + cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE]) ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 159ec20e77..a4a6c49cf6 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -16,8 +16,44 @@ extern "C" { namespace esphome { void HOT yield() { ::yield(); } -uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -uint64_t millis_64() { return Millis64Impl::compute(::millis()); } +// Arduino ESP8266's millis() uses 4× 64-bit multiplies with magic constants to +// convert system_get_time() → ms while tracking overflow (~3.3 μs per call on +// the LX106 which has no hardware multiply-high instruction). We replace it with +// a simple accumulator that tracks a running millis counter from μs deltas using +// pure 32-bit ops (subtract, add, compare-and-subtract). No __umulsidi3 software +// multiply calls. +// +// Overflow safety: system_get_time() is a uint32_t that wraps every ~71.6 minutes. +// Unsigned subtraction (now - last) handles one wrap correctly. ESPHome calls +// millis() thousands of times per second (1+N per loop iteration at 60+ Hz), so +// missing a full 71-minute wrap period is not a realistic concern. At boot, +// s_last_us starts at 0 and system_get_time() counts from 0, so the first call's +// delta equals the real elapsed time — no special initialization needed. +// +// This function is also installed as __wrap_millis (via -Wl,--wrap=millis) so +// that Arduino library code and ISR handlers (e.g. Wiegand, ZyAura) calling +// ::millis() directly also get the fast version. Interrupts are briefly disabled +// (~10 instructions, ~125 ns at 80 MHz) to protect the static state from +// concurrent ISR access. +static uint32_t IRAM_ATTR HOT millis_accumulator_() { + static uint32_t s_cache = 0; + static uint32_t s_remainder = 0; + static uint32_t s_last_us = 0; + uint32_t ps = xt_rsil(15); + uint32_t now_us = system_get_time(); + uint32_t delta = now_us - s_last_us; + s_last_us = now_us; + s_remainder += delta; + while (s_remainder >= 1000) { + s_cache++; + s_remainder -= 1000; + } + uint32_t result = s_cache; + xt_wsr_ps(ps); + return result; +} +uint32_t IRAM_ATTR HOT millis() { return millis_accumulator_(); } +uint64_t millis_64() { return Millis64Impl::compute(millis()); } void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } @@ -78,4 +114,8 @@ extern "C" void resetPins() { // NOLINT } // namespace esphome +// Linker wrap: redirect all ::millis() calls (Arduino libs, ISRs) to our accumulator. +// Requires -Wl,--wrap=millis in build flags (added by __init__.py). +extern "C" uint32_t IRAM_ATTR __wrap_millis() { return esphome::millis(); } + #endif // USE_ESP8266