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 <danieloak@gmail.com>
Assisted-by: Claude:claude-sonnet-5
This commit is contained in:
Daniel P. Carvalho 2026-09-19 17:35:04 -03:00 • committed by Xiang Xiao
parent 8940f6b0d4
commit 3727812c22
2 changed files with 119 additions and 0 deletions

View file

@ -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"

View file

@ -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