Stratum-1 NTP time server with Raspberry Pi, GPS and Chrony:
- how to build a precision time server.
- (optional) how to add a 4x20 LCD to display server status and GPS and PPS information.
- Raspberry Pi
While you can use any model of Raspberry Pi to implement a Stratum-1 NTP server with GPS, the best choice is Raspberry Pi 4, due to 1Gbit network interface and fast hardware for lowest possible latencies.
-
Using a Raspberry Pi 5 gives two additional advantages, but has changed the configuration of the serial port.
- Support for the inbuilt hardware real time clock (RTC) that comes with the Raspberry Pi 5
- Support for the PTP protocol, which allows to transmit precision time information via ethernet hardware
- When connecting the GPS receiver via serial port, the new default is not to use the RX, TX pins on the 40pin connector, but the new three-pin UART connector. To use the 40-pin connector a special configuration is needed.
Both options are of limited advantage for most settings: the hardware clock is only useful for providing time during the first few seconds after boot (and if both GPS and network are inaccessible), and PTP is a time standard requiring IEEE1588-enabled hardware everywhere and provides no advantages over NTP/chrony in most settings. Both however are documented below (See chrony
configuration, PTP-chapter).
-
GPS module
- Adafruit GPS hat: Adafruit ultimate GPS hat
- GPS module with serial output and PPS signal: Adafruit GPS module
- GPS module with USB, SMA connector and PPS: Keystudio GPS module
- Cheap NEO6 modules: Aliexpress NEO6)
- Active GPS antenna
When selecting a GPS antenna, make sure to get an active antenna with 3-5V power input. Passive antennas often look similar, but reception quality is far worse at similar cost.
Some boards have uFL antenna connectors, whereas almost all external active antennas use SMA, so you might need an uFL to SMA adapter:
Some GPS modules come with a passive antenna and and external antenna plug. Check the description of your board to find out what's needed to use the external antenna. The Keystudio GPS module listed above for example, requires the removal of capacitor C2 in order to activate the external antenna.
Make sure your GPS module has an accessible output for the PPS signal. (Red mark on image)
If you are using a GPS Hat that plugs into the Raspberry IO connector, check the documentation to which GPIO pin the PPS signal is connected. Adafruit's Hat uses GPIO 4.
When not using a Pi Hat, decide between USB- and serial connection.
USB-Connection:
- No extra cables for Vcc, GND, Tx, Rx are needed: both powersupply and communication goes over USB.
- No modification of Raspberry's serial console is needed, easier software installation
- Example: Keystudio GPS module
Serial connection:
- Usually cheaper, if a module is purchased.
- All Pi Hats use serial connections.
- Raspberry serial console needs to be disabled
- Sometimes, bluetooth needs to be disabled.
- Raspberry Pi 5 changed the default serial connection to the 3-pin UART connector by default.
- Raspberry PI 4 (or 5)
- Either GPS hat (e.g. adafruit ultimate GPS hat + uFL to SMA-Adapter) or USB-GPS-Module with PPS output (e.g. keystudio gps module)
- Active GPS antenna with SMA connector and 3-5V
NEO6 GPS modules are available at very low cost (1-2Eur) at chinese dealers (Aliexpress) and are perfectly usable as long as they allow connecting an active external GPS antenna and provide a PPS signal.
Typical wiring between GPS module and Raspberry Pi connector.
Note: If your GPS module is connected via USB, you only need to connect the PPS connector on the GPS module to GPIO 4 on the Raspberry. Power (Vin), Gnd and Tx/Rx are handled via USB.
Note: The Raspberry Pi 5 has a new connector for the serial port that is used by default instead of the Pins RX, TX on the diagram:
- Either use new the three-pin UART connector, which is used by default for serial communication,
- or reconfigure the serial ports by editing
/boot/firmware/config.txt
and adding the lines:
dtparam=uart0
dtparam=uart0_console
Documentation for those options is currently in a disarray for Raspberry Pi 5, so some research might be required. See discussion in #6. Also this video on Raspberry Pi 5 Serial Port usage might be helpful.
Before you continue with software: At this point, the GPS module should be connected to the Raspberry Pi and the active antenna. Power up the Raspberry Pi, and check that the GPS module receives the GPS signal (the antenna must have unhindered access to the open sky, it does not work indoors!).
If reception is ok, a led ("FIX" or "PPS") should start blinking on your GPS module.
Check the documentation for your specific hardware how "FIX" is signaled:
- The Adafruit module blinks once per second if there is no fix and once every 10 seconds when there is a fix.
- Adafruit has an excellent GPS guide for their ultimate GPS module.
If you are using laboratory equipment that uses PTP (precision time protocol) information (IEEE 1588), then you will need to use the Raspberry Pi 5 and make sure that your network switches are IEEE 1588 capable. Note that most consumer switches are not IEEE 1588 capable and cannot be used to set up a PTP network. See below (PTP chapter) for setup details and hardware tests.
PTP is completely optional and not required in a network in order to have excellent results with your Raspberry Pi NTP server!
Note: (Optional info) The original repository of
chrony
at https://chrony.tuxfamily.org/ seems to be of low availability. In case of problems, use the mirror at https://github.com/mlichvar/chrony) if you want to reference the originalchrony
sources at some point.
If your GPS board is connected via serial connection (Rx/Tx), you need to "free up" Raspberry's serial port, which by default is used to connect a serial (debug) console. We need to disable that console to prevent it from interferring with the GPS module.
This is not needed, if your module is connected via USB.
How to disable the serial console on Raspberry variies greatly depending on hardware revision and Linux flavour used. A few examples:
- Start raspi-config:
sudo raspi-config
. - Select option 3 - Interface Options.
- Select option P6 - Serial Port.
- At the prompt Would you like a login shell to be accessible over serial? answer 'No'
- At the prompt Would you like the serial port hardware to be enabled? answer 'Yes'
- Exit raspi-config and reboot the Pi for changes to take effect.
See "Disable Linux serial console" at raspberrypi.org.
Older versions of raspi-config
hide the same serial options under "Advanced options"
Note: Recent Raspberry Pi OS has moved the location of kernel config files from
/boot/
to/boot/firmware/
. Adapt the following paths according to your OS version (If moved, a note is left at the old/boot/
location.
- Edit
/boot/firmware/cmdline.txt
and remove references to the serial portttyAMA0
. E.g. remove:console=ttyAMA0,115200
and (if present)kgdboc=ttyAMA0,115200
. - Disable getty on serial port.
sudo systemctl disable getty@ttyAMA0
or, on some Linux distris:sudo systemctl disable serial-getty@ttyAMA0
- Enable uart in
/boot/firmware/config.txt
, add a lineenable_uart=1
- For RPI 3 or later disable bluetooth via overlay, add another line to
/boot/firmware/config.txt
:dtoverlay=pi3-disable-bt-overlay
. In some versions, this is done by enabling the overlaydtoverlay=pi3-miniuart-bt
which disables bluetooth and reestablishes standard serial.
See: "Raspberry Pi 3, 4 and Zero W Serial Port Usage".
Do a cat
on your serial port (e.g. cat /dev/ttyS0
), or, for USB cat /dev/ttyACM0
or cat /dev/ttyUSB0
, and you should receive an output like:
$GPGGA,1413247.000,48432.1655,N,01342323.7322,E,1,06,1.340,2505.5,M,347.6,M,,*69
$GPGSA,A,3,123,303,248,205,037,415,,,,,,,1.59,1.340,0.90*049
Once you successfully receive output from your GPS module, next step is to install gpsd
via your package manager.
For Raspberry Pi OS (and Ubuntu and Debian variants), that would be:
sudo apt install gpsd
Edit /etc/default/gpsd
:
For serial connections (use your device name as tested above):
GPSD_OPTIONS="-n -G"
DEVICES="/dev/ttyS0"
USBAUTO="false"
For USB connections (again use your actual device name):
GPSD_OPTIONS="-n"
DEVICES="/dev/ttyACM0"
USBAUTO="true"
Enable and start gpsd
with:
sudo systemctl enable gpsd
sudo systemctl start gpsd
Now use cgps
or gpsmon
to make sure that you are receiving GPS information and time.
cgps
should display Status: 3D fix (xx secs)
. Do not continue until you have a stable GPS fix.
The serial or USB connection to the GPS module alone does not allow precise time synchronisation. The slow communication has latencies somewhere between 50ms to 200ms, much too high latency for precision time servers.
This is compensated by the PPS signal that is directly connected to Raspberry PI's GPIO 4 (you can use other GPIO pins, simply adapt this correpondingly below).
We need to enable a special kernel driver and overlay in order to receive this once-per-second GPS synchronised pulse as precisely as possible. The location of config files for kernel modules and options has changed recently (2024-03)!
You'll find that /boot/config.txt
just contains a note that file content has moved to /boot/firmware/config.txt
.
- Edit
/boot/firmware/config.txt
and add a linedtoverlay=pps-gpio,gpiopin=4
. (Editingcmdline.txt
is no longer necessary) - Edit
/etc/modules-load.d/raspberrypi.conf
and add two lines withpps-gpio
andpps-ldisc
, to load the required kernel modules
if your /boot/config.txt
does not contain a note that content has moved to /boot/firmware/config.txt
:
- Edit
/boot/cmdline.txt
and addbcm2708.pps_gpio_pin=4
at the end of the line. (Might not be necessary) - Edit
/boot/config.txt
and add a linedtoverlay=pps-gpio,gpiopin=4
. (Depending on your distri, either 1. or 2. is necessary, but it doesn't seem to hurt to do both). - Edit
/etc/modules-load.d/raspberrypi.conf
and add two lines withpps-gpio
andpps-ldisc
, to load the required kernel modules
After a reboot, a new device /dev/pps0
should exist, and dmesg
should show something like:
[ 7.528565] pps_core: LinuxPPS API ver. 1 registered
[ 7.530144] pps_core: Software ver. 5.3.6 - Copyright 2005-2007 Rodolfo Giometti <[email protected]>
[ 7.540372] pps pps0: new PPS source [email protected]
[ 7.542012] pps pps0: Registered IRQ 166 as PPS source
[ 7.550775] pps_ldisc: PPS line discipl43ine registered
Now use ppstest
(you might need to install pps-utils
or pps-tools
depending on your distri):
sudo ppstest /dev/pps0
You should see an output like:
trying PPS source "/dev/pps0"
found PPS source "/dev/pps0"
ok, found 1 source(s), now start fetching data...
source 0 - assert 1606833766.999998552, sequence: 341090 - clear 0.000000000, sequence: 0
source 0 - assert 1606833767.999998573, sequence: 341091 - clear 0.000000000, sequence: 0
source 0 - assert 1606833768.999998751, sequence: 341092 - clear 0.000000000, sequence: 0
assert
and sequence
should both increment by 1 for each line.
Once you receive a PPS signal and GPSD is configured, continue.
Some linux distribution only allow root
to read or access /dev/pps0
. This is no issue, if only root services want to access /dev/pps0
. Note that recent versions of chronyd
drop privileges after start, and then might not be able to access /dev/pps0
anymore. See below for solutions (udev
or running chronyd
as root.)
Note: This problem seems to be fixed with current Raspberry Pi OS 'bookworm' releases (2023-12 status).
sudo ppstest /dev/pps0
yields:
found PPS source "/dev/pps0"
ok, found 1 source(s), now start fetching data...
source 0 - assert 1679340614.003464678, sequence: 84623 - clear 0.000000000, sequence: 0
time_pps_fetch() error -1 (Connection timed out)
See issue 6 for the ongoing discussion.
Note: Before you setup
chrony
, make sure you completedgpsd
configuration and thatgpsd
is running ok, and that you have a valid PPS signal at/dev/pps0
.
Note: Especially when using Raspberry Pi 5 (with new features PTP and RTC), it is recommended to use the standard Raspberry Pi OS, which comes with a chrony version, that is aware of the Raspberry Pi hardware.
Recent chronyd versions drop root privilege after start (check for the -u
option in chronyd.service
to see the user that will be used to run chronyd
). If /dev/pps0
is not accessible for that user, pps won't work. There are two solutions: either use a version of chronyd that doesn't drop privileges and runs as root, or setup a udev
rule that allows access to /dev/pps0
for the process-user of chronyd
.
If you compile, mirror chrony
yourself, there is an option --disable-privdrop
for configure
. See configure -h
for all options for building chrony
.
Udev rules are tricky and depend on the user of the chronyd
process and the type of connection your are using.
Create a file /etc/udev/rules.d/pps-sources.rules
starting with this example (which works with Raspberry Pi OS), which you will need to modify to your configuration:
KERNEL=="pps0", OWNER="root", GROUP="_chrony", MODE="0660"
KERNEL=="ttyS0", RUN+="/bin/setserial -v /dev/%k low_latency irq 4"
To activate those new udev
rules, either reboot, or use: sudo udevadm control --reload-rules && sudo udevadm trigger
.
This assumes _chrony
being the user/group of the chronyd
process (check with ps aux | grep chronyd
) and a serial connection (here /dev/ttyS0
) being used.1
Install chrony
, an alternative NTP server that in my experience results in higher precision time servers than good old ntpd.
Make sure that no other time server (ntdp
, systemd-timedated
) is active, e.g.
sudo systemctl disable systemd-timedated
sudo systemctl stop systemd-timedated
Depending on you distri and chrony versione, the config is either /etc/chrony/chrony.conf
or /etc/chrony.conf
, edit the file and add two lines:
refclock PPS /dev/pps0 lock GPS
refclock SHM 0 refid GPS precision 1e-1 offset 0.01 delay 0.2 noselect
Note: For recent
chrony
versions, an offset of0.0
seems to prevent GPS sync, hence set it tooffset 0.01
(or any small, non-zero value) for start. See below, how to get the actual correctoffset
value.
This uses a shared memory device SHM
to get unprecise time information from GPSD (low precision, marked as noselect
, so that chrony doesn't try to sync to serial time data). This unprecise time information is then synchronised with the much more precise PPS signal.
Verify that chrony.conf
contains the following line (standard in Raspberry Pi OS version):
# This directive enables kernel synchronisation (every 11 minutes) of the
# real-time clock. Note that it can't be used along with the 'rtcfile' directive.
rtcsync
If you want to transmit PTP hardware timestamps via ethernet, add:
hwtimestamp *
See below for some more details on PTP.
Now enable and start chrony
:
# Note: Some distributions use `chronyd` instead of `chrony`, so replace, if necessary:
sudo systemctl enable chrony
sudo systemctl start chrony
Verify that chrony
started ok with sudo systemctl status chronyd
. One possible error is a fatal message that chronyd has been compiled without PPS support
. If that's the case (e.g. Manjaro ARM 64bit), you either need to compile chrony yourself, mirror, or switch to another distri.
Then start the chrony console with chronyc
. At the chronyc>
prompt, enter:
sources
chronyc> sources
210 Number of sources = 6
MS Name/IP address Stratum Poll Reach LastRx Last sample
===============================================================================
#* PPS0 0 4 377 13 -138ns[ -108ns] +/- 120ns
#? GPS 0 4 377 13 -4624us[-4624us] +/- 2148us
^- sismox.com 3 10 377 15 -179us[ -179us] +/- 81ms
^- ntp.fra.de.as206479.net 2 10 377 749 +4235us[+4238us] +/- 16ms
^- mail.trexler.at 2 10 377 1114 +1190us[+1193us] +/- 13ms
^- ntp2.hetzner.de 2 10 377 59 -411us[ -411us] +/- 32ms
chronyc>
If all went well, you should see a PPS
device marked with #*
, indicating the active time source. Error should be in nanosecond range (-138ns[ -108ns] +/- 120ns
). #?
indicates an unusable time source, ^-
a usable but unused source.
The PPS signal is only used, if the divergence between the slow serial/USB time information and the precise PPS signal is less than 200ms. If your PPS device stays on #?
(unusable), and you are sure that gpsd is receiving valid GPS signals and ppstest
shows a correct PPS signal, then we need to tune the offset between serial time information and PPS.
In chronyc
, enter sourcestats
chronyc> sourcestats
210 Number of sources = 9
Name/IP Address NP NR Span Frequency Freq Skew Offset Std Dev
==============================================================================
...
GPS 40 22 626 -4.310 9.467 512ms 2474us
...
the important field is offset
of GPS
: if that is larger 200ms, PPS cannot sync to GPS.
Wait a few minutes for the offset to stabilize, note it's value, and edit /etc/chrony.conf
, and change the offset in the second line with the value above, converted to seconds (512ms are 0.512 sec in this case):
refclock PPS /dev/pps0 lock GPS
refclock SHM 0 refid GPS precision 1e-1 offset 0.512 delay 0.2 noselect
Note: A value of
offset 0.0
seems to prevent synchronisation for some versions of chrony, so use some small non-zero value instead, if a value close to 0 is required for your installation.
Restart chrony
with sudo systemctl restart chronyd
, and check chronyc
again:
After some time the output of sourcestats
should show a much better offset for GPS:
chronyc> sourcestats
210 Number of sources = 9
Name/IP Address NP NR Span Frequency Freq Skew Offset Std Dev
==============================================================================
PPS0 12 5 179 +0.000 0.003 +1ns 102ns
GPS 40 22 626 -4.310 9.467 -8600us 2474us
and sources
should now show PPS as active, marked with #*
.
Use chronyc
and command tracking
to get information about the precision of your new time server:
chronyc> tracking
Reference ID : 50505330 (PPS0)
Stratum : 1
Ref time (UTC) : Tue Dec 01 15:17:22 2020
System time : 0.000000032 seconds slow of NTP time
Last offset : -0.000000059 seconds
RMS offset : 0.000000103 seconds
Frequency : 2.167 ppm fast
Residual freq : -0.000 ppm
Skew : 0.005 ppm
Root delay : 0.000000001 seconds
Root dispersion : 0.000025110 seconds
Update interval : 16.0 seconds
Leap status : Normal
- For more information, check the chrony PPS documentation, mirror
By default, chrony allows only local access, use allow
in /etc/chrony.conf
to allow access in your local network, e.g.:
allow 192.168/16
To be able to administer the chrony time server over the net, add:
cmdallow 192.168/16
The tool sntp
(available by default on macOS, part of the ntp
package for most linux distributions)
allows for simple remote testing:
Note: when installing
ntp
for getting access to thesntp
tool, make sure that you do not accidentally activate thentp
server which will conflict with your chrony installation.
sntp <ip-or-hostname-of-raspberry-chrony-server>
yields (mac connected via WLAN):
sntp chronotron
# Output:
+0.011341 +/- 0.002986 chronotron 192.168.178.3
More information is available with sntp -d
sntp -d chronotron
# Output:
sntp_exchange {
result: 0 (Success)
header: 24 (li:0 vn:4 mode:4)
stratum: 01 (1)
poll: 00 (1)
precision: FFFFFFE8 (5.960464e-08)
delay: 0000.0001 (0.000015259)
dispersion: 0000.000B (0.000167847)
ref: 50505300 ("PPS ")
...
}
Interesting information is for example:
ref: xx.. ("PPS")
precision: (5.9e-08)
Note: The PTP precision time protocol (IEEE 1588) is of limited use for home networks, it requires that every component (server, network switch, client) have hardware support for IEEE 1588 which is not the case for most consumer hardware. You cannot use PTP if a single component does not support it.
An excellent overview over PTP, it's relation to NTP can be found at Redhat's combining ptp and ntp article. If you are not in a professional lab environment, you might not need PTP at all!
You can verify that hardware timestamping is active by executing sudo systemctl status chrony
right after start of chrony. You should see something like:
Dec 20 15:17:15 chronotron chronyd[9525]: chronyd version 4.3 starting (+CMDMON +NTP +REFCLOCK +RTC +PRIVDROP +SCFILTER +SIGND +ASYNCDNS +NTS +SECHASH +IPV6 -DEBUG)
Dec 20 15:17:15 chronotron chronyd[9525]: Enabled HW timestamping on eth0
To verify if a chrony client or server can support the PTP protocol, use:
ethtool -T eth0
You should see something like:
Time stamping parameters for eth0:
Capabilities:
hardware-transmit
software-transmit
hardware-receive
software-receive
software-system-clock
hardware-raw-clock
PTP Hardware Clock: 0
Hardware Transmit Timestamp Modes:
off
on
onestep-sync
Hardware Receive Filter Modes:
none
all
Required are the hardware-transmit
and hardware-receive
options.
Make sure the kernel module ptp
is loaded (sudo modprobe ptp
). In addition, you will need linuxptp
on both server and client. On the
raspberry, it can be installed with sudo apt install linuxptp
On the raspberry server:
sudo ptp4l -i eth0 ptp0 -m -2
The ptp server should start and output something like:
ptp4l[6491.320]: selected /dev/ptp0 as PTP clock
ptp4l[6491.364]: port 1: INITIALIZING to LISTENING on INIT_COMPLETE
ptp4l[6491.364]: port 0: INITIALIZING to LISTENING on INIT_COMPLETE
ptp4l[6498.761]: port 1: LISTENING to MASTER on ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES
ptp4l[6498.761]: selected local clock d83add.fffe.b1d67a as best master
ptp4l[6498.761]: port 1: assuming the grand master role
Now, on a client that has a network card with IEEE 1588 support (check with ethtool -T eth0
that hardware-transmit is supported) and is connected either directly or via a PTP-IEEE 1588 enabled switch (hint: most consumer network switch do not support the required protocol!):
again make sure module ptp
is loaded, install linuxptp
and:
sudo ptp4l -i enp86s0 -m -2 -s
the -s
option enables slave mode for the client.
If it works, you should see something like:
ptp4l[82232.615]: selected /dev/ptp0 as PTP clock
ptp4l[82232.689]: port 1 (enp86s0): INITIALIZING to LISTENING on INIT_COMPLETE
ptp4l[82232.689]: port 0 (/var/run/ptp4l): INITIALIZING to LISTENING on INIT_COMPLETE
ptp4l[82232.689]: port 0 (/var/run/ptp4lro): INITIALIZING to LISTENING on INIT_COMPLETE
ptp4l[82233.617]: port 1 (enp86s0): new foreign master d83add.fffe.b1d67a-1
ptp4l[82237.617]: selected best master clock d83add.fffe.b1d67a
ptp4l[82237.617]: port 1 (enp86s0): LISTENING to UNCALIBRATED on RS_SLAVE
ptp4l[82239.619]: master offset 59823 s0 freq -51831 path delay 4413
ptp4l[82240.618]: master offset 59749 s1 freq -2154985 path delay 4159
ptp4l[82241.618]: master offset 59733 s2 freq +91748 path delay 4159
If you get any received SYNC without timestamp
or similar with DELAY
, then somewhere in your transmission chain there is non-IEEE 1588 hw, and the sync doesn't work.
lock_all
to make sure chrony is always in memory.local stratum 1
to signal precision time.allow
without any address simply allows all networks. (Including external access, if your machine is connected to the internet!)
For more, check the official chrony documenation, mirror: chrony.conf.
Optionally, you can use this sub-project to a status display to the Raspberry Pi.
- 2024-04-30: Changes to Raspberry Pi 5 serial Port configuration, see 6, thanks @aGGreSSiv for figuring out the issue!
- 2024-03-06: Update: location of
/boot/config.txt
has changed to/boot/firmware/config.txt
. Thanks to @Nebulosa-Cat for the information! - 2023-12-21: Cleanup for the display software, display current stratum level (thanks @Lefuneste83, see #7), implemented logging.
- 2023-12-20: Raspberry 5 and suport for RTC and PTP precision time protocol how-tos.
- 2023-07-11: Documentation fixes (thx. @glenne), see #4 for details. Mirror links for
chrony
added. - 2023-02-24: Added information by @cvonderstein on initial
offset 0.01
andudev
rules, see #1 for details. - 2023-01-09: Code for 4x20 LCD display for NTP server PPS state added.