BKPT LabsDOCS/VIEWALYZER CLI/HOW INSTRUMENT TIME SYNC WORKS (FROM FIRST PRINCIPLES) VIEWALYZER · HEADLESS CLI
VIEWALYZER · CLI & AUTOMATION

How instrument time sync works (from first principles)

How does a current sample recorded by a bench instrument end up at the right spot on a firmware trace's time axis? This is the explanation from zero, using the Nucleo-C071 + power-analyzer demo as the running example. The code is va-store/src/external.rs; the user-facing contract is docs/EXTERNAL-SERIES.md.

The two witnesses

Every few hundred milliseconds the firmware executes this, with interrupts masked:

__disable_irq();
SYNC_PIN = high;                 /* witness #1: for the instrument   */
VA_LogTrace(TRACE_JSYNC, seq);   /* witness #2: for the trace        */
SYNC_PIN = low;
__enable_irq();

One physical instant, two witnesses:

  • Witness #2 is an ordinary trace event: jsync = 7, timestamped by the recorder's timer (TIM3 on the C071) like any other event. It rides the normal trace transport; nothing special about it.
  • Witness #1 is a rising edge on a GPIO pin. The instrument does not "take a note" of it; it is simply sampling that digital input continuously (the JS320: every microsecond, in the same stream as current). The adapter script scans the stream afterwards and finds where the level went 0 to 1: "edge at sample number 4,812,003."

Interrupts are masked so the pin edge and the trace event are always the same couple of instructions apart. That skew is constant, and constant offsets are absorbed by the fit below, for free.

The problem: two unrelated clocks

The two witnesses carry timestamps from clocks that share nothing:

  • trace event: tick 3,960,001 (timer ticks since the board reset)
  • instrument edge: sample 4,812,003 (samples since the instrument started recording)

They started counting at different moments. Worse, their "microseconds" are not the same length: the C071 clocks its timer from an internal RC oscillator, and on the bench we measured it 0.6% (~6000 ppm) off from the instrument's crystal. So no single subtraction or scale factor can convert one to the other, and the error grows with time if you pretend it can.

The matching trick: compare gaps, not times

After a capture there are two lists of the same instants:

trace events (ticks):    b0  b1  b2  b3  b4  b5 ...
instrument edges (ids):      a0  a1  a2  a3 ...        (started late; maybe a noise edge)

Absolute values are incomparable, but the gaps between consecutive marks are the same physical durations, seen by both sides. The firmware spaces the marks 400..700 ms apart using an LFSR, so the gap sequence reads like a barcode: 560, 640, 680, 540, 460, ..., aperiodic within any capture. (A fixed period would match at every shift; the dither is what makes the answer unique. The recipe also fires a quick 0/50/100 ms burst at boot so a lock is possible within the first seconds.)

match_pairs() slides one gap list against the other and scores agreement. Where the barcodes line up, you learn the correspondence: "instrument edge number 3 IS trace event number 5." Robustness falls out of the same idea:

  • Instrument started late or stopped early: the overlap still matches; the missing marks just have no partner.
  • A noise edge (dangling wire, coupling): its gaps fit the barcode on neither side, so it is discarded. A refinement pass re-applies the gap-consistency rule to the surviving pairs until stable, which also kills the sneaky case where a repeated gap (the startup burst) lets one wrong pair survive.

Pairs become a converter

Each matched pair is a statement: "instrument sample 4,812,003 happened at trace tick 3,960,001." With dozens of pairs, PiecewiseMap draws a line through them, piecewise-linear rather than one global line, so slow clock drift and RC-oscillator wander are followed, not averaged away. Outside the matched range it extrapolates with the boundary segment's slope.

That map is a plain function: instrument sample number in, trace tick out. The fit also yields honesty numbers: matched pair count, clock drift in ppm, and the residual (rms/max, microseconds) of pairs against a straight line, which is the quality figure import reports and --max-resid-us gates on.

Fusion is just relabeling

Every instrument sample (all 280,000 current readings in the demo) has a sample number. Run each through the converter, and it has a trace tick. Insert it into the .vadb as an ordinary row (ns='external', kind='external_trace'). The charts do nothing special afterwards; they draw events whose timestamps happen to be honest.

The stopwatch analogy

Two people with unsynchronized stopwatches both write down when they see the same lightning flashes. Afterwards you line the notebooks up by the rhythm of the flashes, and from then on you can translate any time in one notebook into the other's. The pin is the lightning, the jsync events and the GPI edges are the two notebooks, the LFSR makes the rhythm unmistakable, and match_pairs + PiecewiseMap are the lining-up and the translating.

What is specific to what (layering)

  • Observation (how an instrument sees the marks: GPI pin, logic-analyzer channel, scope trigger, camera watching an LED) is instrument-specific and lives in the adapter. The contract only carries "times on my clock when the shared instants happened."
  • Correspondence and the map are pure math over two timestamp lists and live in the app, once, tested once, uniform for every instrument.
  • Alignment is a small open set of strategies behind one interface: sync (this document), --offset-s (manual linear, for instruments that cannot observe anything), hardware probe_trigger rows (same math, edges timestamped by the BKPT probe at exact trace-stream positions), and future strategies (UTC anchoring, waveform correlation) slot in beside them.

Practical numbers from the reference bench

Nucleo-C071RB (HSI48 RC clock) + JS320, firmware jsync marks: ~30 pairs in a 15 s capture, measured drift ~+6000 ppm (the C071's RC trim error, real), residual ~90..230 us rms. With the probe's hardware trigger port instead of firmware marks, the U575 demo measured 10.6 us rms. Alignment precision is what bounds cause-to-power attribution, not the instrument's sample rate.

Gotchas

  • The LFSR reseeds identically at every reset, so every boot replays the same barcode. Always import the instrument file recorded alongside that specific capture; a file from another run will align cleanly and lie.
  • The sync marks must actually span the capture; a 5 s overlap gives ~8 pairs, plenty. Fewer than 4 pairs refuses to match.
  • The trace-side channel is found by name (--sync-name, default jsync) in the user_trace namespace; probe_trigger rows win automatically when present.