Add chrony support for GPS clock sources

A couple of folks recommended I post this as a feature request. This is kind of half description, half how-to … let me know if any additional information is needed.

I think a number of assumptions could be made with a simple set command that specifies the GPS and PPS interfaces, as well as some refclock specific parameters for chrony, then stitches the data together in the background.

A lot of this was adapted from domschl/RaspberryNtpServer, which I’d previously used to set up a Raspberry Pi-based NTP server with an Adafruit GPS breakout board.

NOTE: I had a bunch of helpful references linked in this post, but there’s a limit of two links per post for new users - kind of silly.

Requirements

  • GPS source.
  • GPS source device drivers.
  • PPS source.
  • PPS source device drivers.

It’s more or less all via serial, it just depends on how your serial port is delivered:

  • RS232 serial port, ex. DE-9, IDC 2x5 motherboard header, etc.
  • Parallel port, ex. DB-25.
  • USB to serial.
  • PCIe to serial.

I have a Timebeat TimeCard Mini that delivers GPS via /dev/ttyS2 over an Exar XR17V3521 Dual PCIe UART and PPS via u.fl, which I connected to the motherboard’s IDC 2x5 serial header (dev/ttyS0) on pins 1 (DCD) and 5 (GND).

GPS

The kernel module that supports my serial device - CONFIG_SERIAL_8250_EXAR - was enabled by default.

PPS

Since my PPS source is a serial port, I needed to enable CONFIG_PPS_CLIENT_LDISC. This is set to m in the kernel config, so I used modprobe pps_ldisk to enable it on my test bench. I plan to set CONFIG_PPS_CLIENT_LDISC to y via a custom kernel when I build the installer ISO.

Some other possible PPS drivers:

  • CONFIG_PPS_CLIENT_KTIMER - Kernel timer, used for debug purposes.
  • CONFIG_PPS_CLIENT_GPIO - PPS via GPIO.
  • CONFIG_PPS_CLIENT_PARPORT - PPS via parallel port.

Software

Some software needs to be installed:

  • gpsd - required to monitor and process GPS data and provide it to chrony.
  • setserial - needed for udev rules later.
  • gpsd-tools - for cgps; not required, but used to test/debug.
  • pps-tools - for ppstest; not required, but used to test/debug.

I plan to install these via the ISO build process for now.

gpsd

Add/update the lines below to /etc/default/gpsd, depending on your GPS source:

Serial:

GPSD_OPTIONS="-n -G"
DEVICES="/dev/GPS_INTERFACE"
USBAUTO="false"

USB:

GPSD_OPTIONS="-n -G"
DEVICES="/dev/GPS_INTERFACE"
USBAUTO="true"

Replace GPS_INTERFACE with the name of the GPS source interface, ex. ttyS2.

Enable/start the service, etc.

I plan to use a post-boot script to ensure this is configured and not overwritten.

PPS

How you set up your PPS source depends on the platform, source (serial, GPIO, etc.). In my case, I connected my PPS source direct to a serial port, /dev/ttyS0, and needed to create the PPS interface.

ldattach 18 /dev/ttyS0

18 is the line discipline for PPS.

This created /dev/pps0, which is used as the PPS source later. This device is owned by root, so udev rules are needed to allow chrony to access the device.

Create /etc/udev/rules.d/pps-sources.rules and add these rules:

KERNEL=="PPS_INTERFACE", OWNER="root", GROUP="_chrony", MODE="0660"
KERNEL=="GPS_INTERFACE", RUN+="/bin/setserial -v /dev/%k low_latency irq 4"

Replace PPS_INTERFACE with the name of the PPS interface, ex. pps0, and GPS_INTERFACE with the name of the GPS source interface, ex. ttyS2.

Run sudo udevadm control --reload-rules && sudo udevadm trigger to reload and trigger the new rules.

I plan to use a post-boot script to ensure the above is configured and not overwritten.

chrony

chrony needs two lines added to its configuration:

refclock PPS /dev/PPS_INTERFACE lock GPS
refclock SHM 0 refid GPS precision 1e-1 offset 0.01 delay 0.2 noselect

