From 3727812c22fa64e97f9ee54a85e2ab3cb792b4b2 Mon Sep 17 00:00:00 2001 From: "Daniel P. Carvalho" Date: Sat, 19 Sep 2026 17:35:04 -0300 Subject: [PATCH] netutils/ptpd: Discard outlier Sync phase error samples. 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 Assisted-by: Claude:claude-sonnet-5 --- netutils/ptpd/Kconfig | 20 +++++++++ netutils/ptpd/ptpd.c | 99 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/netutils/ptpd/Kconfig b/netutils/ptpd/Kconfig index fc26e821b..371497609 100644 --- a/netutils/ptpd/Kconfig +++ b/netutils/ptpd/Kconfig @@ -209,6 +209,26 @@ config NETUTILS_PTPD_DELAYREQ_AVGCOUNT ---help--- Measured path delay is averaged over this many samples. +config NETUTILS_PTPD_OUTLIER_THRESHOLD_NS + int "PTP outlier rejection threshold (ns)" + default 0 + range 0 1000000000 + ---help--- + A phase error measurement that differs from the median of the + latest accepted ones by more than this many nanoseconds is + discarded instead of being used to correct the clock. It protects + the frequency estimate and the phase correction from a single + disturbed sample, for example a receive timestamp taken late by + the scheduler with software timestamping. + + A change that lasts is accepted after several discarded samples in + a row, so the daemon still follows a real step of the master, and + a short burst of disturbed samples is still ridden out. + + Choose a value well above the normal spread of the measurement: + a few microseconds are typical with hardware timestamping and + hundreds with software timestamping. 0 disables the rejection. + config NETUTILS_PTPD_STATUSFILE string "PTP daemon status file path" default "/tmp/ptpd.status" diff --git a/netutils/ptpd/ptpd.c b/netutils/ptpd/ptpd.c index 8b8adf9a2..969cbc352 100644 --- a/netutils/ptpd/ptpd.c +++ b/netutils/ptpd/ptpd.c @@ -65,6 +65,22 @@ #include "netutils/netlib.h" #include "ptpv2.h" +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#if CONFIG_NETUTILS_PTPD_OUTLIER_THRESHOLD_NS > 0 +/* Outlier rejection of the measured phase error: number of recent samples + * the median is taken over, the least number of samples needed before + * anything is rejected, and how many samples in a row can be rejected + * before they are taken as a real change of the phase. + */ + +# define PTP_OUTLIER_HISTORY 5 +# define PTP_OUTLIER_MIN_HISTORY 3 +# define PTP_OUTLIER_MAX_CONSECUTIVE 8 +#endif + /**************************************************************************** * Private Types ****************************************************************************/ @@ -128,6 +144,12 @@ struct ptp_state_s long drift_avg_total_ms; long drift_ppb; bool has_last_delta; +#if CONFIG_NETUTILS_PTPD_OUTLIER_THRESHOLD_NS > 0 + int64_t delta_hist[PTP_OUTLIER_HISTORY]; + unsigned int delta_hist_count; + unsigned int delta_hist_next; + unsigned int outlier_count; +#endif /* Identity of currently selected clock source, * from the latest announcement message. @@ -1241,6 +1263,69 @@ static int ptp_process_announce(FAR struct ptp_state_s *state, return OK; } +#if CONFIG_NETUTILS_PTPD_OUTLIER_THRESHOLD_NS > 0 +/* Tell whether a phase error measurement is an outlier, i.e. it differs from + * the median of the latest accepted ones by more than the threshold. A + * measurement that is disturbed on its own (a late receive timestamp, for + * example) would otherwise move the frequency and phase corrections. + * + * A change that lasts is not an outlier: after a few rejections in a row + * the measurement is accepted and the history starts over. + */ + +static bool ptp_is_outlier(FAR struct ptp_state_s *state, int64_t delta_ns) +{ + int64_t sorted[PTP_OUTLIER_HISTORY]; + int64_t deviation; + unsigned int count = state->delta_hist_count; + unsigned int i; + unsigned int j; + + if (count >= PTP_OUTLIER_MIN_HISTORY) + { + for (i = 0; i < count; i++) + { + int64_t value = state->delta_hist[i]; + + for (j = i; j > 0 && sorted[j - 1] > value; j--) + { + sorted[j] = sorted[j - 1]; + } + + sorted[j] = value; + } + + deviation = delta_ns - sorted[count / 2]; + if (deviation < 0) + { + deviation = -deviation; + } + + if (deviation > CONFIG_NETUTILS_PTPD_OUTLIER_THRESHOLD_NS) + { + if (++state->outlier_count < PTP_OUTLIER_MAX_CONSECUTIVE) + { + return true; + } + + state->delta_hist_count = 0; + state->delta_hist_next = 0; + } + } + + state->outlier_count = 0; + state->delta_hist[state->delta_hist_next] = delta_ns; + state->delta_hist_next = (state->delta_hist_next + 1) % + PTP_OUTLIER_HISTORY; + if (state->delta_hist_count < PTP_OUTLIER_HISTORY) + { + state->delta_hist_count++; + } + + return false; +} +#endif + /* Update local clock either by smooth adjustment or by jumping. * Remote time was remote_timestamp at local_timestamp. */ @@ -1286,6 +1371,11 @@ static int ptp_update_local_clock(FAR struct ptp_state_s *state, state->drift_avg_total_ms = 0; state->drift_ppb = 0; state->has_last_delta = false; +#if CONFIG_NETUTILS_PTPD_OUTLIER_THRESHOLD_NS > 0 + state->delta_hist_count = 0; + state->delta_hist_next = 0; + state->outlier_count = 0; +#endif if (ret == OK) { @@ -1312,6 +1402,15 @@ static int ptp_update_local_clock(FAR struct ptp_state_s *state, (int64_t)CONFIG_CLOCK_ADJTIME_SLEWLIMIT_PPM * CONFIG_CLOCK_ADJTIME_PERIOD_MS; +#if CONFIG_NETUTILS_PTPD_OUTLIER_THRESHOLD_NS > 0 + if (ptp_is_outlier(state, delta_ns)) + { + ptpwarn("Discarding outlier sample: delta %" PRId64 " ns\n", + delta_ns); + return OK; + } +#endif + if (!state->has_last_delta) { /* First measurement after jump or startup: no previous