$linuxjunkies
>

Build a Stratum-1 NTP Server on a Pi

Build a GPS-disciplined Stratum-1 NTP server on a Raspberry Pi using a PPS-capable GPS module, chrony refclock directives, and the pps-gpio kernel driver.

AdvancedUbuntuDebianFedoraArch12 min readUpdated June 7, 2026

Before you start

  • Raspberry Pi 3B+, 4B, or 5 with Raspberry Pi OS Bookworm or Bullseye (64-bit) installed and SSH or keyboard access
  • GPS module with a PPS output pin and NMEA serial output (e.g. u-blox NEO-6M/7M/8M)
  • Dupont jumper wires and a reliable 5 V power supply for the Pi
  • Basic familiarity with editing Linux config files and using the command line

A Stratum-1 NTP server derives time directly from a hardware reference clock — no upstream NTP servers in the chain. A Raspberry Pi paired with a GPS module and a PPS (pulse-per-second) signal gives you microsecond-level accuracy on your LAN, completely independent of internet connectivity. This guide covers wiring a GPS module with PPS output, configuring the kernel PPS driver, and building a hardened chrony configuration that serves your network.

Hardware Requirements

You need a Raspberry Pi (3B+, 4, or 5 all work), a GPS module with a PPS pin, and a short length of header wire. The most widely used and well-supported module is the u-blox NEO-6M/7M/8M series, but any NMEA-speaking module with a PPS output works. The PPS signal is the critical ingredient: NMEA sentences alone give you roughly ±100 ms accuracy; PPS brings that down to single-digit microseconds.

  • Raspberry Pi 3B+, 4B, or 5 (64-bit Raspberry Pi OS recommended)
  • GPS module with TX, RX, VCC, GND, and PPS pins
  • Dupont jumper wires
  • A stable power supply (avoid cheap USB chargers — clock jitter correlates with power noise)

Wiring the GPS Module

The Pi's GPIO header exposes a hardware UART and several general-purpose pins. Connect the GPS module as follows using the physical pin numbers on the 40-pin header:

GPS PinPi Physical PinFunction
VCCPin 1 (3.3 V) or Pin 4 (5 V)Power — check your module datasheet
GNDPin 6Ground
TX (GPS out)Pin 10 (GPIO 15 / UART RX)NMEA sentences to Pi
RX (GPS in)Pin 8 (GPIO 14 / UART TX)Optional config commands
PPSPin 18 (GPIO 24)One pulse per second

GPIO 24 is the conventional PPS pin used by most Pi GPS HAT designs. You can use another GPIO, but you must update the device tree overlay accordingly.

Prepare Raspberry Pi OS

Disable the serial console, enable the UART

By default, Raspberry Pi OS puts a login shell on the serial port. That must be removed so chrony can read NMEA sentences cleanly.

sudo raspi-config

Navigate to Interface Options → Serial Port. Answer No to "login shell over serial" and Yes to "serial port hardware enabled". Finish and reboot. Alternatively, edit /boot/firmware/config.txt (Raspberry Pi OS Bookworm) or /boot/config.txt (Bullseye) directly:

# Append to /boot/firmware/config.txt
echo 'enable_uart=1' | sudo tee -a /boot/firmware/config.txt
echo 'dtoverlay=pps-gpio,gpiopin=24' | sudo tee -a /boot/firmware/config.txt

The pps-gpio overlay loads the kernel PPS driver and ties it to GPIO 24. Disable the serial console in /boot/firmware/cmdline.txt — remove the console=serial0,115200 fragment if present:

sudo sed -i 's/console=serial0,[0-9]\+\s\?//' /boot/firmware/cmdline.txt

Reboot after these changes.

Install required packages

# Raspberry Pi OS (Debian/Ubuntu base)
sudo apt update && sudo apt install -y chrony gpsd gpsd-clients pps-tools

Verify the GPS and PPS Signals

Check that NMEA data is arriving

sudo cat /dev/ttyAMA0

You should see a stream of NMEA sentences like $GPRMC, $GPGGA, and so on within a few seconds. If nothing appears, re-check wiring and confirm the UART is enabled (ls -l /dev/ttyAMA0 must exist).

Verify the PPS device

ls /dev/pps*

You should see /dev/pps0. Confirm it is firing with:

sudo ppstest /dev/pps0

Expected output (will vary) shows a new timestamp every second once the GPS has a fix:

# Example output — your values will differ
trying PPS source "/dev/pps0"
found PPS source "/dev/pps0"
ok, found 1 source(s), now start fetching data...
source 0 - assert 1715000001.000002341, sequence: 42

If PPS does not appear, confirm the overlay loaded: lsmod | grep pps_gpio. A cold GPS fix can take several minutes outdoors.

Configure chrony as a Stratum-1 Server

chrony supports hardware reference clocks directly through its refclock directive. You will configure two refclock sources: the NMEA stream (coarse, but useful for initial lock) and the PPS signal (precise).

Edit /etc/chrony/chrony.conf

sudo cp /etc/chrony/chrony.conf /etc/chrony/chrony.conf.bak
sudo nano /etc/chrony/chrony.conf

Replace or augment the file with the following. Comment out or remove existing pool or server lines if you want a fully offline Stratum-1, or keep them as fallback.

