Standalone Stratum 1 NTP Server on Raspberry Pi: chrony with DCF77 and GPS PPS in 2026
On Raspberry Pi OS Bookworm/Trixie, old NTP server tutorials fail due to libgpiod v2, the switch to chrony, and changes in GPIO chips. A C++20 implementation uses a DCF77 receiver RC8000 on GPIO 17 and a ublox NEO-M8T GPS module with PPS on GPIO 18. The server achieves <1 µs accuracy without internet, with independent time source verification.
Issues with Outdated Implementations
libgpiod v2 Breaks GPIO Code
libgpiod v2 removed the v1 API without compatibility. Old code with gpiod_chip_open_by_name and gpiod_chip_get_line does not compile:
// libgpiod v1 — DOES NOT COMPILE with v2
struct gpiod_chip *chip = gpiod_chip_open_by_name("gpiochip0");
struct gpiod_line *line = gpiod_chip_get_line(chip, 17);
gpiod_line_request_both_edges_events(line, "dcf77");
The new API requires gpiod_line_settings_new() and gpiod_line_config_new():
// libgpiod v2 — correct approach
const SettingsPtr settings(gpiod_line_settings_new());
gpiod_line_settings_set_edge_detection(settings.get(), GPIOD_LINE_EDGE_BOTH);
gpiod_line_settings_set_event_clock(settings.get(), GPIOD_LINE_CLOCK_REALTIME);
const LineCfgPtr line_cfg(gpiod_line_config_new());
gpiod_line_config_add_line_settings(line_cfg.get(), &offset, 1, settings.get());
const RequestPtr request(gpiod_chip_request_lines(chip.get(), req_cfg.get(), line_cfg.get()));
A key advantage is GPIOD_LINE_CLOCK_REALTIME: events receive timestamps from the kernel at interrupt time, reducing jitter from 40 ms to <5 ms.
Transition from ntpd to chrony
ntpd has been replaced by chrony in Bookworm. Comparison:
| Parameter | ntpd | chrony |
|-----------|------|--------|
| Convergence | minutes | seconds (makestep 0.5 3) |
| Signal loss | degradation | holdover (local stratum 10) |
| PPS | patches | native refclock PPS |
| PPS accuracy | ~100 µs | <1 µs |
Both support NTP SHM for data exchange via shared memory.
GPIO Chip on RPi 5
gpiochip0 is outdated. On RPi 5/Trixie, use gpiochip4 (check with gpiodetect).
System Architecture
Two time sources:
- GPS NEO-M8T: PPS (/dev/pps0) + NMEA via gpsd (NTP SHM #0)
- DCF77 RC8000: GPIO 17 → decoder → NTP SHM #2
- chrony: stratum 1 with prefer PPS
- Web monitor: chronyc + journald on :8080
chrony config:
refclock PPS /dev/pps0 refid PPS precision 1e-9 poll 0 dpoll -2 lock NMEA prefer
refclock SHM 0 refid NMEA precision 1e-3 poll 0 dpoll -2 offset 0.0 delay 0.2 noselect
refclock SHM 2 refid DCF7 precision 1e-2 offset 0.0 delay 0.008 poll 6 dpoll 5
local stratum 10
DCF77 serves as a fallback and GPS spoofing detector (discrepancy >several seconds).
DCF77 Decoder
Kernel Timestamp
GPIOD_LINE_CLOCK_REALTIME provides an interrupt timestamp, not userspace. StdDev in SHM: <5 ms.
NTP SHM for 64-bit
Structure with padding:
struct NtpShmTime {
int32_t mode;
int32_t count;
int64_t clockTimeStampSec;
int32_t clockTimeStampUSec;
int32_t _pad;
int64_t receiveTimeStampSec;
int32_t receiveTimeStampUSec;
int32_t leap;
int32_t precision;
int32_t nsamples;
int32_t valid;
};
volatile NtpShmTime *shm_ = nullptr;
DCF77 Protocol
59 bits/minute in BCD with parity:
- 100 ms HIGH → bit 0
- 200 ms HIGH → bit 1
- Pause >1.5 s → minute marker
Validation: 3 parity groups + timegm(). Bootstrap: 2 frames with a 60 s difference.
NEO-M8T Configuration
- TIM firmware: RAIM, low PPS jitter
- dynModel=Stationary: jitter 5–10 ns
- antCableDelay=50 ns: cable compensation
- 4 constellations: 20–30 satellites
- lpMode=Continuous: no power saving
Web Monitor and Deployment
dcf77_web: HTTP :8080, chronyc in JSON, UI in binary. API: /api/status, /api/prefer (localhost only).
Build with vcpkg + CMake:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
Ansible deployment with systemd hardening.
Results
chronyc tracking:
- Stratum: 1
- System time: +42 ns
- RMS offset: 51 ns
- Root dispersion: 15 ns
DCF77: ~3 ms.
Key Takeaways
- libgpiod v2 requires a new object model with
GPIOD_LINE_CLOCK_REALTIME - chrony outperforms ntpd in PPS accuracy (<1 µs) and holdover
- NTP SHM for 64-bit needs explicit padding for
int64_t - NEO-M8T TIM + Stationary: PPS jitter 5–10 ns
- DCF77 validates GPS against spoofing
— Editorial Team
No comments yet.