When I first started looking at doing a Raspberry Pi GPS NTP server, I did some Googling to try to find some directions. I found a great post on NetworkProfile.org (https://blog.networkprofile.org/gps-backed-local-ntp-server/) that covered all the steps in detail. However, since that was from a year ago and was focused on Pi 3b+ and 1b+ hardware configurations. Since I was looking, I opted to go with a Raspberry Pi 5 to get the newest hardware so it will last a long time.
First the hardware that I opted to get, some based on the hardware used in the post I was following, some from my own choosing.
I opted for a Raspberry Pi 5. Since this was my first foray into the world of Raspberry Pi, I started by looking on Amazon for the 3b+. I quickly found that there are so many options when it comes to Raspberry Pis that I quickly narrowed down my search. I started with looking at the complete kits with the Pi, case, power supply, microSD card, etc. that I decided I wanted to piece together something that would work for me.
Image the microSD card with the OS for the Pi. I opted for Ubuntu 24.04 since I use Ubuntu extensively at work and home. I used the Raspberry Pi Imager to load Ubuntu on the microSD card and performed customizations so I could run headless from he first boot and use SSH for remote access.
Additional Software
First thing is to make sure the OS is up to date and add the required packages.
sudo apt update sudo apt full-upgrade -y sudo apt install gaps gaps-clients ops-tools chrony jq raspi-config -y |
Also cleaned up some additional packages that aren’t needed.
sudo apt purge —remove lxde* sudo apt auto remove -y sudo apt-get remove —auto-remove lxpanel sudo apt-get purge avahi-daemon |
The first this is to enable the serial port on your Raspberry Pi.
sudo raspi-config |
#3 Interface Options
I6 Serial Port
Login shell on serial port: No
Enable serial port: Yes
Reboot when prompted.
Next is where the directions start to diverge for the updated hardware of the Pi 5. Next we configure the GPIO pin for PPS and add the PPS module.
Edit /boot/firmware/cmdline.txt and remove references to the serial port ttyAMA0. E.G. remove: console=ttyAMA0,115200 and (if present) kgdboc=ttyAMA0,115200. I did not have to disable Getty on serial port, but iff needed sudo systemctl disable getty@ttyAMA0. For RPI 3 or later, disable bluetooth via overlay:
sudo bash -c “echo ‘dtoverlay=pi3-disable-bt-overlay’ >> /boot/firmware/config.txt” 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" dtparam=uart0 dtparam=uart0_console |
Now add the PPS module:
sudo bash -c “echo ‘pps-gpio’ >> /etc/modules-load.d/raspberrypi.conf” |
Now the Pi is ready to physically connect the GPS module.
Now you can connect your GPS module to the Pi. For the serial connection, you will have to solder the header to the GPS module, and then plug it into the Pi GPIO pins. It doesn't matter if you solder the connector on with the tall pins at the top of the module or the bottom, just do what works for you. I did it so the tall pins were on the side with the LED, so I could lay it flat on a table while testing. Make sure to turn the Pi off while plugging into the GPIO. You can shut down your pi with
sudo shutdown -h now |
I will use this as a reference for the pinout to connect the wires
V-IN GPS —> RPi Pin 4 GROUND GPS —> RPi Pin 6 RX GPS —>RPi Pin 8 TX GPS —>RPi Pin 10 PPS GPS —>RPi Pin 12 |
As you can tell, they are all in a line which makes it easy
Also make sure to connect the antenna and then power on the Pi.
Now we have all the pre-requisites setup and the hardware connected, we can start testing and configuring things.
Check for PPS pulses to see if the PPS config is correct by doing
sudo ppstest /dev/pps0 |
Next edit the gpsd configuration file
sudo vim /etc/default/gpsd |
START_DAEMON="true" DEVICES="/dev/ttyAMA0 /dev/pps0" # Other options you want to pass to gpsd GPSD_OPTIONS="-n" |
Once you have your config in place, execute the below command to make gpsd start on boot
sudo ln -s /lib/systemd/system/gpsd.service /etc/systemd/system/multi-user.target.wants/ |
Now go ahead and reboot. When it comes back, launch cgps and you should see the output
Note that it requires 4 satellites to get accurate time (Source below)
If you have got to this point, you now have your GPS module connected to the Pi, and should notice the LED light blinking every second. If you ever notice it go off, or not blink every second, its because you are losing the PPS signal and don't have 4 satellites or have completely lost the GPS lock. You may need to reposition your GPS module, or get a better antenna, or make sure your antenna connector is fully clipped in.
If you want to monitor the number of satellites and move around your antenna, you can use this handy command
It will just keep scrolling down the screen how many you have, which can be useful. Just do Control + C to exit
Now we can configure the NTP server Chrony to use the GPS module. We will wait to configure additional options in the next step, right now we just want Chrony to see and use the GPS time. Start by opening the config file
sudo vim /etc/chrony/chrony.conf |
This tells Chrony to add the NMEA (GPS) source, but don't use it for time (The noselect) We can dig into what the rest means later
If using Serial, add the below lines. There is an extra line to get the PPS data. Since PPS only tells you seconds, we lock it to the NMEA source to get the rest of the information.
refclock SHM 0 refid NMEA offset 0.000 precision 1e-3 poll 3 noselect refclock PPS /dev/pps0 refid PPS lock NMEA poll 3 |
Scroll down (With pagedown) and find this line
Do what it says and uncomment it, which will turn on logging. With that changed and the new lines for the GPS, save and exit nano.
Restart Chrony
sudo systemctl restart chrony |
Now, do the following
sudo cat /var/log/chrony/statistics.log | sudo head -2; sudo cat /var/log/chrony/statistics.log | sudo grep NMEA |
It will spit out some information like this. And each time you run it, you will have more data
The reason we are collecting this data is to get the "Est offset" number, so we can tune that in the chrony config, to get the time spot on. You will want to let this run for at least 10 mins, but the longer the better. Personally I ran it for 10 minutes and go the average and left it logging while doing that and then compared to a full hour of data.got it working, and then came back at a later date and re-did the logging with 4-5 hours of data (As it turns out, it was the same...)
Once you have waited as long as you wanted to, go ahead and copy the log file to a system to use a spreadsheet to get the average for the needed column. I did this on my Mac with Excel. We are going to now use Excel to get the average of the Est Offset, which will punch into chrony.
Open Excel and in the Data Tab, click From Text and select your txt file.
Fill in more details here.
Now go back to the chrony config
sudo vim /etc/chrony/chrony.conf |
And change your offset on the NMEA line to the number we found. Personally I added a whole new line, so I can easily switch back to 0.00 for tuning in the future
Also, go back down and re-comment the line to disable logging, save and exit. We can also delete the old log files now, and also restart Chrony
sudo rm /var/log/chrony/statistics.log sudo systemctl restart chrony |
Now do the following command
watch -n 1 chronyc sources |
This will look at the Chrony sources, and refresh every 1 seconds
If you are using Serial/PPS, your time should look something like this with the PPS now getting the primary reference, notated by the *. The internet NTP sources will probably have the ^ symbol, notating that they are ready to take over, and have valid time. The NMEA has the ? saying it will not be used, as we used the noselect option on it
If you are using GPS, the same as the above is true, but your NMEA will have the * and there will be no PPS. If your tuning was correct, it will take over. If your tuning was wrong, its possible one of the internet NTP sources will still be primary.
From here there is not much else to do with the GPS configuration, you really just want to make sure its getting a good offset, but note that it will fluctuate.
If you are using USB, because of the overhead of USB, you may notice it fluctuating wildly. But in my testing its always still better than the internet NTP sources. And if you are using Serial, the last sample for the NMEA source may be wildy out of whack. I do not know why, but it doesn't affect the time. Here as an example it shows my NMEA as +13 milliseconds! When we are dealing with time in the nanoseconds, a millisecond is eternity.
Another very handy command is
watch -n 1 chronyc tracking |
With PPS, you can get literally SPOT ON to real time. Mine is often below 100, and often time sits at exactly 0. In this screenshot we are indicated 2 nanoseconds off real time. That is an insane amount of accuracy. If you are using USB, your number will be a lot higher. (Though, there is some overhead from the Pi and controller not accounted here)
If you got to here, we have now got the GPS completely tuned, and we can move on to the NTP server configuration.
Note, if you decide now you want to re-tune your GPS and gather more data by turning on logging, you MUST revert to a 0.000 offset first in chrony.
Now we can get on to the other NTP configuration, the part most other guides just skip over.
edit your chrony config file
sudo vim /etc/chrony/chrony.conf |
First, if you are happy with your PPS time and its stable, add prefer to the end, so it looks like this
Somewhere in there (Anywhere, it doesn't matter), we need a line to allow clients to connect to us, if we don't enter this, nothing can connect to Chrony, and its not exactly much of an NTP SERVER. I added the following to allow connections from anywhere, as this device is only on my local network anyway, and I want any and all subnets to connect to it, even in the future.
allow 0.0.0.0/0 |
if your home network was in the 192.168.1.0 IP space, you could enter the above, or
allow 192.168.1.0/24 |
The CIDR /24 is probably correct. If you are on a network with something different, you most likely configured it yourself and would know.
next, we need a manual directive which enables support at run-time for the settime command in chronyc. Easy, its just one word
manual |
Then we need an option which the post on NetworkProfile.org highlighted, and its the option for orphan mode and which stratum to report. Here is some more info on stratums
The orphan mode is when the internet gets disconnected, and there are no other time servers available and we are alone. The default configuration for Chrony is that when this happens, it marks your highly accurate GPS time source as unusable. To me, this is crazy. No other guide mentions this, as I suspect they never tested it. Why would I want clients to have no NTP time, vs a highly accurate GPS time source?
I decided to add the line below. Which tells chrony to report to clients, even when there are no other sources online, that this is a stratum 1 time server, which is correct.
local stratum 1 |
Finally, if you don't want to use pool.ntp.org, enter your own time servers and comment out that line and add your own. I decided on the following:
server time.cloudflare.com iburst server time.apple.com iburst server time.nist.gov server tick.usno.navy.mil server tock.usno.navy.mil |
I've not had good luck with using iburst on the US Naval Observatory NTP servers, or NIST. Since we have a GPS clock and other servers in the list, its not really needed anyway. I added the Apple server as so far, its been extremely reliable. It pointed me to a local server in Dallas which is less than 2ms away from my home internet, and the time has stayed very stable from looking at the stats.
People asked my why I don't use pool.ntp.org, and the answer is that I ended up with a bunch of really wacky domains and addresses in my IPS logs. I guess some of the people volunteering in the NTP Pool are on blacklists which is just not something I wanted to deal with. I also read that there have been many cases of pool members using Google's servers and other smeared time servers as sources, which is something you want to avoid. Personally I don't see much of a reason to point at another stratum 2, or stratum 3 servers, when we can point at the NIST and USNO servers.
Here is a screenshot of how mine looks now
Go ahead and save the config and reload chrony
sudo systemctl restart chrony |