This is part 2 of the homelab PTP grandmaster series. Part 1 used a Raspberry Pi CM4 with SatPulse. This build uses a NanoPi R5S-LTS and takes a different path to the same destination.

The R5S has no standard GPIO header. It has an FPC connector. Getting a GPS module talking to it required a breakout board, a ribbon cable, a soldering iron, and more debugging than expected. The results made it worth it.

Hardware

ComponentCost
NanoPi R5S-LTS with case$115.00
HamGeek NEO-M8N GPS module$20.67
MECCANIXITY FPC-12P 0.5mm breakout (5-pack)$8.39
uxcell 12-pin 0.5mm FPC ribbon cable (5-pack)$6.89
Total$150.95

OS: rk3568-sd-ubuntu-noble-core-6.1-arm64-20260319.img.gz.

The R5S has three Ethernet ports. eth0 is the onboard RK3568 GMAC with hardware PTP timestamping. eth1 and eth2 are RTL8125BG 2.5G NICs via PCIe with no reliable hardware timestamping. eth0 is the only interface that matters for this build.

The NEO-M8N comes with pins pre-soldered from the factory. The FPC breakout board needed a 2x6 2.54mm male header soldered on. That is the only soldering required.

NanoPi R5S-LTS board with FPC breakout and NEO-M8N GPS module connected via ribbon cable

Assembled NanoPi R5S-LTS in case with GPS module and FPC breakout board connected externally

The GPS module and FPC breakout board sit outside the enclosure. The ribbon cable exits through the side of the case near the FPC connector.

The FPC connector

The R5S exposes GPIO via a 12-pin 0.5mm pitch FPC connector on the board edge. A breakout board is required to work with standard Dupont wires. The relevant pins:

FPC PinSignal
13.3V
3UART5_RX
5UART5_TX
9UART9
10UART9
12GPIO3_C5

The wiki documents UART5 on pins 3 and 5. UART5 does not work on kernel 6.1. After patching the DTB and verifying the pinmux, GPIO3-18 and GPIO3-19 show the correct configuration but remain unclaimed by the UART driver:

gpio-114 (PIN_05)
gpio-115 (PIN_03)

UART9 on pins 9 and 10 works out of the box with no DTB changes. This build uses /dev/ttyS9.

PPS on pin 12 (GPIO3_C5) requires a DTB patch to enable the pps-gpio driver.

DTB patching for PPS

The DTB lives in the Rockchip resource partition, not in /boot. Extract and decompile:

apt-get install -y device-tree-compiler
git clone https://github.com/friendlyarm/sd-fuse_rk3568.git /root/sd-fuse
dd if=/dev/mmcblk0p4 of=/root/resource.img bs=512
mkdir -p /root/dtb-work && cd /root/dtb-work
/root/sd-fuse/tools/aarch64/resource_tool --unpack --image=/root/resource.img
dtc -I dtb -O dts /root/dtb-work/out/rk3568-nanopi5-rev05.dtb -o /root/r5s.dts 2>/dev/null

Add the pps-gpio node at the end of the root node in /root/r5s.dts:

pps {
    compatible = "pps-gpio";
    gpios = <0xbc 0x15 0x00>;
    assert-falling-edge;
    status = "okay";
};

0xbc is the phandle for GPIO3. 0x15 is 21 decimal, which is GPIO3_C5 (bank C, pin 5: 16 + 5 = 21). Recompile and flash:

dtc -I dts -O dtb /root/r5s.dts -o /root/dtb-work/out/rk3568-nanopi5-rev05.dtb 2>/dev/null
cd /root/dtb-work/out
/root/sd-fuse/tools/aarch64/resource_tool --pack --image=/root/resource-new.img *
dd if=/root/resource-new.img of=/dev/mmcblk0p4 bs=512
reboot

After reboot /dev/pps0 appears and works.

Wiring

FPC PinSignalGPS Pad
13.3VVCC
9UART9TXD
12GPIO3_C5TIMEPULSE (PPS)
GNDGNDGND

No TX wire to the GPS. gpsd only needs to receive NMEA. Connecting a TX wire caused frame errors on the GPS RXD input.

Verify NMEA is flowing:

stty -F /dev/ttyS9 9600 raw -echo
timeout 10 cat /dev/ttyS9

Verify PPS:

ppstest /dev/pps0
source 0 - assert 1776474438.102968279, sequence: 1184 - clear  0.000000000, sequence: 0
source 0 - assert 1776474439.102977401, sequence: 1185 - clear  0.000000000, sequence: 0
source 0 - assert 1776474440.102974722, sequence: 1186 - clear  0.000000000, sequence: 0

Software stack

The CM4 build used SatPulse. This build uses gpsd + chrony + ptp4l + phc2sys directly.

gpsd

/etc/default/gpsd:

START_DAEMON="true"
OPTIONS="-n -b"
DEVICES="/dev/ttyS9 /dev/pps0"
USBAUTO="false"

Use OPTIONS= not GPSD_OPTIONS=. The Ubuntu 24.04 gpsd service reads OPTIONS. Using the wrong variable causes gpsd to start without -n, so it will not write to SHM until a client connects.

