From ed379b402834739d5910d11b6de2f15b564b5dbc Mon Sep 17 00:00:00 2001 From: Miroslav Lichvar Date: Tue, 22 Oct 2013 18:57:15 +0200 Subject: [PATCH] Add clock sanity check. Check the sanity of the synchronized clock by comparing its uncorrected frequency with the system monotonic clock. When the measured frequency offset is larger than the value of the sanity_freq_limit option (20% by default), a warning message will be printed and the servo will be reset. Setting the option to zero disables the check. This is useful to detect when the clock is broken or adjusted by another program. Signed-off-by: Miroslav Lichvar --- clock.c | 25 ++++++++++ clock.h | 7 +++ clockcheck.c | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++ clockcheck.h | 64 ++++++++++++++++++++++++++ config.c | 6 +++ config.h | 2 + default.cfg | 1 + ds.h | 1 + gPTP.cfg | 1 + makefile | 12 ++--- phc2sys.8 | 9 ++++ phc2sys.c | 24 +++++++++- port.c | 3 ++ ptp4l.8 | 7 +++ ptp4l.c | 1 + 15 files changed, 284 insertions(+), 7 deletions(-) create mode 100644 clockcheck.c create mode 100644 clockcheck.h diff --git a/clock.c b/clock.c index 300d2d7..74d5c77 100644 --- a/clock.c +++ b/clock.c @@ -25,6 +25,7 @@ #include "bmc.h" #include "clock.h" #include "clockadj.h" +#include "clockcheck.h" #include "foreign.h" #include "mave.h" #include "missing.h" @@ -96,6 +97,7 @@ struct clock { struct clock_description desc; struct clock_stats stats; int stats_interval; + struct clockcheck *sanity_check; }; struct clock the_clock; @@ -643,6 +645,13 @@ struct clock *clock_create(int phc_index, struct interface *iface, int count, pr_err("failed to create stats"); return NULL; } + if (dds->sanity_freq_limit) { + c->sanity_check = clockcheck_create(dds->sanity_freq_limit); + if (!c->sanity_check) { + pr_err("Failed to create clock sanity check"); + return NULL; + } + } c->dds = dds->dds; @@ -1084,11 +1093,18 @@ enum servo_state clock_synchronize(struct clock *c, clockadj_step(c->clkid, -tmv_to_nanoseconds(c->master_offset)); c->t1 = tmv_zero(); c->t2 = tmv_zero(); + if (c->sanity_check) { + clockcheck_set_freq(c->sanity_check, -adj); + clockcheck_step(c->sanity_check, + -tmv_to_nanoseconds(c->master_offset)); + } break; case SERVO_LOCKED: clockadj_set_freq(c->clkid, -adj); if (c->clkid == CLOCK_REALTIME) sysclk_set_sync(); + if (c->sanity_check) + clockcheck_set_freq(c->sanity_check, -adj); break; } return state; @@ -1204,3 +1220,12 @@ int clock_num_ports(struct clock *c) { return c->nports; } + +void clock_check_ts(struct clock *c, struct timespec ts) +{ + if (c->sanity_check && + clockcheck_sample(c->sanity_check, + ts.tv_sec * NS_PER_SEC + ts.tv_nsec)) { + servo_reset(c->servo); + } +} diff --git a/clock.h b/clock.h index 309f731..bc66dce 100644 --- a/clock.h +++ b/clock.h @@ -238,4 +238,11 @@ struct clock_description *clock_description(struct clock *c); */ int clock_num_ports(struct clock *c); +/** + * Perform a sanity check on a time stamp made by a clock. + * @param c The clock instance. + * @param ts The time stamp. + */ +void clock_check_ts(struct clock *c, struct timespec ts); + #endif diff --git a/clockcheck.c b/clockcheck.c new file mode 100644 index 0000000..d48a578 --- /dev/null +++ b/clockcheck.c @@ -0,0 +1,128 @@ +/** + * @file clockcheck.c + * @note Copyright (C) 2013 Miroslav Lichvar + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include +#include + +#include "clockcheck.h" +#include "print.h" + +#define CHECK_MIN_INTERVAL 100000000 +#define CHECK_MAX_FREQ 900000000 + +struct clockcheck { + /* Sanity frequency limit */ + int freq_limit; + /* Frequency was set at least once */ + int freq_known; + /* Current frequency */ + int current_freq; + /* Maximum and minimum frequency since last update */ + int max_freq; + int min_freq; + uint64_t last_ts; + uint64_t last_mono_ts; +}; + +struct clockcheck *clockcheck_create(int freq_limit) +{ + struct clockcheck *cc; + cc = calloc(1, sizeof(*cc)); + if (!cc) + return NULL; + cc->freq_limit = freq_limit; + cc->max_freq = -CHECK_MAX_FREQ; + cc->min_freq = CHECK_MAX_FREQ; + return cc; +} + +int clockcheck_sample(struct clockcheck *cc, uint64_t ts) +{ + uint64_t mono_ts; + int64_t interval, mono_interval; + double max_foffset, min_foffset; + struct timespec now; + int ret = 0; + + /* Check the sanity of the synchronized clock by comparing its + uncorrected frequency with the system monotonic clock. If + the synchronized clock is the system clock, the measured + frequency offset will be the current frequency correction of + the system clock. */ + + if (!cc->freq_known) + return ret; + + interval = (int64_t)ts - cc->last_ts; + if (interval >= 0 && interval < CHECK_MIN_INTERVAL) + return ret; + + clock_gettime(CLOCK_MONOTONIC, &now); + mono_ts = now.tv_sec * 1000000000LL + now.tv_nsec; + mono_interval = (int64_t)mono_ts - cc->last_mono_ts; + + if (mono_interval < CHECK_MIN_INTERVAL) + return ret; + + if (cc->last_ts && cc->max_freq <= CHECK_MAX_FREQ) { + max_foffset = 1e9 * (interval / + (1.0 + cc->min_freq / 1e9) / + mono_interval - 1.0); + min_foffset = 1e9 * (interval / + (1.0 + cc->max_freq / 1e9) / + mono_interval - 1.0); + + if (min_foffset > cc->freq_limit) { + pr_warning("clockcheck: clock jumped forward or" + " running faster than expected!"); + ret = 1; + } else if (max_foffset < -cc->freq_limit) { + pr_warning("clockcheck: clock jumped backward or" + " running slower than expected!"); + ret = 1; + } + } + + cc->last_mono_ts = mono_ts; + cc->last_ts = ts; + cc->max_freq = cc->min_freq = cc->current_freq; + + return ret; +} + +void clockcheck_set_freq(struct clockcheck *cc, int freq) +{ + if (cc->max_freq < freq) + cc->max_freq = freq; + if (cc->min_freq > freq) + cc->min_freq = freq; + cc->current_freq = freq; + cc->freq_known = 1; +} + +void clockcheck_step(struct clockcheck *cc, int64_t step) +{ + if (cc->last_ts) + cc->last_ts += step; +} + +void clockcheck_destroy(struct clockcheck *cc) +{ + free(cc); +} diff --git a/clockcheck.h b/clockcheck.h new file mode 100644 index 0000000..78aca48 --- /dev/null +++ b/clockcheck.h @@ -0,0 +1,64 @@ +/** + * @file clockcheck.h + * @brief Implements clock sanity checking. + * @note Copyright (C) 2013 Miroslav Lichvar + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#ifndef HAVE_CLOCKCHECK_H +#define HAVE_CLOCKCHECK_H + +#include + +/** Opaque type */ +struct clockcheck; + +/** + * Create a new instance of a clock sanity check. + * @param freq_limit The maximum allowed frequency offset between uncorrected + * clock and the system monotonic clock in ppb. + * @return A pointer to a new clock check on success, NULL otherwise. + */ +struct clockcheck *clockcheck_create(int freq_limit); + +/** + * Perform the sanity check on a time stamp. + * @param cc Pointer to a clock check obtained via @ref clockcheck_create(). + * @param ts Time stamp made by the clock in nanoseconds. + * @return Zero if ts passed the check, non-zero otherwise. + */ +int clockcheck_sample(struct clockcheck *cc, uint64_t ts); + +/** + * Inform clock check about changes in current frequency of the clock. + * @param cc Pointer to a clock check obtained via @ref clockcheck_create(). + * @param freq Frequency correction applied to the clock in ppb. + */ +void clockcheck_set_freq(struct clockcheck *cc, int freq); + +/** + * Inform clock check that the clock was stepped. + * @param cc Pointer to a clock check obtained via @ref clockcheck_create(). + * @param step Step correction applied to the clock in nanoseconds. + */ +void clockcheck_step(struct clockcheck *cc, int64_t step); + +/** + * Destroy a clock check. + * @param cc Pointer to a clock check obtained via @ref clockcheck_create(). + */ +void clockcheck_destroy(struct clockcheck *cc); + +#endif diff --git a/config.c b/config.c index fe4c0f3..7b7534f 100644 --- a/config.c +++ b/config.c @@ -373,6 +373,12 @@ static enum parser_result parse_global_setting(const char *option, return r; *cfg->pi_max_frequency = val; + } else if (!strcmp(option, "sanity_freq_limit")) { + r = get_ranged_int(value, &val, 0, INT_MAX); + if (r != PARSED_OK) + return r; + cfg->dds.sanity_freq_limit = val; + } else if (!strcmp(option, "ptp_dst_mac")) { if (MAC_LEN != sscanf(value, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5])) diff --git a/config.h b/config.h index 40d8978..6c57bc9 100644 --- a/config.h +++ b/config.h @@ -86,6 +86,8 @@ struct config { double *pi_f_offset_const; int *pi_max_frequency; + int *sanity_freq_limit; + unsigned char *ptp_dst_mac; unsigned char *p2p_dst_mac; unsigned char *udp6_scope; diff --git a/default.cfg b/default.cfg index 0a42a92..8794596 100644 --- a/default.cfg +++ b/default.cfg @@ -52,6 +52,7 @@ pi_offset_const 0.0 pi_f_offset_const 0.0000001 pi_max_frequency 900000000 clock_servo pi +sanity_freq_limit 200000000 # # Transport options # diff --git a/ds.h b/ds.h index 55ad043..e8e94ad 100644 --- a/ds.h +++ b/ds.h @@ -54,6 +54,7 @@ struct default_ds { int freq_est_interval; /*log seconds*/ int stats_interval; /*log seconds*/ int kernel_leap; + int sanity_freq_limit; int time_source; struct clock_description clock_desc; }; diff --git a/gPTP.cfg b/gPTP.cfg index 9c26ec5..a94bd01 100644 --- a/gPTP.cfg +++ b/gPTP.cfg @@ -51,6 +51,7 @@ pi_offset_const 0.0 pi_f_offset_const 0.0000001 pi_max_frequency 900000000 clock_servo pi +sanity_freq_limit 200000000 # # Transport options # diff --git a/makefile b/makefile index 2eaeca5..3f7ec90 100644 --- a/makefile +++ b/makefile @@ -23,9 +23,9 @@ VER = -DVER=$(version) CFLAGS = -Wall $(VER) $(incdefs) $(DEBUG) $(EXTRA_CFLAGS) LDLIBS = -lm -lrt $(EXTRA_LDFLAGS) PRG = ptp4l pmc phc2sys hwstamp_ctl -OBJ = bmc.o clock.o clockadj.o config.o fault.o fsm.o ptp4l.o mave.o \ - msg.o phc.o pi.o port.o print.o raw.o servo.o sk.o stats.o tlv.o tmtab.o \ - transport.o udp.o udp6.o uds.o util.o version.o +OBJ = bmc.o clock.o clockadj.o clockcheck.o config.o fault.o \ + fsm.o ptp4l.o mave.o msg.o phc.o pi.o port.o print.o raw.o servo.o \ + sk.o stats.o tlv.o tmtab.o transport.o udp.o udp6.o uds.o util.o version.o OBJECTS = $(OBJ) hwstamp_ctl.o phc2sys.o pmc.o pmc_common.o sysoff.o SRC = $(OBJECTS:.o=.c) @@ -47,9 +47,9 @@ ptp4l: $(OBJ) pmc: msg.o pmc.o pmc_common.o print.o raw.o sk.o tlv.o transport.o udp.o \ udp6.o uds.o util.o version.o -phc2sys: clockadj.o msg.o phc.o phc2sys.o pi.o pmc_common.o print.o servo.o \ - raw.o sk.o stats.o sysoff.o tlv.o transport.o udp.o udp6.o uds.o util.o \ - version.o +phc2sys: clockadj.o clockcheck.o msg.o phc.o phc2sys.o pi.o pmc_common.o \ + print.o raw.o servo.o sk.o stats.o sysoff.o tlv.o transport.o udp.o udp6.o \ + uds.o util.o version.o hwstamp_ctl: hwstamp_ctl.o version.o diff --git a/phc2sys.8 b/phc2sys.8 index d7a4569..5fcd6f0 100644 --- a/phc2sys.8 +++ b/phc2sys.8 @@ -27,6 +27,8 @@ phc2sys \- synchronize two clocks ] [ .BI \-N " clock-readings" ] [ +.BI \-L " freq-limit" +] [ .BI \-u " summary-updates" ] [ .BI \-n " domain-number" @@ -118,6 +120,13 @@ Specify the offset between the slave and master times in seconds. See .B TIME SCALE USAGE below. .TP +.BI \-L " freq-limit" +The maximum allowed frequency offset between uncorrected clock and the system +monotonic clock in parts per billion (ppb). This is used as a sanity check of +the synchronized clock. When a larger offset is measured, a warning message +will be printed and the servo will be reset. When set to 0, the sanity check is +disabled. The default is 200000000 (20%). +.TP .BI \-u " summary-updates" Specify the number of clock updates included in summary statistics. The statistics include offset root mean square (RMS), maximum absolute offset, diff --git a/phc2sys.c b/phc2sys.c index 2012fbd..2b8af49 100644 --- a/phc2sys.c +++ b/phc2sys.c @@ -36,6 +36,7 @@ #include #include "clockadj.h" +#include "clockcheck.h" #include "ds.h" #include "fsm.h" #include "missing.h" @@ -143,6 +144,7 @@ struct clock { int pmc_ds_idx; int pmc_ds_requested; uint64_t pmc_last_update; + struct clockcheck *sanity_check; }; static void update_clock_stats(struct clock *clock, @@ -193,6 +195,9 @@ static void update_clock(struct clock *clock, offset += clock->sync_offset * NS_PER_SEC * clock->sync_offset_direction; + if (clock->sanity_check && clockcheck_sample(clock->sanity_check, ts)) + servo_reset(clock->servo); + ppb = servo_sample(clock->servo, offset, ts, &state); clock->servo_state = state; @@ -201,11 +206,15 @@ static void update_clock(struct clock *clock, break; case SERVO_JUMP: clockadj_step(clock->clkid, -offset); + if (clock->sanity_check) + clockcheck_step(clock->sanity_check, -offset); /* Fall through. */ case SERVO_LOCKED: clockadj_set_freq(clock->clkid, -ppb); if (clock->clkid == CLOCK_REALTIME) sysclk_set_sync(); + if (clock->sanity_check) + clockcheck_set_freq(clock->sanity_check, -ppb); break; } @@ -559,6 +568,7 @@ static void usage(char *progname) " -R [rate] slave clock update rate in HZ (1.0)\n" " -N [num] number of master clock readings per update (5)\n" " -O [offset] slave-master time offset (0)\n" + " -L [limit] sanity frequency limit in ppb (200000000)\n" " -u [num] number of clock updates in summary stats (0)\n" " -w wait for ptp4l\n" " -n [num] domain number (0)\n" @@ -579,6 +589,7 @@ int main(int argc, char *argv[]) int c, domain_number = 0, phc_readings = 5, pps_fd = -1; int max_ppb, r, wait_sync = 0, forced_sync_offset = 0; int print_level = LOG_INFO, use_syslog = 1, verbose = 0; + int sanity_freq_limit = 200000000; double ppb, phc_interval = 1.0, phc_rate; struct timespec phc_interval_tp; struct clock dst_clock = { @@ -594,7 +605,7 @@ int main(int argc, char *argv[]) progname = strrchr(argv[0], '/'); progname = progname ? 1+progname : argv[0]; while (EOF != (c = getopt(argc, argv, - "c:d:s:P:I:S:F:R:N:O:i:u:wn:xl:mqvh"))) { + "c:d:s:P:I:S:F:R:N:O:L:i:u:wn:xl:mqvh"))) { switch (c) { case 'c': dst_clock.clkid = clock_open(optarg); @@ -649,6 +660,10 @@ int main(int argc, char *argv[]) dst_clock.sync_offset_direction = -1; forced_sync_offset = 1; break; + case 'L': + if (get_arg_val_i(c, optarg, &sanity_freq_limit, 0, INT_MAX)) + return -1; + break; case 'u': if (get_arg_val_ui(c, optarg, &dst_clock.stats_max_count, 0, UINT_MAX)) @@ -721,6 +736,13 @@ int main(int argc, char *argv[]) return -1; } } + if (sanity_freq_limit) { + dst_clock.sanity_check = clockcheck_create(sanity_freq_limit); + if (!dst_clock.sanity_check) { + fprintf(stderr, "failed to create clock check"); + return -1; + } + } print_set_progname(progname); print_set_verbose(verbose); diff --git a/port.c b/port.c index d98bb80..bd10d0a 100644 --- a/port.c +++ b/port.c @@ -2082,6 +2082,9 @@ enum fsm_event port_event(struct port *p, int fd_index) msg_put(msg); return EV_NONE; } + if (msg->hwts.ts.tv_sec && msg->hwts.ts.tv_nsec) { + clock_check_ts(p->clock, msg->hwts.ts); + } if (port_ignore(p, msg)) { msg_put(msg); return EV_NONE; diff --git a/ptp4l.8 b/ptp4l.8 index 221d7d1..01438b3 100644 --- a/ptp4l.8 +++ b/ptp4l.8 @@ -340,6 +340,13 @@ The maximum allowed frequency adjustment of the clock in parts per billion set to 0, the hardware limit will be used. The default is 900000000 (90%). .TP +.B sanity_freq_limit +The maximum allowed frequency offset between uncorrected clock and the system +monotonic clock in parts per billion (ppb). This is used as a sanity check of +the synchronized clock. When a larger offset is measured, a warning message +will be printed and the servo will be reset. When set to 0, the sanity check is +disabled. The default is 200000000 (20%). +.TP .B ptp_dst_mac The MAC address where should be PTP messages sent. Relevant only with L2 transport. The default is 01:1B:19:00:00:00. diff --git a/ptp4l.c b/ptp4l.c index dac303a..c828bb4 100644 --- a/ptp4l.c +++ b/ptp4l.c @@ -55,6 +55,7 @@ static struct config cfg_settings = { .freq_est_interval = 1, .stats_interval = 0, .kernel_leap = 1, + .sanity_freq_limit = 200000000, .time_source = INTERNAL_OSCILLATOR, .clock_desc = { .productDescription = {