Skip to content

The measurements

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.

ChangeRMS offsetRoot dispersion
Baseline (stock kernel, stock chrony)823 ns16.8 µs
chrony filter 10 + prefer on PPS refclock440 ns5 µs
PREEMPT_RT, unpatched2468 nsworse11.6 µs
PREEMPT_RT + IRQF_NO_THREAD199 ns6.3 µs
Kerneljitter (σ)peak-to-peak
Stock2134 ns11 µs
PREEMPT_RT (threaded handler)6947 ns38 µs
PREEMPT_RT + patch (hard-irq handler)2568 ns18 µs

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

Dashboard offDashboard on
Before fix1304 ns1912 / 2179 ns (+36%)
After fix1437 ns1169 / 1450 ns (no penalty)
ChangeResult
Baud 9600 → 115200PPS offset −1 ns either way. Identical.
SBAS disabledNo measurable change to PPS.
isolcpus aloneInconclusive-to-harmful (concentrates load onto the PPS core).

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:

Terminal window
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.