Chrony Service Configuration¶
This document covers the software configuration for the Stratum 1 NTP server detailed in Raspberry Pi Zero W 1. It utilizes chrony and gpsd to process the hardware signals, applies thermal drift compensation, and distributes time across the homelab.
Thermal Compensation Setup¶
The Pi Zero W's quartz oscillator drifts based on CPU temperature. To maintain microsecond accuracy during a GPS lock loss, chrony needs a real-time feed of the SoC temperature.
Create the temperature wrapper script:
/usr/local/bin/get_cpu_temp.sh
Tempcomp Coefficient Calculator¶
To generate the quadratic curve coefficients (k0, k1, k2) required by chrony, use this Python script. It parses a drift log and outputs the exact tempcomp directive, while generating a visual graph using the Catppuccin Mocha palette. Save as calc_tempcomp.py:
calc_tempcomp.py
#!/usr/bin/env python3
import csv
import sys
import numpy as np
try:
import matplotlib.pyplot as plt
HAS_MATPLOTLIB = True
except ImportError:
HAS_MATPLOTLIB = False
def main():
if len(sys.argv) < 2:
print("Usage: calc_tempcomp.py <log_file>")
sys.exit(1)
log_file = sys.argv[1]
temps = []
skews = []
with open(log_file, 'r') as f:
reader = csv.reader(f)
for row in reader:
try:
temps.append(float(row[1]))
skews.append(float(row[2]))
except (ValueError, IndexError):
continue
if not temps:
print("Error: No valid data found.")
sys.exit(1)
t_array = np.array(temps)
skew_array = np.array(skews)
t0 = np.round(np.mean(t_array), 2)
delta_t = t_array - t0
coeffs = np.polyfit(delta_t, skew_array, 2)
k2, k1, k0 = coeffs
print("--- Chrony Tempcomp Calculator ---")
print(f"Data points analyzed: {len(t_array)}")
print(f"Reference Temp (T0): {t0}ยฐC")
print(f"k0 (Constant): {k0:.6f}")
print(f"k1 (Linear): {k1:.6f}")
print(f"k2 (Squared): {k2:.6f}\n")
print("Add this line to your chrony.conf:")
print(f"tempcomp /usr/local/bin/get_cpu_temp.sh 1 {t0} {k0:.6f} {k1:.6f} {k2:.6f}")
if HAS_MATPLOTLIB:
# Catppuccin Mocha styling
crust = "#11111b"
text = "#cdd6f4"
blue = "#89b4fa"
flamingo = "#f2cdcd"
plt.figure(facecolor=crust)
ax = plt.axes()
ax.set_facecolor(crust)
for spine in ['bottom', 'top', 'right', 'left']:
ax.spines[spine].set_color(text)
ax.tick_params(axis='x', colors=text)
ax.tick_params(axis='y', colors=text)
ax.yaxis.label.set_color(text)
ax.xaxis.label.set_color(text)
ax.title.set_color(text)
plt.scatter(t_array, skew_array, color=blue, alpha=0.5, label="Raw Skew Data")
t_line = np.linspace(min(t_array), max(t_array), 100)
skew_line = k2 * ((t_line - t0)**2) + k1 * (t_line - t0) + k0
plt.plot(t_line, skew_line, color=flamingo, linewidth=2, label="Quadratic Fit")
plt.xlabel("Temperature (ยฐC)")
plt.ylabel("Frequency Skew (ppm)")
plt.title("Pi Zero W Quartz Thermal Drift")
plt.legend(facecolor=crust, edgecolor=text, labelcolor=text)
plt.show()
if __name__ == "__main__":
main()
chrony.conf¶
This configuration prioritizes the PPS hardware interrupt, uses SHM to interface with gpsd for NMEA data, and enables leap smearing to protect infrastructure databases from sudden time jumps.
/etc/chrony/chrony.conf
# 1. Primary Time Sources
# The physical PPS interrupt (Stratum 0) - Highly precise
refclock PPS /dev/pps0 refid PPS lock NMEA
# The NMEA serial data stream (via gpsd SHM)
refclock SHM 0 offset 0.135 delay 0.2 refid NMEA
# 2. Network Fallbacks (Stratum 2)
pool 2.debian.pool.ntp.org iburst
# 3. Time Adjustments & Leap Seconds
# Smear leap seconds over 24 hours to prevent sudden jumps
leapsecmode slew
maxslewrate 1000
makestep 1 3
# 4. Thermal Compensation
# (Replace coefficients after running calc_tempcomp.py)
tempcomp /usr/local/bin/get_cpu_temp.sh 1 45.0 0.0 0.25 0.005
# 5. Access Control
allow 10.0.0.0/8
allow 192.168.0.0/16
# 6. Logging & Directories
driftfile /var/lib/chrony/chrony.drift
logdir /var/log/chrony
log measurements statistics tracking
Verification & Monitoring¶
Restart the daemon to apply the configuration:
Ensure the * symbol appears next to the PPS reference, indicating it is the actively selected master source.
The System time value dictates the current microsecond error margin.
Time Error Testing & Comparison¶
To evaluate synchronization accuracy and quantify error margins, measure time offsets on both the NTP server and client machines.
1. Source Error & Jitter Analysis (Server)¶
Run chronyc sourcestats on pi00 to inspect error margins and jitter across all registered sources:
Key metrics to evaluate:
- Offset: Estimated time difference between the local clock and the source. For PPS, this typically stays within ยฑ1 ยตs.
- Std Dev: Standard deviation of the offset estimates (jitter). PPS jitter is typically low microseconds or sub-microsecond.
2. Client Offset Testing¶
From any client machine on the network, query the local NTP server in query-only mode to measure the time offset without altering the client's clock:
Alternatively, test using sntp:
3. Local Stratum 1 vs Public Pool Comparison¶
Comparing the local GPS/PPS node against public upstream servers highlights the error reduction achieved on a local Stratum 1 server:
| Metric | Local GPS/PPS Node (pi00) |
Public Upstream Pool (pool.ntp.org) |
|---|---|---|
| Stratum | Stratum 1 (Direct hardware reference) | Stratum 2 / 3 (Hops over WAN) |
| Network Round-Trip | 0.1 โ 0.5 ms (LAN) | 15 โ 60 ms (Internet routing) |
| Network Jitter | < 50 ยตs | 1 โ 10 ms (WAN bufferbloat / route variance) |
| Server Offset Error | < 1 ยตs (SoC locked to PPS interrupt) | 1 โ 5 ms (Subject to asymmetric routing) |
| Client Synchronization | Sub-millisecond (< 100 ยตs) | 1 โ 10 ms |
| Availability | Autonomous (Continues during ISP outages) | Requires WAN access |
UniFi Network Configuration¶
To distribute time from the Raspberry Pi Zero W (192.168.2.190) across the homelab, configure UniFi Network to serve both UniFi infrastructure devices and DHCP clients.
1. Device NTP (UniFi Infrastructure)¶
Configure the UniFi Gateway, switches, and access points to synchronize with the local NTP server:
- Open the UniFi Network dashboard.
- Navigate to Settings > System > Advanced.
- In the NTP section, set the server mode to Custom.
- Add the Pi Zero W IP address:
192.168.2.190. - (Optional) Add a fallback public pool (such as
time.cloudflare.comorpool.ntp.org). - Click Apply Changes.
2. DHCP NTP Distribution (Clients & VLANs)¶
Advertise the NTP server via DHCP (Option 42) so client devices on local networks automatically use the Stratum 1 node:
- Navigate to Settings > Networks.
- Select a target network or VLAN (e.g., Default, IoT).
- Expand Advanced (or scroll to DHCP Service).
- Under NTP, switch from Auto to Manual.
- Set the primary NTP server to
192.168.2.190. - Click Apply Changes. Repeat for any additional VLANs.
3. Verify Client Activity¶
Verify on the Pi Zero W that UniFi devices and network clients are actively querying the server:
Troubleshooting¶
Use chronyc and system diagnostics to identify whether the hardware PPS signal or the GPS serial stream is failing.
1. Diagnosing PPS Signal & Hardware Interrupts¶
Run chronyc sources -v to inspect the status of the PPS refclock:
- State Symbol (
S):*(Best / Current): Normal. PPS is the active master synchronization source.?(Unreachable): Chrony is not receiving pulse events from the PPS device.x(Falseticker): Chrony rejected PPS because its timestamps disagree with the secondary sources (often caused by an invalid NMEA offset).
- Reach Value (
Reach):377: Fully healthy (octal representation of11111111, indicating 8 successful consecutive pulses).0(or failing to reach377): Chrony is missing pulse interrupts.
If the PPS source is marked unreachable (?) or has Reach 0:
- Verify Kernel Registration:
Ensure a line like
pps pps0: new PPS source pps-gpio.-1appears. If it registered underpps1, update/etc/chrony/chrony.confto use/dev/pps1. - Test Raw Hardware Pulses:
If
ppstesttimes out (connection timed out), verify the physical connection to GPIO 4 (Pin 7), confirm the HAT is firmly seated, and ensure the GPS antenna has an unobstructed view of the sky (the receiver only emits PPS once satellite lock is acquired).
2. Diagnosing GPS / NMEA Data Stream¶
Chrony relies on the NMEA data stream (via gpsd SHM) to identify the correct second for the PPS pulse.
Run chronyc sources -v and examine the NMEA entry:
- If
NMEAshows?orReach 0, Chrony cannot read the SHM segment fromgpsd. - If
NMEAshowsx, its offset difference from the network pool or PPS is too large. Adjust theoffsetparameter in/etc/chrony/chrony.conf.
Run the following checks to isolate GPS connection issues:
- Verify Serial Data Stream:
Raw NMEA sentences (e.g.,
$GNRMC,$GNGGA) should continuously scroll across the terminal. If blank:- Ensure the serial console was disabled in
/boot/cmdline.txt. - Verify
enable_uart=1anddtoverlay=disable-btare present in/boot/config.txt.
- Ensure the serial console was disabled in
- Verify
gpsdDaemon Status: - Inspect Satellite Fix and Signal Quality:
Confirm that
Statusindicates 3D FIX and at least 4 satellites are actively used. If in NO FIX or 2D FIX status, relocate the antenna to a clear window or exterior mounting point.