Skip to content

fix: wrap-safe micros() timing so the stepper doesn't freeze every ~71 min - #50

Open
ZMan88 wants to merge 1 commit into
slimcdk:masterfrom
ZMan88:fix/stepper-micros-wrap
Open

fix: wrap-safe micros() timing so the stepper doesn't freeze every ~71 min#50
ZMan88 wants to merge 1 commit into
slimcdk:masterfrom
ZMan88:fix/stepper-micros-wrap

Conversation

@ZMan88

@ZMan88 ZMan88 commented Jul 9, 2026

Copy link
Copy Markdown

Problem

On a long-running device the stepper periodically stops moving — commands are accepted but no STEP pulses come out — and then recovers on its own after a while. It looks like a driver/UART glitch but it's a clock bug.

micros() is a uint32_t that overflows every 2³² µs ≈ 71.6 min. The stepper stores those timestamps in time_t and computes deltas from them:

  • Stepper::calculate_speed_float dt = (now - last_calculation_) * 1e-6f (accel/decel ramp)
  • TMC2209Stepper::loop (PULSES_CONTROL / STEP-DIR) — time_t dt = now - last_step_ (the step gate)

Because time_t is signed and wider than 32-bit, right after a micros() wrap now < last_*_, so the delta goes negative. The dt >= interval gate is then never true and no STEP pulses are emitted until micros() climbs back up to the stored value — a freeze of up to ~71 min that then self-heals. The ramp math is corrupted the same way.

should_step_() already used a uint32_t local and is wrap-safe, but the STEP/DIR path in TMC2209Stepper::loop doesn't go through it, so it never benefited.

Fix

Store last_calculation_ / last_step_ as uint32_t and take the now argument as uint32_t, so the subtraction wraps correctly. This mirrors core esphome/components/stepper, which uses uint32_t for exactly this reason. Minimal, type-level change — no behavioural change except that the wrap no longer freezes stepping.

Testing

  • Reproduced on an ESP32-C6 (Seeed XIAO) driving a TMC2209 over UART in STEP/DIR mode: the full command path executed correctly (target set, direction computed) while the position counter stayed frozen; a background monitor caught the self-recovery ~33 min into a wrap window — consistent with the wrap theory and not with heat/UART/register drift.
  • Builds clean with ESPHome 2026.5.0 (esphome compile → ESP32-C6 image created).

Files changed: stepper/stepper.{h,cpp}, tmc2209/stepper/tmc2209_stepper.cpp.

…1 min

The stepper stores micros() timestamps in time_t and computes timing
deltas from them (last_calculation_ in Stepper::calculate_speed_, and
last_step_ in TMC2209Stepper::loop). micros() is a uint32_t that
overflows every 2^32 us ~= 71.6 min. In a signed/wider time_t the
subtraction `now - last_step_` goes negative right after a wrap, so the
`dt >= interval` step gate is never true and NO STEP pulses are emitted
until micros() climbs back past the stored value — the motor freezes for
up to ~71 min, then recovers on its own. The accel/decel ramp in
calculate_speed_ is corrupted the same way.

Fix by storing these timestamps as uint32_t (and taking the now argument
as uint32_t), so the delta arithmetic wraps correctly. This mirrors core
esphome/components/stepper, which uses uint32_t for exactly this reason.
should_step_() already used a uint32_t local, but the STEP/DIR
(PULSES_CONTROL) path in TMC2209Stepper::loop did not go through it.

Reproduced on an ESP32-C6 (XIAO) driving a TMC2209 in UART/STEP-DIR mode:
the command path ran correctly while the position counter stayed frozen,
and a monitor caught the self-recovery at ~33 min into a wrap window.
ZMan88 added a commit to ZMan88/esphome-window-opener that referenced this pull request Jul 9, 2026
…ypes)

Switch the vendored micros()-wrap fix from local defensive casts to the
same type-level change submitted upstream as
slimcdk/esphome-custom-components#50: store last_calculation_/last_step_
(and the now args) as uint32_t so the delta arithmetic is naturally
wrap-safe, mirroring core esphome/components/stepper. The vendored tree
is now identical to that PR, so when it merges we can drop the vendoring
cleanly. Updates firmware/components/README.md and docs/build-log.md to
link the PR. Compiles clean (ESP32-C6 image built).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ZMan88 added a commit to ZMan88/esphome-window-opener that referenced this pull request Jul 9, 2026
Now that the micros()-wrap fix lives on our fork (submitted upstream as
slimcdk/esphome-custom-components#50), point external_components straight
at it instead of carrying a local vendored copy:
github://ZMan88/esphome-custom-components@bcfdbb4facdf379e7ca4deb0cd5b32bfc254f4da

Pinned to the commit (not the branch) so the build is reproducible even
after the branch is deleted post-merge. Removes firmware/components/.
When #50 merges, repoint at the upstream merge commit. Config validates
and compiles clean pulling the source from GitHub (ESP32-C6 image built).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant