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