# NMEA refclock via shared memory (SHM) from gpsd, or direct serial
# Use SOCK if gpsd is running; use direct serial if gpsd is not used
refclock SHM 0 refid GPS precision 1e-1 offset 0.9 delay 0.2 noselect
refclock PPS /dev/pps0 refid PPS lock GPS precision 1e-7 prefer

# Keep public NTP as fallback only — comment out for fully offline operation
pool 2.debian.pool.ntp.org iburst

# Allow your LAN to query this server
allow 192.168.0.0/24

# Serve time even if not synced (useful during GPS cold start)
local stratum 1

# Log useful data for tuning
log tracking measurements statistics
logdir /var/log/chrony

# Reduce makestep aggression after initial sync
makestep 1.0 3
rtcsync

The lock GPS parameter on the PPS refclock tells chrony to only use PPS when the NMEA (GPS) source is also valid — this prevents chrony from locking to a PPS edge with an ambiguous second boundary.

Configure gpsd

sudo nano /etc/default/gpsd
DEVICES="/dev/ttyAMA0 /dev/pps0"
GPSD_OPTIONS="-n"
USBAPP=""
START_DAEMON="true"

The -n flag makes gpsd start polling without waiting for a client — essential so chrony's SHM refclock gets data immediately at boot.

sudo systemctl enable --now gpsd

Restart chrony and check tracking

sudo systemctl restart chrony
watch -n2 chronyc tracking

After a GPS fix (and a few minutes for PPS to stabilize), you should see Reference ID: PPS and a system time offset in the tens of nanoseconds to low microseconds range. Stratum: 1 confirms correct operation.

Verify NTP Service on the Network

# Check sources and their jitter
chronyc sources -v

# Check what clients can see from another machine
chronyc -h 192.168.0.x tracking

From another machine on your LAN, configure chrony or systemd-timesyncd to use your Pi's IP as an NTP server and confirm it synchronises:

# On a client machine — test a single query
chronyc -h 192.168.0.x ntpdata

Comparing Accuracy to Public NTP

Run chrony's sourcestats command to see the standard deviation of your GPS/PPS source versus what public pool servers would show:

chronyc sourcestats -v

A well-configured PPS refclock on a Pi 4 typically yields offset standard deviations under 500 ns. Public NTP pool servers typically contribute 1–10 ms of offset depending on network path. For LAN clients pointing at your Stratum-1 Pi, expect offsets of 10–100 µs — two to three orders of magnitude better than pool.ntp.org over the internet.

Troubleshooting

  • No /dev/pps0: Confirm dtoverlay=pps-gpio,gpiopin=24 is in config.txt and run sudo dtoverlay pps-gpio gpiopin=24 without rebooting to test.
  • NMEA data but PPS not locking: The GPS needs a 3D fix. Run cgps -s to check fix status. Indoors, consider a patch antenna with a clear sky view or a window-mounted antenna.
  • chrony shows GPS as «falseticker»: Tune the offset value on the SHM refclock. The 0.9 s value accounts for gpsd processing latency; measure your actual offset with chronyc sources and adjust.
  • gpsd not feeding SHM: Verify with gpsmon that gpsd is receiving and decoding sentences. Check journalctl -u gpsd for permission errors on /dev/ttyAMA0 — add the chrony user to the dialout group if needed.
  • Pi 5 users: The Pi 5 uses /dev/ttyAMA0 differently due to the new RP1 southbridge. Check the Pi 5-specific UART overlay documentation; uart0 may map to a different device node.
tested on:Debian 12 (Bookworm) on Raspberry Pi OS 64-bitDebian 11 (Bullseye) on Raspberry Pi OS 64-bitUbuntu 24.04 LTS (Pi-optimised image)

Frequently asked questions

Why do I need a PPS signal — can't I just use the NMEA sentences alone?
NMEA sentences arrive over a serial link with variable software latency, giving roughly ±100 ms accuracy at best. The PPS pulse is a hardware-level 1-Hz edge with sub-microsecond jitter; chrony uses NMEA only to resolve which second the PPS edge belongs to, and PPS to nail the precise sub-second offset.
Which GPS modules are best supported on Linux?
Any module that outputs standard NMEA-0183 sentences over UART and has a PPS pin works. The u-blox NEO-6M, NEO-7M, and NEO-8M are most commonly used because they are cheap, widely documented, and have configurable PPS output. Avoid modules that only expose USB — USB latency undermines PPS accuracy.
How do I handle the GPS needing several minutes to acquire a fix at boot?
The local stratum 1 directive in chrony.conf lets chrony serve time to LAN clients even before GPS lock, using the Pi's local clock. Combine this with makestep 1.0 3 so chrony steps the clock quickly once GPS locks rather than slewing slowly. For faster cold starts, consider a GPS module with battery-backed RTC for saved almanac data.
Can I skip gpsd and feed NMEA directly into chrony?
Yes. Replace the SHM refclock with a direct serial refclock: refclock NMEA /dev/ttyAMA0 refid GPS precision 1e-1. This removes the gpsd dependency and the SHM latency offset, but you lose the ability to use gpsd client tools like gpsmon for diagnostics.
Does this work on a Pi 5?
Mostly yes, but the Pi 5 introduced the RP1 I/O controller which changes UART device assignments. Check that your UART maps to the expected /dev/ttyAMA0 node and consult the Pi 5 GPIO/UART documentation; you may need a different uart overlay in config.txt.

Related guides