Frequency synchronization without phase in NTP

I've been working on my high accuracy RTC project.

Here's a picture of one of the two systems:

Test setup


The TCXO devboard, BME280, and PCF RTC are all on the i2c bus. The TMP36 sensor is hidden behind a wire in this picture and is connected to an analog input pin on the TCXO devboard.

The GPS's PPS (pulse per second) is connected to both the Raspberry Pi 2 ("Pi2") and an input capture pin on the TCXO devboard. The PCF RTC is set to output a PPS and is also connected to a dedicated input capture pin on the TCXO devboard. Lastly, the Pi2 is configured to output a 50Hz PWM signal in hardware which is on a third input capture pin on the TCXO devboard.

The 50Hz PWM is divided down in software to 1Hz (PPS) and the timestamps for all three channels are exported from the TCXO devboard via I2C.

Input capture data

The data from the input capture is below. By random chance, everything is close together in frequency. The "GPS" channel is measuring the TCXO devboard's frequency. The other two channels are measured relative to the GPS channel (so the TCXO devboard's frequency error isn't added to the other channels). 64 seconds worth of samples (at 1 sample per second) are averaged together. Samples outside of +/-500ppm are thrown out (this happens when the GPS module loses lock and stops its PPS).

All Channels

There is additional data on the PCF2129 RTC on another blog post.

NTP's control of the local clock

I'm going to take a moment to explain how ntpd/chronyd/etc keep the local clock in sync.

Like the ubiquitous PID controller, the various NTP servers measure the difference between where the local clock is now and where it should be (proportional term/phase lock loop) as well as the frequency of the local clock and what that should be (derivative term/frequency lock loop). I have not seen any NTP system that uses an integral term, because using the proportional+derivative terms is enough (steady state errors tend to be low relative to errors from jitter and wander).

ntpd and chrony have different ways of measuring and calculating the proportional and derivative terms as well as applying them, which I won't go into here.

PWM as a derivative term measurement

In order to measure the Pi2's clock frequency with the highest precision, my goal was to use the input capture timer hardware in the stm32 chip on the TCXO devboard. But that requires the Pi2 export its local clock in a way that isn't affected by CPU load. PWM hardware is perfect for this, as it runs off a timer in the Pi2 hardware. It's using the same local clock as the Pi2's system clock, and doesn't require the CPU to do anything after PWM is setup on boot. Because of the other system (which I'll cover in a different blog post) having a minimum PWM frequency of 50Hz, I've set the Pi2 to output 50Hz as well. C program to setup pi PWM

Output the derivative term into NTP

Once I've measured the Pi2's clock frequency, I can feed it back into NTP's local clock control. I'm using chrony's tempcomp feature for this.

# arguments: path interval T0 k0 k1 k2
# ppm = k0 + (T - T0) * k1 + (T - T0)^2 * k2
# ppm = 0 + (T - 0) * 1 + (T - 0)^2 * 0 
#     = T
tempcomp /run/tcxo 1 0 0 1 0

This configuration reads the file /run/tcxo every second (which becomes the variable "T" in the above equation) and adjusts the local clock frequency based on its changes. So if one second the file has the value "1" and the next second the file has the value "2", the local clock is adjusted +1ppm. In this way, the local clock frequency can have the same frequency (plus an error) as an external clock.


Tempcomp vs tracking

The purple line is the tempcomp value fed into chrony and the green line is the value chrony added on top. The ideal green line is completely flat. You can see that chrony did make some adjustments but they were minor compared to the tempcomp adjustments. Both the TCXO devboard and chrony were using the GPS PPS as their time reference.

Some stats:

tracking.log (chrony's local frequency control)
mean:    -0.0006 ppm
std dev:  0.0030
min:     -0.029 ppm
max:      0.042 ppm

mean:     0.3025 ppm
std dev:  0.2421
min:     -0.403 ppm
max:      1.274 ppm

Chrony's calculated clock offsets looked like this:

Local clock offset

Compared to a system without this sync:


The 99% and 1% values are roughly half. The 25% and 75% values sightly higher.


The code for this project is in github

Further work

Temperature measurements & experiments