Skip to content

Fix off-by-one in moveTimed() duration chunking at 131072 boundary#367

Merged
gin66 merged 1 commit into
gin66:masterfrom
94xhn:fix-movetimed-131072-boundary
Jul 16, 2026
Merged

Fix off-by-one in moveTimed() duration chunking at 131072 boundary#367
gin66 merged 1 commit into
gin66:masterfrom
94xhn:fix-movetimed-131072-boundary

Conversation

@94xhn

@94xhn 94xhn commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Bug

In FastAccelStepper::moveTimed(), when a single step's rate requires more
than one queue command (rate > 65535), the per-step duration is chunked in
a loop:

if (this_duration > 131072) {
  cmd.ticks = 65535;
} else if (this_duration > 65535) {
  cmd.ticks = this_duration / 2;
} else {
  cmd.ticks = this_duration;
}

When this_duration is exactly 131072, the first condition (>) is
false, so it falls into the this_duration / 2 branch, computing
65536. Since stepper_command_s::ticks is a uint16_t
(src/fas_arch/common.h), this silently truncates to 0.
addQueueEntry() then rejects the zero-tick command with
AQE_ERROR_TICKS_TOO_LOW, and moveTimed() immediately returns that
error — so a perfectly ordinary single-step move (duration = 131072
ticks, e.g. ~8.19 ms at 16 MHz) is spuriously rejected, while durations
of 131071 and 131073 both succeed.

The sibling loop just above (for steps == 0, i.e. the
already-queued-commands path) has the same boundary but already uses
>=:

} else if (duration >= 131072) {
  // need more than one command
  cmd.ticks = 65535;

Fix

Change the steps > 0 loop's comparison from > to >= to match its
sibling, so this_duration == 131072 takes the cmd.ticks = 65535
branch instead of the truncating divide-by-2 branch.

Testing

Built the library's own PC-based host tests (extras/tests/pc_based/)
against both the original and patched source. The existing moveTimed()
test (test_16.cpp) and all other quick-running tests produce identical
output before/after the fix. Additionally verified directly:

  • moveTimed(1, 131072, &actual) — before: returns an error, actual == 0. After: succeeds, actual == 131072.
  • moveTimed(1, 131071, &actual) and moveTimed(1, 131073, &actual) — succeed identically before and after (unaffected boundary neighbors).

When this_duration == 131072 exactly, the check `this_duration > 131072`
falls through to the `this_duration > 65535` branch, which computes
this_duration / 2 == 65536. Since cmd.ticks is a uint16_t, this silently
truncates to 0, causing addQueueEntry() to reject the command with
AQE_ERROR_TICKS_TOO_LOW and moveTimed() to return an error for an
otherwise perfectly ordinary single-step move.

Changing the comparison to >= mirrors the already-correct sibling
check in the steps==0 loop above (line 768: `duration >= 131072`),
which never exhibits this bug.
@gin66

gin66 commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Good catch. Thanks for the patch.

@gin66
gin66 merged commit 10dc79c into gin66:master Jul 16, 2026
110 checks passed
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.

2 participants