Skip to content

netutils/ptpd: IEEE 1588 P2P delay mechanism, hardware clock phase-lock and outlier rejection - #3782

Open
daniel-p-carvalho wants to merge 7 commits into
apache:masterfrom
daniel-p-carvalho:feat/ptpd-p2p-hw-sync
Open

daniel-p-carvalho wants to merge 7 commits into
apache:masterfrom
daniel-p-carvalho:feat/ptpd-p2p-hw-sync

Conversation

@daniel-p-carvalho

@daniel-p-carvalho daniel-p-carvalho commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Summary

apps/netutils/ptpd only implemented the end-to-end (E2E) delay mechanism. The IEC/IEEE 61850-9-3 profile uses the peer-to-peer (P2P) delay mechanism instead, so a Grandmaster running that profile (linuxptp's ptp4l) could not be synchronized against without switching it to E2E. This PR adds P2P, lets the daemon discipline a hardware PTP clock (/dev/ptp0) in phase as well as in frequency, and adds an optional filter that keeps a single disturbed Sync sample from moving the clock.

The series was rewritten after review: the custom socket ioctls (SIOCG_TX_HW_TIMESTAMP, SIOCS_PTP_ADJFREQ, SIOCS_PTP_ADJPHASE) that an earlier revision consumed are gone, replaced by the POSIX clock_adjtime() interface on a PTP hardware clock device, as suggested in the review of the kernel side. It is rebased on current master (which already contains #3779).

Commits

  1. Fix Delay_Resp consumption by sendmsg. ptp_sendmsg() did a blocking recvmsg() on the transmit socket right after sending a Delay_Req when hardware timestamping was selected. That socket shares the connection with the event socket, so the call swallowed the next PTP packet on the wire, normally the Delay_Resp, into a buffer that was then discarded, and path_delay_ns stayed at 0. The path delay is also computed with the IEEE 1588-2008 clause 11.3 formula instead of an approximation that only held once the clock had converged. This bug is still present in master.
  2. Implement IEEE 1588 peer-to-peer (P2P) delay mechanism. Adds the Pdelay_Req, Pdelay_Resp and Pdelay_Resp_Follow_Up messages, the -P option (exclusive with -E), requester and responder logic, and replaces bool delay_e2e with enum ptp_delay_mechanism_e in struct ptpd_config_s. The messages carry PTP version 2.0 and controlField 0x05, which linuxptp requires to accept them. An orphaned Pdelay_Resp_Follow_Up from an abandoned cycle is rejected instead of being paired with stale timestamps, P2P without CONFIG_SCHED_TICKLESS prints a warning at startup, and IP multicast handling is skipped for AF_PACKET.
  3. Discard implausible drift-rate samples before averaging. A single drift sample taken over an unusually short or long interval could pass the slew-rate check and corrupt the long-term average. Adds CONFIG_NETUTILS_PTPD_MAX_DRIFT_PPB (default 500000) as a dedicated plausibility bound.
  4. Phase-lock hardware clock via POSIX clock_adjtime. When the clock is a PTP hardware clock, the residual phase offset is now converted to a frequency correction over CONFIG_CLOCK_ADJTIME_PERIOD_MS and applied with ADJ_FREQUENCY, so the counter, and any PPS output driven by it, is pulled into phase instead of only tracking the frequency.
  5. Add ingress latency compensation for RX timestamps. Adds CONFIG_NETUTILS_PTPD_INGRESS_LATENCY_NS (default 0) and the -I option. The value is subtracted from every hardware receive timestamp, which is the ingressLatency port parameter of IEEE 1588.
  6. Retain in-memory IPC for CONFIG_BUILD_FLAT in ptpd_status(). The file based IPC introduced for the protected and kernel builds made ptpd -t time out on flat builds without a mounted /tmp. The in-memory queue is kept for CONFIG_BUILD_FLAT and the file is used otherwise.
  7. Discard outlier Sync phase error samples. A Sync sample whose receive timestamp was taken late, for example because the task was scheduled late with software timestamping, went straight into the phase correction and the drift estimate. Adds CONFIG_NETUTILS_PTPD_OUTLIER_THRESHOLD_NS (default 0, which disables the check). A phase error that differs by more than this many nanoseconds from the median of the last five accepted samples is discarded with a warning. After eight consecutive rejections the sample is accepted and the history restarts from it, so a real step of the master is still followed while a short burst of disturbed samples is ridden out. The history is also restarted whenever the clock is stepped.

Impact

  • New options: -P and -I, CONFIG_NETUTILS_PTPD_MAX_DRIFT_PPB, CONFIG_NETUTILS_PTPD_INGRESS_LATENCY_NS, CONFIG_NETUTILS_PTPD_OUTLIER_THRESHOLD_NS. The defaults leave the previous behaviour unchanged (E2E remains available with -E, ingress compensation is 0, outlier rejection is off).
  • struct ptpd_config_s changes: delay_e2e is replaced by delay_mechanism, and ingress_latency_ns is added. The only user in the tree is system/ptpd, which is updated here.
  • The phase-lock in commit 4 needs a PTP hardware clock device. On STM32 that is /dev/ptp0 from arch/arm/stm32: implement PTP hardware clock driver (/dev/ptp0) nuttx#20180. Users that keep the default CLOCK_REALTIME are not affected.
  • Hardware transmit timestamps are not part of this PR. Until they land (a follow-up series, together with arch/arm/stm32: hardware TX timestamping via SO_TIMESTAMPING nuttx#20148) the transmit side of Delay_Req and Pdelay_Req is timestamped in software.

Testing

Built with stm32f4discovery:netnsh plus NETUTILS_PTPD, in two configurations (with and without NET_PKT and NET_TIMESTAMP), without errors or warnings from ptpd. The last commit was also built with CONFIG_NETUTILS_PTPD_OUTLIER_THRESHOLD_NS at 0 and at 100000. ./tools/checkpatch.sh -g <base>..HEAD passes.

On hardware: a custom STM32F407 board with the STM32 Ethernet MAC and a DP83848 PHY, over AF_PACKET (ptpd -2 -s -p /dev/ptp0 -B -H -P -i eth0), against a GNSS-referenced ptp4l Grandmaster using the IEC/IEEE 61850-9-3 profile. These runs were done with this series together with the hardware transmit timestamp series mentioned above:

  • Direct cable: path_delay of about 1.2 µs.
  • Through a Fast Ethernet switch, 20 minute run, 159 samples, no failed queries: path_delay 9265.3 ns (σ = 2.5 ns), drift about -79.95 ppm, phase offset about -2.2 µs.
  • Several 5 minute runs after rebasing: path_delay between 9.26 and 9.33 µs and no failed queries.

This exact series, without the transmit timestamp series, was also run on the same hardware (5 minutes). Its transmit timestamps are taken in software, after sendmsg() returns, and the measured peer delay came out negative (about -18 µs), which the path delay filter rejects, so path_delay_ns stayed at 0 and the phase offset was worse (about -6.7 µs against -2.4 µs with hardware transmit timestamps). In other words P2P on this hardware needs accurate transmit timestamps. The ingress compensation is implemented but its value has not been calibrated, so the default 0 applies no correction.

Outlier rejection (commit 7):

  • The selection logic was checked on the host with the function extracted from the source: a burst of four bad samples is discarded and the next normal one is accepted, and a real 500 µs step has seven samples discarded and the eighth accepted, after which the new level keeps being accepted.
  • With hardware timestamps and the threshold at 100000 ns, a 220 sample run discarded none: the deviation from the median had a median of 2.4 µs, a 95th percentile of 2.8 µs and a maximum of 81 µs, so the filter does not interfere in the normal case.
  • With software timestamps (-S) the deviations are hundreds of microseconds and come in bursts; a limit of three consecutive rejections let one of those bursts through, which is why the limit is eight. The accuracy of -S is still bounded by the receive timestamp being taken when the task wakes up, and this filter does not change that.

acassis
acassis previously approved these changes Sep 15, 2026
jerpelea
jerpelea previously approved these changes Sep 15, 2026
@xiaoxiang781216

Copy link
Copy Markdown
Contributor

please fix the conflict. @daniel-p-carvalho

Comment thread netutils/ptpd/ptpd.c Outdated
Comment thread netutils/ptpd/ptpd.c
Comment thread netutils/ptpd/ptpd.c
Comment thread netutils/ptpd/ptpd.c Outdated
Comment thread netutils/ptpd/ptpd.c Outdated
Comment thread netutils/ptpd/ptpd.c Outdated
Comment thread netutils/ptpd/ptpd.c Outdated
Comment thread netutils/ptpd/ptpd.c Outdated
Comment thread netutils/ptpd/ptpd.c Outdated
Comment thread netutils/ptpd/ptpd.c
Comment thread netutils/ptpd/ptpd.c Outdated
@daniel-p-carvalho
daniel-p-carvalho dismissed stale reviews from jerpelea and acassis via ee5f983 September 16, 2026 20:06
@github-actions github-actions Bot added Size: L and removed Size: XL labels Sep 16, 2026
@daniel-p-carvalho
daniel-p-carvalho force-pushed the feat/ptpd-p2p-hw-sync branch 3 times, most recently from 310ea5d to 3b8c815 Compare September 17, 2026 21:00
@daniel-p-carvalho

Copy link
Copy Markdown
Contributor Author

Rebased onto latest upstream/master and updated the hardware clock disciplining approach:

  1. POSIX clock_adjtime instead of proprietary ioctls:

    • Removed all custom ioctl calls (SIOCS_PTP_ADJFREQ, SIOCS_PTP_ADJPHASE, SIOCG_TX_HW_TIMESTAMP).
    • When a hardware PTP clock device (e.g. /dev/ptp0) is used via -p /dev/ptp0, ptpd now acts as a PI servo driving the hardware clock's frequency and phase alignment directly via standard POSIX clock_adjtime(state->clockid, &buf) with ADJ_FREQUENCY.
    • This aligns with the new POSIX PTP hardware clock driver introduced in arch/arm/stm32: implement PTP hardware clock driver (/dev/ptp0) nuttx#20180.
  2. Clean P2P delay & bugfix series:

    • Fixed Delay_Resp consumption by sendmsg() and implemented canonical IEEE 1588 §11.3 mean path delay calculation.
    • Implemented IEEE 1588 Peer-to-Peer (P2P) delay mechanism (-P).
    • Added warning when P2P is used without CONFIG_SCHED_TICKLESS.
    • Reset pdelay_waiting_followup when initiating a new P2P cycle to prevent stale timestamp pairing.
    • Discard implausible crystal oscillator drift rate spikes before averaging to prevent drift divergence.

All 6 commits have been verified with checkpatch.sh (all checks pass) and validated on physical hardware (STM32F4 Discovery with STM32 Ethernet PTP MAC against a Meinberg PTP Grandmaster).

acassis
acassis previously approved these changes Sep 17, 2026
@xiaoxiang781216

Copy link
Copy Markdown
Contributor

@daniel-p-carvalho please rebase and fix the conflict.

@daniel-p-carvalho

daniel-p-carvalho commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Hi @wenquan1,

Following your request to test PR #3789 on physical hardware, we identified a regression when running on flat builds (CONFIG_BUILD_FLAT=y) without a filesystem mounted on /tmp (e.g. bare-metal microcontrollers like STM32): ptpd -t <pid> timed out with error 110 because the status file in /tmp could not be created or read.

To address this cleanly without breaking your improvements for Protected/Kernel modes, we updated this PR with a hybrid approach in commit 35073e5e0 (netutils/ptpd: retain in-memory IPC for CONFIG_BUILD_FLAT in ptpd_status()):

  • On CONFIG_BUILD_FLAT: retains the original fast in-memory sigqueue + semaphore IPC directly in address space, removing the filesystem requirement for lightweight embedded boards.
  • On !CONFIG_BUILD_FLAT (Protected / Kernel builds): retains your file-based IPC via /tmp to safely cross user/kernel address space boundaries.

We have validated this on real hardware (running NuttX flat build). Could you please take a look and share your review?

Thanks!

ptp_sendmsg() called a blocking recvmsg(state->tx_socket, ...) right
after sending a Delay_Req whenever hardware_ts was set, assuming a
Linux-style MSG_ERRQUEUE/loopback semantics NuttX does not have.
Since tx_socket and event_socket share the same underlying
connection, this call instead blocked on and consumed whatever PTP
packet arrived next on the wire — almost always the Delay_Resp,
which typically arrives within milliseconds of the request. Its
payload was read into a local buffer that went out of scope on
return, so the packet never reached ptp_process_rx_packet() and
path_delay_ns stayed at 0 in -H mode. t3 is now captured locally
via ptp_gettime(), the same way -S mode already did, until
hardware TX timestamping is supported.

Also replaces the path delay heuristic in ptp_process_delay_resp()
(which derived an approximation of (t2-t1) from path_delay_ns and
last_delta_ns, only valid once the clock had already converged) with
the canonical IEEE 1588-2008 §11.3 formula: store (t2-t1) directly
from Sync/Follow_Up as sync_diff_ns, then average it with (t4-t3)
from the Delay_Req/Delay_Resp exchange. Relaxes the path delay
ceiling to 10ms unconditionally, since Delay_Req's t3 is software-
timestamped in both modes until hardware TX timestamping is supported.

Assisted-by: Claude:claude-sonnet-5
Assisted-by: Gemini:gemini-3.8-flash-medium
Signed-off-by: Daniel P. Carvalho <danieloak@gmail.com>
Implements the Peer-to-Peer (P2P) transparent clock delay measurement
mechanism (IEEE 1588-2008 §11.4 / IEEE 802.1AS / IEC/IEEE 61850-9-3)
in apps/netutils/ptpd:

- Add PTP_MSGTYPE_PDELAY_REQ, PTP_MSGTYPE_PDELAY_RESP, and
  PTP_MSGTYPE_PDELAY_RESP_FOLLOW_UP definitions and structs in ptpv2.h.
- Define IEEE 1588-2008 Annex F peer delay multicast MAC address
  01:80:c2:00:00:0e and Annex D peer delay IP address 224.0.0.107.
- Replace bool delay_e2e with enum ptp_delay_mechanism_e (PTP_DELAY_NONE,
  PTP_DELAY_E2E, PTP_DELAY_P2P) in include/netutils/ptpd.h.
- Add -P CLI option in system/ptpd/ptpd_main.c with mutual exclusion
  check against -E, and display last_transmitted_pdelayreq in status.
- Implement responder logic in ptp_process_pdelay_req() sending
  Pdelay_Resp (t2) and Pdelay_Resp_Follow_Up (t3) regardless of master
  or slave state.
- Implement requester logic in ptp_send_pdelay_req() gated on the
  physical link without requiring prior BMCA master selection.
- Implement ptp_process_pdelay_resp() and
  ptp_process_pdelay_resp_followup() using canonical mean path delay
  formula ((t4 - t1) - (t3 - t2)) / 2.
- Refactor path delay bounds checking and moving average filter into
  ptp_record_path_delay() shared across E2E and P2P mechanisms.
- Set PTP version 2.0 and controlField 0x05 in Pdelay_Req, Pdelay_Resp
  and Pdelay_Resp_Follow_Up, and in the own-identity header, so that
  peers such as linuxptp accept the messages.
- Clear pdelay_waiting_followup when a new Pdelay_Req is sent, so an
  orphaned Pdelay_Resp_Follow_Up from an abandoned cycle is not paired
  with stale timestamps.
- Warn at startup when P2P is selected without CONFIG_SCHED_TICKLESS,
  since a tick-driven clock cannot resolve the peer delay.
- Skip IP multicast join/leave handling for AF_PACKET.

Assisted-by: Claude:claude-sonnet-5
Assisted-by: Gemini:gemini-3.8-flash-medium
Signed-off-by: Daniel P. Carvalho <danieloak@gmail.com>
A single drift-rate sample computed between two consecutive sync
updates was clamped against CLOCK_ADJTIME_SLEWLIMIT_PPM - the
hardware's slew-rate safety limit, not a bound on how large a real
crystal-oscillator drift measurement can plausibly be. An abnormally
short or long measurement interval (e.g. right after a clock
source outage/reconnect, or a burst of closely spaced sync packets
following packet loss) could therefore produce a wildly implausible
sample that still passed the check and corrupted the long-term
drift_ppb average.

Add CONFIG_NETUTILS_PTPD_MAX_DRIFT_PPB (default 500000, well above
any real crystal's few-hundred-ppm drift) as a dedicated plausibility
bound, intentionally much tighter than CLOCK_ADJTIME_SLEWLIMIT_PPM.
A sample outside this bound is discarded and the previous averaged
drift_ppb is kept unchanged instead of being corrupted.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Daniel P. Carvalho <danieloak@gmail.com>
When state->clockid is configured to a hardware PTP clock device
(e.g., /dev/ptp0) instead of CLOCK_REALTIME, ptp_adjtime() previously
passed only the measured frequency drift (-ppb) to clock_adjtime(),
ignoring the residual phase offset (delta_ns / adjustment_ns).
As a result, while the hardware counter tracked frequency, its phase
was never pulled into alignment with the master clock.

Convert delta_ns (which combines frequency drift and current phase error
clamped to max_adjust_ns) to ppb over CONFIG_CLOCK_ADJTIME_PERIOD_MS,
acting as a proportional-integral (PI) phase servo. This drives the
hardware clock to phase lock with the master via POSIX clock_adjtime()
using ADJ_FREQUENCY without requiring proprietary ioctl calls.

Assisted-by: Claude:claude-sonnet-5
Assisted-by: Gemini:gemini-3.8-flash-medium
Signed-off-by: Daniel P. Carvalho <danieloak@gmail.com>
The MAC latches a hardware receive timestamp later than the frame
reaches the wire, because of the PHY and the clock domain crossing.
This fixed delay is the ingressLatency port parameter of IEEE 1588 and
shows up as a constant phase error between the local and the master
clock.

Subtract the configured latency from every hardware receive timestamp
in ptp_getrxtime(), the single place where they enter the daemon, so
Sync, Delay_Resp and the peer delay messages are all corrected.

- Add CONFIG_NETUTILS_PTPD_INGRESS_LATENCY_NS (default 0, which applies
  no compensation).
- Add the -I option to override it at run time.
- Add ingress_latency_ns to struct ptpd_config_s.

Software timestamps are not affected.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Daniel P. Carvalho <danieloak@gmail.com>
…tus()

PR apache#3789 replaced the in-memory sigqueue + shared memory IPC in ptpd_status()
with file-based IPC to support Protected and Kernel modes across address
spaces. However, on microcontrollers running CONFIG_BUILD_FLAT, a filesystem
or /tmp (TMPFS) is rarely mounted or available, causing ptpd_status() to fail
with -ETIMEDOUT (errno 110) because the status file cannot be created.

Retain the file-based IPC for !CONFIG_BUILD_FLAT (Protected and Kernel modes)
while restoring the zero-overhead in-memory sigqueue + semaphore IPC for
CONFIG_BUILD_FLAT. Both modes share the status serialization logic via
ptp_populate_status() and support all fields including P2P.

Assisted-by: Gemini:gemini-3.8-flash-medium
Signed-off-by: Daniel P. Carvalho <danieloak@gmail.com>
A single Sync sample whose receive timestamp was taken late, for
example because the task was scheduled late with software
timestamping, was fed straight into the phase correction and the
drift estimate, and could pull the clock away from the master.

- Add CONFIG_NETUTILS_PTPD_OUTLIER_THRESHOLD_NS (default 0, which
  disables the check). A phase error that differs by more than this
  many nanoseconds from the median of the last five accepted samples
  is discarded, with a warning.
- Accept the sample after eight consecutive rejections and restart
  the history from it, so that a real step of the master is still
  followed while a short burst of disturbed samples is ridden out.
- Restart the history whenever the clock is stepped, since the old
  samples no longer describe the new time base.
- With the default of 0 the behaviour is unchanged.

Signed-off-by: Daniel P. Carvalho <danieloak@gmail.com>
Assisted-by: Claude:claude-sonnet-5
@github-actions github-actions Bot added Size: XL and removed Size: L labels Sep 19, 2026
@daniel-p-carvalho daniel-p-carvalho changed the title netutils/ptpd: IEEE 1588 P2P delay mechanism and hardware clock phase-lock netutils/ptpd: IEEE 1588 P2P delay mechanism, hardware clock phase-lock and outlier rejection Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants