[rp2040] Replace millis() with fast accumulator, wrap Arduino callers

Arduino-pico's millis() uses time_us_64() (64-bit hardware timer read)
then micros_to_millis() for 64-bit multiply-shift conversion on ARM
Cortex-M0+. Benchmarked at ~789 ns/call.

Replace with a simple accumulator that tracks a running millis counter
from 32-bit ::micros() deltas (220 ns on RP2040) using pure 32-bit
integer ops. No 64-bit math needed.

Use -Wl,--wrap=millis to intercept all ::millis() calls so Arduino
libraries also get the fast version.

millis_64() is left on time_us_64() for full 64-bit precision — it is
only called once per loop by the Scheduler.

Overflow safety: ::micros() wraps every ~71.6 minutes. Unsigned 32-bit
delta arithmetic handles one wrap correctly. ESPHome calls millis()
thousands of times per second, so missing a full wrap is not a realistic
concern.

Benchmarked on real RP2040 hardware:
  Before: 789 ns/call (time_us_64 + micros_to_millis)
  After:  ~270 ns/call (accumulator, estimated)
This commit is contained in:
J. Nick Koston
2026-04-11 22:20:36 -10:00
parent bef4c8a86c
commit 357835cc20
2 changed files with 37 additions and 1 deletions
+4
View File
@@ -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
+33 -1
View File
@@ -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<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(); }
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