Getting accurate time GPS PPS over USB

2023 Sep 07 - Brian Kloppenborg


Since returning to astronomy as the Executive Director of the AAVSO in September 2022, I’ve been participating in more public outreach events and attending star parties in different states. Whenever I can, I take along my telescope so it can continue collecting data on my science projects while I’m taking with the local crowd.

Unfortunately, moving my telescope around presents some minor challenges. In order for the telescope to point at astronomical sources correctly, it needs to know its position within a few km and its time within a few seconds. That’s good enough for most of my science cases, but I also track satellites. In this situation, you must know your position within 10s of meters and the current time to within 1 millisecond (ms). That level of accuracy is pretty easy to achieve at home using Google maps and some network time software. Unfortunately, it is very difficult to achieve when you go places where cellular service is spotty. This is where position, navigation, and timing (PNT) services provided by global navigation satellite systems (GNSS), like the Global Positioning System (GPS), fit in.

What can you get from GNSS/GPS?

Most consumer-grade GNSS receivers provide PNT data through NEMA messages. These message encode GPS Fixed Data (GGA messages, which include time), Geographic position (GGL messages), information about active satellites (GSA messages), information about the satellites in view of the receiver (GSV messages), and other data. One technical item to note is that most GNSS providers to not report UTC. Instead, their concept of time is a consecutive number of seconds since the start date of their GNSS system (called the “epoch”). For example, the US GPS system counts consecutive seconds since January, 5, 1980. Unfortunately, these seconds are encoded in a 10-bit number which rolls over every few decades.

The data provided by GNSS is very accurate. Current GNSS receivers report their position to within 10 meters. Unfortunately, the time information provided by GGA messages can lag the start of a second by 10 - 100 milliseconds. To compensate for this delta, some GNSS receivers also provide a pulse per second (PPS) signal to mark the start of a second. On an oscilloscope, this signal looks like a square wave with abruptly rising/falling edges. Most receivers generate this signal within picoseconds to nanoseconds of the true start of a second, so they are exceptionally accurate.

How does one normally get the PPS signal?

While I would normally describe a topic like this in detail, I’m only going to present a summary this time around. If you want to dig into this further, I would recommend reading the GPSD HowTo which is much more comprehensive than what I present here.

The most common way to receive PPS signals from a consumer-grade GNSS device is by way of a RS-232 serial port, DB-25 parallel port, or GPIO pin. In this setup the PPS signal is routed to a pin that provides a hardware interrupt signal to the underlying operating system. The interrupt is timestamped by the operating system and passed off to specialized software that combines the PPS signal, GGA time, and leap second information to yield a time in UTC.

In the case of RS-232, the PPS signal is typically connected to the Data Carrier Detect (DCD) pin; however, it can also appear on the Ring Indicator (RI), Data Set Ready (DSR), and Clear to Send (CTS) pins. On Linux, the ppscheck application looks at all of these pins for the signal (see ppscheck.c). Oddly, the version 3.22 of gpsd on my computer only seems to monitor DCD.

It is also possible to receive the PPS signal on a Network Interface Card (NIC), like the Intel I210 card (see chrony’s documentation); however, none of the computer I’m targeting have a PCIe slot so I’ll ignore this option for now.

What other time standards are there and what accuracy can be achieved?

Beyond PNT data provided by GNSS, there are a variety of other ways to get accurate time on a computer. These include:

As you might imagine, the accuracy (and price) of these sources varies quite dramatically. PTP, which requires specialized networking hardware, can sync time to within 1.5 nanoseconds (ns) whereas NTP doesn’t do much better than 1 millisecond (ms). The chrony daemon on my laptop uses NTP servers to sync my clock to within 1.1 seconds of UTC.

Source Error (ms) Error (ns)
PTP LAN [1] 1.5E-6 1.5E+0
GPS Atomic Clock [3] 5.0E-5 5.0E+1
chrony local HW [2] 7.0E-5 7.0E+1
chrony local SW [2] 2.0E-3 2.0E+3
KPPS [3] 1.0E-3 1.0E+3
HPPS [3] 5.0E-3 5.0E+3
chrony PPS [2] 1.0E-2 1.0E+5
NTP LAN [3] 1.0E+0 1.0E+6
chrony WAN (me) 1.1E+0 1.1E+6
chrony WAN [2] 2.0E+0 2.0E+6
NTP WAN [3] 1.0E+1 1.0E+7

See 1, 2, and 3, for performance data from other sources.

RS-232 serial? Seriously? Surely USB can do better!

Unfortunately, there are very few computers that have the interfaces required to receive the PPS signal. Outside of business desktop computers, I haven’t seen a RS-232 serial port or DB-25 parallel port on a computer for about a decade. Sometimes you can find an unpopulated header on a motherboard of a laptop, but none of my computers have one. Single board computers, like the Raspberry Pi, often have GPIO ports, but these are ARM devices and the orbital propagation software I use is x86_64 only. So I had to look for other alternatives.

The most obvious solution is to use a USB to RS-232 serial converter; however, there are some technical aspects of USB which must be taken into consideration. As discussed above, the arrival of a PPS signal at a real RS-232 serial port will result in a hardware interrupt that is timestamped against the current system clock. This typically reduces the accuracy of the PPS signal from 10-30 nanoseconds to 1,000 nanoseconds.

Unlike RS-232 which has hardware interrupts, USB is a polled bus. As shown in the table below, this means that a PPS signal can only be received once every 0.125 - 1 millisecond. This will introduce jitter in the PPS signal which needs to be compensated for in whatever receiving software is used.

In my situation, time with an accuracy of 1 millisecond is fine, so a jitter of 0.125 milliseconds is acceptable.

