Setting up a Stratum 1 NTP server on a Raspberry Pi

If you're as excited about precision time keeping as I am, then setting up your own Stratum 1 time source may have occurred to you at some point. If not, then it still makes for a fairly interesting project!

What even is a Stratum?

The Network Time Protocol(NTP) uses a series of layers, or strata, of time sources.

Starting at the top, we have stratum 0, these are high precision time-keeping devices such as atomic clocks, GPS clocks, or radio clocks. They are also known as reference clocks.

These stratum 0 devices are in turn connected to stratum 1 devices, these are usually computers whose system clocks are synchronized to within microseconds of stratum 0. Their job is to provide accurate time signals to other machines over a network. They are also known as primary time servers.

Stratum 2 servers are computers receive time signals from stratum 1 servers over a network. They may query several stratum 1 servers and stratum 2 servers for sanity and stability.

Stratum 3 servers receive their time signal from a stratum 2 servers, stratum 4 servers receive their time signal from a stratum 3 servers, and so on. The stratum of a time server is always 1 higher than the stratum of the time source it is synchronized to.

The highest stratum is stratum 15, and stratum 16 is used to denote an unsynchronized time source.

So, by connecting our Raspberry Pi to a stratum 0 time source (GPS), it becomes a stratum 1 time source.

What do we need?

  • A Raspberry Pi (If you're using a Pi 3, make sure to correct the UART baud rate first!)
  • A GPS module with PPS output (I will elaborate on this later), such as the Adafruit Ultimate GPS Breakout
  • A clear view of the sky, to receive a decent GPS signal (ideally with an extenal antenna)
  • Install the required packages with sudo apt-get install picocom pps-tools ntp

Setting up the hardware

This is the easy bit. Connect up the GPS module to the Pi's GPIO header like so:

  • Connect VIN on the module to pin 4 (5V) on the Pi
  • Connect GND on the module to pin 6 (GND) on the Pi
  • Connect RX on the module to pin 8 (TXD0) on the Pi
  • Connect TX on the module to pin 10 (RXD0) on the Pi
  • Connect PPS on the module to pin 12 (GPIO18) on the Pi

Make sure the levels coming out of your GPS module are 3.3V safe! If you're using the Adafruit module everything should be fine.

Configuring the Pi

The first thing we need to do is to stop linux putting a serial console on the UART. To do this we need to edit the kernel arguments with sudo nano /boot/cmdline.txt. For the Pi/Pi 2 remove the section console=ttyAMA0,115200, for the Pi 3, remove the section console=serial0,115200.

Next, we need to get the Pi to load the pps-gpio kernel module on boot with sudo sh -c "echo pps-gpio >> /etc/modules"

And then tell it to use GPIO18 as a PPS input with sudo sh -c "echo dtoverlay=pps-gpio,gpiopin=18 >> /boot/config.txt"

If we reboot at this point, we can check everything is working as expected. If we open a serial console with picocom -b 9600 -f n /dev/ttyS0 (replace ttyS0 with ttyAMA0 on Pi/Pi 2), we should see some output from the GPS module, specifically we are looking for lines that start with $GPRMC.

If you have a garbled mess, your GPS module may be set at a different baud. If you don't see any $GPRMC messages, you need to configure your module to output them.

Next, we need to check our PPS has been configured correctly. Make sure you GPS module has a fix (usually indicated by an LED), and execute the command sudo ppstest /dev/pps0. You should get an output similar to below:

~ $ 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 1457361736.999999817, sequence: 4986 - clear  0.000000000, sequence: 0  
source 0 - assert 1457361737.999997945, sequence: 4987 - clear  0.000000000, sequence: 0  

Finally, we need to configure udev to create some symlinks in /dev/ to make our gps serial and PPS interfaces easier to find. Create a new file with sudo nano /etc/udev/rules.d/10-pps.rules with the following contents:

KERNEL=="ttyS0", SYMLINK+="gps0"  
KERNEL=="pps0", OWNER="root", GROUP="tty", MODE="0660", SYMLINK+="gpspps0"  

For the Pi/Pi 2, replace ttyS0 with ttyAMA0.

What's the big deal with PPS?

You may have been wondering why I keep going on about PPS.

Well, imagine you ask your friend the time, and they reply "It's 14:53". This is fine for a rough idea, but you have no idea when that minute started. It could be 14:53:00, or 14:53:59. Similarly, it's all very well your GPS module telling you the time is 14:53:42, but you have no way of knowing precisely when that second started.

This is what PPS is for.

The PPS signal specifies EXACTLY when a new UTC second starts. This allows for incredibly accurate timekeeping, down to the microsecond, rather than to the nearest second.

Getting an ntpd with PPS support

The version of ntpd that ships with the Pi doesn't include PPS (or ATOM, as the clock is called) support. So, we must compile our own!

First, we need to enable the source repositories with sudo nano /etc/apt/sources.list, and then uncomment the line containing deb-src. Then run sudo apt-get update to get the package lists.

We're not going to get the source for the ntp package this way, but we do need the required build tools, so install them with sudo apt-get build-dep ntp.

Grab the latest (at time of writing) version of ntp from the project home page, extract it, and change directory into the source directory:

wget http://archive.ntp.org/ntp4/ntp-4.2/ntp-4.2.8p6.tar.gz  
tar zxvf ntp-4.2.8p6.tar.gz  
cd ntp-4.2.8p6  

If you're using a Pi 2/3, configure with ./configure --enable-linuxcaps --prefix=/usr, else use ./configure --prefix=/usr. Wait for this to complete.

Then compile and install with

make  
sudo service ntp stop  
sudo make install  

If you're using a Pi 2/3, you can use make -j5 instead of just make, but be sure that your Pi is plugged in to a 2A power supply, or it may reset mid-compile!

As we've just overwritten the package version of ntp with our own compiled version, we need to stop apt-get upgrade from overwriting it with echo "ntp hold" | sudo dpkg --set-selections.

Stopping dhcpcd from interfering

Later Raspian versions ship with the dhcpcd5 package. This has a nasty habit of interfering where it's not wanted. Specifically, in this instance, if your DHCP server specifies an NTP server, it will start ntpd with it's own version of ntp.conf. Not what we want at all if we're setting up our own NTP server!

So, to stop this behavior, remove the offending script with sudo rm /lib/dhcpcd/dhcpcd-hooks/50-ntp.conf, and remove any configuration files it may have created with sudo rm /var/lib/ntp/ntp.conf.dhcp.

Configuring ntpd

Now we have everything else set up, we can configure ntpd. Open up the configuration file with sudo nano /etc/ntp.conf and replace the contents with the following (I'll explain the interesting bits further down):

# /etc/ntp.conf, configuration for ntpd; see ntp.conf(5) for help

# Local
server 127.127.1.0  
fudge 127.127.1.0 stratum 10

# GPS with PPS enabled
server 127.127.20.0 mode 17 minpoll 4 maxpoll 4 iburst true prefer  
fudge 127.127.20.0 flag1 1 refid GPS

# Internet time servers for sanity
server 0.pool.ntp.org iburst prefer  
server 1.pool.ntp.org iburst  
server 2.pool.ntp.org iburst  
server 3.pool.ntp.org iburst

# By default, exchange time with everybody, but don't allow configuration.
restrict default kod nomodify notrap nopeer noquery  
restrict -6 default kod nomodify notrap nopeer noquery

# Local users may interrogate the ntp server more closely.
restrict 127.0.0.1  
restrict -6 ::1

# Drift file etc.
driftfile /var/lib/ntp/ntp.drift  
  • Local: This section sets up a local clock that nptd can refer to if it can't connect to anything else.
  • GPS with PPS enabled: This section specifies the particulars for our GPS setup
    • server 127.127.20.0 tells it that we have a serial GPS reciever on /dev/gps0
    • mode 17 tells it to look for $GPRMC messages at a baud of 9600bps
    • fudge 127.127.20.0 tells it that we have some further configuration for our GPS
    • flag1 1 tells it that we have a PPS interface from our GPS on /dev/gpspps0
    • refid GPS tells it to label this time source with the reference "GPS"
    • Full documentation for the ntpd NMEA driver (for if you require different baud rates etc) can be found here
  • Internet time servers for sanity This section specifies some NTP pool servers to act as a sanity check for our GPS time. They will also keep the time accurate if you lose GPS signal for whatever reason, such as your cat entangling itself within the antenna wire and disconnecting it.

Check everything is working!

Now we just need to start ntpd back up with sudo service ntp restart. If we wait a few minutes, and then run ntpq -pn we should see a line similar to

o127.127.20.0    .GPS.            0 l    1   16  377    0.000    0.005   0.003  

The o at the beginning of the line shows that ntpd is receiving a PPS signal from our GPS module!

EDIT: Thanks to Jeremy for correcting a typo!