amy_sysclock(): integer math - fix clock wrap at ~25 h and precision decay; drop per-check 64-bit divide in the sequencer#827
Open
rt-rtos wants to merge 1 commit into
Conversation
…decay amy_sysclock() computed ms-since-start through single-precision float, with two correctness problems on long uptimes: - total_blocks * AMY_BLOCK_SIZE is a u32 multiply that wraps at 2^32 samples (~24.9 h at 48 kHz), long before the intended 49.7-day ms-domain wrap the comment describes. When it hits, the clock jumps back near zero and the sequencer re-anchors against a clock that went backwards. - the float conversion quantizes the clock once total samples exceed the 24-bit mantissa (~6 min at 48 kHz); by 12 h uptime the returned clock advances in 8 ms jumps vs the true 5.33 ms block step. Replace with exact 64-bit integer math (verified against wide-integer reference math over the full u32 range, zero mismatches); the u32 result now genuinely wraps at 2^32 ms = 49.7 days. In the sequencer, compare tick deadlines in the us domain: the old ms-domain compare cost a second amy_sysclock() call plus a 64-bit divide per loop-condition check - a real __udivdi3 libcall on 32-bit targets, executed in every 500 us timer callback. next_amy_tick_us accumulates exactly as before, so the long-term tick rate is unchanged. Also keep sequencer_recompute()'s tempo math in single precision; the unsuffixed double literals pulled software double emulation into the build on 32-bit targets.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Feedback wanted: Long term drift not measured on HW. Do we want a 24h+ A/B drift test as verification?
Summary
amy_sysclock()computes milliseconds-since-start through single-precisionfloat. That has two correctness problems that show up on long uptimes, plus
avoidable per-callback cost in the sequencer timer path on 32-bit targets.
This PR makes the clock exact integer math and tightens the sequencer's
tick-deadline check. Behavior is otherwise unchanged; the sequencer tick
rate is identical.
Bug 1: the clock wraps at ~24.9 hours at 48 kHz, not 49.7 days
total_blocksisuint32_tandAMY_BLOCK_SIZEis an int constant, sototal_blocks * AMY_BLOCK_SIZEis computed in 32-bit integer arithmetic andwraps at 2^32 samples - 89478 s (24.85 h) at 48 kHz, ~27 h at 44.1 kHz -
before the float conversion ever sees it. The comment ("uint32_t rollover is
49.7 days") describes the intended ms-domain wrap; the actual wrap comes ~48x
sooner. When it hits, the clock jumps back to near zero, and the sequencer's
1-second catch-up guard re-anchors: pending absolute-tick events are
re-evaluated against a clock that went backwards.
Bug 2: float precision quantizes the clock as uptime grows
The sample count passes through
float(24-bit mantissa). Once total samplesexceed 2^24 (~6 min at 48 kHz) the conversion is no longer exact, and the
error grows with uptime. Measured with the test program below (48 kHz,
256-sample blocks): by 12 h of uptime the returned clock advances in 8 ms
jumps, while the true per-block step is 5.33 ms. Everything slaved to
amy_sysclock()- most importantly the sequencer, i.e. every scheduledevent - inherits that granularity.
Fix
Exact for the full
total_blocksrange (verified against 128-bit referencemath: zero mismatches over the sampled u32 range including the wrap
boundary), monotonic, and the u32 result now genuinely wraps at 2^32 ms =
49.7 days as the original comment intended.
Sequencer: compare tick deadlines in the us domain
sequencer_check_and_fill()ran, per check:That is a second
amy_sysclock()call plus a 64-bit divide per loop-conditionevaluation. GCC cannot strength-reduce a 64-bit divide by a constant on
32-bit targets, so on ESP32/RP2040/RP2350 the divide is a real
__udivdi3libcall executed on every 500 us timer callback. The function already
computes
now_usat the top; comparing in the us domain removes both:Behavior note: the old compare rounded the tick deadline down to a ms
boundary, so a tick could fire up to 1 ms early relative to its us
deadline; now it fires at the first 500 us callback at-or-after the deadline
(up to 1 ms later than before).
next_amy_tick_usaccumulates exactly asbefore, so the long-term tick rate is bit-identical. Not re-sampling the
clock inside the catch-up loop is also safe: the existing 1-second skip-ahead
guard already bounds how much catch-up a single callback performs.
Sequencer: keep tempo math in single precision
sequencer_recompute()used unsuffixed double literals:which promotes the whole expression to double - software-emulated on these
targets (
__extendsfdf2/__divdf3/__muldf3/__fixunsdfsiin thedisassembly). Algebraically identical single-divide float form:
Cold path (tempo changes only); included for hygiene since it is the same
clock code.
Measured effect (ESP32-S3, gcc 15.2, -O2 + LTO)
The 2 kHz sequencer timer callback goes from 77 instructions with
__divsf3(float divide inside the inlined
amy_sysclock()) plus__udivdi3perdeadline check, to 53 instructions with a single
__udivdi3(the newsysclock's divide-by-constant) and no float conversion traffic. Small in
absolute CPU, but it runs on the shared timer-dispatch task; the correctness
fixes are the point.
Verification
python -m amy.testrenders identically before and after this change(same pass/fail set, same per-test error dB), as expected: offline test
renders never reach the uptimes where the float clock misbehaves.
Host-side check of old vs new (48 kHz / 256-sample blocks), comparing both
against 128-bit integer reference math:
Verification program source (gcc -O2 -o sysclock_check sysclock_check.c)