Skip to content

The findings, in brief

For people who want the whole thing in ninety seconds.

A GPS-disciplined Stratum 1 NTP server: Raspberry Pi 4 + BerryGPS-IMU v4 (u-blox CAM-M8C), PPS on GPIO18, gpsd + chrony. Final state: RMS offset 199 ns, root delay ~1 ns, survives a cold power cut unattended. About $130 of parts, replacing an appliance that costs $1,500–$10,000.

ClaimReality
”Use PTP for real precision”Impossible. ethtool -T eth0PTP Hardware Clock: none. No hardware timestamping exists on this NIC.
”Isolate the PPS IRQ on a dedicated core”Not permitted. GPIO IRQs demux through pinctrl-bcm2835 and reject smp_affinity.
”Install PREEMPT_RT”Made jitter 3× worse until patched — it threads the handler that takes the timestamp.
”Raise the GPS baud rate”Irrelevant. PPS offset measured −1 ns at 9600 vs 115200. Identical. NMEA only labels the second.
ChangeRMS offset
Baseline823 ns
chrony filter 10 + prefer on the PPS refclock440 ns
PREEMPT_RT + IRQF_NO_THREAD patch199 ns

Two failures that a reboot will never reveal. Only a cold power cut exposes them, which is why you must actually pull the plug:

  1. gpsd was being started by the dashboard. It’s socket-activated; chrony reads its shared memory, never its socket, so nothing else triggered it. The monitoring page was load-bearing for the time server.
  2. Pinning gpsd’s baud turned a module quirk into an outage. The CAM-M8 keeps config in supercap-backed RAM and reverts to 9600 on power loss. With gpsd pinned to 115200 it came back talking to a module that wasn’t listening: no GPS at all. Use GPSD_OPTIONS="-n" and let it auto-probe.

And the instrument was bending the measurement

Section titled “And the instrument was bending the measurement”

Our own dashboard cost 36% more PPS jitter by forking chronyc four times a second onto CPU 0 — the one core the PPS interrupt is welded to and cannot be moved from. Batching it to one process and pinning it off CPU 0 erased the penalty entirely. → The observer effect