linuxptp/ts2phc.c
Vladimir Oltean f078f19339 ts2phc_phc_master: make use of new kernel API for perout waveform
This API was introduced for 2 reasons:

1. Some hardware can emit PPS signals but not starting from arbitrary
   absolute times, but rather phase-aligned to the beginning of a
   second. We _could_ patch ts2phc to always specify a start time of
   0.000000000 to PTP_PEROUT_REQUEST, but in practice, we would never
   know whether that would actually work with all in-tree PHC drivers.
   So there was a need for a new flag that only specifies the phase of
   the periodic signal, and not the absolute start time.

2. Some hardware can, rather unfortunately, not distinguish between a
   rising and a falling extts edge. And, since whatever rises also has
   to fall before rising again, the strategy in ts2phc is to set a
   'large' pulse width (half the period) and ignore the extts event
   corresponding to the mid-way between one second and another. This is
   all fine, but currently, ts2phc.pulsewidth is a read-only property in
   the config file. The kernel is not instructed in any way to use this
   value, it is simply that must be configured based on prior knowledge
   of the PHC's implementation. This API changes that.

The introduction of a phase adjustment for the master PHC means we have
to adjust our approximation of the precise perout timestamp. We put that
code into a common function and convert all call sites to call that. We
also need to do the same thing for the edge ignoring logic.

Signed-off-by: Vladimir Oltean <olteanv@gmail.com>
2020-08-30 02:56:49 +03:00

778 lines
18 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,
&timestamping, 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 int ts2phc_approximate_master_tstamp(struct ts2phc_private *priv,
tmv_t *master_tmv)
{
struct timespec master_ts;
tmv_t tmv;
int err;
err = ts2phc_master_getppstime(priv->master, &master_ts);
if (err < 0) {
pr_err("master ts not valid");
return err;
}
tmv = timespec_to_tmv(master_ts);
tmv = tmv_sub(tmv, priv->perout_phase);
master_ts = tmv_to_timespec(tmv);
/*
* As long as the kernel doesn't support a proper API for reporting
* a precise perout timestamp, we'll have to use this crude
* approximation.
*/
if (master_ts.tv_nsec > NS_PER_SEC / 2)
master_ts.tv_sec++;
master_ts.tv_nsec = 0;
tmv = timespec_to_tmv(master_ts);
tmv = tmv_add(tmv, priv->perout_phase);
*master_tmv = tmv;
return 0;
}
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 {
err = ts2phc_approximate_master_tstamp(priv, &source_tmv);
if (err < 0)
return;
}
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 int ts2phc_collect_master_tstamp(struct ts2phc_private *priv)
{
struct clock *master_clock;
tmv_t master_tmv;
int err;
master_clock = ts2phc_master_get_clock(priv->master);
/*
* Master isn't a PHC (it may be a generic or a GPS master),
* don't error out, just don't do anything. If it doesn't have a PHC,
* there is nothing to synchronize, which is the only point of
* collecting its perout timestamp in the first place.
*/
if (!master_clock)
return 0;
err = ts2phc_approximate_master_tstamp(priv, &master_tmv);
if (err < 0)
return err;
clock_add_tstamp(master_clock, master_tmv);
return 0;
}
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) {
const char *dev = interface_name(iface);
if (1 == config_get_int(cfg, dev, "ts2phc.master")) {
int perout_phase;
if (pps_source) {
fprintf(stderr, "too many PPS sources\n");
ts2phc_cleanup(&priv);
return -1;
}
pps_source = dev;
perout_phase = config_get_int(cfg, dev,
"ts2phc.perout_phase");
/*
* We use a default value of -1 to distinguish whether
* to use the PTP_PEROUT_PHASE API or not. But if we
* don't use that (and therefore we use absolute start
* time), the phase is still zero, by our application's
* convention.
*/
if (perout_phase < 0)
perout_phase = 0;
priv.perout_phase = nanoseconds_to_tmv(perout_phase);
} 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) {
err = ts2phc_collect_master_tstamp(&priv);
if (err) {
pr_err("failed to collect master tstamp");
break;
}
ts2phc_synchronize_clocks(&priv, autocfg);
}
}
ts2phc_cleanup(&priv);
return err;
}