Skip to content

Add rx888_ppm: online glitch-free ppm trim of the sample clock#33

Draft
ringof wants to merge 1 commit into
mainfrom
claude/si5351-ppm-trim
Draft

Add rx888_ppm: online glitch-free ppm trim of the sample clock#33
ringof wants to merge 1 commit into
mainfrom
claude/si5351-ppm-trim

Conversation

@ringof

@ringof ringof commented Jul 15, 2026

Copy link
Copy Markdown
Owner

What

A stock-Python-3 tool — scripts/rx888_ppm.py — that bends the RX888 ADC sample clock by a few ppm while the receiver is streaming, so you can null a known carrier (WWV/WWVH/CHU, a broadcast pilot, a signal generator) by hand. It's the manual-calibration path for a radio with no GPSDO: no restart, no rebuild, no reset.

How it stays glitch-free

The sample clock is synthesized in the Si5351A (or the MS5351M clone): reference → fractional-N PLL → output Multisynth divider → CLK. The tool perturbs only the PLL-A feedback fractional divider (MSNA, regs 26–33) and never issues a PLL soft reset (reg 177).

  • The PLL is an analog loop, so a small change to b/c slews the VCO continuously.
  • The output Multisynth divider is left untouched, so its counter never reloads — no output edge is added or dropped. At worst a tiny phase wobble while the VCO settles, which a sampling clock doesn't care about; never a lost edge, which it would.
  • Same technique QRP Labs radios use to change frequency and key clean FSK without stopping their Si5351 clock.

Because it moves the actual clock (not a software rate relabel), nulling one reference calibrates the whole span, at the true nominal rate.

Coexisting with a live stream

Writes go through fx3_cmd --force (implies --no-claim): EP0 control transfers, no interface-0 claim, so they run alongside rx888_stream / rx888d. fx3_cmd marks the generic i2cw/i2cr stream-unsafe out of caution; this tool is the classified-safe wrapper — it writes only MSNA fractional bytes (regs 31–33, or 28–33 on a carry tick) and never the reset. Those registers sit outside the firmware's reserved-register fence.

Commands

read · baseline · nudge <±ppm> (relative, the everyday knob) · set <±ppm> (absolute vs baseline) · zero · sweep <start> <stop> <step> (bench characterization). nudge/set/zero/sweep take --check-lock (poll fx3_cmd stats_pll); global --dry-run, --claim, --max-ppm, --state, -v.

Tests

  • tests/rx888_ppm_selftest.py — hardware-free coverage of the MSNA decode/encode round-trip, ppm math, carry-into-a, and the minimal reset-free write planner (P2-only vs the P1 carry tick; refuses a c change). Wired into make check; full make check passes locally.
  • Verified end-to-end offline against a stub fx3_cmd: read decodes correctly, a +0.05 ppm nudge plans a 3-byte write to reg 0x1F (P2-only, no reset), and the applied value is reported honestly-quantized.

Verifying it's really edge-clean (follow-up, needs hardware)

The PLL-slew argument is sound but worth measuring on the actual part, since a few Si5351 libraries reset unconditionally. doc/rx888_ppm.md spells out two checks: sweep --check-lock (PLL never drops lock), and — the real bar — nudging while a stream runs with the existing counter_cf32.py / verify_cf32.py continuity tooling watching for a slipped sample count.

Files

  • scripts/rx888_ppm.py — the tool
  • tests/rx888_ppm_selftest.py — hardware-free math tests (in make check)
  • doc/rx888_ppm.md — how it works, commands, and the glitch-free verification plan
  • Makefile, README.md, .gitignore — wire-in and pointers

🤖 Generated with Claude Code

https://claude.ai/code/session_011R1ghwtgxfeKNMPNq9k5vX


Generated by Claude Code

A stock-Python-3 tool (scripts/rx888_ppm.py) that bends the RX888 ADC
sample clock by a few ppm while the receiver is streaming, for manual
calibration against a known carrier when no GPSDO is present.

It perturbs only the Si5351/MS5351M PLL-A feedback fractional divider
(MSNA, regs 26-33) and never issues a PLL soft reset (reg 177). The PLL
is an analog loop, so a small fractional change slews the VCO
continuously; the output Multisynth divider is untouched, so the output
counter never reloads and no clock edge is dropped. Writes go through
fx3_cmd --force (EP0 control, no interface claim), so they coexist with a
running rx888_stream/rx888d. The tool only ever writes the minimal
reset-free register set (31-33, or 28-33 on a carry tick).

Commands: read, baseline, nudge (relative), set (absolute vs baseline),
zero, sweep (bench characterization with optional --check-lock). The
hardware-free register decode/encode/ppm/write-planning math is covered
by tests/rx888_ppm_selftest.py, wired into make check. Adds doc/rx888_ppm.md
and a README pointer.
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