Replace PPS_INTERFACE with the name of the PPS interface, ex. pps0. The value of offset may need to be adjusted based on the initial offset of the GPS source - it should be < 200ms. If it isn’t, the offset needs to be adjusted to match. For example, if the GPS offset is +500ms, set offset to 0.5.

Restart the service.

Since /run/chrony/chrony.conf is auto-generated by service_ntp.py, I’ll probably set up a post-commit hook to re-add the refclock lines if I ever need to mess with the NTP configuration after the initial setup.

Status

There are a couple of show commands mapped to chrony/chronyc commands already that are useful:

  • sh ntp = chronyc > sourcestats
  • sh ntp system = chronyc > tracking

The sources command is also useful, but is not built-in. It’s close to sourcestats, but provides a small amount of additional detail.

vbash-4.1# chronyc sources
MS Name/IP address         Stratum Poll Reach LastRx Last sample               
===============================================================================
#* PPS0                          0   4   377    17    -13us[  -15us] +/-   32us
#? GPS                           0   4   377    17    +16ms[  +16ms] +/-  200ms
^- ec2-34-206-168-146.compu>     2   8   377   171   +390us[ +399us] +/-   57ms
^- ec2-18-193-41-138.eu-cen>     2   8   377   145   +404us[ +416us] +/-   57ms
^- ec2-122-248-201-177.ap-s>     2   7   377    72   +391us[ +403us] +/-   58ms

Here’s what it looks like from vyos’ point of view:

vyos@vyos:~$ sh ntp
                             .- Number of sample points in measurement set.
                            /    .- Number of residual runs with same sign.
                           |    /    .- Length of measurement set (time).
                           |   |    /      .- Est. clock freq error (ppm).
                           |   |   |      /           .- Est. error in freq.
                           |   |   |     |           /         .- Est. offset.
                           |   |   |     |          |          |   On the -.
                           |   |   |     |          |          |   samples. \
                           |   |   |     |          |          |             |
Name/IP Address            NP  NR  Span  Frequency  Freq Skew  Offset  Std Dev
==============================================================================
PPS0                       58  27   913     -0.000      0.037     -0ns    24us
GPS                        27  13   416     +5.900      3.165    +16ms   550us
ec2-34-206-168-146.compu>  13   9   36m     -0.040      0.022   +391us    10us
ec2-18-193-41-138.eu-cen>   6   3   21m     -0.045      0.151   +370us    22us
ec2-122-248-201-177.ap-s>  11   5   21m     -0.043      0.020   +391us  6050ns
vyos@vyos:~$ sh ntp system
Reference ID    : 50505330 (PPS0)
Stratum         : 1
Ref time (UTC)  : Wed Feb 07 02:15:23 2024
System time     : 0.000000120 seconds fast of NTP time
Last offset     : -0.000002066 seconds
RMS offset      : 0.000003705 seconds
Frequency       : 19.267 ppm fast
Residual freq   : -0.000 ppm
Skew            : 0.039 ppm
Root delay      : 0.000000001 seconds
Root dispersion : 0.000051959 seconds
Update interval : 16.0 seconds
Leap status     : Normal

Debugging

  • cat your GPS serial interface; you should see fast-scrolling lines comma-delimited text that start with $GPGGA.
  • Use cgps to see the GNSS data; look for Status: 3D fix (xx secs) - this means you’ve connected to GPS satellites.
  • Run ppstest /dev/pps0 to check for a valid PPS signal; you should see lines that look like source 0 - assert <timestamp>, sequence: <seq_number> - clear <timestamp>, sequence: <seq_num>

Examples:

# cat /dev/ttyS2
$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
# 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 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

For best timing accuracy, such a stratum 1 NTP server is best done on a dedicated physical machine not loaded with other tasks like routing. I have such a setup with Alpine Linux installed on PC Engines APU2 with PPS connected from GPS module to onboard LAN i210AT SDP0 pin for very accurate hardware timestamping. See APU2 NTP server for some instructions. I’ve also seen some PCIe gigabit ethernet cards with i210AT NIC and its SDP pins easily available on a pin header, that avoids the need for hardware hacking (soldering a thin wire to a small test point on the board).