Ryan Malloy 6881489bf6 The Cuckoo Escapement: field report, kernel patch, dashboard
What the Raspberry Pi time-server guides get wrong on a Pi 4, with the
measurements. The headline artifact is a four-line pps-gpio patch: PREEMPT_RT
force-threads IRQ handlers, and pps-gpio takes its timestamp inside its handler,
so the realtime kernel puts a scheduler between the electrical edge and the
clock. IRQF_NO_THREAD takes RMS offset from 2468 ns to 199 ns.

- kernel/     the patch
- dashboard/  live status page (position hidden by default)
- docs-site/  the write-up (Astro/Starlight, brass, no tutorial section)
2026-07-14 09:21:25 -06:00

2.4 KiB
Raw Blame History

title description sidebar
The measurements Every number on this site, with the methodology that produced it. Check our work.
order
2

All numbers from one Raspberry Pi 4 + BerryGPS-IMU v4 (u-blox CAM-M8C), PPS on GPIO18. n = 1. Check them against your own board.

Headline progression

Change RMS offset Root dispersion
Baseline (stock kernel, stock chrony) 823 ns 16.8 µs
chrony filter 10 + prefer on PPS refclock 440 ns 5 µs
PREEMPT_RT, unpatched 2468 nsworse 11.6 µs
PREEMPT_RT + IRQF_NO_THREAD 199 ns 6.3 µs

Raw PPS jitter (kernel-timestamped)

Kernel jitter (σ) peak-to-peak
Stock 2134 ns 11 µs
PREEMPT_RT (threaded handler) 6947 ns 38 µs
PREEMPT_RT + patch (hard-irq handler) 2568 ns 18 µs

The dashboard's tax

A/B/A, 62 s per round. Why this matters.

Dashboard off Dashboard on
Before fix 1304 ns 1912 / 2179 ns (+36%)
After fix 1437 ns 1169 / 1450 ns (no penalty)

Things that did nothing

Change Result
Baud 9600 → 115200 PPS offset 1 ns either way. Identical.
SBAS disabled No measurable change to PPS.
isolcpus alone Inconclusive-to-harmful (concentrates load onto the PPS core).

Methodology

Don't trust chrony's own stats for this. chronyc sourcestats reports a windowed, median-filtered figure that lags reality and hides what you're trying to see. Measure the kernel's PPS timestamps directly:

sudo timeout 62 ppstest /dev/pps0 | awk '
/assert/ {
  split($0, a, "assert "); split(a[2], b, ","); t = b[1] + 0;
  if (prev > 0) {
    d = (t - prev - 1.0) * 1e9;          # deviation from exactly 1.000000000 s, in ns
    n++; sum += d; sumsq += d*d;
    if (d > max || n == 1) max = d;
    if (d < min || n == 1) min = d;
  }
  prev = t
}
END {
  mean = sum/n; sd = sqrt(sumsq/n - mean*mean);
  printf "n=%d  jitter_sd=%.0f ns  p2p=%.0f ns\n", n, sd, max-min
}'

Each pulse should be exactly 1.000000000 s after the last. The deviation is the jitter.

Always run A/B/A, never A/B. Clock behaviour drifts on the scale of minutes; if you measure on-then-off you cannot tell a real effect from thermal drift or a satellite geometry change. Go on → off → on, and require the two "on" rounds to agree before you believe the middle one.