systemctl enable gpsd
systemctl start gpsd

Verify SHM output with ntpshmmon. You should see NTP0 (GPS NMEA) and NTP2 (PPS).

chrony

Add to /etc/chrony/chrony.conf:

refclock PPS /dev/pps0 refid PPS precision 1e-9 trust
refclock SHM 0 refid GPS precision 1e-1 offset 0.060 delay 0.2 noselect
allow 172.22.22.0/24
makestep 1 3
leapsectz right/UTC

When locked to PPS, tracking looks like this:

Reference ID    : 50505300 (PPS)
Stratum         : 1
Last offset     : +0.000000068 seconds
RMS offset      : 0.000000109 seconds

ptp4l

/etc/linuxptp/ptp4l.conf:

[global]
serverOnly 1
tx_timestamp_timeout 100
ptp_minor_version 0
utc_offset 37

[eth0]

/etc/systemd/system/ptp4l.service:

[Unit]
Description=Precision Time Protocol (PTP) service
After=network-online.target chrony.service
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/sbin/ptp4l -f /etc/linuxptp/ptp4l.conf
ExecStartPost=/bin/sh -c 'sleep 15 && pmc -u -b 0 "SET GRANDMASTER_SETTINGS_NP clockClass 6 clockAccuracy 0x22 offsetScaledLogVariance 0xffff currentUtcOffset 37 leap61 0 leap59 0 currentUtcOffsetValid 1 ptpTimescale 1 timeTraceable 1 frequencyTraceable 1 timeSource 0x20"'
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

The ExecStartPost pmc command is required. Without it, ptp4l announces currentUtcOffsetValid 0 and clients cannot correctly apply the TAI offset. The -t 1 flag must be omitted. With it, the SET appears to succeed but the values do not apply. The sleep 15 gives ptp4l time to reach MASTER state first.

Verify it applied:

pmc -u -b 0 'GET TIME_PROPERTIES_DATA_SET'
currentUtcOffset      37
currentUtcOffsetValid 1
timeTraceable         1

phc2sys

The PHC runs in TAI. The system clock runs in UTC. TAI is currently 37 seconds ahead of UTC. phc2sys keeps the PHC at system clock + 37 seconds.

/etc/systemd/system/phc2sys.service:

[Unit]
Description=Synchronize PTP hardware clock to system clock
After=ptp4l.service chrony.service
Requires=ptp4l.service

[Service]
Type=simple
ExecStart=/usr/sbin/phc2sys -s CLOCK_REALTIME -c eth0 -O 37 -r -r -m
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Bugs and gotchas

UART5 is broken on kernel 6.1

The FriendlyElec wiki documents UART5 on FPC pins 3 and 5. On kernel 6.1 with the stock Ubuntu Noble image, these pins show the correct pinmux but remain unclaimed by the UART driver regardless of DTB configuration. Use UART9 on pins 9 and 10 (/dev/ttyS9) instead.

pmc SET requires no -t 1 flag

On this version of linuxptp, SET GRANDMASTER_SETTINGS_NP appears to succeed with -t 1 but the values do not take effect. Run pmc without it and verify with GET TIME_PROPERTIES_DATA_SET.

gpsd OPTIONS vs GPSD_OPTIONS

The Ubuntu 24.04 gpsd service reads OPTIONS from /etc/default/gpsd. Using GPSD_OPTIONS instead causes gpsd to start without -n and the SHM refclocks stay empty.

makestep 1 -1 causes runaway clock stepping

WAN NTP servers are typically ~100ms behind GPS truth. With makestep 1 -1, chrony steps the clock on every update indefinitely and oscillates between GPS time and WAN NTP time. Use makestep 1 3.

Clients need trust prefer on the PHC refclock

With enough WAN NTP servers in the config, chrony’s majority vote algorithm marks the PHC as a falseticker because it disagrees with WAN NTP by ~100ms. The fix:

refclock PHC /dev/ptp0 poll 0 dpoll -2 tai refid PTP trust prefer

trust prevents the PHC from being marked as a falseticker. prefer selects it over other sources. WAN NTP servers remain as fallback.

Client configuration

Both clients are unchanged from the CM4 build. No phc2sys on clients. Chrony reads the PHC directly via the tai refclock.

/etc/chrony/conf.d/ptp.conf:

refclock PHC /dev/ptp0 poll 0 dpoll -2 tai refid PTP trust prefer
server nanopi-rp5s-lts.lab.opscode.io iburst minpoll 0 maxpoll 4
server rpi-cm4-ptp.lab.opscode.io iburst minpoll 0 maxpoll 4

/etc/systemd/system/chrony.service.d/ptp.conf:

[Unit]
After=ptp4l.service

RK1 (Ubuntu 22.04, linuxptp 3) at /etc/linuxptp/ptp4l.conf:

[global]
slaveOnly 1
tx_timestamp_timeout 100

[eth0]

CM4 (Ubuntu 24.04, linuxptp 4) at /etc/linuxptp/ptp4l.conf:

[global]
clientOnly 1
tx_timestamp_timeout 200
ptp_minor_version 0

[eth0]

Results

Grandmaster accuracy

$ chronyc sourcestats
Name/IP Address            NP  NR  Span  Frequency  Freq Skew  Offset  Std Dev
==============================================================================
PPS                        22  11   333     -0.000      0.005     -0ns   625ns

$ chronyc tracking
Reference ID    : 50505300 (PPS)
Stratum         : 1
Last offset     : +0.000000068 seconds
RMS offset      : 0.000000109 seconds

109ns RMS from GPS truth.

Three-way timing comparison

SourceOffsetUncertainty
PTP, hardware timestamping-1 to -6 ns±84 to 291 ns
LAN NTP, from this grandmaster-7 to -66 µs±171 to 253 µs
WAN NTP, public pools-96 to -104 ms±31 to 96 ms

Same three-tier pattern as Part 1. Each tier is roughly 1000x worse than the one above it. The WAN NTP offset of ~100ms from GPS is not a misconfiguration. Those servers are genuinely less accurate than GPS.

Client comparison: CM4 vs RK1

#* PTP   0   0   377   1    -1ns[  -1ns] +/-  291ns   <- RK1 (Rockchip GMAC)
#* PTP   0   0   210   2    -6ns[  -6ns] +/-   84ns   <- CM4 (BCM54210PE)

Same result as Part 1. The BCM54210PE timestamps at the PHY layer. The Rockchip GMAC timestamps at the MAC layer. That extra stage costs about 3-4x in uncertainty.

Topology matters

Same test as Part 1. See that post for the explanation. Numbers here are consistent with Part 1.

Through the TP-Link AXE5400 consumer router:

ptp4l: master offset     -84886 s2 path delay    299121
ptp4l: master offset     167775 s2 path delay    299121
ptp4l: master offset    -700862 s2 path delay    373459

Direct to the Turing Pi switch:

ptp4l: master offset         12 s2 path delay      2829
ptp4l: master offset         76 s2 path delay      2824
ptp4l: master offset        -20 s2 path delay      2824

300µs path delay through the router. 2.8µs direct to the switch.

Holdover test

GPS antenna disconnected at 01:08 UTC. Reconnected at 15:47 UTC.

Duration: 14 hours 39 minutes. Total drift from GPS truth: 82.6 microseconds.

Both clients stayed on PTP the entire time without falling back to WAN NTP.

#* PTP   0   0   377   1    +62ns[  +148ns] +/-  291ns   <- RK1 after 14h holdover
#* PTP   0   0   210   3     -9ns[   -23ns] +/-   77ns   <- CM4 after 14h holdover

Recovery after antenna reconnect: under 2 minutes.

Monitoring

Same Prometheus and Grafana stack as Part 1. node_exporter and chrony_exporter on all three machines. No satpulse exporter on this build.

PTP Timing Dashboard showing GPS locked, stratum 1, nanosecond accuracy on both clients

CM4 vs R5S: which is the better grandmaster

Both builds produce a working stratum 1 PTP grandmaster at sub-microsecond client accuracy. The differences are in software complexity, physical form factor, and holdover performance.

The CM4 build uses SatPulse, which handles GPS configuration, PHC discipline, and chrony integration in a single daemon. It is significantly easier to configure, and everything fits cleanly inside the enclosure. The R5S build requires manually wiring together gpsd, chrony, ptp4l, phc2sys, and pmc, and adds meaningful configuration complexity. The GPS module and breakout board sit outside the case.

The R5S has three Ethernet ports, and with the right image it can serve as a router as well, making it a flexible platform if you want more than just a grandmaster.

The holdover results were not expected:

BuildDurationTotal driftDrift rate
CM4 + SR1723U1010 hours~15ms~1.5ms/hr
R5S + NEO-M8N14h 39m82.6µs~5.6µs/hr

The R5S crystal held 270x tighter over a longer test period. Whether this reflects a better crystal in the RK3568 SoC or operating temperature differences during the test is unclear. Either way, 82.6µs of drift over nearly 15 hours from an uncompensated crystal is a strong result.

What matters most is what clients actually see:

ClientCM4 grandmasterR5S grandmaster
RK1 (Rockchip GMAC)±291ns±291ns
CM4 (BCM54210PE)±66ns±84ns

Identical on RK1. The CM4 client is marginally tighter against the CM4 grandmaster, but the difference is small enough that it could be test conditions rather than a meaningful hardware gap. Both grandmasters deliver the same practical accuracy.

For cost perspective*: a single TimeHAT build requires the TimeHAT ($200), an OCP M.2 GNSS module ($195), and a Pi 5 4GB ($110), totaling $505. Both builds documented in this series cost me $254 in new parts, exactly half the price of a single TimeHAT grandmaster, and deliver comparable client accuracy. The TimeHAT’s TCXO will hold time more accurately during GPS outages, which matters in production environments. In a homelab this is a non-issue, and having two grandmasters opens up BMCA priority failover testing.

* Prices at time of writing.

References