USB Version Linux Driver Name Bandwidth Frame Time (ms)
USB 1.0 UHCI Low Speed 1.5 Mbps 1 ms
USB 1.1 OHCI Full Speed 12 Mbps 1 ms
USB 2.0 EHCI High Speed 480 Mbps 0.125 ms
USB 3.0 XHCI SS / Gen 1 5 Gbps 0.125 ms
USB 3.1 XHCI SS+ / Gen 2 10 Gbps 0.125 ms
USB 3.2 XHCI USB 3.2 / Gen 2x2 20 Gbps 0.125 ms
USB 4.0 TBD USB4 / Gen 3x2 40 Gbps ?

Table sources:

Hardware Setup

For my initial experiment, I purchased a FTDI Chip C232HD-DDHSP-0 USB to UART Serial Cable Adapter for $41.70 and a Goouuu Tech GT-U7 GPS / GNSS receiver with PPS output for $14.99. I wired it up as specified in the table below and placed it on my patio with a clear view of the sky.

The setup is wired up as follows:

UART Pin Color GT-7 Pin
TXD Orange RX
RXD Yellow TX
DCD# White PPS
GPS Receiver and USB to UART adapter wired up

Software Setup

Setting up the software was fairly straightforward. After connecting the USB adapter to my computer I first verified that gpsmon could communicate with the receiver:

sudo gpsmon /dev/ttyUSB1 
gpsmon output showing that both GPS and PPS signals are received.

Next I edited the gpsd configuration file to use that device and started the service:

brian@epislon:~$ cat /etc/default/gpsd 
# Devices gpsd should collect to at boot time.
# They need to be read/writeable, either by user gpsd or the group dialout.

# Other options you want to pass to gpsd

# Automatically hot add/remove USB GPS devices via gpsdctl

and then restarted the service to load the new configuration file

sudo systemctl restart gpsd

Lastly I edited the chrony configuration file to read the output from gpsd using shared memory (SHM). I initially configured the offset for the GPS time signal to be zero to allow chrony track how far off it is from other time sources

brian@epislon:~$ tail /etc/chrony/chrony.conf 
# one second, but only in the first three clock updates.
makestep 1 3

# Get TAI-UTC offset and leap seconds from the system tz database.
# This directive must be commented out when using time sources serving
# leap-smeared time.
leapsectz right/UTC

refclock SHM 0 refid GPS precision 1e-1 offset 0.0
refclock SHM 1 refid PPS precision 1e-7

Then I restarted chrony to load the new configuration file:

sudo systemctl restart chrony

and verified that both GPS and PPS were picked up. After a few minutes, both time sources were identified and reporting data:

brian@epislon:~$ chronyc sources
MS Name/IP address         Stratum Poll Reach LastRx Last sample               
#x GPS                           0   4   377    23   +116ms[ +116ms] +/-  100ms
#* PPS                           0   4    37    23    -31us[  -31us] +/- 4273ns
^- prod-ntp-4.ntp4.ps5.cano>     2  10   377   226  -2956us[-2960us] +/-   65ms
^- prod-ntp-3.ntp1.ps5.cano>     2  10   377   620  -2610us[-2621us] +/-   67ms
^- prod-ntp-5.ntp4.ps5.cano>     2  10   377   776  -5930us[-5941us] +/-   70ms
^- prod-ntp-5.ntp4.ps5.cano>     2  10   377   812  -2791us[-2802us] +/-   67ms
^-               2  10   377   747  -6047us[-6057us] +/-  186ms
^-            2  10   377   841  -5754us[-5765us] +/-   37ms
^- 2607:f298:5:101d:f816:3e>     2  10   377   488   -936us[ -944us] +/-   37ms
^- 2604:a880:400:d0::83:2002     2  10   377    29  -4112us[-4113us] +/-   33ms

I then let chrony run in this configuration for several hours to derive experimental offsets. As can be seen in the output of chronyc sourcestats below, the GPS source is off by about 112 +/- 2 milliseconds. This is larger than I would have anticipated, but it appears stable enough that it can be calibrated out. The PPS signal has an offset of -0.001 +/- 10 microseconds. This is much smaller than I would have anticipated given the USB 2.0 polling interval is 124 microseconds.

brian@epislon:~$ chronyc sourcestats
Name/IP Address            NP  NR  Span  Frequency  Freq Skew  Offset  Std Dev
GPS                         7   5    94    -25.843    180.399   +112ms  2204us
PPS                        64  43  109m     -0.000      0.002     -1ns    10us
prod-ntp-4.ntp1.ps5.cano>  54  26  475m     +0.030      0.012  -1922us   247us
prod-ntp-3.ntp1.ps5.cano>  55  26  486m     +0.004      0.013  -2527us   258us
prod-ntp-5.ntp4.ps5.cano>  55  29  483m     -0.002      0.063  -4508us  1234us
prod-ntp-5.ntp1.ps5.cano>  23  12  378m     -0.006      0.022  -2024us   184us            28  10  440m     +0.060      0.022  -3816us   241us         15   8  240m     -0.088      0.324  -5822us  1228us
2607:f298:5:101d:f816:3e>  52  23  471m     +0.018      0.014   +750us   267us
2604:a880:400:d0::83:2002  47  27  384m     -0.001      0.043  -3341us   568us

Lastly, I applied the 112 millsecond offset to the GPS timesource to the chrony configuration file and restarted the service.

brian@epislon:~$ tail /etc/chrony/chrony.conf 
# one second, but only in the first three clock updates.
makestep 1 3

# Get TAI-UTC offset and leap seconds from the system tz database.
# This directive must be commented out when using time sources serving
# leap-smeared time.
leapsectz right/UTC

refclock SHM 0 refid GPS precision 1e-1 offset 0.112
refclock SHM 1 refid PPS precision 1e-7