From 58b1f3f37d5d0194aa28f9026eb17b3329bf26c4 Mon Sep 17 00:00:00 2001 From: Miroslav Lichvar Date: Thu, 7 Mar 2013 17:27:28 +0100 Subject: [PATCH 1/6] phc2sys: Update sync offset periodically with -w. Modify the pmc to allow non-blocking operation. Run it on each clock update to have the sync offset updated from currentUtcOffset with every other call. Signed-off-by: Miroslav Lichvar --- phc2sys.8 | 15 +++--- phc2sys.c | 152 +++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 112 insertions(+), 55 deletions(-) diff --git a/phc2sys.8 b/phc2sys.8 index 0542ba7..10faea9 100644 --- a/phc2sys.8 +++ b/phc2sys.8 @@ -106,13 +106,10 @@ minimize the error caused by random delays in scheduling and bus utilization. The default is 5. .TP .BI \-O " offset" -Specify the offset between the slave and master times in seconds. With the +Specify the offset between the slave and master times in seconds. The default +is set automatically with the .B \-w -option the default value is set automatically according to the currentUtcOffset -value obtained from ptp4l and the direction of the clock synchronization. -Without -.B \-w -the default is 0. +option, 0 otherwise. .TP .BI \-u " summary-updates" Specify the number of clock updates included in summary statistics. The @@ -124,7 +121,11 @@ statistics. The messages are printed at the LOG_INFO level. The default is 0 (disabled). .TP .B \-w -Wait until ptp4l is in a synchronized state. +Wait until ptp4l is in a synchronized state. If the +.B \-O +option is not used, also keep the offset between the slave and master +times updated according to the currentUtcOffset value obtained from ptp4l and +the direction of the clock synchronization. .TP .BI \-l " print-level" Set the maximum syslog level of messages which should be printed or sent to diff --git a/phc2sys.c b/phc2sys.c index 582492d..a600709 100644 --- a/phc2sys.c +++ b/phc2sys.c @@ -55,6 +55,9 @@ #define PHC_PPS_OFFSET_LIMIT 10000000 +struct clock; +static int update_sync_offset(struct clock *clock); + static clockid_t clock_open(char *device) { int fd; @@ -160,6 +163,11 @@ struct clock { struct stats *freq_stats; struct stats *delay_stats; unsigned int stats_max_count; + int sync_offset; + int sync_offset_direction; + struct pmc *pmc; + int pmc_ds_idx; + int pmc_ds_requested; }; static void update_clock_stats(struct clock *clock, @@ -203,6 +211,13 @@ static void update_clock(struct clock *clock, enum servo_state state; double ppb; + if (update_sync_offset(clock)) + return; + + if (clock->sync_offset_direction) + offset += clock->sync_offset * NS_PER_SEC * + clock->sync_offset_direction; + ppb = servo_sample(clock->servo, offset, ts, &state); switch (state) { @@ -253,13 +268,17 @@ static int read_pps(int fd, int64_t *offset, uint64_t *ts) } static int do_pps_loop(struct clock *clock, int fd, - clockid_t src, int n_readings, int sync_offset) + clockid_t src, int n_readings) { int64_t pps_offset, phc_offset, phc_delay; uint64_t pps_ts, phc_ts; clock->source_label = "pps"; + /* The sync offset can't be applied with PPS alone. */ + if (src == CLOCK_INVALID) + clock->sync_offset_direction = 0; + while (1) { if (!read_pps(fd, &pps_offset, &pps_ts)) { continue; @@ -284,7 +303,6 @@ static int do_pps_loop(struct clock *clock, int fd, phc_ts = phc_ts / NS_PER_SEC * NS_PER_SEC; pps_offset = pps_ts - phc_ts; - pps_offset -= sync_offset * NS_PER_SEC; } update_clock(clock, pps_offset, pps_ts, -1); @@ -294,7 +312,7 @@ static int do_pps_loop(struct clock *clock, int fd, } static int do_sysoff_loop(struct clock *clock, clockid_t src, - int rate, int n_readings, int sync_offset) + int rate, int n_readings) { uint64_t ts; int64_t offset, delay; @@ -308,14 +326,13 @@ static int do_sysoff_loop(struct clock *clock, clockid_t src, err = -1; break; } - offset -= sync_offset * NS_PER_SEC; update_clock(clock, offset, ts, delay); } return err; } static int do_phc_loop(struct clock *clock, clockid_t src, - int rate, int n_readings, int sync_offset) + int rate, int n_readings) { uint64_t ts; int64_t offset, delay; @@ -328,7 +345,6 @@ static int do_phc_loop(struct clock *clock, clockid_t src, &offset, &ts, &delay)) { continue; } - offset -= sync_offset * NS_PER_SEC; update_clock(clock, offset, ts, delay); } return 0; @@ -360,58 +376,75 @@ static void *get_mgt_data(struct ptp_message *msg) return ((struct management_tlv *) msg->management.suffix)->data; } -static int run_pmc(int wait_sync, int *utc_offset) +static int init_pmc(struct clock *clock) +{ + clock->pmc = pmc_create(TRANS_UDS, "/var/run/phc2sys", 0, 0, 0); + if (!clock->pmc) { + pr_err("failed to create pmc"); + return -1; + } + + return 0; +} + +static int run_pmc(struct clock *clock, int timeout, + int wait_sync, int get_utc_offset) { struct ptp_message *msg; - struct pmc *pmc; void *data; #define N_FD 1 struct pollfd pollfd[N_FD]; + int cnt, ds_done; #define N_ID 2 - int cnt, i = 0, ds_done, ds_requested = 0; int ds_ids[N_ID] = { PORT_DATA_SET, TIME_PROPERTIES_DATA_SET }; - pmc = pmc_create(TRANS_UDS, "/var/run/phc2sys", 0, 0, 0); - if (!pmc) { - pr_err("failed to create pmc"); - return -1; - } + while (clock->pmc_ds_idx < N_ID) { + /* Check if the data set is really needed. */ + if ((ds_ids[clock->pmc_ds_idx] == PORT_DATA_SET && + !wait_sync) || + (ds_ids[clock->pmc_ds_idx] == TIME_PROPERTIES_DATA_SET && + !get_utc_offset)) { + clock->pmc_ds_idx++; + continue; + } - while (i < N_ID) { - pollfd[0].fd = pmc_get_transport_fd(pmc); + pollfd[0].fd = pmc_get_transport_fd(clock->pmc); pollfd[0].events = POLLIN|POLLPRI; - if (!ds_requested) + if (!clock->pmc_ds_requested) pollfd[0].events |= POLLOUT; - cnt = poll(pollfd, N_FD, 1000); + cnt = poll(pollfd, N_FD, timeout); if (cnt < 0) { pr_err("poll failed"); return -1; } if (!cnt) { - /* Request the data set again. */ - ds_requested = 0; - pr_notice("Waiting for ptp4l..."); - continue; + /* Request the data set again in the next run. */ + clock->pmc_ds_requested = 0; + return 0; } - if (pollfd[0].revents & POLLOUT) { - pmc_send_get_action(pmc, ds_ids[i]); - ds_requested = 1; + /* Send a new request if there are no pending messages. */ + if ((pollfd[0].revents & POLLOUT) && + !(pollfd[0].revents & (POLLIN|POLLPRI))) { + pmc_send_get_action(clock->pmc, + ds_ids[clock->pmc_ds_idx]); + clock->pmc_ds_requested = 1; } if (!(pollfd[0].revents & (POLLIN|POLLPRI))) continue; - msg = pmc_recv(pmc); + msg = pmc_recv(clock->pmc); if (!msg) continue; - if (!is_msg_mgt(msg) || get_mgt_id(msg) != ds_ids[i]) { + if (!is_msg_mgt(msg) || + get_mgt_id(msg) != ds_ids[clock->pmc_ds_idx]) { msg_put(msg); continue; } @@ -421,9 +454,6 @@ static int run_pmc(int wait_sync, int *utc_offset) switch (get_mgt_id(msg)) { case PORT_DATA_SET: - if (!wait_sync) - ds_done = 1; - switch (((struct portDS *)data)->portState) { case PS_MASTER: case PS_SLAVE: @@ -433,22 +463,35 @@ static int run_pmc(int wait_sync, int *utc_offset) break; case TIME_PROPERTIES_DATA_SET: - *utc_offset = ((struct timePropertiesDS *)data)-> - currentUtcOffset; + clock->sync_offset = ((struct timePropertiesDS *)data)-> + currentUtcOffset; ds_done = 1; break; } if (ds_done) { /* Proceed with the next data set. */ - i++; - ds_requested = 0; + clock->pmc_ds_idx++; + clock->pmc_ds_requested = 0; } msg_put(msg); } - pmc_destroy(pmc); + clock->pmc_ds_idx = 0; + return 1; +} +static void close_pmc(struct clock *clock) +{ + pmc_destroy(clock->pmc); + clock->pmc = NULL; +} + +static int update_sync_offset(struct clock *clock) +{ + if (clock->pmc) { + run_pmc(clock, 0, 0, 1); + } return 0; } @@ -479,8 +522,8 @@ int main(int argc, char *argv[]) { char *progname, *ethdev = NULL; clockid_t src = CLOCK_INVALID; - int c, phc_readings = 5, phc_rate = 1, sync_offset = 0, pps_fd = -1; - int wait_sync = 0, forced_sync_offset = 0; + int c, phc_readings = 5, phc_rate = 1, pps_fd = -1; + int r, wait_sync = 0, forced_sync_offset = 0; int print_level = LOG_INFO, use_syslog = 1, verbose = 0; double ppb; struct clock dst_clock = { .clkid = CLOCK_REALTIME }; @@ -524,7 +567,8 @@ int main(int argc, char *argv[]) phc_readings = atoi(optarg); break; case 'O': - sync_offset = atoi(optarg); + dst_clock.sync_offset = atoi(optarg); + dst_clock.sync_offset_direction = -1; forced_sync_offset = 1; break; case 'i': @@ -596,18 +640,33 @@ int main(int argc, char *argv[]) print_set_level(print_level); if (wait_sync) { - int ptp_utc_offset; + if (init_pmc(&dst_clock)) + return -1; - run_pmc(wait_sync, &ptp_utc_offset); + while (1) { + r = run_pmc(&dst_clock, 1000, + wait_sync, !forced_sync_offset); + if (r < 0) + return -1; + else if (r > 0) + break; + else + pr_notice("Waiting for ptp4l..."); + } if (!forced_sync_offset) { if (src != CLOCK_REALTIME && dst_clock.clkid == CLOCK_REALTIME) - sync_offset = -ptp_utc_offset; + dst_clock.sync_offset_direction = 1; else if (src == CLOCK_REALTIME && dst_clock.clkid != CLOCK_REALTIME) - sync_offset = ptp_utc_offset; + dst_clock.sync_offset_direction = -1; + else + dst_clock.sync_offset_direction = 0; } + + if (forced_sync_offset || !dst_clock.sync_offset_direction) + close_pmc(&dst_clock); } ppb = clock_ppb_read(dst_clock.clkid); @@ -618,14 +677,11 @@ int main(int argc, char *argv[]) dst_clock.servo = servo_create(CLOCK_SERVO_PI, -ppb, max_ppb, 0); if (pps_fd >= 0) - return do_pps_loop(&dst_clock, pps_fd, src, - phc_readings, sync_offset); + return do_pps_loop(&dst_clock, pps_fd, src, phc_readings); if (dst_clock.clkid == CLOCK_REALTIME && SYSOFF_SUPPORTED == sysoff_probe(CLOCKID_TO_FD(src), phc_readings)) - return do_sysoff_loop(&dst_clock, src, phc_rate, - phc_readings, sync_offset); + return do_sysoff_loop(&dst_clock, src, phc_rate, phc_readings); - return do_phc_loop(&dst_clock, src, phc_rate, - phc_readings, sync_offset); + return do_phc_loop(&dst_clock, src, phc_rate, phc_readings); } From a29b1a41058ec56b971bda42315abc358225dc29 Mon Sep 17 00:00:00 2001 From: Miroslav Lichvar Date: Thu, 7 Mar 2013 17:27:29 +0100 Subject: [PATCH 2/6] Move clock_adjtime wrapping to clockadj.c. Signed-off-by: Miroslav Lichvar --- clock.c | 55 ++++------------------------------------- clockadj.c | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ clockadj.h | 47 +++++++++++++++++++++++++++++++++++ makefile | 10 ++++---- phc2sys.c | 56 ++++-------------------------------------- 5 files changed, 134 insertions(+), 106 deletions(-) create mode 100644 clockadj.c create mode 100644 clockadj.h diff --git a/clock.c b/clock.c index 96be630..71c4791 100644 --- a/clock.c +++ b/clock.c @@ -24,6 +24,7 @@ #include "bmc.h" #include "clock.h" +#include "clockadj.h" #include "foreign.h" #include "mave.h" #include "missing.h" @@ -404,52 +405,6 @@ static enum servo_state clock_no_adjust(struct clock *c) return state; } -static void clock_ppb(clockid_t clkid, double ppb) -{ - struct timex tx; - memset(&tx, 0, sizeof(tx)); - tx.modes = ADJ_FREQUENCY; - tx.freq = (long) (ppb * 65.536); - if (clock_adjtime(clkid, &tx) < 0) - pr_err("failed to adjust the clock: %m"); -} - -static double clock_ppb_read(clockid_t clkid) -{ - double f = 0.0; - struct timex tx; - memset(&tx, 0, sizeof(tx)); - if (clock_adjtime(clkid, &tx) < 0) - pr_err("failed to read out the clock frequency adjustment: %m"); - else - f = tx.freq / 65.536; - return f; -} - -static void clock_step(clockid_t clkid, int64_t ns) -{ - struct timex tx; - int sign = 1; - if (ns < 0) { - sign = -1; - ns *= -1; - } - memset(&tx, 0, sizeof(tx)); - tx.modes = ADJ_SETOFFSET | ADJ_NANO; - tx.time.tv_sec = sign * (ns / NS_PER_SEC); - tx.time.tv_usec = sign * (ns % NS_PER_SEC); - /* - * The value of a timeval is the sum of its fields, but the - * field tv_usec must always be non-negative. - */ - if (tx.time.tv_usec < 0) { - tx.time.tv_sec -= 1; - tx.time.tv_usec += 1000000000; - } - if (clock_adjtime(clkid, &tx) < 0) - pr_err("failed to step clock: %m"); -} - static void clock_update_grandmaster(struct clock *c) { struct parentDS *pds = &c->dad.pds; @@ -580,7 +535,7 @@ struct clock *clock_create(int phc_index, struct interface *iface, int count, } if (c->clkid != CLOCK_INVALID) { - fadj = (int) clock_ppb_read(c->clkid); + fadj = (int) clockadj_get_freq(c->clkid); } c->servo = servo_create(servo, -fadj, max_adj, sw_ts); if (!c->servo) { @@ -1035,13 +990,13 @@ enum servo_state clock_synchronize(struct clock *c, case SERVO_UNLOCKED: break; case SERVO_JUMP: - clock_ppb(c->clkid, -adj); - clock_step(c->clkid, -c->master_offset); + clockadj_set_freq(c->clkid, -adj); + clockadj_step(c->clkid, -c->master_offset); c->t1 = tmv_zero(); c->t2 = tmv_zero(); break; case SERVO_LOCKED: - clock_ppb(c->clkid, -adj); + clockadj_set_freq(c->clkid, -adj); break; } return state; diff --git a/clockadj.c b/clockadj.c new file mode 100644 index 0000000..a2b55fd --- /dev/null +++ b/clockadj.c @@ -0,0 +1,72 @@ +/** + * @file clockadj.c + * @note Copyright (C) 2013 Richard Cochran + * + * 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 "clockadj.h" +#include "missing.h" +#include "print.h" + +#define NS_PER_SEC 1000000000LL + +void clockadj_set_freq(clockid_t clkid, double freq) +{ + struct timex tx; + memset(&tx, 0, sizeof(tx)); + tx.modes = ADJ_FREQUENCY; + tx.freq = (long) (freq * 65.536); + if (clock_adjtime(clkid, &tx) < 0) + pr_err("failed to adjust the clock: %m"); +} + +double clockadj_get_freq(clockid_t clkid) +{ + double f = 0.0; + struct timex tx; + memset(&tx, 0, sizeof(tx)); + if (clock_adjtime(clkid, &tx) < 0) + pr_err("failed to read out the clock frequency adjustment: %m"); + else + f = tx.freq / 65.536; + return f; +} + +void clockadj_step(clockid_t clkid, int64_t step) +{ + struct timex tx; + int sign = 1; + if (step < 0) { + sign = -1; + step *= -1; + } + memset(&tx, 0, sizeof(tx)); + tx.modes = ADJ_SETOFFSET | ADJ_NANO; + tx.time.tv_sec = sign * (step / NS_PER_SEC); + tx.time.tv_usec = sign * (step % NS_PER_SEC); + /* + * The value of a timeval is the sum of its fields, but the + * field tv_usec must always be non-negative. + */ + if (tx.time.tv_usec < 0) { + tx.time.tv_sec -= 1; + tx.time.tv_usec += 1000000000; + } + if (clock_adjtime(clkid, &tx) < 0) + pr_err("failed to step clock: %m"); +} diff --git a/clockadj.h b/clockadj.h new file mode 100644 index 0000000..b40d7a2 --- /dev/null +++ b/clockadj.h @@ -0,0 +1,47 @@ +/** + * @file clockadj.h + * @brief Wraps clock_adjtime functionality. + * @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_CLOCKADJ_H +#define HAVE_CLOCKADJ_H + +#include +#include + +/* + * Set clock's frequency offset. + * @param clkid A clock ID obtained using phc_open() or CLOCK_REALTIME. + * @param freq The frequency offset in parts per billion (ppb). + */ +void clockadj_set_freq(clockid_t clkid, double freq); + +/* + * Read clock's frequency offset. + * @param clkid A clock ID obtained using phc_open() or CLOCK_REALTIME. + * @return The frequency offset in parts per billion (ppb). + */ +double clockadj_get_freq(clockid_t clkid); + +/* + * Step clock's time. + * @param clkid A clock ID obtained using phc_open() or CLOCK_REALTIME. + * @param step The time step in nanoseconds. + */ +void clockadj_step(clockid_t clkid, int64_t step); + +#endif diff --git a/makefile b/makefile index 671de48..e9ecb03 100644 --- a/makefile +++ b/makefile @@ -29,9 +29,9 @@ VER = -DVER=$(version) CFLAGS = -Wall $(VER) $(INC) $(DEBUG) $(FEAT_CFLAGS) $(EXTRA_CFLAGS) LDLIBS = -lm -lrt $(EXTRA_LDFLAGS) PRG = ptp4l pmc phc2sys hwstamp_ctl -OBJ = bmc.o clock.o config.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 config.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) @@ -52,8 +52,8 @@ 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: msg.o phc2sys.o pmc_common.o print.o pi.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 msg.o phc2sys.o pmc_common.o print.o pi.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 hwstamp_ctl: hwstamp_ctl.o version.o diff --git a/phc2sys.c b/phc2sys.c index a600709..ac036dd 100644 --- a/phc2sys.c +++ b/phc2sys.c @@ -27,13 +27,13 @@ #include #include #include -#include #include #include #include #include +#include "clockadj.h" #include "ds.h" #include "fsm.h" #include "missing.h" @@ -78,52 +78,6 @@ static clockid_t clock_open(char *device) return FD_TO_CLOCKID(fd); } -static void clock_ppb(clockid_t clkid, double ppb) -{ - struct timex tx; - memset(&tx, 0, sizeof(tx)); - tx.modes = ADJ_FREQUENCY; - tx.freq = (long) (ppb * 65.536); - if (clock_adjtime(clkid, &tx) < 0) - pr_err("failed to adjust the clock: %m"); -} - -static double clock_ppb_read(clockid_t clkid) -{ - double f = 0.0; - struct timex tx; - memset(&tx, 0, sizeof(tx)); - if (clock_adjtime(clkid, &tx) < 0) - pr_err("failed to read out the clock frequency adjustment: %m"); - else - f = tx.freq / 65.536; - return f; -} - -static void clock_step(clockid_t clkid, int64_t ns) -{ - struct timex tx; - int sign = 1; - if (ns < 0) { - sign = -1; - ns *= -1; - } - memset(&tx, 0, sizeof(tx)); - tx.modes = ADJ_SETOFFSET | ADJ_NANO; - tx.time.tv_sec = sign * (ns / NS_PER_SEC); - tx.time.tv_usec = sign * (ns % NS_PER_SEC); - /* - * The value of a timeval is the sum of its fields, but the - * field tv_usec must always be non-negative. - */ - if (tx.time.tv_usec < 0) { - tx.time.tv_sec -= 1; - tx.time.tv_usec += 1000000000; - } - if (clock_adjtime(clkid, &tx) < 0) - pr_err("failed to step clock: %m"); -} - static int read_phc(clockid_t clkid, clockid_t sysclk, int readings, int64_t *offset, uint64_t *ts, int64_t *delay) { @@ -224,10 +178,10 @@ static void update_clock(struct clock *clock, case SERVO_UNLOCKED: break; case SERVO_JUMP: - clock_step(clock->clkid, -offset); + clockadj_step(clock->clkid, -offset); /* Fall through. */ case SERVO_LOCKED: - clock_ppb(clock->clkid, -ppb); + clockadj_set_freq(clock->clkid, -ppb); break; } @@ -669,10 +623,10 @@ int main(int argc, char *argv[]) close_pmc(&dst_clock); } - ppb = clock_ppb_read(dst_clock.clkid); + ppb = clockadj_get_freq(dst_clock.clkid); /* The reading may silently fail and return 0, reset the frequency to make sure ppb is the actual frequency of the clock. */ - clock_ppb(dst_clock.clkid, ppb); + clockadj_set_freq(dst_clock.clkid, ppb); dst_clock.servo = servo_create(CLOCK_SERVO_PI, -ppb, max_ppb, 0); From eb93926bc926fa8e212525ff4fb5016685fe9e60 Mon Sep 17 00:00:00 2001 From: Miroslav Lichvar Date: Thu, 7 Mar 2013 17:27:30 +0100 Subject: [PATCH 3/6] phc2sys: Handle leap seconds. Update the currentUtcOffset and leap61/59 values at one minute interval. When a leap second is detected, set the STA_INS/STA_DEL bit for the system clock. Signed-off-by: Miroslav Lichvar --- clockadj.c | 24 +++++++++++++++++ clockadj.h | 7 +++++ phc2sys.c | 75 +++++++++++++++++++++++++++++++++++++++++++++++++----- util.c | 41 +++++++++++++++++++++++++++++ util.h | 20 +++++++++++++++ 5 files changed, 161 insertions(+), 6 deletions(-) diff --git a/clockadj.c b/clockadj.c index a2b55fd..31fc765 100644 --- a/clockadj.c +++ b/clockadj.c @@ -70,3 +70,27 @@ void clockadj_step(clockid_t clkid, int64_t step) if (clock_adjtime(clkid, &tx) < 0) pr_err("failed to step clock: %m"); } + +void clockadj_set_leap(clockid_t clkid, int leap) +{ + struct timex tx; + const char *m = NULL; + memset(&tx, 0, sizeof(tx)); + tx.modes = ADJ_STATUS; + switch (leap) { + case -1: + tx.status = STA_DEL; + m = "clock set to delete leap second at midnight (UTC)"; + break; + case 1: + tx.status = STA_INS; + m = "clock set to insert leap second at midnight (UTC)"; + break; + default: + tx.status = 0; + } + if (clock_adjtime(clkid, &tx) < 0) + pr_err("failed to set the clock status: %m"); + else if (m) + pr_notice(m); +} diff --git a/clockadj.h b/clockadj.h index b40d7a2..6e76e0f 100644 --- a/clockadj.h +++ b/clockadj.h @@ -44,4 +44,11 @@ double clockadj_get_freq(clockid_t clkid); */ void clockadj_step(clockid_t clkid, int64_t step); +/* + * Insert/delete leap second at midnight. + * @param clkid CLOCK_REALTIME. + * @param leap +1 to insert leap second, -1 to delete leap second, + * 0 to reset the leap state. + */ +void clockadj_set_leap(clockid_t clkid, int leap); #endif diff --git a/phc2sys.c b/phc2sys.c index ac036dd..678b0f3 100644 --- a/phc2sys.c +++ b/phc2sys.c @@ -45,6 +45,7 @@ #include "stats.h" #include "sysoff.h" #include "tlv.h" +#include "util.h" #include "version.h" #define KP 0.7 @@ -54,9 +55,10 @@ #define max_ppb 512000 #define PHC_PPS_OFFSET_LIMIT 10000000 +#define PMC_UPDATE_INTERVAL (60 * NS_PER_SEC) struct clock; -static int update_sync_offset(struct clock *clock); +static int update_sync_offset(struct clock *clock, int64_t offset, uint64_t ts); static clockid_t clock_open(char *device) { @@ -112,6 +114,7 @@ static int read_phc(clockid_t clkid, clockid_t sysclk, int readings, struct clock { clockid_t clkid; struct servo *servo; + enum servo_state servo_state; const char *source_label; struct stats *offset_stats; struct stats *freq_stats; @@ -119,9 +122,12 @@ struct clock { unsigned int stats_max_count; int sync_offset; int sync_offset_direction; + int leap; + int leap_set; struct pmc *pmc; int pmc_ds_idx; int pmc_ds_requested; + uint64_t pmc_last_update; }; static void update_clock_stats(struct clock *clock, @@ -165,7 +171,7 @@ static void update_clock(struct clock *clock, enum servo_state state; double ppb; - if (update_sync_offset(clock)) + if (update_sync_offset(clock, offset, ts)) return; if (clock->sync_offset_direction) @@ -173,6 +179,7 @@ static void update_clock(struct clock *clock, clock->sync_offset_direction; ppb = servo_sample(clock->servo, offset, ts, &state); + clock->servo_state = state; switch (state) { case SERVO_UNLOCKED: @@ -419,6 +426,12 @@ static int run_pmc(struct clock *clock, int timeout, case TIME_PROPERTIES_DATA_SET: clock->sync_offset = ((struct timePropertiesDS *)data)-> currentUtcOffset; + if (((struct timePropertiesDS *)data)->flags & LEAP_61) + clock->leap = 1; + else if (((struct timePropertiesDS *)data)->flags & LEAP_59) + clock->leap = -1; + else + clock->leap = 0; ds_done = 1; break; } @@ -441,11 +454,57 @@ static void close_pmc(struct clock *clock) clock->pmc = NULL; } -static int update_sync_offset(struct clock *clock) +static int update_sync_offset(struct clock *clock, int64_t offset, uint64_t ts) { - if (clock->pmc) { - run_pmc(clock, 0, 0, 1); + int clock_leap; + + if (clock->pmc && + !(ts > clock->pmc_last_update && + ts - clock->pmc_last_update < PMC_UPDATE_INTERVAL)) { + if (run_pmc(clock, 0, 0, 1) > 0) + clock->pmc_last_update = ts; } + + /* Handle leap seconds. */ + + if (!clock->leap && !clock->leap_set) + return 0; + + /* If the system clock is the master clock, get a time stamp from + it, as it is the clock which will include the leap second. */ + if (clock->clkid != CLOCK_REALTIME) { + struct timespec tp; + if (clock_gettime(CLOCK_REALTIME, &tp)) { + pr_err("failed to read clock: %m"); + return -1; + } + ts = tp.tv_sec * NS_PER_SEC + tp.tv_nsec; + } + + /* If the clock will be stepped, the time stamp has to be the + target time. Ignore possible 1 second error in UTC offset. */ + if (clock->clkid == CLOCK_REALTIME && + clock->servo_state == SERVO_UNLOCKED) { + ts -= offset + clock->sync_offset * NS_PER_SEC * + clock->sync_offset_direction; + } + + /* Suspend clock updates in the last second before midnight. */ + if (is_utc_ambiguous(ts)) { + pr_info("clock update suspended due to leap second"); + return -1; + } + + clock_leap = leap_second_status(ts, clock->leap_set, + &clock->leap, &clock->sync_offset); + + if (clock->leap_set != clock_leap) { + /* Only the system clock can leap. */ + if (clock->clkid == CLOCK_REALTIME) + clockadj_set_leap(clock->clkid, clock_leap); + clock->leap_set = clock_leap; + } + return 0; } @@ -480,7 +539,10 @@ int main(int argc, char *argv[]) int r, wait_sync = 0, forced_sync_offset = 0; int print_level = LOG_INFO, use_syslog = 1, verbose = 0; double ppb; - struct clock dst_clock = { .clkid = CLOCK_REALTIME }; + struct clock dst_clock = { + .clkid = CLOCK_REALTIME, + .servo_state = SERVO_UNLOCKED, + }; configured_pi_kp = KP; configured_pi_ki = KI; @@ -627,6 +689,7 @@ int main(int argc, char *argv[]) /* The reading may silently fail and return 0, reset the frequency to make sure ppb is the actual frequency of the clock. */ clockadj_set_freq(dst_clock.clkid, ppb); + clockadj_set_leap(dst_clock.clkid, 0); dst_clock.servo = servo_create(CLOCK_SERVO_PI, -ppb, max_ppb, 0); diff --git a/util.c b/util.c index 7699fac..55ec49f 100644 --- a/util.c +++ b/util.c @@ -22,6 +22,10 @@ #include "sk.h" #include "util.h" +#define NS_PER_SEC 1000000000LL +#define NS_PER_HOUR (3600 * NS_PER_SEC) +#define NS_PER_DAY (24 * NS_PER_HOUR) + char *ps_str[] = { "NONE", "INITIALIZING", @@ -149,3 +153,40 @@ int static_ptp_text_set(struct static_ptp_text *dst, const char *src) dst->text[len] = '\0'; return 0; } + +int is_utc_ambiguous(uint64_t ts) +{ + /* The Linux kernel inserts leap second by stepping the clock backwards + at 0:00 UTC, the last second before midnight is played twice. */ + if (NS_PER_DAY - ts % NS_PER_DAY <= NS_PER_SEC) + return 1; + return 0; +} + +int leap_second_status(uint64_t ts, int leap_set, int *leap, int *utc_offset) +{ + int leap_status = leap_set; + + /* The leap bits obtained by PTP should be set at most 12 hours before + midnight and unset at most 2 announce intervals after midnight. + Split updates which are too early and which are too late at 6 hours + after midnight. */ + if (ts % NS_PER_DAY > 6 * NS_PER_HOUR) { + if (!leap_status) + leap_status = *leap; + } else { + if (leap_status) + leap_status = 0; + } + + /* Fix early or late update of leap and utc_offset. */ + if (!*leap && leap_status) { + *utc_offset -= leap_status; + *leap = leap_status; + } else if (*leap && !leap_status) { + *utc_offset += *leap; + *leap = leap_status; + } + + return leap_status; +} diff --git a/util.h b/util.h index a2d7089..d2734d1 100644 --- a/util.h +++ b/util.h @@ -94,4 +94,24 @@ int ptp_text_set(struct PTPText *dst, const char *src); */ int static_ptp_text_set(struct static_ptp_text *dst, const char *src); +/** + * Check if UTC time stamp can be both before and after a leap second. + * + * @param ts UTC time stamp in nanoseconds. + * @return 0 if not, 1 if yes. + */ +int is_utc_ambiguous(uint64_t ts); + +/** + * Get leap second status in given time. + * + * @param ts UTC time stamp in nanoseconds. + * @param leap_set Previous leap second status (+1/0/-1). + * @param leap Announced leap second (+1/0/-1), will be corrected if + * early/late. + * @param utc_offset Announced UTC offset, will be corrected if early/late. + * @return 0 if the leap second passed, +1 if leap second will be + * inserted, -1 if leap second will be deleted. + */ +int leap_second_status(uint64_t ts, int leap_set, int *leap, int *utc_offset); #endif From 09667479b889e82d71645437af86f42e2f1fc9df Mon Sep 17 00:00:00 2001 From: Miroslav Lichvar Date: Thu, 7 Mar 2013 17:27:31 +0100 Subject: [PATCH 4/6] Add missing conversions from tmv_t to nanoseconds. Signed-off-by: Miroslav Lichvar --- clock.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/clock.c b/clock.c index 71c4791..9a9e435 100644 --- a/clock.c +++ b/clock.c @@ -382,11 +382,13 @@ static enum servo_state clock_no_adjust(struct clock *c) freq = (1.0 - ratio) * 1e9; if (c->stats.max_count > 1) { - clock_stats_update(&c->stats, c->master_offset, freq); + clock_stats_update(&c->stats, + tmv_to_nanoseconds(c->master_offset), freq); } else { pr_info("master offset %10" PRId64 " s%d freq %+7.0f " "path delay %9" PRId64, - c->master_offset, state, freq, c->path_delay); + tmv_to_nanoseconds(c->master_offset), state, freq, + tmv_to_nanoseconds(c->path_delay)); } fui = 1.0 + (c->status.cumulativeScaledRateOffset + 0.0) / POW2_41; @@ -905,7 +907,7 @@ void clock_path_delay(struct clock *c, struct timespec req, struct timestamp rx, pr_debug("path delay %10lld %10lld", c->path_delay, pd); if (c->stats.delay) - stats_add_value(c->stats.delay, pd); + stats_add_value(c->stats.delay, tmv_to_nanoseconds(pd)); } void clock_peer_delay(struct clock *c, tmv_t ppd, double nrr) @@ -914,7 +916,7 @@ void clock_peer_delay(struct clock *c, tmv_t ppd, double nrr) c->nrr = nrr; if (c->stats.delay) - stats_add_value(c->stats.delay, ppd); + stats_add_value(c->stats.delay, tmv_to_nanoseconds(ppd)); } void clock_remove_fda(struct clock *c, struct port *p, struct fdarray fda) @@ -976,14 +978,17 @@ enum servo_state clock_synchronize(struct clock *c, if (c->free_running) return clock_no_adjust(c); - adj = servo_sample(c->servo, c->master_offset, ingress, &state); + adj = servo_sample(c->servo, tmv_to_nanoseconds(c->master_offset), + tmv_to_nanoseconds(ingress), &state); if (c->stats.max_count > 1) { - clock_stats_update(&c->stats, c->master_offset, adj); + clock_stats_update(&c->stats, + tmv_to_nanoseconds(c->master_offset), adj); } else { pr_info("master offset %10" PRId64 " s%d freq %+7.0f " "path delay %9" PRId64, - c->master_offset, state, adj, c->path_delay); + tmv_to_nanoseconds(c->master_offset), state, adj, + tmv_to_nanoseconds(c->path_delay)); } switch (state) { @@ -991,7 +996,7 @@ enum servo_state clock_synchronize(struct clock *c, break; case SERVO_JUMP: clockadj_set_freq(c->clkid, -adj); - clockadj_step(c->clkid, -c->master_offset); + clockadj_step(c->clkid, -tmv_to_nanoseconds(c->master_offset)); c->t1 = tmv_zero(); c->t2 = tmv_zero(); break; From e21af9709192acd26b468c173e022de8b9242367 Mon Sep 17 00:00:00 2001 From: Miroslav Lichvar Date: Thu, 7 Mar 2013 17:27:32 +0100 Subject: [PATCH 5/6] ptp4l: Handle leap seconds. Extend the clock_utc_correct function to handle leap seconds announced in the time properties data set. With software time stamping, it sets the STA_INS/STA_DEL bit for the system clock. Clock updates are suspended in the last second of the day. Signed-off-by: Miroslav Lichvar --- clock.c | 73 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/clock.c b/clock.c index 9a9e435..f608236 100644 --- a/clock.c +++ b/clock.c @@ -78,6 +78,8 @@ struct clock { int free_running; int freq_est_interval; int utc_timescale; + int leap_set; + enum servo_state servo_state; tmv_t master_offset; tmv_t path_delay; struct mave *avg_delay; @@ -449,23 +451,66 @@ static void clock_update_slave(struct clock *c) } } -static void clock_utc_correct(struct clock *c) +static int clock_utc_correct(struct clock *c, tmv_t ingress) { struct timespec offset; + int utc_offset, leap, clock_leap; + uint64_t ts; + if (!c->utc_timescale) - return; - if (!(c->tds.flags & PTP_TIMESCALE)) - return; + return 0; + if (c->tds.flags & UTC_OFF_VALID && c->tds.flags & TIME_TRACEABLE) { - offset.tv_sec = c->tds.currentUtcOffset; + utc_offset = c->tds.currentUtcOffset; } else if (c->tds.currentUtcOffset > CURRENT_UTC_OFFSET) { - offset.tv_sec = c->tds.currentUtcOffset; + utc_offset = c->tds.currentUtcOffset; } else { - offset.tv_sec = CURRENT_UTC_OFFSET; + utc_offset = CURRENT_UTC_OFFSET; } + + if (c->tds.flags & LEAP_61) { + leap = 1; + } else if (c->tds.flags & LEAP_59) { + leap = -1; + } else { + leap = 0; + } + + /* Handle leap seconds. */ + if ((leap || c->leap_set) && c->clkid == CLOCK_REALTIME) { + /* If the clock will be stepped, the time stamp has to be the + target time. Ignore possible 1 second error in utc_offset. */ + if (c->servo_state == SERVO_UNLOCKED) { + ts = tmv_to_nanoseconds(tmv_sub(ingress, + c->master_offset)); + if (c->tds.flags & PTP_TIMESCALE) + ts -= utc_offset * NS_PER_SEC; + } else { + ts = tmv_to_nanoseconds(ingress); + } + + /* Suspend clock updates in the last second before midnight. */ + if (is_utc_ambiguous(ts)) { + pr_info("clock update suspended due to leap second"); + return -1; + } + + clock_leap = leap_second_status(ts, c->leap_set, + &leap, &utc_offset); + if (c->leap_set != clock_leap) { + clockadj_set_leap(c->clkid, clock_leap); + c->leap_set = clock_leap; + } + } + + if (!(c->tds.flags & PTP_TIMESCALE)) + return 0; + + offset.tv_sec = utc_offset; offset.tv_nsec = 0; /* Local clock is UTC, but master is TAI. */ c->master_offset = tmv_add(c->master_offset, timespec_to_tmv(offset)); + return 0; } static int forwarding(struct clock *c, struct port *p) @@ -534,7 +579,10 @@ struct clock *clock_create(int phc_index, struct interface *iface, int count, c->clkid = CLOCK_REALTIME; c->utc_timescale = 1; max_adj = 512000; + clockadj_set_leap(c->clkid, 0); } + c->leap_set = 0; + c->kernel_leap = dds->kernel_leap; if (c->clkid != CLOCK_INVALID) { fadj = (int) clockadj_get_freq(c->clkid); @@ -544,6 +592,7 @@ struct clock *clock_create(int phc_index, struct interface *iface, int count, pr_err("Failed to create clock servo"); return NULL; } + c->servo_state = SERVO_UNLOCKED; c->avg_delay = mave_create(MAVE_LENGTH); if (!c->avg_delay) { pr_err("Failed to create moving average"); @@ -968,18 +1017,20 @@ enum servo_state clock_synchronize(struct clock *c, c->master_offset = tmv_sub(ingress, tmv_add(origin, tmv_add(c->path_delay, tmv_add(c->c1, c->c2)))); - clock_utc_correct(c); - - c->cur.offsetFromMaster = tmv_to_TimeInterval(c->master_offset); - if (!c->path_delay) return state; + if (clock_utc_correct(c, ingress)) + return c->servo_state; + + c->cur.offsetFromMaster = tmv_to_TimeInterval(c->master_offset); + if (c->free_running) return clock_no_adjust(c); adj = servo_sample(c->servo, tmv_to_nanoseconds(c->master_offset), tmv_to_nanoseconds(ingress), &state); + c->servo_state = state; if (c->stats.max_count > 1) { clock_stats_update(&c->stats, From 4e24248a71263ea9d5c144471dbaf8b412587adf Mon Sep 17 00:00:00 2001 From: Miroslav Lichvar Date: Thu, 7 Mar 2013 17:27:33 +0100 Subject: [PATCH 6/6] Add options to not apply leap seconds in kernel. Add kernel_leap option for ptp4l and -x option for phc2sys to disable setting of the STA_INS/STA_DEL bit to slowly correct the one-second offset by servo. Signed-off-by: Miroslav Lichvar --- clock.c | 5 ++++- config.c | 5 +++++ default.cfg | 1 + ds.h | 1 + gPTP.cfg | 1 + phc2sys.8 | 9 +++++++++ phc2sys.c | 10 ++++++++-- ptp4l.8 | 8 ++++++++ ptp4l.c | 1 + 9 files changed, 38 insertions(+), 3 deletions(-) diff --git a/clock.c b/clock.c index f608236..6e0b0d9 100644 --- a/clock.c +++ b/clock.c @@ -79,6 +79,7 @@ struct clock { int freq_est_interval; int utc_timescale; int leap_set; + int kernel_leap; enum servo_state servo_state; tmv_t master_offset; tmv_t path_delay; @@ -498,7 +499,8 @@ static int clock_utc_correct(struct clock *c, tmv_t ingress) clock_leap = leap_second_status(ts, c->leap_set, &leap, &utc_offset); if (c->leap_set != clock_leap) { - clockadj_set_leap(c->clkid, clock_leap); + if (c->kernel_leap) + clockadj_set_leap(c->clkid, clock_leap); c->leap_set = clock_leap; } } @@ -559,6 +561,7 @@ struct clock *clock_create(int phc_index, struct interface *iface, int count, c->free_running = dds->free_running; c->freq_est_interval = dds->freq_est_interval; + c->kernel_leap = dds->kernel_leap; c->desc = dds->clock_desc; if (c->free_running) { diff --git a/config.c b/config.c index dd96fc1..7991378 100644 --- a/config.c +++ b/config.c @@ -383,6 +383,11 @@ static enum parser_result parse_global_setting(const char *option, return BAD_VALUE; cfg->dds.stats_interval = val; + } else if (!strcmp(option, "kernel_leap")) { + if (1 != sscanf(value, "%d", &val)) + return BAD_VALUE; + cfg->dds.kernel_leap = val; + } else return NOT_PARSED; diff --git a/default.cfg b/default.cfg index 7b3e2f7..12878c5 100644 --- a/default.cfg +++ b/default.cfg @@ -33,6 +33,7 @@ tx_timestamp_retries 100 use_syslog 1 verbose 0 summary_interval 0 +kernel_leap 1 # # Servo Options # diff --git a/ds.h b/ds.h index 06cb30a..555bc19 100644 --- a/ds.h +++ b/ds.h @@ -52,6 +52,7 @@ struct default_ds { int free_running; int freq_est_interval; /*log seconds*/ int stats_interval; /*log seconds*/ + int kernel_leap; struct clock_description clock_desc; }; diff --git a/gPTP.cfg b/gPTP.cfg index ecd5f71..cfb388b 100644 --- a/gPTP.cfg +++ b/gPTP.cfg @@ -32,6 +32,7 @@ tx_timestamp_retries 100 use_syslog 1 verbose 0 summary_interval 0 +kernel_leap 1 # # Servo options # diff --git a/phc2sys.8 b/phc2sys.8 index 10faea9..67b2f27 100644 --- a/phc2sys.8 +++ b/phc2sys.8 @@ -31,6 +31,8 @@ phc2sys \- synchronize two clocks ] [ .B \-w ] [ +.B \-x +] [ .BI \-l " print-level" ] [ .B \-m @@ -127,6 +129,13 @@ option is not used, also keep the offset between the slave and master times updated according to the currentUtcOffset value obtained from ptp4l and the direction of the clock synchronization. .TP +.B \-x +When a leap second is announced, don't apply it in the kernel by stepping the +clock, but let the servo correct the one-second offset slowly by changing the +clock frequency (unless the +.B \-S +option is used). +.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). diff --git a/phc2sys.c b/phc2sys.c index 678b0f3..1f851f7 100644 --- a/phc2sys.c +++ b/phc2sys.c @@ -124,6 +124,7 @@ struct clock { int sync_offset_direction; int leap; int leap_set; + int kernel_leap; struct pmc *pmc; int pmc_ds_idx; int pmc_ds_requested; @@ -500,7 +501,7 @@ static int update_sync_offset(struct clock *clock, int64_t offset, uint64_t ts) if (clock->leap_set != clock_leap) { /* Only the system clock can leap. */ - if (clock->clkid == CLOCK_REALTIME) + if (clock->clkid == CLOCK_REALTIME && clock->kernel_leap) clockadj_set_leap(clock->clkid, clock_leap); clock->leap_set = clock_leap; } @@ -525,6 +526,7 @@ static void usage(char *progname) " -O [offset] slave-master time offset (0)\n" " -u [num] number of clock updates in summary stats (0)\n" " -w wait for ptp4l\n" + " -x apply leap seconds by servo instead of kernel\n" " -h prints this message and exits\n" " -v prints the software version and exits\n" "\n", @@ -542,6 +544,7 @@ int main(int argc, char *argv[]) struct clock dst_clock = { .clkid = CLOCK_REALTIME, .servo_state = SERVO_UNLOCKED, + .kernel_leap = 1, }; configured_pi_kp = KP; @@ -551,7 +554,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:hs:P:I:S:R:N:O:i:u:wl:mqv"))) { + "c:d:hs:P:I:S:R:N:O:i:u:wxl:mqv"))) { switch (c) { case 'c': dst_clock.clkid = clock_open(optarg); @@ -596,6 +599,9 @@ int main(int argc, char *argv[]) case 'w': wait_sync = 1; break; + case 'x': + dst_clock.kernel_leap = 0; + break; case 'l': print_level = atoi(optarg); break; diff --git a/ptp4l.8 b/ptp4l.8 index 3ee222d..a0d223d 100644 --- a/ptp4l.8 +++ b/ptp4l.8 @@ -340,6 +340,14 @@ empty string. .B manufacturerIdentity The manufacturer id which should be an OUI owned by the manufacturer. The default is 00:00:00. +.TP +.B kernel_leap +When a leap second is announced, let the kernel apply it by stepping the clock +instead of correcting the one-second offset with servo, which would correct the +one-second offset slowly by changing the clock frequency (unless the +.B pi_offset_const +option is set to correct such offset by stepping). +Relevant only with software time stamping. The default is 1 (enabled). .SH SEE ALSO .BR pmc (8), diff --git a/ptp4l.c b/ptp4l.c index 32b69d0..dffaad0 100644 --- a/ptp4l.c +++ b/ptp4l.c @@ -53,6 +53,7 @@ static struct config cfg_settings = { .free_running = 0, .freq_est_interval = 1, .stats_interval = 0, + .kernel_leap = 1, .clock_desc = { .productDescription = { .max_symbols = 64,