Lots of acronyms in that title. If I expand them out, it says – “microsecond accurate network time protocol with a Raspberry Pi and pulse per second global positioning system [receiver]”. What it means is you can get super accurate timekeeping (1 microsecond = 0.000001 seconds) with a Raspberry Pi and a GPS receiver (the GT-U7 is less than $12) that spits out pulses every second. By following this guide, you will your very own Stratum 1 NTP server at home!
2025 Update – I wrote a new post with some newer best practices and guidance for 2025, including how to synchronize machines with nanosecond precision using PTP (precision time protocol). Read that post first then come back here – not all discussion is in both places.
You don’t. There aren’t many applications for this level of timekeeping in general, and even fewer at home. But this blog is called Austin’s Nerdy Things so here we are. Using standard, default internet NTP these days will get your computers to within a 10-20 milliseconds of actual time (1 millisecond = 0.001 seconds). By default, Windows computers get time from time.windows.com. MacOS computers get time from time.apple.com. Linux devices get time from [entity].pool.ntp.org, like debian.pool.ntp.org. PPS gets you to the next SI prefix in terms of accuracy (milli -> micro), which means 1000x more accurate timekeeping.
If you prefer a video version – https://www.youtube.com/watch?v=YfgX7qPeiqQ
This NTP guide assumes you have a Raspberry Pi ready to go. If you don’t, I have a 16 minute video on YouTube that goes through flashing the SD card and initial setup – https://youtu.be/u5dHvEYwr9M.
You should update your Pi to latest before basically any project. We will install some other packages as well. pps-tools help us check that the Pi is receiving PPS signals from the GPS module. We also need GPSd for the GPS decoding of both time and position. I use chrony instead of NTPd because it seems to sync faster than NTPd in most instances and also handles PPS without compiling from source (the default Raspbian NTP doesn’t do PPS) Installing chrony will remove ntpd.
sudo apt update sudo apt upgrade sudo rpi-update sudo apt install pps-tools gpsd gpsd-clients python-gps chrony
In /boot/config.txt, add ‘dtoverlay=pps-gpio,gpiopin=18’ to a new line. This is necessary for PPS. If you want to get the NMEA data from the serial line, you must also enable UART and set the initial baud rate.
sudo bash -c "echo '# the next 3 lines are for GPS PPS signals' >> /boot/config.txt" sudo bash -c "echo 'dtoverlay=pps-gpio,gpiopin=18' >> /boot/config.txt" sudo bash -c "echo 'enable_uart=1' >> /boot/config.txt" sudo bash -c "echo 'init_uart_baud=9600' >> /boot/config.txt"
In /etc/modules, add ‘pps-gpio’ to a new line.
sudo bash -c "echo 'pps-gpio' >> /etc/modules"
Reboot
sudo reboot
I used the Adafruit Ultimate GPS breakout. It has 9 pins but we are only interested in 5. There is also the Adafruit GPS hat which fits right on the Pi but that seems expensive for what it does (but it is significantly neater in terms of wiring).
Pin connections:



Now place your GPS antenna (if you have one) in a spot with a good view of the sky. If you don’t have an antenna, you might have to get a bit creative with how you locate your Raspberry Pi with attached GPS.
I honestly have my antenna in the basement (with only the kitchen and attic above) and I generally have 8-10 satellites locked all the time (11 as of writing). Guess that means the antenna works better than expected! Either way, better exposure to the sky will in theory work better. Pic:

Run raspi-config -> 3 – Interface options -> I6 – Serial Port -> Would you like a login shell to be available over serial -> No. -> Would you like the serial port hardware to be enabled -> Yes.

Finish. Yes to reboot.
First, check that the PPS module is loaded:
lsmod | grep pps
The output should be like:
pi@raspberrypi:~ $ lsmod | grep pps pps_gpio 16384 0 pps_core 16384 1 pps_gpio
Second, check for the PPS pulses:
sudo ppstest /dev/pps0
The output should spit out a new line every second that looks something like this (your output will be a bit farther from x.000000 since it isn’t yet using the GPS PPS):
pi@pi-ntp:~ $ sudo ppstest /dev/pps0 trying PPS source "/dev/pps0" found PPS source "/dev/pps0" ok, found 1 source(s), now start fetching data... source 0 - assert 1618799061.999999504, sequence: 882184 - clear 0.000000000, sequence: 0 source 0 - assert 1618799062.999999305, sequence: 882185 - clear 0.000000000, sequence: 0 source 0 - assert 1618799063.999997231, sequence: 882186 - clear 0.000000000, sequence: 0 source 0 - assert 1618799064.999996827, sequence: 882187 - clear 0.000000000, sequence: 0 source 0 - assert 1618799065.999995852, sequence: 882188 - clear 0.000000000, sequence: 0 ^C
Edit /etc/default/gpsd and change GPSD_OPTIONS=”” to GPSD_OPTIONS=”-n” and change DEVICES=”” to DEVICES=”/dev/ttyS0 /dev/pps0″, then reboot. My full /etc/default/gpsd is below:
pi@raspberrypi:~ $ sudo cat /etc/default/gpsd # Default settings for the gpsd init script and the hotplug wrapper. # Start the gpsd daemon automatically at boot time START_DAEMON="true" # Use USB hotplugging to add new USB devices automatically to the daemon USBAUTO="true" # Devices gpsd should collect to at boot time. # They need to be read/writeable, either by user gpsd or the group dialout. DEVICES="/dev/ttyS0 /dev/pps0" # Other options you want to pass to gpsd GPSD_OPTIONS="-n"
sudo reboot
To ensure your GPS has a valid position, you can run gpsmon or cgps to check satellites and such. This check also ensures GPSd is functioning as expected. If your GPS doesn’t have a position solution, you won’t get a good time signal. If GPSd isn’t working, you won’t get any updates on the screen. The top portion will show the analyzed GPS data and the bottom portion will scroll by with the raw GPS sentences from the GPS module.

