diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index e452780d41..4f4244a8d5 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -223,6 +223,10 @@ 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 use our fast accumulator instead of + # the expensive time_us_64() + micros_to_millis() 64-bit conversion path. + cg.add_build_flag("-Wl,--wrap=millis") + cg.add_platformio_option("board_build.core", "earlephilhower") # In testing mode, use all flash for sketch to allow linking grouped component tests. # Real RP2040 hardware uses 1MB filesystem + 1MB sketch, but CI tests may combine diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index b7a9000612..ffd5e65765 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -14,8 +14,36 @@ namespace esphome { void HOT yield() { ::yield(); } +// Arduino-pico's millis() uses time_us_64() (64-bit hardware timer read) then +// micros_to_millis() which does 64-bit multiply-shift conversion on an ARM +// Cortex-M0+ (~789 ns/call measured). Replace with a simple accumulator that +// tracks a running millis counter from 32-bit micros() deltas using pure +// 32-bit ops. ::micros() on RP2040 is a fast 32-bit hardware read (~220 ns). +// +// Overflow safety: ::micros() wraps every ~71.6 minutes. Unsigned subtraction +// handles one wrap correctly. ESPHome calls millis() thousands of times per +// second, so missing a full wrap is not a realistic concern. At boot, both +// s_last_us and ::micros() start at 0, so no special initialization needed. +// +// Also installed as __wrap_millis (via -Wl,--wrap=millis) so Arduino library +// code calling ::millis() directly gets the fast version. +static uint32_t HOT millis_accumulator_() { + static uint32_t s_cache = 0; + static uint32_t s_remainder = 0; + static uint32_t s_last_us = 0; + uint32_t now_us = ::micros(); + 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; + } + return s_cache; +} +uint32_t HOT millis() { return millis_accumulator_(); } +// millis_64() keeps the full 64-bit timer for precision — called once per loop by Scheduler. 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(); } void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } @@ -42,4 +70,8 @@ uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } } // namespace esphome +// Linker wrap: redirect all ::millis() calls (Arduino libs) to our accumulator. +// Requires -Wl,--wrap=millis in build flags (added by __init__.py). +extern "C" uint32_t __wrap_millis() { return esphome::millis(); } + #endif // USE_RP2040