PTP (Precision Time Protocol) on Raspberry Pi, served from a GNSS/PPS‑disciplined
grandmaster to Pi clients, and fed back into ntpsec on each client as a
low‑jitter refclock.
Warning. This is optional and experimental. Proceed at your own risk.
Useful references that informed this document:
- https://github.com/tiagofreire-pt/rpi_uputronics_stratum1_chrony/blob/main/steps/advanced_system_tuning.md
- https://gpsd.gitlab.io/gpsd/gpsd-time-service-howto.html#_providing_local_ntp_service_using_ptp
- https://docs.fedoraproject.org/en-US/fedora/latest/system-administrators-guide/servers/Configuring_PTP_Using_ptp4l/#sec-Serving_NTP_Time_with_PTP
- https://sourceforge.net/p/linuxptp/mailman/linuxptp-devel/thread/1424738292.9759.53.camel%40intel.com/
- Debian packaging README: https://salsa.debian.org/multimedia-team/linuxptp/-/blob/master/debian/README.Debian
Thanks to @tiagofreire-pt and the linuxptp-devel contributors.
- Hardware: Raspberry Pi CM4, CM5, or Pi 5 on both sides. These have a PTP Hardware Clock (PHC) in the Ethernet MAC.
- OS: Debian 12 (bookworm) or 13 (trixie); Raspberry Pi OS based on either.
linuxptp4.0+ is assumed — older versions use different option names (slaveOnlyinstead ofclientOnly, numericfault_reset_interval, etc.). - Topology: server and clients on the same L2 segment, ideally direct or through a PTP‑aware switch. Unmanaged switches in the path will cause path‑asymmetry offsets; see the troubleshooting section.
- Time source on server: GNSS + PPS, disciplining
ntpsec. - Server role: PTP grandmaster, clockClass 6.
- Client role: PTP client, consuming time via
phc2sys -E ntpshmand feeding it tontpsecas a refclock.
Server:
GNSS ──► gpsd ──► ntpsec ──► system clock (UTC)
│
▼
phc2sys -w (applies UTC→TAI offset, +37s)
│
▼
PHC (TAI) ──► ptp4l ──► network
│
▼
Client: network
│
▼
PHC (TAI) ◄── ptp4l ◄───┘
│
▼
phc2sys -E ntpshm (strips TAI→UTC, -37s)
│
▼
SHM(2) (UTC) ──► ntpsec
Why phc2sys in both directions? ntpsec (and ntpd) do not discipline the
NIC's PHC. phc2sys is the bridge that moves time between the system clock
and the PHC, and it's also the only component that correctly applies the
UTC↔TAI offset when it crosses that boundary. Doing this inside ptp4l.conf
(ntpshm as a servo there) uses subtly different semantics and silently
produces 37‑second errors on clients.
sudo apt update && sudo apt install linuxptpConfirm version 4.x:
ptp4l --versionIf you're on 3.x, several options in this document (clientOnly,
fault_reset_interval ASAP) need to be written in their 3.x form; consider
upgrading to trixie or backports instead.
ethtool -T eth0You need to see entries like:
PTP Hardware Clock: <N>
Hardware Transmit Timestamp Modes: ... on
Hardware Receive Filter Modes: ... ptp-v2-l2-event (or similar)
If PTP Hardware Clock: none, this NIC cannot do hardware timestamping and the
rest of the guide does not apply as written; you'd need time_stamping software
and accept ~tens of µs accuracy. On CM4, CM5, and Pi 5 the on‑board Ethernet
has a PHC; on older Pis it does not.
ethtool -T eth0 | grep 'PTP Hardware Clock'If it says PTP Hardware Clock: 0, that's /dev/ptp0; 1 → /dev/ptp1, etc.
On a Pi with only one PHC this is always /dev/ptp0, but the explicit check is
good hygiene — commands later in the document use this path.
The server's phc2sys needs to know the current UTC↔TAI offset (37 s) in order
to feed the PHC in TAI. It picks this up from the kernel's TAI offset, which
ntpsec sets — but only if ntpsec has been given a leap‑seconds file.
The file ships with the base tzdata package at
/usr/share/zoneinfo/leap-seconds.list — no extra package install needed. Just
point ntpsec at it. In /etc/ntpsec/ntp.conf, add:
leapfile /usr/share/zoneinfo/leap-seconds.list
Then:
sudo systemctl restart ntpsecWithout this, phc2sys -w on the server will happily copy UTC time into the
PHC without offset, and every client will be 37 seconds off.
Note: leap-seconds.list has an expiration date (re‑issued semi‑annually by
IERS). Debian ships updated tzdata packages as new IERS bulletins are
published, so routine apt upgrade keeps it fresh. ntpsec logs a warning as
the expiration approaches.
Ethernet drivers coalesce packet interrupts for throughput, at the cost of latency. For PTP we want the opposite tradeoff. Current values:
sudo ethtool -c eth0Set to the minimum the Pi's driver accepts:
sudo ethtool -C eth0 rx-usecs 4 tx-usecs 4Make it persistent with a small service:
sudo systemctl edit --force --full eth0-coalescence.servicePaste:
[Unit]
Description=Reduce eth0 coalescence for PTP
Wants=network.target
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/sbin/ethtool -C eth0 rx-usecs 4 tx-usecs 4
RemainAfterExit=yes
[Install]
WantedBy=multi-user.targetsudo systemctl enable --now eth0-coalescence.serviceThis shaves roughly 40 µs off chrony/ntp response jitter. Apply it on both server and clients.
The server's PHC is driven by phc2sys from the system clock (which ntpsec
disciplines from GNSS). ptp4l only publishes that PHC to the network — it
does not discipline it.
Create /etc/linuxptp/ptp4l.conf:
[global]
# Grandmaster attributes — clockClass 6 = "synchronized to primary reference".
clockClass 6
# Clock accuracy. Conservative for a GPSDO-fed Pi:
# 0x20 25ns 0x23 1us 0x26 25us 0x29 1ms
# 0x21 100ns 0x24 2.5us 0x27 100us 0x2a 2.5ms
# 0x22 250ns 0x25 10us 0x28 250us 0xfe unknown
clockAccuracy 0x23
# Make this node unambiguously preferred over any accidental second grandmaster.
priority1 64
priority2 128
# ptp4l uses the PHC as its clock. It does NOT discipline the PHC here;
# phc2sys does that, driven by ntpsec. "free_running" prevents ptp4l from
# trying to steer the PHC if it ever receives PTP messages itself.
free_running 1
# Clear port faults immediately (helps during driver or link hiccups).
fault_reset_interval ASAP
# One summary log line per 2^10 = 1024 s.
summary_interval 10
# Linear-regression servo. Irrelevant on a free-running master, but set anyway.
clock_servo linreg
# Time source: 0x10 atomic, 0x20 GPS, 0x30 terrestrial radio, 0x40 PTP,
# 0x50 NTP, 0x60 hand-set, 0x90 other, 0xa0 internal osc.
timeSource 0x20
# Layer-2 transport with auto delay mechanism (E2E by default; P2P if peer advertises).
network_transport L2
delay_mechanism Auto
# Hardware timestamping (default, but explicit is better).
time_stamping hardwareNote: no ntpshm_segment here — the server does not feed SHM, only clients do.
Debian's linuxptp 4.x ships the template unit
/usr/lib/systemd/system/ptp4l@.service. The vendor unit's one rough edge is
that it doesn't wait for the network to be fully up, which on Pis can race
against PHY initialization and produce
ioctl SIOCSHWTSTAMP failed: Invalid argument at boot
(Debian bug #1070847).
Fix via a drop‑in, not by editing the vendor file:
sudo systemctl edit ptp4l@eth0.serviceAdd only:
[Unit]
After=network-online.target
Wants=network-online.target
[Service]
Restart=on-failure
RestartSec=2sEnable and start:
sudo systemctl enable --now ptp4l@eth0.service
systemctl status ptp4l@eth0.serviceYou should see port 1 (eth0): INITIALIZING to LISTENING and, shortly after,
LISTENING to MASTER on ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES.
The vendor phc2sys@.service has two problems we need to work around:
- Its
ExecStart=runsphc2sys -w -s %I, which is the client direction (PHC as source). We need the server direction (system clock as source). - It declares
Requires=ptp4l.serviceandAfter=ptp4l.service— a non-templated unit name that doesn't exist on modern Debian, which makessystemctl startfail withUnit ptp4l.service not found.
Problem 1 alone could be fixed with a drop-in that overrides ExecStart=.
Problem 2 can't: systemd's drop-in mechanism does not allow resetting
dependency directives (Requires=, After=, Wants=, Before=) to an empty
list — only adding to them. So we have to override the whole unit file.
sudo systemctl edit --full phc2sys@.serviceReplace the entire content with:
[Unit]
Description=Synchronize system clock or PTP hardware clock (PHC) for %I
Documentation=man:phc2sys
Requires=ptp4l@%i.service
After=ntpsec.service ptp4l@%i.service
Before=time-sync.target
[Service]
Type=simple
ExecStart=/usr/sbin/phc2sys -s CLOCK_REALTIME -c %I -w -q
[Install]
WantedBy=multi-user.targetKey changes from the vendor: the bare ptp4l.service references are replaced
with the templated ptp4l@%i.service; ntpsec.service is added to After= so
that the system clock is disciplined before we start copying it to the PHC;
and ExecStart= switches to server direction (-s CLOCK_REALTIME -c %I).
The cost of --full is that the override is a full local copy of the unit
rather than a patch — future upstream improvements won't flow in
automatically. If Debian eventually fixes the vendor unit
(worth a bug report), you can systemctl revert phc2sys@.service to adopt
the fix.
Enable and start:
sudo systemctl enable --now phc2sys@eth0.service
systemctl status phc2sys@eth0.service# Exactly one ptp4l and one phc2sys, and phc2sys is in system→PHC direction:
ps auxww | grep -E '[p]tp4l|[p]hc2sys'
# PHC should be ~37 s ahead of UTC (i.e. TAI):
sudo phc_ctl /dev/ptp0 cmp
# ptp4l is advertising the current UTC offset and the right clockClass:
sudo pmc -u -b 0 "GET TIME_PROPERTIES_DATA_SET" # currentUtcOffset 37, currentUtcOffsetValid 1
sudo pmc -u -b 0 "GET PARENT_DATA_SET" # grandmasterClockClass 6
# ntpsec is in charge of system time:
ntpq -c "rv 0 refid,stratum"phc_ctl ... cmp prints the offset between the PHC and CLOCK_REALTIME
atomically; don't compare phc_ctl get against date by eye, the shell latency
swamps the real error.
Create /etc/linuxptp/ptp4l.conf:
[global]
# This node only ever acts as a PTP client.
clientOnly 1
# Client reports clockClass 255 (not a potential master).
clockClass 255
# Same defensive settings as server.
fault_reset_interval ASAP
summary_interval 10
clock_servo linreg
# SHM segment for phc2sys -E ntpshm to write to.
# Must match the "unit N" in ntp.conf's refclock shm.
ntpshm_segment 2
network_transport L2
delay_mechanism Auto
time_stamping hardware
# Time source: 0x40 PTP (we receive time from a PTP grandmaster).
timeSource 0x40
# --- Raspberry Pi CM4/CM5/Pi5 quirks ---------------------------------------
# The macb/bcmgenet driver is slow to return TX timestamps. The default
# tx_timestamp_timeout of 1 ms causes "timed out while polling for tx timestamp"
# followed by FAULT_DETECTED, which in this configuration loops forever.
# 100 ms is plenty of margin on this hardware.
tx_timestamp_timeout 100
# The Pi Ethernet IP doesn't handle PTP 2.1 correctly. Pin the on-wire minor
# version to 0 (PTP 2.0) for compatibility. Required on linuxptp 4.x with Pi
# hardware; harmless to leave on linuxptp 3.x.
ptp_minor_version 0sudo systemctl edit ptp4l@eth0.service[Unit]
After=network-online.target
Wants=network-online.target
[Service]
Restart=on-failure
RestartSec=2ssudo systemctl enable --now ptp4l@eth0.serviceSame vendor-unit problem as on the server: the shipped phc2sys@.service has
non-removable Requires=ptp4l.service / After=ptp4l.service that won't
resolve, and an ExecStart= that doesn't match what we want. We override the
whole unit.
sudo systemctl edit --full phc2sys@.service[Unit]
Description=Synchronize system clock or PTP hardware clock (PHC) for %I
Documentation=man:phc2sys
Requires=ptp4l@%i.service
After=ntpsec.service ptp4l@%i.service
[Service]
Type=simple
ExecStart=/usr/sbin/phc2sys -s %I -E ntpshm -M 2 -w -q
[Install]
WantedBy=multi-user.target-s %I— source is the interface's PHC.-E ntpshm— servo writes to an NTP SHM segment instead of steering a local clock. (The-hhelp text lists onlypiandlinreg, butntpshmandnullfare also valid.)-M 2— use SHM segment 2. Must match theunit 2inntp.conf.-w— wait forptp4lto lock, then inherit the UTC↔TAI offset from it. Without this (or-O <offset>), phc2sys refuses to start with time offset must be specified using -w or -O.-wis the better choice because the offset tracks any future leap-second changes automatically.- No
Before=time-sync.targethere — on the client,ntpsecis the authority; we don't want to signal "time is synced" beforentpsechas actually locked onto the SHM refclock.
Why not -a (automatic mode)? -a has phc2sys query ptp4l's management
interface and dynamically follow whichever port ptp4l has selected as slave —
useful for boundary-clock hosts with multiple PTP ports that can fail over.
For a single-NIC client feeding ntpsec, there's nothing dynamic to follow, so
the simpler -s %I -E ntpshm -M N -w form is canonical and matches the
examples in the Red Hat, Fedora, and linuxptp documentation.
sudo systemctl enable --now phc2sys@eth0.serviceIn /etc/ntpsec/ntp.conf, add:
refclock shm unit 2 refid PTP
unit 2 must match the ntpshm_segment set above. Restart:
sudo systemctl restart ntpsecVerify SHM is actually being written and consumed:
sudo ntpshmmon -c 10 # should show 10 samples on unit 2 with small offsets
ntpq -pn # refid=PTP should eventually get '*' (system peer)ntpq -pn can take a few minutes to promote the PTP refclock to system peer
as ntpsec collects enough samples to trust it.
A stable residual offset (typically tens to hundreds of µs) between client and server after full convergence usually indicates an asymmetric network path — different physical delays in each direction. Unmanaged switches are the common cause. PTP assumes the two directions are symmetric and splits the round‑trip in half, so an asymmetric path appears as a fixed bias.
Measure the mean offset with ntpq -pn or chronyc sources over an hour of
quiet operation, then calibrate with time2 in the refclock line:
refclock shm unit 2 refid PTP time2 0.000180 # +180 µs example
Use half the observed one‑way bias as the time2 value. Better: replace the
unmanaged switch with a PTP transparent clock or run a direct cable.
# Exactly one ptp4l and one phc2sys:
ps auxww | grep -E '[p]tp4l|[p]hc2sys'
# ptp4l should be SLAVE with small master offsets:
journalctl -u ptp4l@eth0.service -n 30 --no-pager
# Client PHC should also be ~37 s ahead of UTC (it's slaved to the GM's PHC):
sudo phc_ctl /dev/ptp0 cmp
# SHM is ticking:
sudo ntpshmmon -c 10
# ntpsec has adopted PTP as its system peer:
ntpq -pnHealthy ptp4l log lines look like:
port 1 (eth0): UNCALIBRATED to SLAVE on MASTER_CLOCK_SELECTED
master offset -38 s2 freq -2374 path delay 8123
master offset -12 s2 freq -2369 path delay 8120
s2 = servo state locked. Offsets of a few tens of ns to low µs on a direct
cable, tens of µs through a switch, are normal.
Four common causes, roughly in order:
- Interface not up yet. The ptp4l drop‑in above
(
After=network-online.target,Restart=on-failure,RestartSec=2s) addresses this. - NIC can't do HW timestamping at all.
ethtool -T eth0will show no filters orPTP Hardware Clock: none. Switch totime_stamping software. - Transport/filter mismatch.
network_transport L2but the driver only advertises UDP filters, or vice versa. Readethtool -Tcarefully and match. - Second
ptp4lrunning. See next item — this is the surprise.
Symptoms: timed out while polling for tx timestamp, port flaps between
LISTENING and UNCALIBRATED, never converges. If tx_timestamp_timeout 100 is
already set and the issue is new on previously‑working hardware, check for a
duplicate daemon:
ps auxww | grep '[p]tp4l'Two processes = two daemons racing for the same PHC and socket. The usual
cause is a leftover non‑templated unit from an older linuxptp version
(pre‑4.0 shipped /usr/lib/systemd/system/ptp4l.service, modern only ships
ptp4l@.service). To find stray references across the whole systemd tree:
sudo grep -rH 'ptp4l\.service\|phc2sys\.service' \
/etc/systemd /run/systemd /usr/lib/systemd 2>/dev/null | grep -v '@'On a clean modern Debian system the only expected hit is
timemaster.service: Conflicts=... ptp4l.service ... in the vendor unit file —
a harmless no‑op because the referenced unit doesn't exist. Anything else is a
leftover to investigate:
- A non‑templated
ptp4l.serviceorphc2sys.serviceunder/usr/lib/systemd/system/thatdpkg -Sreports as unowned — delete it. - Full replacement units under
/etc/systemd/system/that shadow vendor packages (ntpd.service,gpsd.serviceare common victims) —systemctl revert <unit>then recreate as drop‑ins.
Confirm package‑owned files haven't been hand‑edited:
dpkg -V linuxptpThe only expected flag is ??5?????? c /etc/linuxptp/ptp4l.conf (your config,
marked as a conffile). Anything else in the output means a package file has
been modified in place.
The vendor phc2sys@.service has Requires=ptp4l.service / After=ptp4l.service
(non-templated) which on modern Debian doesn't exist — only ptp4l@.service
does. A drop-in cannot fix this: systemd does not allow dependency
directives (Requires=, After=, etc.) to be reset or subtracted from in a
drop-in, only added to. The vendor's broken references stay in effect no
matter what you put in a drop-in.
The fix is to override the whole unit with systemctl edit --full phc2sys@.service
(shown above in the server and client sections). If you previously tried a
drop-in approach with empty Requires= / After= lines expecting them to
reset, remove that drop-in first:
sudo rm -rf /etc/systemd/system/phc2sys@.service.d
sudo systemctl daemon-reloadThen do the full override.
To verify dependencies resolve correctly after the override:
systemctl show phc2sys@eth0.service -p Requires -p AfterRequires= should contain only ptp4l@eth0.service plus systemd-auto-injected
targets (sysinit.target, system-phc2sys.slice). If bare ptp4l.service
still appears, the override didn't take — check with systemctl cat phc2sys@eth0.service that it's reading your /etc/systemd/system/phc2sys@.service
and not the vendor file.
-
A drop‑in is a patch, not a replacement. Include only what differs from the vendor unit.
-
Exec‑type list directives (
ExecStart=,ExecStartPre=,ExecStop=, etc.) and other list directives likeEnvironment=,EnvironmentFile=,ConditionPathExists=can be reset by an empty assignment, then reassigned:[Service] ExecStart= ExecStart=/new/command
-
Dependency directives cannot be reset in a drop‑in.
Requires=,After=,Wants=,Before=,Conflicts=,Requisite=,BindsTo=,PartOf=— these can only be added to via drop‑in. To remove a dependency declared in the vendor unit, you have to override the whole unit withsystemctl edit --full <unit>and edit the offending line out of the local copy. -
[Install]sections in drop‑ins are ignored. Enable/disable is done viasystemctl enable/disable. -
Always verify with
systemctl cat <unit>(merged view) after editing, andsystemd-analyze verify <unit>to catch syntax problems before trying to start.systemctl show <unit> -p Requires -p Aftershows what the parser actually resolved, which is the authoritative view. -
Never edit files under
/usr/lib/systemd/system/or/lib/systemd/system/directly — they get overwritten on package upgrades. Usesystemctl edit <unit>(instance drop‑in),systemctl edit <template>@.service(template‑level drop‑in), orsystemctl edit --full <unit>(full override).
Almost always a UTC↔TAI handling bug:
- Server's
ntpsechas no leapfile configured → kernel TAI offset is 0 →phc2sys -wcopies UTC to PHC without offset →ptp4ladvertises a PHC that's TAI‑labeled but UTC‑valued. - Or someone put
ntpshmas a servo insideptp4l.confinstead of routing SHM throughphc2sys -E ntpshm. The two don't apply the offset the same way.
Check the server:
sudo phc_ctl /dev/ptp0 cmp # should be ~+37 s
sudo pmc -u -b 0 "GET TIME_PROPERTIES_DATA_SET" # currentUtcOffset 37
cat /etc/ntpsec/ntp.conf | grep leapfileNo automatic clockClass demotion on GNSS loss. If the server loses GPS
lock, ptp4l continues advertising clockClass 6 while its PHC runs free off
the crystal. Clients keep trusting it. A production setup needs an external
watchdog that monitors gpsd (e.g. via gpspipe -w) and, on sustained loss
of fix, either stops ptp4l@eth0.service or rewrites ptp4l.conf with
clockClass 7 (holdover) or clockClass 52 (degraded) and reloads.
phc2sys can't hot‑swap direction either, so a single‑box bidirectional
fallback (become a client when GPS is lost) isn't possible with the current
tooling — it needs orchestration on top.
This documentation: CC0 / public domain. Use, modify, and redistribute without restriction.