This project is to house experiments in timestamping PPS signals on a computer running macOS.
The context is that you have
- a GPS receiver with a PPS output
- a Mac such as a MacBook or a Mac mini with no inputs other than USB and possible a microphone
- you want to run a NTP server i.e. chrony on the Mac
- you therefore want to determine very precisely the time of each pulse with respect to the Mac's system clock
For more background, see Jeff Geerling's blog post. Jeff suggests a couple of possibilities
- don't use PPS; just rely on timing derived from NMEA messages (Jeff claims this can achieve 1ms precision, but I think that is an illusion)
- use Linux in a VM (e.g. using Docker) with USB pass through; this allows you to leverage Linux kernel support for PPS (I haven't tried this but it's a plausible approach)
This repo is an attempt to provide some additional possibilities. This is proof-of-concept code, not production quality. I mostly vibe-coded it (with Claude Code).
The first experiment, which is implemented by the pollpps program, explores the idea of polling the modem status lines, specifically the CTS line. This requires a USB-to-TTL adapter that supports CTS/RTS, and then connect the PPS output of the GPS receiver to the CTS pin of the adapter. A suitable adapter is the Waveshare USB-to-TTL converter, which uses the FTDI FT232RNL.
In fact, gpsd implements a similar approach but requires an OS that supports TIOCMIWAIT, which avoids the need to poll. Unfortunately macOS doesn't support this, so gpsd does not support PPS on macOS.
The obvious downside of polling is the CPU usage from having to poll extremely frequently. But modern CPUs have sufficient capacity to make this approach is viable. There are also some tricks (not yet implemented) that we could use to reduce CPU usage. For example, once we have detected a pulse edge, we know that the next edge will not happen for a second, so we can stop polling frequently for nearly a second.
The level of precision that can be achieved with this is limited. The timestamping is being done completely in user space and USB introduces significant extra jitter compared to a direct serial port.
This experiment has chrony refclock sock support integrated. Run with
and add something like this to your chrony config file
The second experiment is more interesting. macOS has no support for precision time keeping, but it has excellent support for audio, including audio synchronization. The idea is to piggy back PPS support on top of the audio support.
The starting point is to build a simple, passive circuit to turn the PPS input pulse into something that can be fed into the LINE input of a USB audio card. Then we use the macOS CoreAudio framework to read the audio samples and detect the pulse. CoreAudio has kernel support for timestamping audio samples. Each sample packet is associated with a host time (in Linux terms, a raw monotonic time). We can map this onto a system time. USB audio uses isochronous USB which avoids much of the jitter that occurs with regular USB. This has the potential for much greater precision that the first approach.
The circuit looks like this
The parts you need for this are:
- 10kΩ resistor
- 1kΩ resistor
- 0.1µF film capacitor
- a TRRS breakout board
I used a small breadboard to assemble it. The only soldering needed is to solder header pins onto the TRRS breakout board.
You also need a USB audio card with a LINE IN. The one I found has multiple inputs and outputs including SPDIF. It cost about $9. This circuit is designed for a LINE IN, and should not be plugged into a MIC IN, which expects a much lower voltage.
This needs a command like:
First argument is the device; second argument is the input source. Use ./audiopps --list-devices to get the available devices and their sources.
Here's an example of what I see (on a Mac mini with the system clock synchronized using chrony to a high quality stratum 1 NTP server on the LAN):
This is an order of magnitude better than the modem status line polling.
To use with chrony, add a --chrony option, run with sudo and put something like this in your chrony config file
The important option here is the offset. It turns that the time has a constant error of about 0.35ms. I determined this by calibrating against a high quality NTP server on my LAN. Once this constant error is corrected, the performance is impressive.
The tracking RMS offset settles down to about 1.6µs.
CPU usage is about 1.8% (of one core) on a Mac mini M4.
A future possibility would be to plug into the headset jack of a Mac. This uses a TRRS plug, with Sleeve being the MIC in, and Ring 2 (next to sleeve) being GND. The expected voltage is much smaller, so the resistor values would need to change.