Skip to content

Instantly share code, notes, and snippets.

@rickbassham
Last active January 28, 2025 18:35
Show Gist options
  • Save rickbassham/89ba516b14809dfb56ba9f3e83d5059f to your computer and use it in GitHub Desktop.
Save rickbassham/89ba516b14809dfb56ba9f3e83d5059f to your computer and use it in GitHub Desktop.
Raspberry Pi 4 Chrony GPS Stratum 1 NTP Server

Inspiration

https://blog.networkprofile.org/gps-backed-local-ntp-server/

Hardware

  • Raspberry Pi 4
  • GT-U7 GPS Module
  • U.Fl to SMA Adapter
  • SMA GPS Antenna

Initial Setup

sudo bash -c "echo '# the next 3 lines are for GPS PPS signals' >> /boot/firmware/config.txt"
sudo bash -c "echo 'dtoverlay=pps-gpio,gpiopin=18' >> /boot/firmware/config.txt"
sudo bash -c "echo 'enable_uart=1' >> /boot/firmware/config.txt"
sudo bash -c "echo 'init_uart_baud=57600' >> /boot/firmware/config.txt"

sudo bash -c "echo 'pps-gpio' >> /etc/modules"

sudo apt update
sudo apt upgrade -y
sudo apt install gpsd gpsd-clients pps-tools chrony jq tcpdump -y

sudo shutdown -h now

Connect Hardware

Raspberry Pi GPS
Pin04 - 5V VCC
Pin06 - GND GND
Pin08 - TXD RXD
Pin10 - RXD TXD
Pin12 - GPIO18 PPS

Initial Config

Turn the Raspberry Pi back on. Wait until you see the red light on the GPS module blink once per second.

sudo ppstest /dev/pps0

You should see something like this:

trying PPS source "/dev/pps0"
found PPS source "/dev/pps0"
ok, found 1 source(s), now start fetching data...
source 0 - assert 1689037624.000000607, sequence: 19592 - clear  0.000000000, sequence: 0
source 0 - assert 1689037625.000001413, sequence: 19593 - clear  0.000000000, sequence: 0
source 0 - assert 1689037625.999999218, sequence: 19594 - clear  0.000000000, sequence: 0
sudo tee /etc/default/gpsd <<EOF > /dev/null
START_DAEMON="true"
DEVICES="/dev/ttyS0 /dev/pps0"
GPSD_OPTIONS="-n"
USBAUTO="false"
EOF
sudo systemctl enable gpsd
sudo systemctl start gpsd

gpsmon

You need at least 4 satellites to get good time. This command will print the number of satellites we are locked on to. Move your antenna around to get a good lock. Press CTRL-C to quit.

gpspipe -w | jq ".uSat| select( . != null )"
sudo nano /etc/chrony/chrony.conf 

Find the line that has pool.ntp.org and add this on the next line:

refclock SHM 0 refid NMEA offset 0.000 precision 1e-3 poll 3 noselect
refclock PPS /dev/pps0 refid PPS lock NMEA poll 3

Find and uncomment the line that says log tracking measurements statistics.

sudo systemctl restart chrony

After a few minutes, check the logs for our calculated offset:

sudo cat /var/log/chrony/statistics.log \
  | sudo head -2; sudo cat /var/log/chrony/statistics.log \
  | sudo grep NMEA

I'd recommend letting this run for a while, minimum of 10 minutes, but really as long as a day to get a really good offset.

Once you've let it run for a while, you can average the offsets and update the chrony config.

sudo cat /var/log/chrony/statistics.log \
  | grep NMEA \
  | awk -F" " '{ print $5 }' \
  | awk '{ sum += $1 } END { if (NR > 0) print sum / NR; }'

Take the number this calculates and copy it.

sudo nano /etc/chrony/chrony.conf 

Replace 0.000 on the line with the number you copied from above:

refclock SHM 0 refid NMEA offset 0.000 precision 1e-3 poll 3 noselect

Save and exit.

sudo systemctl restart chrony
watch -n 1 chronyc sources -v

After a few seconds you should see a * next to PPS. This shows you are using the PPS signal from GPS for timing.

Make it available to the network

sudo nano /etc/chrony/chrony.conf 

Add prefer to the end of the refclock PPS line.

refclock PPS /dev/pps0 refid PPS lock NMEA poll 3 prefer

and at the bottom of the file, add the following lines:

allow 0.0.0.0/0
manual
local stratum 1

Then reboot to ensure everything works correctly.

sudo reboot now
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment