Building A NTP Server With Raspberry Pi and GPS
Overview
I’m introducing my youngest son to new things. Time keeping is always fun to geek out with because you can always get more precise. I thought this would be a quick and fun project to do together too. He pushed the buttons on this project with only some guidance from me.
🐘 𓃰
Let’s talk about our elephant in the room. The Pi is never going to be atomic clock accurate. This little computer doesn’t even ship with an RTC. It was never intended to act as a time source. You breathing on it can affect the time. When my status at the end of this guide very confidently says “Stratum 1!”, let’s take that with a grain of salt. I will say the Pi is likely better at telling time than ChatGPT is at giving me legal advice. Both are very confident, nonetheless.
Hardware
We used (roughly) the following hardware:
- Raspberry Pi 4B 8GB
- GPS with PPS output with the Adafruit Ultimate GPS
- GPS antenna
- SMA to SMT IPX/U.FL adapter if your antenna didn’t include one
- Breadboard wires
- CR2025 3V Lithium Battery
- SanDisk MicroSDXC Card
- SD Card Reader
I recommend researching product alternatives. I had all the needed hardware on hand from other projects. The products linked here aren’t necessarily the ones I used but should be compatible alternatives. For example Adafruit now makes a USB-C enabled Ultimate GPS
Connecting GPS Module
We need to connect 5 pins from the GPS module to the Pi. You can get more info here on the Raspberry Pi 40-pin Header, and more info on the Adafruit Ultimate GPS pins here.
Expanded View - Click to Expand
Pi Pin | GPS Pin | Pictured Wire Color |
---|---|---|
4 | VIN | Green |
6 | GND | Brown |
8 | RX | Orange |
10 | TX | Red |
12 | PPS | Yellow |
Pi GPIO Pin Numbers - Click to Expand
Connecting External Antenna
This is an optional component. Depending on where you live and where the GPS module is located you might not need an external antenna. I have one because my Pi sits in an old steel HP server rack. Basically a faraday cage.
On the GPS module you have an IPX/U.FL male connector. This connector is sometimes referred to as, Ultra Small Surface Mount Coaxial Connector, uFL, Hirose U.FL, Hirose W.FL, IPX, I-PEX MHF, MHF, IPEX, IPAX, AMC, UMCC, Amphenol AMMC, HF type, SMT Coaxial, AM, and that little thing that breaks all the time for antennas.
These connectors are fragile because they are only rated for about 10-20 connections max. They are also easy to misalign and break. If you’re interested you can buy a tool for attaching these. When looking for a tool be sure to get the correct type (ex. MHF4 vs. MHF3) for your connector.
When attaching the connector make sure you hear the satisfying click. Check out this video for a demonstration.
This video goes into great detail on how to work with these connectors.
The other end is a simple SMA connector. You screw these together like a garden hose.
Installing Raspbian
The easiest way to install Raspberry Pi OS is to use the Raspberry Pi Imager. I used Raspberry Pi OS Lite (64-bit) for this project. For brevity I won’t cover the installation of Raspberry Pi OS. You can find help installing the OS on the download page.
Network
I prefer wired networking for NTP servers, so I won’t cover Wi-Fi. I’m sure you can run your server over wireless if you’re determined. One thing you should do here is set a static IP. I accomplish this in my DHCP server. You can also do the same by modifying the Pi.
Edit the configuration file /etc/dhcpcd.conf
.
1
sudo vi /etc/dhcpcd.conf
Insert your config lines:
1
2
3
4
interface eth0
static ip_address=192.168.0.2/24
static routers=192.168.0.1
static domain_name_servers=192.168.0.1
Reboot the Pi or restart networking to have the changes take effect.
1
service networking restart
Time Zone
Again, I prefer UTC time. I think that comes from working multi-time zone networks for so long. I’ll show North America Central time in my example. To see what your Pi is currently configured for run the following:
1
timedatectl
Your output should look like this:
1
2
3
4
5
6
7
Local time: Sun 2023-08-06 01:19:08 BST
Universal time: Sun 2023-08-06 00:19:08 UTC
RTC time: n/a
Time zone: Europe/London (BST, +0100)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
To change the time zone you will need to know your time zone name. List all the time zones with the following:
1
timedatectl list-timezones
Output should look like this:
1
2
3
4
5
6
7
America/Caracas
America/Cayenne
America/Chicago
America/Chihuahua
America/Ciudad_Juarez
...
You can set the time with the following command:
1
sudo timedatectl set-timezone America/Chicago
Running timedatectl
again will show you the updated time zone:
1
2
3
4
5
6
7
Local time: Sat 2023-08-06 15:20:08 CDT
Universal time: Sat 2023-08-06 00:20:08 UTC
RTC time: n/a
Time zone: America/Chicago (CDT, -0500)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
Updates
Update Raspberry Pi OS with following commands and reboot. Press “Y” when it asks otherwise you won’t install any updates.
1
2
3
4
sudo apt update
sudo apt upgrade
sudo reboot
Install Packages
We will install the following:
- pps-tools - Includes the headers for using LinuxPPS PPS (pulse per second) API kernel interface.
- gpsd - A service daemon for monitoring our GPS receiver through serial and making the data available via TCP port 2947.
- gpsd-clients - A client for interacting with gpsd.
- python3-gps - This package contains a Python 3 interface to connect to gpsd.
- chrony - An alternative NTP implementation of ntpd.
Use this command to install the above.
1
sudo apt install pps-tools gpsd gpsd-clients python3-gps chrony
Prepare System
We need to edit /boot/config.txt
1
2
3
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"
Edit /etc/modules
1
sudo bash -c "echo 'pps-gpio' >> /etc/modules"
Enable the serial port.
1
sudo raspi-config
Select:
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
Reboot the Pi.
1
sudo reboot
Validate System Configuration
Validate PPS is loaded with the following command:
1
lsmod | grep pps
You should see an output like this:
1
pps_gpio 16384 0
or like this:
1
2
pps_ldisc 16384 2
pps_gpio 16384 2
This command will show you PPS responses.
1
sudo ppstest /dev/pps0
You should see results like this:
1
2
3
4
5
6
trying PPS source "/dev/pps0"
found PPS source "/dev/pps0"
ok, found 1 source(s), now start fetching data...
source 0 - assert 1691335285.001247820, sequence: 126 - clear 0.000000000, sequence: 0
source 0 - assert 1691335286.001242576, sequence: 127 - clear 0.000000000, sequence: 0
source 0 - assert 1691335287.001238592, sequence: 128 - clear 0.000000000, sequence: 0
gpsd Configurations
We want gpsd to auto-start, edit the gpsd config.
1
vi /etc/default/gpsd
Change the values as follows:
1
2
3
4
START_DAEMON="true"
DEVICES="/dev/ttyS0 /dev/pps0"
GPSD_OPTIONS="-n"
USBAUTO="true"
Reboot the system to apply the changes.
1
sudo reboot
Let’s look at the GPS data. Run the following command for a live view (hit ctrl+c to exit):
1
gpsmon
If you see the above, it works!
If you only see the garbage below, you likely don’t have your GPS module plugged in correctly.
1
2
3
4
5
6
tcp://localhost:2947 JSON slave driver>
(82) {"class":"VERSION","release":"3.22","rev":"3.22","proto_major":3,"proto_minor":14}
(262) {"class":"DEVICES","devices":[{"class":"DEVICE","path":"/dev/ttyS0","activated":"2023-08-06T15:25:27.449Z","native":0,"bps":9600,"parity":"N","stopbits":1,"cycle":1.00},{"class"
:"DEVICE","path":"/dev/pps0","driver":"PPS","activated":"2023-08-06T15:25:27.472Z"}]}
(122) {"class":"WATCH","enable":true,"json":false,"nmea":false,"raw":2,"scaled":false,"timing":false,"split24":false,"pps":true}
Setup chrony
Update the chrony config. Edit /etc/chrony/chrony.conf
.
1
vi /etc/chrony/chrony.conf
I made some adjustments to the config for my personal preferences. I explain below some of these settings below the code block. Go to this page for a full explanation of all chrony settings.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
###### My Changes ######
server time-a-b.nist.gov iburst
server time-a-g.nist.gov
server time-c-wwv.nist.gov
server utcnist3.colorado.edu
server 0.us.pool.ntp.org
server time.cloudflare.com
server time.windows.com
server time.apple.com
local stratum 10
refclock SHM 0 offset 0.436 delay 0.2 refid NMEA noselect
refclock PPS /dev/pps0 lock NMEA refid GPS prefer
allow 192.168.0.0/16
###### Modified Default Options ######
confdir /etc/chrony/conf.d
keyfile /etc/chrony/chrony.keys
driftfile /var/lib/chrony/chrony.drift
ntsdumpdir /var/lib/chrony
#log tracking measurements statistics
logdir /var/log/chrony
maxupdateskew 10.0
rtcsync
makestep 1 3
leapsectz right/UTC
Settings Explained
server time-a-b.nist.gov iburst
is telling chrony to use this server as a time source. You don’t need this many servers. I’m using them to gather data for another project. I’ll modify my config later withpool {FQDN} iburst
when I’m finished playing.iburst
will quickly get a sync going, even if we’re not 100% accurate.local stratum 10
command says in the event you lose contact with all time sources, continue to act authoritative and give out your best guess to clients.- Let’s look at our two refclock lines.
refclock SHM 0 offset 0.436 delay 0.2 refid NMEA noselect
andrefclock PPS /dev/pps0 lock NMEA refid GPS prefer
.refclock
is a hardware source.refid {name}
is an alias name.SHM
andPPS
driver names or clock sources within the hardware.offset
used to compensate for a constant errors (in seconds).delay
sets the NTP delay of the source (in seconds). Increasing the delay is useful to avoid having no majority in the source selection or to make chrony prefer other sources.lock {refid}
used to lock a PPS refclock to another refclock. PPS samples are paired directly with raw samples from the specified refclock.noselect
Never select this source, monitor only.prefer
Prefer this source over sources without prefer.
allow 192.168.0.0/16
- This setting
#log tracking measurements statistics
combined withlogdir /var/log/chrony
will turn on logging for debugging. Don’t leave this on as it will kill your SD card. maxupdateskew 100.0
set the threshold for chrony to accept an calculated estimate. If our clock source error range is too large it probably indicates that the estimated gain or loss rate is not very reliable yet.rtcsync
syncs the system clock with chrony’s time.- The next two items closely relate. This is really important only if you have applications that don’t accept big jumps well, NYSE financial transactions or timed sciencey stuff for example. For time keeping, Earth is an inaccurate wobbly flying ball of mud. To compensate leap seconds are inserted and deleted from UTC time to keep UTC time synchronized with the Earth’s rotation. Without this, it would eventually snow in our calendar’s summer or be dark at noon. When the system’s time needs to be adjusted, we have two options. To step the clock or smear (sliding) it. Steps are just like it sounds (think stairs); the system clock is stepped or jumped to the accurate time. Slewing adds an increasing amount of time to slowly bring the clock into alignment. Normally chrony will gradually correct any time offset. However, sometimes a jump is required, like at boot. Check out this for more on leap second smearing.
makestep 1 3
The config says make the adjustment if it is larger than one second, but only in the first three clock updates.leapsectz right/UTC
tells chronyd what timezone to use for leap seconds.
Apply Settings
Once you updated and saved your config restart chrony to apply the changes.
1
sudo systemctl restart chrony
Verify chrony Status
chrony has lots of capability. We’ll use 3 basic commands to see what the server is doing.
chronyc -n sourcestats
, chronyc -n sources
, and chronyc tracking
Check this page out for explanations on everything in each command: Checking if chrony is Synchronized.
1
chronyc -n sourcestats
This command shows us the statics of our candidate NTP servers. This is a capture directly after restarting the process. We don’t have any useful data yet. Remember, it can take 5 minutes or more for data to be calculated properly for the candidates. Be patient, wait 5-10 minutes between config changes for things to settle.
1
2
3
4
5
6
7
8
9
10
11
12
Name/IP Address NP NR Span Frequency Freq Skew Offset Std Dev
==============================================================================
NMEA 0 0 0 +0.000 2000.000 +0ns 4000ms
GPS 0 0 0 +0.000 2000.000 +0ns 4000ms
132.163.96.1 1 0 0 +0.000 2000.000 +1585us 4000ms
129.6.15.28 1 0 0 +0.000 2000.000 -1381us 4000ms
132.163.97.3 1 0 0 +0.000 2000.000 -750us 4000ms
128.138.140.211 1 0 0 +0.000 2000.000 -1720us 4000ms
5.78.71.97 1 0 0 +0.000 2000.000 +1208us 4000ms
162.159.200.1 1 0 0 +0.000 2000.000 -2466us 4000ms
40.119.6.228 1 0 0 +0.000 2000.000 -2860us 4000ms
17.253.126.253 1 0 0 +0.000 2000.000 -2831us 4000ms
1
chronyc -n sources -v
This command shows us information about the current sources. -v
prints the help at the top. Key take away here:
- “*” indicates a synchronized source. We’re getting time from here now.
- ”+” indicates acceptable sources.
- ”-“ indicates acceptable but excluded sources.
- ”?” indicates sources with lost/bad connectivity. Everything starts with this until 3 or more samples have been received.
- “x” indicates source which chronyd thinks is a falseticker (bad, fake, broken, spoofed).
- ”~” indicates a source with too much variability.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.-- Source mode '^' = server, '=' = peer, '#' = local clock.
/ .- Source state '*' = current best, '+' = combined, '-' = not combined,
| / 'x' = may be in error, '~' = too variable, '?' = unusable.
|| .- xxxx [ yyyy ] +/- zzzz
|| Reachability register (octal) -. | xxxx = adjusted offset,
|| Log2(Polling interval) --. | | yyyy = measured offset,
|| \ | | zzzz = estimated error.
|| | | \
MS Name/IP address Stratum Poll Reach LastRx Last sample
===============================================================================
#? NMEA 0 4 377 14 +44ms[ +44ms] +/- 108ms
#* GPS 0 4 377 14 +936ns[+1316ns] +/- 177ns
^- 132.163.96.1 1 10 377 967 +1556us[+1573us] +/- 18ms
^- 129.6.15.28 1 10 377 241 -1953us[-1952us] +/- 23ms
^- 132.163.97.3 1 10 377 121 -1576us[-1576us] +/- 22ms
^- 128.138.140.211 1 10 377 24m -1938us[-1923us] +/- 21ms
^- 5.78.71.97 2 10 377 409 +1838us[+1835us] +/- 37ms
^- 162.159.200.1 3 10 377 458 -5633us[-5637us] +/- 19ms
^- 40.119.6.228 3 10 377 396 -6626us[-6629us] +/- 37ms
^- 17.253.126.253 1 10 377 447 -4726us[-4729us] +/- 7672us
1
chronyc -n tracking
This command shows us information about the currently locked source.
1
2
3
4
5
6
7
8
9
10
11
12
13
Reference ID : 47505300 (GPS)
Stratum : 1
Ref time (UTC) : Sat Aug 06 00:55:22 2023
System time : 0.000000141 seconds fast of NTP time
Last offset : +0.000000302 seconds
RMS offset : 0.000000300 seconds
Frequency : 11.530 ppm fast
Residual freq : +0.000 ppm
Skew : 0.004 ppm
Root delay : 0.000000001 seconds
Root dispersion : 0.000016511 seconds
Update interval : 16.0 seconds
Leap status : Normal
Tuning chrony
This isn’t terribly complicated, but I didn’t find anyone who really connected the dots for someone who reads every 3rd word in a sentence (if I’m concentrating).
See the +717 below? 717 is bad. This number needs to be as close to 0 as we can get it. Our GPS line (PPS) depends on the NMEA offset to be within a certain amount.
Why? PPS knows the start and end of a second. NMEA knows which second and passes this to PPS. If they are too far apart they can’t lock.
1
2
3
4
5
6
7
chronyc -n sourcestats
Name/IP Address NP NR Span Frequency Freq Skew Offset Std Dev
==============================================================================
NMEA 21 12 316 +59.729 66.137 +717ms 7044us
GPS 0 0 0 +0.000 2000.000 +0ns 4000ms
132.163.96.1 9 7 330 -1.763 12.824 +1242us 733us
~~~~
How do we fix this? Simple, remember the refclock lines in our config? We need to adjust the offset. I ended up with 0.436 as my offset.
refclock SHM 0 offset 0.436 delay 0.2 refid NMEA noselect
To generate this wild +717 I set my config as follows:
refclock SHM 0 offset -0.236 delay 0.2 refid NMEA noselect
A negative offset adds to your total. A positive number subtracts. For example, if you have +500ms offset and change your config to 0.100, your offset should show around +400ms. Take the same +500ms and you set your config to -0.100 your offset should increase to +600ms.
Keep in mind, your offset will fluctuate as satellites fly in and out of view, temps change, loads on CPU fluctuate, etc. Getting the number to within +/- 100ms is usually good enough to get a lock to happen.
After about an hour of running I had this:
1
2
3
4
5
Name/IP Address NP NR Span Frequency Freq Skew Offset Std Dev
==============================================================================
NEMA 39 21 609 -2.620 26.729 +21ms 8699us
GPS 8 6 113 +0.000 0.031 +1ns 476ns
132.163.96.1 29 14 141m -0.041 0.072 +3483us 204us
The next day it looked like this:
1
2
3
4
5
Name/IP Address NP NR Span Frequency Freq Skew Offset Std Dev
==============================================================================
NEMA 19 9 286 -17.101 100.307 +64ms 8164us
GPS 6 3 78 -0.023 0.061 -226ns 509ns
132.163.96.1 13 10 276m +0.111 0.036 +2194us 163us
Let’s look at that time now with the date command.
1
date +"%a %F %T.%N %Z %z"
1
Sat 2023-08-06 15:56:38.772947939 CDT -0500
Conclusions
That should about cover it. The next step is to start updating your clients. For devices like IP phones and some network gear you can set option 42 in your DHCP server. Option 42 specifies servers that provide NTP/SNTP services (RFC 1769) on the network.
You can likely get similar results to this project with NTP and a RTC module. Here is a tutorial that looks like it covers all the points.
You can always roll your own RTC.
I put the GPS module in a little parts box I had.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
chronyc -n sources
MS Name/IP address Stratum Poll Reach LastRx Last sample
===============================================================================
#? NMEA 0 4 377 13 +73ms[ +73ms] +/- 105ms
#* GPS 0 4 377 13 -429ns[ -457ns] +/- 493ns
^- 132.163.96.1 1 6 377 30 +406us[ +405us] +/- 19ms
^- 129.6.15.28 1 6 377 35 -1908us[-1908us] +/- 24ms
^- 132.163.97.3 1 6 377 35 +384us[ +383us] +/- 21ms
^- 128.138.140.211 1 6 377 34 -1171us[-1171us] +/- 21ms
^- 108.61.56.35 2 6 377 33 -2877us[-2877us] +/- 55ms
^- 162.159.200.1 3 6 377 35 -2618us[-2618us] +/- 16ms
^- 40.119.6.228 3 6 377 33 -4100us[-4100us] +/- 43ms
^- 17.253.126.125 1 6 377 161 -2783us[-2783us] +/- 5846us
chronyc -n tracking
Reference ID : 47505300 (GPS)
Stratum : 1
Ref time (UTC) : Sun Aug 07 21:30:33 2023
System time : 0.000000002 seconds fast of NTP time
Last offset : -0.000000095 seconds
RMS offset : 0.000098806 seconds
Frequency : 11.318 ppm fast
Residual freq : -0.000 ppm
Skew : 0.010 ppm
Root delay : 0.000000001 seconds
Root dispersion : 0.000021171 seconds
Update interval : 16.0 seconds
Leap status : Normal
Sources - Helpful Links - Geek Stuff
- I ran across this post a year ago or more on Reddit (RIP Apollo App). It was the inspiration for me to put this together.
- This page has some good info on the differences between ntpd and chrony.
- As does this page.
- I enjoyed this discussion on setting up chrony with PPS. Thanks for keeping a 7 year old post up!
- I skimmed the chrony.conf help page a few dozen times. I still don’t use half the functions chrony is capable of.
- Helpful explanation of how PPS and NMEA work together.
- Some thoughts on running GPS only timing.
- Good read if you have trouble.
- What Exactly Is GPS NMEA Data?
- Why do GPS receivers have a 1 PPS output?
- Leap Second Smearing with NTP
- Info on the Adafruit Ultimate GPS
- Troubleshooting GPSD
Comments powered by Disqus.