Compare commits

...

6 Commits

Author SHA1 Message Date
J. Nick Koston
feb734c8b7 [rp2040] Add interrupt guard to millis() accumulator
Wiegand and ZyAura call millis() from ISR handlers on all platforms
including RP2040 (no platform restrictions on either component). The
accumulator's mutable statics need protection from concurrent ISR
access. Uses save_and_disable_interrupts()/restore_interrupts() —
same pattern as InterruptLock in rp2040/helpers.cpp.
2026-04-12 09:11:41 -10:00
J. Nick Koston
12d227dee8 [rp2040] Bound the while loop in millis() accumulator
Same approach as ESP8266: split into common path (while loop, ≤10
iterations) and rare path (constant-time /1000 multiply for gaps
>10 ms). RP2040 has no ISR callers so no interrupt guard needed,
but the bounded loop avoids worst-case latency if millis() is
called from a context that was blocked for a long time.
2026-04-11 23:22:12 -10:00
J. Nick Koston
85c51967f5 [rp2040] Merge accumulator into millis(), struct statics, cleanup
Merge millis_accumulator() into millis() and pack statics into a struct
for smaller code. Remove unused USE_FAST_MILLIS_ACCUMULATOR define.
Add NOLINTNEXTLINE for __wrap_millis reserved identifier.
2026-04-11 23:17:12 -10:00
J. Nick Koston
31aa00f1d5 [rp2040] Add USE_FAST_MILLIS_ACCUMULATOR define for benchmark guard 2026-04-11 22:38:29 -10:00
J. Nick Koston
a50a24aed3 [rp2040] Fix clang-format: drop trailing underscore from static function 2026-04-11 22:22:32 -10:00
J. Nick Koston
357835cc20 [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)
2026-04-11 22:20:36 -10:00
2 changed files with 53 additions and 1 deletions

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

View File

@@ -14,8 +14,51 @@
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. Interrupts are
// briefly disabled to protect the static state — Wiegand and ZyAura call
// millis() from ISR handlers on all platforms including RP2040.
static constexpr uint32_t MILLIS_RARE_PATH_THRESHOLD_US = 10000;
static constexpr uint32_t US_PER_MS = 1000;
uint32_t HOT millis() {
static struct {
uint32_t cache;
uint32_t remainder;
uint32_t last_us;
} state = {0, 0, 0};
uint32_t ps = save_and_disable_interrupts();
uint32_t now_us = ::micros();
uint32_t delta = now_us - state.last_us;
state.last_us = now_us;
state.remainder += delta;
if (state.remainder >= MILLIS_RARE_PATH_THRESHOLD_US) {
uint32_t ms = state.remainder / US_PER_MS;
state.cache += ms;
state.remainder -= ms * US_PER_MS;
} else {
while (state.remainder >= US_PER_MS) {
state.cache++;
state.remainder -= US_PER_MS;
}
}
uint32_t result = state.cache;
restore_interrupts(ps);
return result;
}
// 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 +85,9 @@ 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).
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" uint32_t __wrap_millis() { return esphome::millis(); }
#endif // USE_RP2040