Monitor the port state change events from ptp4l, and use that information to determine the "source" clock. Then synchronize all other clocks in our list to that source, by feeding into their respective servo loop an offset equal to the delta between their timestamp and the timestamp of the source clock. All timestamps are representative of the same event, which is the most recent perout pulse of the ts2phc master. Signed-off-by: Vladimir Oltean <olteanv@gmail.com>
705 lines
16 KiB
C
705 lines
16 KiB
C
/**
|
|
* @file ts2phc.c
|
|
* @brief Utility program to synchronize the PHC clock to external events
|
|
* @note Copyright (C) 2013 Balint Ferencz <fernya@sch.bme.hu>
|
|
* @note Based on the phc2sys utility
|
|
* @note Copyright (C) 2012 Richard Cochran <richardcochran@gmail.com>
|
|
* @note SPDX-License-Identifier: GPL-2.0+
|
|
*/
|
|
#include <stdlib.h>
|
|
#include <net/if.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
|
|
#include "clockadj.h"
|
|
#include "config.h"
|
|
#include "contain.h"
|
|
#include "interface.h"
|
|
#include "phc.h"
|
|
#include "print.h"
|
|
#include "ts2phc.h"
|
|
#include "version.h"
|
|
|
|
#define NS_PER_SEC 1000000000LL
|
|
#define SAMPLE_WEIGHT 1.0
|
|
|
|
struct interface {
|
|
STAILQ_ENTRY(interface) list;
|
|
};
|
|
|
|
static void ts2phc_cleanup(struct ts2phc_private *priv)
|
|
{
|
|
struct port *p, *tmp;
|
|
|
|
ts2phc_slave_cleanup(priv);
|
|
if (priv->master)
|
|
ts2phc_master_destroy(priv->master);
|
|
if (priv->cfg)
|
|
config_destroy(priv->cfg);
|
|
|
|
close_pmc_node(&priv->node);
|
|
|
|
/*
|
|
* Clocks are destroyed by the cleanup methods of the individual
|
|
* master and slave PHC modules.
|
|
*/
|
|
LIST_FOREACH_SAFE(p, &priv->ports, list, tmp)
|
|
free(p);
|
|
|
|
msg_cleanup();
|
|
}
|
|
|
|
/* FIXME: Copied from phc2sys */
|
|
static int normalize_state(int state)
|
|
{
|
|
if (state != PS_MASTER && state != PS_SLAVE &&
|
|
state != PS_PRE_MASTER && state != PS_UNCALIBRATED) {
|
|
/* treat any other state as "not a master nor a slave" */
|
|
state = PS_DISABLED;
|
|
}
|
|
return state;
|
|
}
|
|
|
|
/* FIXME: Copied from phc2sys */
|
|
static struct port *port_get(struct ts2phc_private *priv, unsigned int number)
|
|
{
|
|
struct port *p;
|
|
|
|
LIST_FOREACH(p, &priv->ports, list) {
|
|
if (p->number == number)
|
|
return p;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* FIXME: Copied from phc2sys */
|
|
static int clock_compute_state(struct ts2phc_private *priv,
|
|
struct clock *clock)
|
|
{
|
|
int state = PS_DISABLED;
|
|
struct port *p;
|
|
|
|
LIST_FOREACH(p, &priv->ports, list) {
|
|
if (p->clock != clock)
|
|
continue;
|
|
/* PS_SLAVE takes the highest precedence, PS_UNCALIBRATED
|
|
* after that, PS_MASTER is third, PS_PRE_MASTER fourth and
|
|
* all of that overrides PS_DISABLED, which corresponds
|
|
* nicely with the numerical values */
|
|
if (p->state > state)
|
|
state = p->state;
|
|
}
|
|
return state;
|
|
}
|
|
|
|
#define node_to_ts2phc(node) \
|
|
container_of(node, struct ts2phc_private, node)
|
|
|
|
static int ts2phc_recv_subscribed(struct pmc_node *node,
|
|
struct ptp_message *msg, int excluded)
|
|
{
|
|
struct ts2phc_private *priv = node_to_ts2phc(node);
|
|
int mgt_id, state;
|
|
struct portDS *pds;
|
|
struct port *port;
|
|
struct clock *clock;
|
|
|
|
mgt_id = get_mgt_id(msg);
|
|
if (mgt_id == excluded)
|
|
return 0;
|
|
switch (mgt_id) {
|
|
case TLV_PORT_DATA_SET:
|
|
pds = get_mgt_data(msg);
|
|
port = port_get(priv, pds->portIdentity.portNumber);
|
|
if (!port) {
|
|
pr_info("received data for unknown port %s",
|
|
pid2str(&pds->portIdentity));
|
|
return 1;
|
|
}
|
|
state = normalize_state(pds->portState);
|
|
if (port->state != state) {
|
|
pr_info("port %s changed state",
|
|
pid2str(&pds->portIdentity));
|
|
port->state = state;
|
|
clock = port->clock;
|
|
state = clock_compute_state(priv, clock);
|
|
if (clock->state != state || clock->new_state) {
|
|
clock->new_state = state;
|
|
priv->state_changed = 1;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
struct servo *servo_add(struct ts2phc_private *priv, struct clock *clock)
|
|
{
|
|
enum servo_type type = config_get_int(priv->cfg, NULL, "clock_servo");
|
|
struct servo *servo;
|
|
int fadj, max_adj;
|
|
|
|
fadj = (int) clockadj_get_freq(clock->clkid);
|
|
/* Due to a bug in older kernels, the reading may silently fail
|
|
and return 0. Set the frequency back to make sure fadj is
|
|
the actual frequency of the clock. */
|
|
clockadj_set_freq(clock->clkid, fadj);
|
|
|
|
max_adj = phc_max_adj(clock->clkid);
|
|
|
|
servo = servo_create(priv->cfg, type, -fadj, max_adj, 0);
|
|
if (!servo)
|
|
return NULL;
|
|
|
|
servo_sync_interval(servo, SERVO_SYNC_INTERVAL);
|
|
|
|
return servo;
|
|
}
|
|
|
|
void clock_add_tstamp(struct clock *clock, tmv_t t)
|
|
{
|
|
struct timespec ts = tmv_to_timespec(t);
|
|
|
|
pr_debug("adding tstamp %ld.%09ld to clock %s",
|
|
ts.tv_sec, ts.tv_nsec, clock->name);
|
|
clock->last_ts = t;
|
|
clock->is_ts_available = 1;
|
|
}
|
|
|
|
static int clock_get_tstamp(struct clock *clock, tmv_t *ts)
|
|
{
|
|
if (!clock->is_ts_available)
|
|
return 0;
|
|
clock->is_ts_available = 0;
|
|
*ts = clock->last_ts;
|
|
return 1;
|
|
}
|
|
|
|
static void clock_flush_tstamp(struct clock *clock)
|
|
{
|
|
clock->is_ts_available = 0;
|
|
}
|
|
|
|
struct clock *clock_add(struct ts2phc_private *priv, const char *device)
|
|
{
|
|
clockid_t clkid = CLOCK_INVALID;
|
|
int phc_index = -1;
|
|
struct clock *c;
|
|
int err;
|
|
|
|
clkid = posix_clock_open(device, &phc_index);
|
|
if (clkid == CLOCK_INVALID)
|
|
return NULL;
|
|
|
|
LIST_FOREACH(c, &priv->clocks, list) {
|
|
if (c->phc_index == phc_index) {
|
|
/* Already have the clock, don't add it again */
|
|
posix_clock_close(clkid);
|
|
return c;
|
|
}
|
|
}
|
|
|
|
c = calloc(1, sizeof(*c));
|
|
if (!c) {
|
|
pr_err("failed to allocate memory for a clock");
|
|
return NULL;
|
|
}
|
|
c->clkid = clkid;
|
|
c->phc_index = phc_index;
|
|
c->servo_state = SERVO_UNLOCKED;
|
|
c->servo = servo_add(priv, c);
|
|
c->no_adj = config_get_int(priv->cfg, NULL, "free_running");
|
|
err = asprintf(&c->name, "/dev/ptp%d", phc_index);
|
|
if (err < 0) {
|
|
free(c);
|
|
posix_clock_close(clkid);
|
|
return NULL;
|
|
}
|
|
|
|
LIST_INSERT_HEAD(&priv->clocks, c, list);
|
|
return c;
|
|
}
|
|
|
|
|
|
void clock_destroy(struct clock *c)
|
|
{
|
|
servo_destroy(c->servo);
|
|
posix_clock_close(c->clkid);
|
|
free(c->name);
|
|
free(c);
|
|
}
|
|
|
|
/* FIXME: Copied from phc2sys */
|
|
static struct port *port_add(struct ts2phc_private *priv, unsigned int number,
|
|
char *device)
|
|
{
|
|
struct clock *c = NULL;
|
|
struct port *p, *tmp;
|
|
|
|
p = port_get(priv, number);
|
|
if (p)
|
|
return p;
|
|
/* port is a new one, look whether we have the device already on
|
|
* a different port */
|
|
LIST_FOREACH(tmp, &priv->ports, list) {
|
|
if (tmp->number == number) {
|
|
c = tmp->clock;
|
|
break;
|
|
}
|
|
}
|
|
if (!c) {
|
|
c = clock_add(priv, device);
|
|
if (!c)
|
|
return NULL;
|
|
}
|
|
p = malloc(sizeof(*p));
|
|
if (!p) {
|
|
pr_err("failed to allocate memory for a port");
|
|
clock_destroy(c);
|
|
return NULL;
|
|
}
|
|
p->number = number;
|
|
p->clock = c;
|
|
LIST_INSERT_HEAD(&priv->ports, p, list);
|
|
return p;
|
|
}
|
|
|
|
static int auto_init_ports(struct ts2phc_private *priv)
|
|
{
|
|
int state, timestamping;
|
|
int number_ports, res;
|
|
char iface[IFNAMSIZ];
|
|
struct clock *clock;
|
|
struct port *port;
|
|
unsigned int i;
|
|
|
|
while (1) {
|
|
if (!is_running())
|
|
return -1;
|
|
res = run_pmc_clock_identity(&priv->node, 1000);
|
|
if (res < 0)
|
|
return -1;
|
|
if (res > 0)
|
|
break;
|
|
/* res == 0, timeout */
|
|
pr_notice("Waiting for ptp4l...");
|
|
}
|
|
|
|
number_ports = run_pmc_get_number_ports(&priv->node, 1000);
|
|
if (number_ports <= 0) {
|
|
pr_err("failed to get number of ports");
|
|
return -1;
|
|
}
|
|
|
|
res = run_pmc_subscribe(&priv->node, 1000);
|
|
if (res <= 0) {
|
|
pr_err("failed to subscribe");
|
|
return -1;
|
|
}
|
|
|
|
for (i = 1; i <= number_ports; i++) {
|
|
res = run_pmc_port_properties(&priv->node, 1000, i, &state,
|
|
×tamping, iface);
|
|
if (res == -1) {
|
|
/* port does not exist, ignore the port */
|
|
continue;
|
|
}
|
|
if (res <= 0) {
|
|
pr_err("failed to get port properties");
|
|
return -1;
|
|
}
|
|
if (timestamping == TS_SOFTWARE) {
|
|
/* ignore ports with software time stamping */
|
|
continue;
|
|
}
|
|
port = port_add(priv, i, iface);
|
|
if (!port)
|
|
return -1;
|
|
port->state = normalize_state(state);
|
|
}
|
|
if (LIST_EMPTY(&priv->clocks)) {
|
|
pr_err("no suitable ports available");
|
|
return -1;
|
|
}
|
|
LIST_FOREACH(clock, &priv->clocks, list) {
|
|
clock->new_state = clock_compute_state(priv, clock);
|
|
}
|
|
priv->state_changed = 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void ts2phc_reconfigure(struct ts2phc_private *priv)
|
|
{
|
|
struct clock *c, *src = NULL, *last = NULL;
|
|
int src_cnt = 0, dst_cnt = 0;
|
|
|
|
pr_info("reconfiguring after port state change");
|
|
priv->state_changed = 0;
|
|
|
|
LIST_FOREACH(c, &priv->clocks, list) {
|
|
if (c->new_state) {
|
|
c->state = c->new_state;
|
|
c->new_state = 0;
|
|
}
|
|
|
|
switch (c->state) {
|
|
case PS_FAULTY:
|
|
case PS_DISABLED:
|
|
case PS_LISTENING:
|
|
case PS_PRE_MASTER:
|
|
case PS_MASTER:
|
|
case PS_PASSIVE:
|
|
if (!c->is_destination) {
|
|
pr_info("selecting %s for synchronization",
|
|
c->name);
|
|
c->is_destination = 1;
|
|
}
|
|
dst_cnt++;
|
|
break;
|
|
case PS_UNCALIBRATED:
|
|
src_cnt++;
|
|
break;
|
|
case PS_SLAVE:
|
|
src = c;
|
|
src_cnt++;
|
|
break;
|
|
}
|
|
last = c;
|
|
}
|
|
if (dst_cnt >= 1 && !src) {
|
|
priv->source = last;
|
|
priv->source->is_destination = 0;
|
|
/* Reset to original state in next reconfiguration. */
|
|
priv->source->new_state = priv->source->state;
|
|
priv->source->state = PS_SLAVE;
|
|
pr_info("no source, selecting %s as the default clock",
|
|
last->name);
|
|
return;
|
|
}
|
|
if (src_cnt > 1) {
|
|
pr_info("multiple source clocks available, postponing sync...");
|
|
priv->source = NULL;
|
|
return;
|
|
}
|
|
if (src_cnt > 0 && !src) {
|
|
pr_info("source clock not ready, waiting...");
|
|
priv->source = NULL;
|
|
return;
|
|
}
|
|
if (!src_cnt && !dst_cnt) {
|
|
pr_info("no PHC ready, waiting...");
|
|
priv->source = NULL;
|
|
return;
|
|
}
|
|
if (!src_cnt) {
|
|
pr_info("nothing to synchronize");
|
|
priv->source = NULL;
|
|
return;
|
|
}
|
|
src->is_destination = 0;
|
|
priv->source = src;
|
|
pr_info("selecting %s as the source clock", src->name);
|
|
}
|
|
|
|
static void ts2phc_synchronize_clocks(struct ts2phc_private *priv, int autocfg)
|
|
{
|
|
tmv_t source_tmv;
|
|
struct clock *c;
|
|
int valid, err;
|
|
|
|
if (autocfg) {
|
|
if (!priv->source) {
|
|
pr_debug("no source, skipping");
|
|
return;
|
|
}
|
|
valid = clock_get_tstamp(priv->source, &source_tmv);
|
|
if (!valid) {
|
|
pr_err("source clock (%s) timestamp not valid, skipping",
|
|
priv->source->name);
|
|
return;
|
|
}
|
|
} else {
|
|
struct timespec source_ts;
|
|
|
|
err = ts2phc_master_getppstime(priv->master, &source_ts);
|
|
if (err < 0) {
|
|
pr_err("source ts not valid");
|
|
return;
|
|
}
|
|
if (source_ts.tv_nsec > NS_PER_SEC / 2)
|
|
source_ts.tv_sec++;
|
|
source_ts.tv_nsec = 0;
|
|
|
|
source_tmv = timespec_to_tmv(source_ts);
|
|
}
|
|
|
|
LIST_FOREACH(c, &priv->clocks, list) {
|
|
int64_t offset;
|
|
double adj;
|
|
tmv_t ts;
|
|
|
|
if (!c->is_destination)
|
|
continue;
|
|
|
|
valid = clock_get_tstamp(c, &ts);
|
|
if (!valid) {
|
|
pr_debug("%s timestamp not valid, skipping", c->name);
|
|
continue;
|
|
}
|
|
|
|
offset = tmv_to_nanoseconds(tmv_sub(ts, source_tmv));
|
|
|
|
if (c->no_adj) {
|
|
pr_info("%s offset %10" PRId64, c->name,
|
|
offset);
|
|
continue;
|
|
}
|
|
|
|
adj = servo_sample(c->servo, offset, tmv_to_nanoseconds(ts),
|
|
SAMPLE_WEIGHT, &c->servo_state);
|
|
|
|
pr_info("%s offset %10" PRId64 " s%d freq %+7.0f",
|
|
c->name, offset, c->servo_state, adj);
|
|
|
|
switch (c->servo_state) {
|
|
case SERVO_UNLOCKED:
|
|
break;
|
|
case SERVO_JUMP:
|
|
clockadj_set_freq(c->clkid, -adj);
|
|
clockadj_step(c->clkid, -offset);
|
|
break;
|
|
case SERVO_LOCKED:
|
|
case SERVO_LOCKED_STABLE:
|
|
clockadj_set_freq(c->clkid, -adj);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void usage(char *progname)
|
|
{
|
|
fprintf(stderr,
|
|
"\n"
|
|
"usage: %s [options]\n\n"
|
|
" -a turn on autoconfiguration\n"
|
|
" -c [dev|name] phc slave clock (like /dev/ptp0 or eth0)\n"
|
|
" (may be specified multiple times)\n"
|
|
" -f [file] read configuration from 'file'\n"
|
|
" -h prints this message and exits\n"
|
|
" -l [num] set the logging level to 'num'\n"
|
|
" -m print messages to stdout\n"
|
|
" -q do not print messages to the syslog\n"
|
|
" -s [dev|name] source of the PPS signal\n"
|
|
" may take any of the following forms:\n"
|
|
" generic - an external 1-PPS without ToD information\n"
|
|
" /dev/ptp0 - a local PTP Hardware Clock (PHC)\n"
|
|
" eth0 - a local PTP Hardware Clock (PHC)\n"
|
|
" nmea - a gps device connected by serial port or network\n"
|
|
" -v prints the software version and exits\n"
|
|
"\n",
|
|
progname);
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
int c, err = 0, have_slave = 0, index, print_level;
|
|
char uds_local[MAX_IFNAME_SIZE + 1];
|
|
enum ts2phc_master_type pps_type;
|
|
struct ts2phc_private priv = {0};
|
|
char *config = NULL, *progname;
|
|
const char *pps_source = NULL;
|
|
struct config *cfg = NULL;
|
|
struct interface *iface;
|
|
struct option *opts;
|
|
int autocfg = 0;
|
|
|
|
handle_term_signals();
|
|
|
|
cfg = config_create();
|
|
if (!cfg) {
|
|
ts2phc_cleanup(&priv);
|
|
return -1;
|
|
}
|
|
|
|
opts = config_long_options(cfg);
|
|
|
|
/* Process the command line arguments. */
|
|
progname = strrchr(argv[0], '/');
|
|
progname = progname ? 1 + progname : argv[0];
|
|
while (EOF != (c = getopt_long(argc, argv, "ac:f:hi:l:mqs:v", opts, &index))) {
|
|
switch (c) {
|
|
case 0:
|
|
if (config_parse_option(cfg, opts[index].name, optarg)) {
|
|
ts2phc_cleanup(&priv);
|
|
return -1;
|
|
}
|
|
break;
|
|
case 'a':
|
|
autocfg = 1;
|
|
break;
|
|
case 'c':
|
|
if (!config_create_interface(optarg, cfg)) {
|
|
fprintf(stderr, "failed to add slave\n");
|
|
ts2phc_cleanup(&priv);
|
|
return -1;
|
|
}
|
|
have_slave = 1;
|
|
break;
|
|
case 'f':
|
|
config = optarg;
|
|
break;
|
|
case 'l':
|
|
if (get_arg_val_i(c, optarg, &print_level,
|
|
PRINT_LEVEL_MIN, PRINT_LEVEL_MAX)) {
|
|
ts2phc_cleanup(&priv);
|
|
return -1;
|
|
}
|
|
config_set_int(cfg, "logging_level", print_level);
|
|
print_set_level(print_level);
|
|
break;
|
|
case 'm':
|
|
config_set_int(cfg, "verbose", 1);
|
|
print_set_verbose(1);
|
|
break;
|
|
case 'q':
|
|
config_set_int(cfg, "use_syslog", 0);
|
|
print_set_syslog(0);
|
|
break;
|
|
case 's':
|
|
if (pps_source) {
|
|
fprintf(stderr, "too many PPS sources\n");
|
|
ts2phc_cleanup(&priv);
|
|
return -1;
|
|
}
|
|
pps_source = optarg;
|
|
break;
|
|
case 'v':
|
|
ts2phc_cleanup(&priv);
|
|
version_show(stdout);
|
|
return 0;
|
|
case 'h':
|
|
ts2phc_cleanup(&priv);
|
|
usage(progname);
|
|
return -1;
|
|
case '?':
|
|
default:
|
|
ts2phc_cleanup(&priv);
|
|
usage(progname);
|
|
return -1;
|
|
}
|
|
}
|
|
if (config && (c = config_read(config, cfg))) {
|
|
fprintf(stderr, "failed to read config\n");
|
|
ts2phc_cleanup(&priv);
|
|
return -1;
|
|
}
|
|
print_set_progname(progname);
|
|
print_set_tag(config_get_string(cfg, NULL, "message_tag"));
|
|
print_set_verbose(config_get_int(cfg, NULL, "verbose"));
|
|
print_set_syslog(config_get_int(cfg, NULL, "use_syslog"));
|
|
print_set_level(config_get_int(cfg, NULL, "logging_level"));
|
|
|
|
STAILQ_INIT(&priv.slaves);
|
|
priv.cfg = cfg;
|
|
|
|
snprintf(uds_local, sizeof(uds_local), "/var/run/ts2phc.%d",
|
|
getpid());
|
|
|
|
if (autocfg) {
|
|
err = init_pmc_node(cfg, &priv.node, uds_local,
|
|
ts2phc_recv_subscribed);
|
|
if (err) {
|
|
ts2phc_cleanup(&priv);
|
|
return -1;
|
|
}
|
|
err = auto_init_ports(&priv);
|
|
if (err) {
|
|
ts2phc_cleanup(&priv);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
STAILQ_FOREACH(iface, &cfg->interfaces, list) {
|
|
if (1 == config_get_int(cfg, interface_name(iface), "ts2phc.master")) {
|
|
if (pps_source) {
|
|
fprintf(stderr, "too many PPS sources\n");
|
|
ts2phc_cleanup(&priv);
|
|
return -1;
|
|
}
|
|
pps_source = interface_name(iface);
|
|
} else {
|
|
if (ts2phc_slave_add(&priv, interface_name(iface))) {
|
|
fprintf(stderr, "failed to add slave\n");
|
|
ts2phc_cleanup(&priv);
|
|
return -1;
|
|
}
|
|
have_slave = 1;
|
|
}
|
|
}
|
|
if (!have_slave) {
|
|
fprintf(stderr, "no slave clocks specified\n");
|
|
ts2phc_cleanup(&priv);
|
|
usage(progname);
|
|
return -1;
|
|
}
|
|
if (!pps_source) {
|
|
fprintf(stderr, "no PPS source specified\n");
|
|
ts2phc_cleanup(&priv);
|
|
usage(progname);
|
|
return -1;
|
|
}
|
|
if (ts2phc_slaves_init(&priv)) {
|
|
fprintf(stderr, "failed to initialize slaves\n");
|
|
ts2phc_cleanup(&priv);
|
|
return -1;
|
|
}
|
|
|
|
if (!strcasecmp(pps_source, "generic")) {
|
|
pps_type = TS2PHC_MASTER_GENERIC;
|
|
} else if (!strcasecmp(pps_source, "nmea")) {
|
|
pps_type = TS2PHC_MASTER_NMEA;
|
|
} else {
|
|
pps_type = TS2PHC_MASTER_PHC;
|
|
}
|
|
priv.master = ts2phc_master_create(&priv, pps_source, pps_type);
|
|
if (!priv.master) {
|
|
fprintf(stderr, "failed to create master\n");
|
|
ts2phc_cleanup(&priv);
|
|
return -1;
|
|
}
|
|
|
|
while (is_running()) {
|
|
struct clock *c;
|
|
|
|
if (autocfg) {
|
|
/*
|
|
* Make sure ptp4l sees us as alive and doesn't prune
|
|
* us from the list of subscribers
|
|
*/
|
|
err = update_pmc_node(&priv.node, 1);
|
|
if (err < 0) {
|
|
pr_err("update_pmc_node returned %d", err);
|
|
break;
|
|
}
|
|
run_pmc_events(&priv.node);
|
|
if (priv.state_changed)
|
|
ts2phc_reconfigure(&priv);
|
|
}
|
|
|
|
LIST_FOREACH(c, &priv.clocks, list)
|
|
clock_flush_tstamp(c);
|
|
|
|
err = ts2phc_slave_poll(&priv);
|
|
if (err < 0) {
|
|
pr_err("poll failed");
|
|
break;
|
|
}
|
|
if (err > 0)
|
|
ts2phc_synchronize_clocks(&priv, autocfg);
|
|
}
|
|
|
|
ts2phc_cleanup(&priv);
|
|
return err;
|
|
}
|