Skip to content

Improve external clock/reset sync (interrupt-based edge capture, no blocking stalls) - #11

Open
othmar52 wants to merge 5 commits into
Befaco:masterfrom
othmar52:improve-external-clock-sync
Open

othmar52 wants to merge 5 commits into
Befaco:masterfrom
othmar52:improve-external-clock-sync

Conversation

@othmar52

Copy link
Copy Markdown

When clocking the Muxlicer from an external CV clock + reset, the playhead
was often slightly late and occasionally ended up a full step behind until
the next reset. This PR removes the causes:

  • External clock via pin change interrupt (PCINT20). The clock input
    (PD4) was polled once per loop() pass, so edge detection jittered by
    the loop period (a few hundred microseconds, dominated by blocking
    analogRead calls) and pulses shorter than a blocking call were missed
    entirely. The ISR only stores a micros() timestamp and a flag; all
    processing stays in read_clock(). Timing references (clock out, gate
    windows, step advance) now use the captured edge timestamp, so the
    measured clock period is free of polling jitter.

  • Reset via pin change interrupt (PCINT11). Same treatment for the
    reset input (PC3/A3); short reset pulses can no longer be missed. The
    processing order within the loop (reset before clock) is unchanged.

  • EEPROM writes deferred while running. EEPROM.write blocks ~3.3 ms
    per byte; persisting tempo + clock-in mult (up to 8 bytes) froze the
    loop for up to ~26 ms. These writes were triggered 3 s after the last
    encoder move, on tap-button release and when the external clock was
    unplugged — i.e. typically while playing, stalling steps/gates/clock out
    audibly and (before the interrupt capture) even losing pulses. All
    writes now go through service_pending_EEPROM_writes(), which performs
    them only while the sequencer is stopped; pending writes stay flagged
    until then. Note: settings changed while running are only persisted
    once the sequencer is stopped before power-down.

  • Address CV throttled to 500 Hz. analogRead blocks ~112 µs and ran
    every loop pass; reading the address CV every 2 ms is still far faster
    than any musically relevant CV change and roughly halves the loop
    period.

No hardware changes required (ATmega328P pin change interrupts).

Tested on a Muxlicer (PCB v1.3) clocked from an Arturia BeatStep Pro
(clock + reset): with clock-in mult/div at 0, ~50 stop/start cycles were
perfectly in sync (previously the playhead lagged audibly and
intermittently lost steps).

Commits

  1. Capture external clock via pin change interrupt
  2. Defer EEPROM writes while the sequencer is running
  3. Throttle address CV reads to every 2 ms
  4. Capture reset input via pin change interrupt

engine and others added 5 commits July 22, 2026 09:35
The external clock input (PD4) was polled once per loop() pass, so edge
detection jittered by the loop period (a few hundred microseconds,
dominated by blocking analogRead calls) and pulses shorter than a
blocking call (e.g. an EEPROM write of up to ~26 ms) were missed
entirely, leaving the step counter lagging behind the external clock.

A PCINT20 ISR now stores a micros() timestamp and a flag; all
processing stays in read_clock(). Timing references for clock out,
gate windows and step advance use the captured edge timestamp instead
of the loop's current time, so the measured clock period and all
derived timings are free of polling jitter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EEPROM.write blocks ~3.3 ms per byte; persisting tempo plus clock-in
mult (up to 8 bytes) freezes the loop for up to ~26 ms. These writes
were triggered 3 s after the last encoder move, on tap-tempo button
release and when the external clock was unplugged - i.e. typically
while the sequencer was playing, causing steps, gates and clock out
to stall audibly (and, before external clock edges were captured by
interrupt, even losing pulses entirely).

All EEPROM writes now go through service_pending_EEPROM_writes(),
which performs them only while the sequencer is stopped. Pending
writes are kept flagged until then, so nothing is lost as long as the
module is stopped once before power-down.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
analogRead blocks for ~112 us and read_address() ran once per loop()
pass, making it the dominant contributor to the loop period and thus
to input polling latency. Reading the address CV at 500 Hz is still
far faster than any musically relevant CV change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Like the external clock, the reset input (PC3 / A3) was polled once
per loop() pass, so short reset pulses could be missed during blocking
calls and reset timing jittered by the loop period. A PCINT11 ISR now
latches the falling edge; the pulse is processed in
read_one_shot_reset_input() as before, keeping the processing order
(reset before clock) within the loop unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The clock input is non-inverting (series resistor plus clamp diodes
straight into PD4), so the pin follows the jack and the trigger fires
on the falling edge of the incoming clock - as documented in the manual
and identical to the polled detection before. Only the reset input is
inverted by a transistor stage, making reset act on the rising edge.
Comments only, no functional change.

Co-Authored-By: Claude Fable 5 <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