Jacob Keller
2014-07-18 20:46:02 UTC
This is an updated version of a script I wrote a couple years ago for
debugging the PHC when writing a new driver. I figured that it might be
handy for the LinuxPTP project to include, as it can give some insight
into the PHC directly. I have updated it to make use of the shared code
here, in order to reduce duplication. Hopefully this is of some use to
everyone.
Signed-off-by: Jacob Keller <***@intel.com>
---
This version fixes several issues,
* make clean wasn't removing phc_ctl.o
* manpage used 1% instead of 10% clock adjustment
* default command was not properly added
.gitignore | 1 +
makefile | 6 +-
phc_ctl.8 | 108 ++++++++++++
phc_ctl.c | 571 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 684 insertions(+), 2 deletions(-)
create mode 100644 phc_ctl.8
create mode 100644 phc_ctl.c
diff --git a/.gitignore b/.gitignore
index 098dcdfe1ea7..0ca22afe2b9a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@
/phc2sys
/pmc
/ptp4l
+/phc_ctl
diff --git a/makefile b/makefile
index e36835b05d40..74a7fe20e88c 100644
--- a/makefile
+++ b/makefile
@@ -22,13 +22,13 @@ CC = $(CROSS_COMPILE)gcc
VER = -DVER=$(version)
CFLAGS = -Wall $(VER) $(incdefs) $(DEBUG) $(EXTRA_CFLAGS)
LDLIBS = -lm -lrt $(EXTRA_LDFLAGS)
-PRG = ptp4l pmc phc2sys hwstamp_ctl
+PRG = ptp4l pmc phc2sys hwstamp_ctl phc_ctl
OBJ = bmc.o clock.o clockadj.o clockcheck.o config.o fault.o \
filter.o fsm.o linreg.o mave.o mmedian.o msg.o ntpshm.o phc.o \
pi.o port.o print.o ptp4l.o raw.o servo.o sk.o stats.o tlv.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
+OBJECTS = $(OBJ) hwstamp_ctl.o phc2sys.o pmc.o pmc_common.o sysoff.o phc_ctl.o
SRC = $(OBJECTS:.o=.c)
DEPEND = $(OBJECTS:.o=.d)
srcdir := $(dir $(lastword $(MAKEFILE_LIST)))
@@ -54,6 +54,8 @@ phc2sys: clockadj.o clockcheck.o linreg.o msg.o ntpshm.o phc.o phc2sys.o pi.o \
hwstamp_ctl: hwstamp_ctl.o version.o
+phc_ctl: phc_ctl.o phc.o sk.o util.o clockadj.o sysoff.o print.o version.o
+
version.o: .version version.sh $(filter-out version.d,$(DEPEND))
.version: force
diff --git a/phc_ctl.8 b/phc_ctl.8
new file mode 100644
index 000000000000..609dbab16519
--- /dev/null
+++ b/phc_ctl.8
@@ -0,0 +1,108 @@
+.TH PHC_CTL 8 "June 2014" "linuxptp"
+.SH NAME
+phc_ctl \- directly control PHC device clock
+
+.SH SYNOPSIS
+.B phc_ctl
+[ options ] <device> [ commands ]
+
+.SH DESCRIPTION
+.B phc_ctl
+is a program which can be used to directly control a PHC clock device.
+Typically, it is used for debugging purposes, and has little use for general
+control of the device. For general control of PHC clock devices,
+.B phc2sys (8)
+should be preferred.
+
+<device> may be either CLOCK_REALTIME, any /dev/ptpX device, or any ethernet
+device which supports ethtool's get_ts_info ioctl.
+
+.SH OPTIONS
+.TP
+.BI \-l " print-level"
+Set the maximum syslog level of messages which should be printed or sent to the
+system logger. The default is 6 (LOG_INFO).
+.TP
+.BI \-q
+Do not send messages to syslog. By default messages will be sent.
+.TP
+.BI \-Q
+Do not print messages to standard output. By default messages will be printed.
+.TP
+.BI \-h
+Display a help message.
+.TP
+.B \-v
+Prints the software version and exits.
+
+.SH COMMANDS
+
+.B phc_ctl
+is controlled by passing commands which take either an optional or required
+parameter. The commands (outlined below) will control aspects of the PHC clock
+device. These commands may be useful for inspecting or debugging the PHC
+driver, but may have adverse side effects on running instances of
+.B ptp4l (8)
+or
+.B phc2sys (8)
+
+.TP
+.BI set " seconds"
+Set the PHC clock time to the value specified in seconds. Defaults to reading
+CLOCK_REALTIME if no value is provided.
+.TP
+.BI get
+Get the current time of the PHC clock device.
+.TP
+.BI adj " seconds"
+Adjust the PHC clock by an amount of seconds provided. This argument is required.
+.TP
+.BI freq " ppb"
+Adjust the frequency of the PHC clock by the specified parts per billion. If no
+argument is provided, it will attempt to read the current frequency and report
+it.
+.TP
+.BI cmp
+Compare the PHC clock device to CLOCK_REALTIME, using the best method available.
+.TP
+.BI caps
+Display the device capabiltiies. This is the default command if no commands are
+provided.
+.TP
+.BI wait " seconds"
+Sleep the process for the specified period of time, waking up and resuming
+afterwards. This command may be useful for sanity checking whether the PHC
+clock is running as expected.
+
+The arguments specified in seconds are read as double precision floating point
+values, and will scale to nanoseconds. This means providing a value of 5.5
+means 5 and one half seconds. This allows specifying fairly precise values for time.
+
+.SH EXAMPLES
+
+Read the current clock time from the device
+.RS
+\f(CWphc_ctl /dev/ptp0 get\fP
+.RE
+
+Set the PHC clock time to CLOCK_REALTIME
+.RS
+\f(CWphc_ctl /dev/ptp0 set\fP
+.RE
+
+Set PHC clock time to 0 (seconds since Epoch)
+.RS
+\f(CWphc_ctl /dev/ptp0 set 0.0\fP
+.RE
+
+Quickly sanity check frequency slewing by setting slewing frequency by positive
+10%, resetting clock to 0.0 time, waiting for 10 seconds, and then reading
+time. The time read back should be (roughly) 11 seconds, since the clock was
+slewed 10% faster.
+.RS
+\f(CWphc_ctl /dev/ptp0 freq 100000000 set 0.0 wait 10.0 get
+.RE
+
+.SH SEE ALSO
+.BR ptp4l (8)
+.BR phc2sys (8)
diff --git a/phc_ctl.c b/phc_ctl.c
new file mode 100644
index 000000000000..dc9c29ccf681
--- /dev/null
+++ b/phc_ctl.c
@@ -0,0 +1,571 @@
+/*
+ * @file phc_ctl.c
+ * @brief Utility program to directly control and debug a PHC device.
+ * @note Copyright (C) 2014 Jacob Keller <***@gmail.com>
+ *
+ * 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 <errno.h>
+#include <fcntl.h>
+#include <float.h>
+#include <signal.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <net/if.h>
+#include <poll.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/queue.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <math.h>
+
+#include <linux/pps.h>
+#include <linux/ptp_clock.h>
+
+#include "clockadj.h"
+#include "missing.h"
+#include "phc.h"
+#include "print.h"
+#include "sysoff.h"
+#include "util.h"
+#include "version.h"
+
+#define NSEC2SEC 1000000000.0
+
+/* trap the alarm signal so that pause() will wake up on receipt */
+static void handle_alarm(int s)
+{
+ return;
+}
+
+static void double_to_timespec(double d, struct timespec *ts)
+{
+ double fraction, whole;
+
+ fraction = modf(d, &whole);
+
+ /* cast the whole value to a time_t to store as seconds */
+ ts->tv_sec = (time_t)whole;
+ /* tv_nsec is a long, so we multiply the nanoseconds per second double
+ * value by our fractional component. This results in a correct
+ * timespec from the double representing seconds.
+ */
+ ts->tv_nsec = (long)(NSEC2SEC * fraction);
+}
+
+static int install_handler(int signum, void(*handler)(int))
+{
+ struct sigaction action;
+ sigset_t mask;
+
+ /* Unblock the signal */
+ sigemptyset(&mask);
+ sigaddset(&mask, signum);
+ sigprocmask(SIG_UNBLOCK, &mask, NULL);
+
+ /* Install the signal handler */
+ action.sa_handler = handler;
+ action.sa_flags = 0;
+ sigemptyset(&action.sa_mask);
+ sigaction(signum, &action, NULL);
+
+ return 0;
+}
+
+static int64_t calculate_offset(struct timespec *ts1,
+ struct timespec *rt,
+ struct timespec *ts2)
+{
+ int64_t interval;
+ int64_t offset;
+
+#define NSEC_PER_SEC 1000000000ULL
+ /* calculate interval between clock realtime */
+ interval = (ts2->tv_sec - ts1->tv_sec) * NSEC_PER_SEC;
+ interval += ts2->tv_nsec - ts1->tv_nsec;
+
+ /* assume PHC read occured half way between CLOCK_REALTIME reads */
+
+ offset = (rt->tv_sec - ts1->tv_sec) * NSEC_PER_SEC;
+ offset += (rt->tv_nsec - ts1->tv_nsec) - (interval / 2);
+
+ return offset;
+}
+
+static clockid_t clock_open(char *device)
+{
+ struct sk_ts_info ts_info;
+ char phc_device[16];
+ int clkid;
+
+ /* check if device is CLOCK_REALTIME */
+ if (!strcasecmp(device, "CLOCK_REALTIME"))
+ return CLOCK_REALTIME;
+
+ /* check if device is valid phc device */
+ clkid = phc_open(device);
+ if (clkid != CLOCK_INVALID)
+ return clkid;
+
+ /* check if device is a valid ethernet device */
+ if (sk_get_ts_info(device, &ts_info) || !ts_info.valid) {
+ pr_err("unknown clock %s: %m", device);
+ return CLOCK_INVALID;
+ }
+
+ if (ts_info.phc_index < 0) {
+ pr_err("interface %s does not have a PHC", device);
+ return CLOCK_INVALID;
+ }
+
+ sprintf(phc_device, "/dev/ptp%d", ts_info.phc_index);
+ clkid = phc_open(phc_device);
+ if (clkid == CLOCK_INVALID)
+ pr_err("cannot open %s for %s: %m", phc_device, device);
+ return clkid;
+}
+
+static void usage(const char *progname)
+{
+ fprintf(stderr,
+ "\n"
+ "usage: %s [options] <device> -- [command]\n\n"
+ " device ethernet or ptp clock device"
+ "\n"
+ " options\n"
+ " -l [num] set the logging level to 'num'\n"
+ " -q do not print messages to the syslog\n"
+ " -Q do not print messages to stdout\n"
+ " -v prints the software version and exits\n"
+ " -h prints this message and exits\n"
+ "\n"
+ " commands\n"
+ " specify commands with arguments. Can specify multiple\n"
+ " commands to be executed in order. Seconds are read as\n"
+ " double precision floating point values.\n"
+ " set [seconds] set PHC time (defaults to time on CLOCK_REALTIME)\n"
+ " get get PHC time\n"
+ " adj <seconds> adjust PHC time by offset\n"
+ " freq [ppb] adjust PHC frequency (default returns current offset)\n"
+ " cmp compare PHC offset to CLOCK_REALTIME\n"
+ " caps display device capabilities (default if no command given)\n"
+ " wait <seconds> pause between commands\n"
+ "\n",
+ progname);
+}
+
+typedef int (*cmd_func_t)(clockid_t, int, char *[]);
+struct cmd_t {
+ const char *name;
+ const cmd_func_t function;
+};
+
+static cmd_func_t get_command_function(const char *name);
+static inline int name_is_a_command(const char *name);
+
+static int do_set(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ struct timespec ts;
+ double time_arg = 0;
+ int args_to_eat = 0;
+
+ enum parser_result r;
+
+ memset(&ts, 0, sizeof(ts));
+
+ /* if we have no more arguments, or the next argument is the ";"
+ * separator, then we run set as default parameter mode */
+ if (cmdc < 1 || name_is_a_command(cmdv[0])) {
+ clock_gettime(CLOCK_REALTIME, &ts);
+
+ /* since we aren't using the options, we can simply ensure
+ * that we don't eat any arguments
+ */
+ args_to_eat = 0;
+ } else {
+ /* parse the double */
+ r = get_ranged_double(cmdv[0], &time_arg, 0.0, DBL_MAX);
+ switch (r) {
+ case PARSED_OK:
+ break;
+ case MALFORMED:
+ pr_err("set: '%s' is not a valid double", cmdv[0]);
+ return -2;
+ case OUT_OF_RANGE:
+ pr_err("set: '%s' is out of range", cmdv[0]);
+ return -2;
+ default:
+ pr_err("set: couldn't process '%s'", cmdv[0]);
+ return -2;
+ }
+
+ double_to_timespec(time_arg, &ts);
+
+ /* in order for processing to work, we need to ensure the
+ * run_cmds loop eats the optional set argument
+ */
+ args_to_eat = 1;
+ }
+
+ if (clock_settime(clkid, &ts)) {
+ pr_err("set: failed to set clock time: %s",
+ strerror(errno));
+ return -1;
+ } else {
+ pr_notice("set clock time to %ld.%09ld or %s",
+ ts.tv_sec, ts.tv_nsec, ctime(&ts.tv_sec));
+ }
+
+ return args_to_eat;
+}
+
+static int do_get(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ struct timespec ts;
+
+ memset(&ts, 0, sizeof(ts));
+ if (clock_gettime(clkid, &ts)) {
+ pr_err("get: failed to get clock time: %s",
+ strerror(errno));
+
+ return -1;
+ } else {
+ pr_notice("clock time is %ld.%09lu or %s",
+ ts.tv_sec, ts.tv_nsec, ctime(&ts.tv_sec));
+ }
+
+ /* get operation does not require any arguments */
+ return 0;
+}
+
+static int do_adj(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ double time_arg;
+ int64_t nsecs;
+ enum parser_result r;
+
+ if (cmdc < 1 || name_is_a_command(cmdv[0])) {
+ pr_err("adj: missing required time argument");
+ return -2;
+ }
+
+ /* parse the double time offset argument */
+ r = get_ranged_double(cmdv[0], &time_arg, DBL_MIN, DBL_MAX);
+ switch (r) {
+ case PARSED_OK:
+ break;
+ case MALFORMED:
+ pr_err("adj: '%s' is not a valid double", cmdv[0]);
+ return -2;
+ case OUT_OF_RANGE:
+ pr_err("adj: '%s' is out of range.", cmdv[0]);
+ return -2;
+ default:
+ pr_err("adj: couldn't process '%s'", cmdv[0]);
+ return -2;
+ }
+
+ nsecs = (int64_t)(NSEC2SEC * time_arg);
+
+ clockadj_init(clkid);
+ clockadj_step(clkid, nsecs);
+
+ pr_notice("adjusted clock by %lf seconds", time_arg);
+
+ /* adjustment always consumes one argument */
+ return 1;
+}
+
+static int do_freq(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ double ppb;
+ enum parser_result r;
+
+ clockadj_init(clkid);
+
+ if (cmdc < 1 || name_is_a_command(cmdv[0])) {
+ ppb = clockadj_get_freq(clkid);
+ pr_err("clock frequency offset is %lfppb", ppb);
+
+ /* no argument was used */
+ return 0;
+ }
+
+ /* parse the double ppb argument */
+ r = get_ranged_double(cmdv[0], &ppb, -NSEC2SEC, NSEC2SEC);
+ switch (r) {
+ case PARSED_OK:
+ break;
+ case MALFORMED:
+ pr_err("freq: '%s' is not a valid double", cmdv[0]);
+ return -2;
+ case OUT_OF_RANGE:
+ pr_err("freq: '%s' is out of range.", cmdv[0]);
+ return -2;
+ default:
+ pr_err("freq: couldn't process '%s'", cmdv[0]);
+ return -2;
+ }
+
+ clockadj_set_freq(clkid, ppb);
+ pr_err("adjusted clock frequency offset to %lfppb", ppb);
+
+ /* consumed one argument to determine the frequency adjustment value */
+ return 1;
+}
+
+static int do_caps(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ struct ptp_clock_caps caps;
+
+ if (clkid == CLOCK_REALTIME) {
+ pr_warning("CLOCK_REALTIME is not a PHC device.");
+ return 0;
+ }
+
+ if (ioctl(CLOCKID_TO_FD(clkid), PTP_CLOCK_GETCAPS, &caps)) {
+ pr_err("get capabilities failed: %s",
+ strerror(errno));
+ return -1;
+ }
+
+ pr_notice("\n"
+ "capabilities:\n"
+ " %d maximum frequency adjustment (ppb)\n"
+ " %d programable alarms\n"
+ " %d external time stamp channels\n"
+ " %d programmable periodic signals\n"
+ " %s pulse per second support",
+ caps.max_adj,
+ caps.n_alarm,
+ caps.n_ext_ts,
+ caps.n_per_out,
+ caps.pps ? "has" : "doesn't have");
+ return 0;
+}
+
+static int do_cmp(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ struct timespec ts, rta, rtb;
+ int64_t sys_offset, delay = 0, offset;
+ uint64_t sys_ts;
+
+ if (SYSOFF_SUPPORTED ==
+ sysoff_measure(CLOCKID_TO_FD(clkid),
+ 9, &sys_offset, &sys_ts, &delay)) {
+ pr_notice( "offset from CLOCK_REALTIME is %"PRId64"ns\n",
+ sys_offset);
+ return 0;
+ }
+
+ memset(&ts, 0, sizeof(ts));
+ memset(&ts, 0, sizeof(rta));
+ memset(&ts, 0, sizeof(rtb));
+ if (clock_gettime(CLOCK_REALTIME, &rta) ||
+ clock_gettime(clkid, &ts) ||
+ clock_gettime(CLOCK_REALTIME, &rtb)) {
+ pr_err("cmp: failed clock reads: %s\n",
+ strerror(errno));
+ return -1;
+ }
+
+ offset = calculate_offset(&rta, &ts, &rtb);
+ pr_notice( "offset from CLOCK_REALTIME is approximately %"PRId64"ns\n",
+ offset);
+
+ return 0;
+}
+
+static int do_wait(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ double time_arg;
+ struct timespec ts;
+ struct itimerval timer;
+ enum parser_result r;
+
+ if (cmdc < 1 || name_is_a_command(cmdv[0])) {
+ pr_err("wait: requires sleep duration argument\n");
+ return -2;
+ }
+
+ memset(&timer, 0, sizeof(timer));
+
+ /* parse the double time offset argument */
+ r = get_ranged_double(cmdv[0], &time_arg, 0.0, DBL_MAX);
+ switch (r) {
+ case PARSED_OK:
+ break;
+ case MALFORMED:
+ pr_err("wait: '%s' is not a valid double", cmdv[0]);
+ return -2;
+ case OUT_OF_RANGE:
+ pr_err("wait: '%s' is out of range.", cmdv[0]);
+ return -2;
+ default:
+ pr_err("wait: couldn't process '%s'", cmdv[0]);
+ return -2;
+ }
+
+ double_to_timespec(time_arg, &ts);
+ timer.it_value.tv_sec = ts.tv_sec;
+ timer.it_value.tv_usec = ts.tv_nsec / 1000;
+ setitimer(ITIMER_REAL, &timer, NULL);
+ pause();
+
+ /* the SIGALRM is already trapped during initialization, so we will
+ * wake up here once the alarm is handled.
+ */
+ pr_notice( "process slept for %lf seconds\n", time_arg);
+
+ return 1;
+}
+
+static const struct cmd_t all_commands[] = {
+ { "set", &do_set },
+ { "get", &do_get },
+ { "adj", &do_adj },
+ { "freq", &do_freq },
+ { "cmp", &do_cmp },
+ { "caps", &do_caps },
+ { "wait", &do_wait },
+ { 0, 0 }
+};
+
+static cmd_func_t get_command_function(const char *name)
+{
+ int i;
+ cmd_func_t cmd = NULL;
+
+ for (i = 0; all_commands[i].name != NULL; i++) {
+ if (!strncmp(name,
+ all_commands[i].name,
+ strlen(all_commands[i].name)))
+ cmd = all_commands[i].function;
+ }
+
+ return cmd;
+}
+
+static inline int name_is_a_command(const char *name)
+{
+ return get_command_function(name) != NULL;
+}
+
+static int run_cmds(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ int i = 0, result = 0;
+ cmd_func_t action = NULL;
+
+ while (i < cmdc) {
+ char *arg = cmdv[i];
+
+ /* increment now to remove the command argument */
+ i++;
+
+ action = get_command_function(arg);
+ if (action)
+ result = action(clkid, cmdc - i, &cmdv[i]);
+ else
+ pr_err("unknown command %s.", arg);
+
+ /* result is how many arguments were used up by the command,
+ * not including the ";". We will increment the loop counter
+ * to avoid processing the arguments as commands.
+ */
+ if (result < 0)
+ return result;
+ else
+ i += result;
+ }
+
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ const char *progname;
+ char **cmdv, *default_cmdv[] = { "caps" };
+ int c, result, cmdc;
+ int print_level = LOG_INFO, verbose = 1, use_syslog = 1;
+ clockid_t clkid;
+
+ install_handler(SIGALRM, handle_alarm);
+
+ /* Process the command line arguments. */
+ progname = strrchr(argv[0], '/');
+ progname = progname ? 1+progname : argv[0];
+ while (EOF != (c = getopt(argc, argv,
+ "l:qQvh"))) {
+ switch (c) {
+ case 'l':
+ if (get_arg_val_i(c, optarg, &print_level,
+ PRINT_LEVEL_MIN, PRINT_LEVEL_MAX))
+ return -1;
+ break;
+ case 'q':
+ use_syslog = 0;
+ break;
+ case 'Q':
+ verbose = 0;
+ break;
+ case 'v':
+ version_show(stdout);
+ return 0;
+ case 'h':
+ usage(progname);
+ return 0;
+ default:
+ usage(progname);
+ return -1;
+ }
+ }
+
+ print_set_progname(progname);
+ print_set_verbose(verbose);
+ print_set_syslog(use_syslog);
+ print_set_level(print_level);
+
+ if ((argc - optind) < 1) {
+ usage(progname);
+ return -1;
+ }
+
+ if ((argc - optind) == 1) {
+ cmdv = default_cmdv;
+ cmdc = 1;
+ } else {
+ cmdv = &argv[optind+1];
+ cmdc = argc - optind - 1;
+ }
+
+ clkid = clock_open(argv[optind]);
+ if (clkid == CLOCK_INVALID)
+ return -1;
+
+ /* pass the remaining arguments to the run_cmds loop */
+ result = run_cmds(clkid, cmdc, cmdv);
+ if (result < -1) {
+ /* show usage when command fails */
+ usage(progname);
+ return result;
+ }
+
+ return 0;
+}
debugging the PHC when writing a new driver. I figured that it might be
handy for the LinuxPTP project to include, as it can give some insight
into the PHC directly. I have updated it to make use of the shared code
here, in order to reduce duplication. Hopefully this is of some use to
everyone.
Signed-off-by: Jacob Keller <***@intel.com>
---
This version fixes several issues,
* make clean wasn't removing phc_ctl.o
* manpage used 1% instead of 10% clock adjustment
* default command was not properly added
.gitignore | 1 +
makefile | 6 +-
phc_ctl.8 | 108 ++++++++++++
phc_ctl.c | 571 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 684 insertions(+), 2 deletions(-)
create mode 100644 phc_ctl.8
create mode 100644 phc_ctl.c
diff --git a/.gitignore b/.gitignore
index 098dcdfe1ea7..0ca22afe2b9a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@
/phc2sys
/pmc
/ptp4l
+/phc_ctl
diff --git a/makefile b/makefile
index e36835b05d40..74a7fe20e88c 100644
--- a/makefile
+++ b/makefile
@@ -22,13 +22,13 @@ CC = $(CROSS_COMPILE)gcc
VER = -DVER=$(version)
CFLAGS = -Wall $(VER) $(incdefs) $(DEBUG) $(EXTRA_CFLAGS)
LDLIBS = -lm -lrt $(EXTRA_LDFLAGS)
-PRG = ptp4l pmc phc2sys hwstamp_ctl
+PRG = ptp4l pmc phc2sys hwstamp_ctl phc_ctl
OBJ = bmc.o clock.o clockadj.o clockcheck.o config.o fault.o \
filter.o fsm.o linreg.o mave.o mmedian.o msg.o ntpshm.o phc.o \
pi.o port.o print.o ptp4l.o raw.o servo.o sk.o stats.o tlv.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
+OBJECTS = $(OBJ) hwstamp_ctl.o phc2sys.o pmc.o pmc_common.o sysoff.o phc_ctl.o
SRC = $(OBJECTS:.o=.c)
DEPEND = $(OBJECTS:.o=.d)
srcdir := $(dir $(lastword $(MAKEFILE_LIST)))
@@ -54,6 +54,8 @@ phc2sys: clockadj.o clockcheck.o linreg.o msg.o ntpshm.o phc.o phc2sys.o pi.o \
hwstamp_ctl: hwstamp_ctl.o version.o
+phc_ctl: phc_ctl.o phc.o sk.o util.o clockadj.o sysoff.o print.o version.o
+
version.o: .version version.sh $(filter-out version.d,$(DEPEND))
.version: force
diff --git a/phc_ctl.8 b/phc_ctl.8
new file mode 100644
index 000000000000..609dbab16519
--- /dev/null
+++ b/phc_ctl.8
@@ -0,0 +1,108 @@
+.TH PHC_CTL 8 "June 2014" "linuxptp"
+.SH NAME
+phc_ctl \- directly control PHC device clock
+
+.SH SYNOPSIS
+.B phc_ctl
+[ options ] <device> [ commands ]
+
+.SH DESCRIPTION
+.B phc_ctl
+is a program which can be used to directly control a PHC clock device.
+Typically, it is used for debugging purposes, and has little use for general
+control of the device. For general control of PHC clock devices,
+.B phc2sys (8)
+should be preferred.
+
+<device> may be either CLOCK_REALTIME, any /dev/ptpX device, or any ethernet
+device which supports ethtool's get_ts_info ioctl.
+
+.SH OPTIONS
+.TP
+.BI \-l " print-level"
+Set the maximum syslog level of messages which should be printed or sent to the
+system logger. The default is 6 (LOG_INFO).
+.TP
+.BI \-q
+Do not send messages to syslog. By default messages will be sent.
+.TP
+.BI \-Q
+Do not print messages to standard output. By default messages will be printed.
+.TP
+.BI \-h
+Display a help message.
+.TP
+.B \-v
+Prints the software version and exits.
+
+.SH COMMANDS
+
+.B phc_ctl
+is controlled by passing commands which take either an optional or required
+parameter. The commands (outlined below) will control aspects of the PHC clock
+device. These commands may be useful for inspecting or debugging the PHC
+driver, but may have adverse side effects on running instances of
+.B ptp4l (8)
+or
+.B phc2sys (8)
+
+.TP
+.BI set " seconds"
+Set the PHC clock time to the value specified in seconds. Defaults to reading
+CLOCK_REALTIME if no value is provided.
+.TP
+.BI get
+Get the current time of the PHC clock device.
+.TP
+.BI adj " seconds"
+Adjust the PHC clock by an amount of seconds provided. This argument is required.
+.TP
+.BI freq " ppb"
+Adjust the frequency of the PHC clock by the specified parts per billion. If no
+argument is provided, it will attempt to read the current frequency and report
+it.
+.TP
+.BI cmp
+Compare the PHC clock device to CLOCK_REALTIME, using the best method available.
+.TP
+.BI caps
+Display the device capabiltiies. This is the default command if no commands are
+provided.
+.TP
+.BI wait " seconds"
+Sleep the process for the specified period of time, waking up and resuming
+afterwards. This command may be useful for sanity checking whether the PHC
+clock is running as expected.
+
+The arguments specified in seconds are read as double precision floating point
+values, and will scale to nanoseconds. This means providing a value of 5.5
+means 5 and one half seconds. This allows specifying fairly precise values for time.
+
+.SH EXAMPLES
+
+Read the current clock time from the device
+.RS
+\f(CWphc_ctl /dev/ptp0 get\fP
+.RE
+
+Set the PHC clock time to CLOCK_REALTIME
+.RS
+\f(CWphc_ctl /dev/ptp0 set\fP
+.RE
+
+Set PHC clock time to 0 (seconds since Epoch)
+.RS
+\f(CWphc_ctl /dev/ptp0 set 0.0\fP
+.RE
+
+Quickly sanity check frequency slewing by setting slewing frequency by positive
+10%, resetting clock to 0.0 time, waiting for 10 seconds, and then reading
+time. The time read back should be (roughly) 11 seconds, since the clock was
+slewed 10% faster.
+.RS
+\f(CWphc_ctl /dev/ptp0 freq 100000000 set 0.0 wait 10.0 get
+.RE
+
+.SH SEE ALSO
+.BR ptp4l (8)
+.BR phc2sys (8)
diff --git a/phc_ctl.c b/phc_ctl.c
new file mode 100644
index 000000000000..dc9c29ccf681
--- /dev/null
+++ b/phc_ctl.c
@@ -0,0 +1,571 @@
+/*
+ * @file phc_ctl.c
+ * @brief Utility program to directly control and debug a PHC device.
+ * @note Copyright (C) 2014 Jacob Keller <***@gmail.com>
+ *
+ * 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 <errno.h>
+#include <fcntl.h>
+#include <float.h>
+#include <signal.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <net/if.h>
+#include <poll.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/queue.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <math.h>
+
+#include <linux/pps.h>
+#include <linux/ptp_clock.h>
+
+#include "clockadj.h"
+#include "missing.h"
+#include "phc.h"
+#include "print.h"
+#include "sysoff.h"
+#include "util.h"
+#include "version.h"
+
+#define NSEC2SEC 1000000000.0
+
+/* trap the alarm signal so that pause() will wake up on receipt */
+static void handle_alarm(int s)
+{
+ return;
+}
+
+static void double_to_timespec(double d, struct timespec *ts)
+{
+ double fraction, whole;
+
+ fraction = modf(d, &whole);
+
+ /* cast the whole value to a time_t to store as seconds */
+ ts->tv_sec = (time_t)whole;
+ /* tv_nsec is a long, so we multiply the nanoseconds per second double
+ * value by our fractional component. This results in a correct
+ * timespec from the double representing seconds.
+ */
+ ts->tv_nsec = (long)(NSEC2SEC * fraction);
+}
+
+static int install_handler(int signum, void(*handler)(int))
+{
+ struct sigaction action;
+ sigset_t mask;
+
+ /* Unblock the signal */
+ sigemptyset(&mask);
+ sigaddset(&mask, signum);
+ sigprocmask(SIG_UNBLOCK, &mask, NULL);
+
+ /* Install the signal handler */
+ action.sa_handler = handler;
+ action.sa_flags = 0;
+ sigemptyset(&action.sa_mask);
+ sigaction(signum, &action, NULL);
+
+ return 0;
+}
+
+static int64_t calculate_offset(struct timespec *ts1,
+ struct timespec *rt,
+ struct timespec *ts2)
+{
+ int64_t interval;
+ int64_t offset;
+
+#define NSEC_PER_SEC 1000000000ULL
+ /* calculate interval between clock realtime */
+ interval = (ts2->tv_sec - ts1->tv_sec) * NSEC_PER_SEC;
+ interval += ts2->tv_nsec - ts1->tv_nsec;
+
+ /* assume PHC read occured half way between CLOCK_REALTIME reads */
+
+ offset = (rt->tv_sec - ts1->tv_sec) * NSEC_PER_SEC;
+ offset += (rt->tv_nsec - ts1->tv_nsec) - (interval / 2);
+
+ return offset;
+}
+
+static clockid_t clock_open(char *device)
+{
+ struct sk_ts_info ts_info;
+ char phc_device[16];
+ int clkid;
+
+ /* check if device is CLOCK_REALTIME */
+ if (!strcasecmp(device, "CLOCK_REALTIME"))
+ return CLOCK_REALTIME;
+
+ /* check if device is valid phc device */
+ clkid = phc_open(device);
+ if (clkid != CLOCK_INVALID)
+ return clkid;
+
+ /* check if device is a valid ethernet device */
+ if (sk_get_ts_info(device, &ts_info) || !ts_info.valid) {
+ pr_err("unknown clock %s: %m", device);
+ return CLOCK_INVALID;
+ }
+
+ if (ts_info.phc_index < 0) {
+ pr_err("interface %s does not have a PHC", device);
+ return CLOCK_INVALID;
+ }
+
+ sprintf(phc_device, "/dev/ptp%d", ts_info.phc_index);
+ clkid = phc_open(phc_device);
+ if (clkid == CLOCK_INVALID)
+ pr_err("cannot open %s for %s: %m", phc_device, device);
+ return clkid;
+}
+
+static void usage(const char *progname)
+{
+ fprintf(stderr,
+ "\n"
+ "usage: %s [options] <device> -- [command]\n\n"
+ " device ethernet or ptp clock device"
+ "\n"
+ " options\n"
+ " -l [num] set the logging level to 'num'\n"
+ " -q do not print messages to the syslog\n"
+ " -Q do not print messages to stdout\n"
+ " -v prints the software version and exits\n"
+ " -h prints this message and exits\n"
+ "\n"
+ " commands\n"
+ " specify commands with arguments. Can specify multiple\n"
+ " commands to be executed in order. Seconds are read as\n"
+ " double precision floating point values.\n"
+ " set [seconds] set PHC time (defaults to time on CLOCK_REALTIME)\n"
+ " get get PHC time\n"
+ " adj <seconds> adjust PHC time by offset\n"
+ " freq [ppb] adjust PHC frequency (default returns current offset)\n"
+ " cmp compare PHC offset to CLOCK_REALTIME\n"
+ " caps display device capabilities (default if no command given)\n"
+ " wait <seconds> pause between commands\n"
+ "\n",
+ progname);
+}
+
+typedef int (*cmd_func_t)(clockid_t, int, char *[]);
+struct cmd_t {
+ const char *name;
+ const cmd_func_t function;
+};
+
+static cmd_func_t get_command_function(const char *name);
+static inline int name_is_a_command(const char *name);
+
+static int do_set(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ struct timespec ts;
+ double time_arg = 0;
+ int args_to_eat = 0;
+
+ enum parser_result r;
+
+ memset(&ts, 0, sizeof(ts));
+
+ /* if we have no more arguments, or the next argument is the ";"
+ * separator, then we run set as default parameter mode */
+ if (cmdc < 1 || name_is_a_command(cmdv[0])) {
+ clock_gettime(CLOCK_REALTIME, &ts);
+
+ /* since we aren't using the options, we can simply ensure
+ * that we don't eat any arguments
+ */
+ args_to_eat = 0;
+ } else {
+ /* parse the double */
+ r = get_ranged_double(cmdv[0], &time_arg, 0.0, DBL_MAX);
+ switch (r) {
+ case PARSED_OK:
+ break;
+ case MALFORMED:
+ pr_err("set: '%s' is not a valid double", cmdv[0]);
+ return -2;
+ case OUT_OF_RANGE:
+ pr_err("set: '%s' is out of range", cmdv[0]);
+ return -2;
+ default:
+ pr_err("set: couldn't process '%s'", cmdv[0]);
+ return -2;
+ }
+
+ double_to_timespec(time_arg, &ts);
+
+ /* in order for processing to work, we need to ensure the
+ * run_cmds loop eats the optional set argument
+ */
+ args_to_eat = 1;
+ }
+
+ if (clock_settime(clkid, &ts)) {
+ pr_err("set: failed to set clock time: %s",
+ strerror(errno));
+ return -1;
+ } else {
+ pr_notice("set clock time to %ld.%09ld or %s",
+ ts.tv_sec, ts.tv_nsec, ctime(&ts.tv_sec));
+ }
+
+ return args_to_eat;
+}
+
+static int do_get(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ struct timespec ts;
+
+ memset(&ts, 0, sizeof(ts));
+ if (clock_gettime(clkid, &ts)) {
+ pr_err("get: failed to get clock time: %s",
+ strerror(errno));
+
+ return -1;
+ } else {
+ pr_notice("clock time is %ld.%09lu or %s",
+ ts.tv_sec, ts.tv_nsec, ctime(&ts.tv_sec));
+ }
+
+ /* get operation does not require any arguments */
+ return 0;
+}
+
+static int do_adj(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ double time_arg;
+ int64_t nsecs;
+ enum parser_result r;
+
+ if (cmdc < 1 || name_is_a_command(cmdv[0])) {
+ pr_err("adj: missing required time argument");
+ return -2;
+ }
+
+ /* parse the double time offset argument */
+ r = get_ranged_double(cmdv[0], &time_arg, DBL_MIN, DBL_MAX);
+ switch (r) {
+ case PARSED_OK:
+ break;
+ case MALFORMED:
+ pr_err("adj: '%s' is not a valid double", cmdv[0]);
+ return -2;
+ case OUT_OF_RANGE:
+ pr_err("adj: '%s' is out of range.", cmdv[0]);
+ return -2;
+ default:
+ pr_err("adj: couldn't process '%s'", cmdv[0]);
+ return -2;
+ }
+
+ nsecs = (int64_t)(NSEC2SEC * time_arg);
+
+ clockadj_init(clkid);
+ clockadj_step(clkid, nsecs);
+
+ pr_notice("adjusted clock by %lf seconds", time_arg);
+
+ /* adjustment always consumes one argument */
+ return 1;
+}
+
+static int do_freq(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ double ppb;
+ enum parser_result r;
+
+ clockadj_init(clkid);
+
+ if (cmdc < 1 || name_is_a_command(cmdv[0])) {
+ ppb = clockadj_get_freq(clkid);
+ pr_err("clock frequency offset is %lfppb", ppb);
+
+ /* no argument was used */
+ return 0;
+ }
+
+ /* parse the double ppb argument */
+ r = get_ranged_double(cmdv[0], &ppb, -NSEC2SEC, NSEC2SEC);
+ switch (r) {
+ case PARSED_OK:
+ break;
+ case MALFORMED:
+ pr_err("freq: '%s' is not a valid double", cmdv[0]);
+ return -2;
+ case OUT_OF_RANGE:
+ pr_err("freq: '%s' is out of range.", cmdv[0]);
+ return -2;
+ default:
+ pr_err("freq: couldn't process '%s'", cmdv[0]);
+ return -2;
+ }
+
+ clockadj_set_freq(clkid, ppb);
+ pr_err("adjusted clock frequency offset to %lfppb", ppb);
+
+ /* consumed one argument to determine the frequency adjustment value */
+ return 1;
+}
+
+static int do_caps(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ struct ptp_clock_caps caps;
+
+ if (clkid == CLOCK_REALTIME) {
+ pr_warning("CLOCK_REALTIME is not a PHC device.");
+ return 0;
+ }
+
+ if (ioctl(CLOCKID_TO_FD(clkid), PTP_CLOCK_GETCAPS, &caps)) {
+ pr_err("get capabilities failed: %s",
+ strerror(errno));
+ return -1;
+ }
+
+ pr_notice("\n"
+ "capabilities:\n"
+ " %d maximum frequency adjustment (ppb)\n"
+ " %d programable alarms\n"
+ " %d external time stamp channels\n"
+ " %d programmable periodic signals\n"
+ " %s pulse per second support",
+ caps.max_adj,
+ caps.n_alarm,
+ caps.n_ext_ts,
+ caps.n_per_out,
+ caps.pps ? "has" : "doesn't have");
+ return 0;
+}
+
+static int do_cmp(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ struct timespec ts, rta, rtb;
+ int64_t sys_offset, delay = 0, offset;
+ uint64_t sys_ts;
+
+ if (SYSOFF_SUPPORTED ==
+ sysoff_measure(CLOCKID_TO_FD(clkid),
+ 9, &sys_offset, &sys_ts, &delay)) {
+ pr_notice( "offset from CLOCK_REALTIME is %"PRId64"ns\n",
+ sys_offset);
+ return 0;
+ }
+
+ memset(&ts, 0, sizeof(ts));
+ memset(&ts, 0, sizeof(rta));
+ memset(&ts, 0, sizeof(rtb));
+ if (clock_gettime(CLOCK_REALTIME, &rta) ||
+ clock_gettime(clkid, &ts) ||
+ clock_gettime(CLOCK_REALTIME, &rtb)) {
+ pr_err("cmp: failed clock reads: %s\n",
+ strerror(errno));
+ return -1;
+ }
+
+ offset = calculate_offset(&rta, &ts, &rtb);
+ pr_notice( "offset from CLOCK_REALTIME is approximately %"PRId64"ns\n",
+ offset);
+
+ return 0;
+}
+
+static int do_wait(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ double time_arg;
+ struct timespec ts;
+ struct itimerval timer;
+ enum parser_result r;
+
+ if (cmdc < 1 || name_is_a_command(cmdv[0])) {
+ pr_err("wait: requires sleep duration argument\n");
+ return -2;
+ }
+
+ memset(&timer, 0, sizeof(timer));
+
+ /* parse the double time offset argument */
+ r = get_ranged_double(cmdv[0], &time_arg, 0.0, DBL_MAX);
+ switch (r) {
+ case PARSED_OK:
+ break;
+ case MALFORMED:
+ pr_err("wait: '%s' is not a valid double", cmdv[0]);
+ return -2;
+ case OUT_OF_RANGE:
+ pr_err("wait: '%s' is out of range.", cmdv[0]);
+ return -2;
+ default:
+ pr_err("wait: couldn't process '%s'", cmdv[0]);
+ return -2;
+ }
+
+ double_to_timespec(time_arg, &ts);
+ timer.it_value.tv_sec = ts.tv_sec;
+ timer.it_value.tv_usec = ts.tv_nsec / 1000;
+ setitimer(ITIMER_REAL, &timer, NULL);
+ pause();
+
+ /* the SIGALRM is already trapped during initialization, so we will
+ * wake up here once the alarm is handled.
+ */
+ pr_notice( "process slept for %lf seconds\n", time_arg);
+
+ return 1;
+}
+
+static const struct cmd_t all_commands[] = {
+ { "set", &do_set },
+ { "get", &do_get },
+ { "adj", &do_adj },
+ { "freq", &do_freq },
+ { "cmp", &do_cmp },
+ { "caps", &do_caps },
+ { "wait", &do_wait },
+ { 0, 0 }
+};
+
+static cmd_func_t get_command_function(const char *name)
+{
+ int i;
+ cmd_func_t cmd = NULL;
+
+ for (i = 0; all_commands[i].name != NULL; i++) {
+ if (!strncmp(name,
+ all_commands[i].name,
+ strlen(all_commands[i].name)))
+ cmd = all_commands[i].function;
+ }
+
+ return cmd;
+}
+
+static inline int name_is_a_command(const char *name)
+{
+ return get_command_function(name) != NULL;
+}
+
+static int run_cmds(clockid_t clkid, int cmdc, char *cmdv[])
+{
+ int i = 0, result = 0;
+ cmd_func_t action = NULL;
+
+ while (i < cmdc) {
+ char *arg = cmdv[i];
+
+ /* increment now to remove the command argument */
+ i++;
+
+ action = get_command_function(arg);
+ if (action)
+ result = action(clkid, cmdc - i, &cmdv[i]);
+ else
+ pr_err("unknown command %s.", arg);
+
+ /* result is how many arguments were used up by the command,
+ * not including the ";". We will increment the loop counter
+ * to avoid processing the arguments as commands.
+ */
+ if (result < 0)
+ return result;
+ else
+ i += result;
+ }
+
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ const char *progname;
+ char **cmdv, *default_cmdv[] = { "caps" };
+ int c, result, cmdc;
+ int print_level = LOG_INFO, verbose = 1, use_syslog = 1;
+ clockid_t clkid;
+
+ install_handler(SIGALRM, handle_alarm);
+
+ /* Process the command line arguments. */
+ progname = strrchr(argv[0], '/');
+ progname = progname ? 1+progname : argv[0];
+ while (EOF != (c = getopt(argc, argv,
+ "l:qQvh"))) {
+ switch (c) {
+ case 'l':
+ if (get_arg_val_i(c, optarg, &print_level,
+ PRINT_LEVEL_MIN, PRINT_LEVEL_MAX))
+ return -1;
+ break;
+ case 'q':
+ use_syslog = 0;
+ break;
+ case 'Q':
+ verbose = 0;
+ break;
+ case 'v':
+ version_show(stdout);
+ return 0;
+ case 'h':
+ usage(progname);
+ return 0;
+ default:
+ usage(progname);
+ return -1;
+ }
+ }
+
+ print_set_progname(progname);
+ print_set_verbose(verbose);
+ print_set_syslog(use_syslog);
+ print_set_level(print_level);
+
+ if ((argc - optind) < 1) {
+ usage(progname);
+ return -1;
+ }
+
+ if ((argc - optind) == 1) {
+ cmdv = default_cmdv;
+ cmdc = 1;
+ } else {
+ cmdv = &argv[optind+1];
+ cmdc = argc - optind - 1;
+ }
+
+ clkid = clock_open(argv[optind]);
+ if (clkid == CLOCK_INVALID)
+ return -1;
+
+ /* pass the remaining arguments to the run_cmds loop */
+ result = run_cmds(clkid, cmdc, cmdv);
+ if (result < -1) {
+ /* show usage when command fails */
+ usage(progname);
+ return result;
+ }
+
+ return 0;
+}
--
2.0.1.475.g9b8d714
2.0.1.475.g9b8d714