For chrony, add these two lines to the /etc/chrony/chrony.conf file somewhere near the rest of the server lines:
refclock SHM 0 refid NMEA offset 0.200
refclock PPS /dev/pps0 refid PPS lock NMEA
2025 update: see the newer post chrony config for how to get single source (i.e. just your single GPS to use both NMEA and PPS) timing to works successfully.
My entire /etc/chrony/chrony.conf file looks like this:
###### below this line are custom config changes ####### server 10.98.1.1 iburst minpoll 3 maxpoll 5 server time-a-b.nist.gov iburst server time-d-b.nist.gov server utcnist.colorado.edu server time.windows.com server time.apple.com # delay determined experimentally by setting noselect then monitoring for a few hours # 0.325 means the NMEA time sentence arrives 325 milliseconds after the PPS pulse # the delay adjusts it forward refclock SHM 0 delay 0.325 refid NMEA refclock PPS /dev/pps0 refid PPS allow 10.98.1.0/24 # my home network ###### above this line are custom config changes ####### ###### below this line is standard chrony stuff ####### keyfile /etc/chrony/chrony.keys driftfile /var/lib/chrony/chrony.drift #log tracking measurements statistics logdir /var/log/chrony maxupdateskew 100.0 hwclockfile /etc/adjtime rtcsync makestep 1 3
Restart chrony, wait a few minutes, and verify.
sudo systemctl restart chrony
Right after a chrony restart, the sources will look like this (shown by running ‘chronyc sources’)
pi@pi-ntp:~ $ chronyc sources 210 Number of sources = 9 MS Name/IP address Stratum Poll Reach LastRx Last sample =============================================================================== #? NMEA 0 4 0 - +0ns[ +0ns] +/- 0ns #? PPS 0 4 0 - +0ns[ +0ns] +/- 0ns ^? pfsense.home.fluffnet.net 0 3 3 - +0ns[ +0ns] +/- 0ns ^? time-a-b.nist.gov 1 6 3 1 -2615us[-2615us] +/- 8218us ^? time-d-b.nist.gov 1 6 1 3 -2495us[-2495us] +/- 7943us ^? india.colorado.edu 0 6 0 - +0ns[ +0ns] +/- 0ns ^? 13.86.101.172 3 6 1 4 -4866us[-4866us] +/- 43ms ^? usdal4-ntp-002.aaplimg.c> 1 6 1 4 -2143us[-2143us] +/- 13ms ^? time.cloudflare.com 3 6 1 3 -3747us[-3747us] +/- 9088us
The # means locally connected source of time. The question marks mean it is still determine the status of each source.
After a couple minutes, you can check again:
pi@pi-ntp:~ $ chronyc -n sources 210 Number of sources = 9 MS Name/IP address Stratum Poll Reach LastRx Last sample =============================================================================== #x NMEA 0 4 377 23 -37ms[ -37ms] +/- 1638us #* PPS 0 4 377 25 -175ns[ -289ns] +/- 126ns ^? 10.98.1.1 0 5 377 - +0ns[ +0ns] +/- 0ns ^- 132.163.96.1 1 6 177 22 -3046us[-3046us] +/- 8233us ^- 2610:20:6f96:96::4 1 6 17 28 -2524us[-2524us] +/- 7677us ^? 128.138.140.44 1 6 3 30 -3107us[-3107us] +/- 8460us ^- 13.86.101.172 3 6 17 28 -8233us[-8233us] +/- 47ms ^- 17.253.2.253 1 6 17 29 -3048us[-3048us] +/- 14ms ^- 2606:4700:f1::123 3 6 17 29 -3325us[-3325us] +/- 8488us
For the S column, * means the source that is active. + means it is considered a good source and would be used if the current one is determined to be bad or is unavailable. The x shown for the NMEA source means it is a “false ticker”, which means it isn’t being used. In our case that is fine because the PPS source is active and valid. Anything else generally means it won’t be used.
In this case, chrony is using the PPS signal. The value inside the brackets is how far off chrony is from the source. It is showing that we are 289 nanoseconds off of GPS PPS time. This is very, very close to atomic clock level accuracy. The last column (after the +/-) includes latency to the NTP source as well as how far out of sync chrony thinks it is (for example, the 17.253.2.253 server is 12.5 milliseconds away one-way via ping):
pi@pi-ntp:~ $ ping -c 5 17.253.2.253 PING 17.253.2.253 (17.253.2.253) 56(84) bytes of data. 64 bytes from 17.253.2.253: icmp_seq=1 ttl=54 time=25.2 ms 64 bytes from 17.253.2.253: icmp_seq=2 ttl=54 time=27.7 ms 64 bytes from 17.253.2.253: icmp_seq=3 ttl=54 time=23.8 ms 64 bytes from 17.253.2.253: icmp_seq=4 ttl=54 time=24.4 ms 64 bytes from 17.253.2.253: icmp_seq=5 ttl=54 time=23.4 ms --- 17.253.2.253 ping statistics --- 5 packets transmitted, 5 received, 0% packet loss, time 4007ms rtt min/avg/max/mdev = 23.403/24.954/27.780/1.547 ms
For a full list of information about how to interpret these results, check here – https://docs.fedoraproject.org/en-US/Fedora/18/html/System_Administrators_Guide/sect-Checking_if_chrony_is_synchronized.html
A day after the bulk of writing this post, I turned on logging via the .conf file and restarted chrony. It settled to microsecond level accuracy in 57 seconds. For the offset column, the scientific notation e-7 means the results are in nanoseconds. This means that for the 19:54:35 line, the clock is -450.9 nanoseconds off of the PPS signal. There is that e-10 line in there which says that it is 831 picoseconds off PPS (I had to look up SI prefixes to make sure pico came after nano! also I doubt the Pi can actually keep track of time that closely.). After the initial sync, there is only 1 line in the below log that is actually at the microsecond level (the others are all better than microsecond) – the 20:00:59 entry, which shows the clock is -1.183 microseconds off.

Thought I’d toss in this section for completeness (i.e. thanks for all the good info Austin but how can I make this even better?). There are a few things that affect how well the time is kept on the Pi:
After running through the steps in this guide, you should now have a functional Stratum 1 NTP server running on your Raspberry Pi with microsecond level accuracy provided by a PPS GPS. This system can obtain time in the absence of any external sources other than the GPS (for example, internet time servers), and then sync up with the extremely precise GPS PPS signal. Our NTP GPS PPS clock is now within a few microseconds of actual GPS time.
Update 2024-01-19: RIP Dave Mills, inventor/creator of NTP – https://arstechnica.com/gadgets/2024/01/inventor-of-ntp-protocol-that-keeps-time-on-billions-of-devices-dies-at-age-85/
I read a ton on https://www.satsignal.eu/ntp/Raspberry-Pi-NTP.html and other pages on that domain over the years which has been extremely helpful in getting GPS PPS NTP going for my setup. There is a lot of background info for how/why this stuff works and many useful graphics. Another source is https://wellnowweknow.com/index.php/2019/12/27/how-to-ntp-a-raspberry-pi-4-via-gps-and-pps/ for a short and sweet (maybe too short) set of commands.
Post Views: 67,751