summaryrefslogtreecommitdiffstats
path: root/sys_generic.c
blob: 5c42df1dcb85c3ee5962951fe23f09ba1e2d8b32 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
/*
  chronyd/chronyc - Programs for keeping computer clocks accurate.

 **********************************************************************
 * Copyright (C) Miroslav Lichvar  2014-2015
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of version 2 of the GNU General Public License as
 * published by the Free Software Foundation.
 * 
 * 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.
 * 
 **********************************************************************

  =======================================================================

  Generic driver functions to complete system-specific drivers
  */

#include "config.h"

#include "sysincl.h"

#include "sys_generic.h"

#include "conf.h"
#include "local.h"
#include "localp.h"
#include "logging.h"
#include "privops.h"
#include "sched.h"
#include "util.h"

/* ================================================== */

/* System clock drivers */
static lcl_ReadFrequencyDriver drv_read_freq;
static lcl_SetFrequencyDriver drv_set_freq;
static lcl_SetSyncStatusDriver drv_set_sync_status;
static lcl_AccrueOffsetDriver drv_accrue_offset;
static lcl_OffsetCorrectionDriver drv_get_offset_correction;

/* Current frequency as requested by the local module (in ppm) */
static double base_freq;

/* Maximum frequency that can be set by drv_set_freq (in ppm) */
static double max_freq;

/* Maximum expected delay in the actual frequency change (e.g. kernel ticks)
   in local time */
static double max_freq_change_delay;

/* Maximum allowed frequency offset relative to the base frequency */
static double max_corr_freq;

/* Amount of outstanding offset to process */
static double offset_register;

/* Minimum offset to correct */
#define MIN_OFFSET_CORRECTION 1.0e-9

/* Current frequency offset between base_freq and the real clock frequency
   as set by drv_set_freq (not in ppm) */
static double slew_freq;

/* Time (raw) of last update of slewing frequency and offset */
static struct timespec slew_start;

/* Limits for the slew length */
#define MIN_SLEW_DURATION 1.0
#define MAX_SLEW_DURATION 1.0e4

/* Scheduler timeout ID for ending of the currently running slew */
static SCH_TimeoutID slew_timeout_id;

/* Scheduled duration of the currently running slew */
static double slew_duration;

/* Expected delay in ending of the slew due to process scheduling and
   execution time, tracked as a decaying maximum value */
static double slew_excess_duration;

/* Maximum accepted excess duration to ignore large jumps after resuming
   suspended system and other reasons (which should be handled in the
   scheduler), a constant to determine the minimum slew duration to avoid
   oscillations due to the excess, and the decay constant */
#define MAX_SLEW_EXCESS_DURATION 100.0
#define MIN_SLEW_DURATION_EXCESS_RATIO 5.0
#define SLEW_EXCESS_DURATION_DECAY 0.9

/* Suggested offset correction rate (correction time * offset) */
static double correction_rate;

/* Maximum expected offset correction error caused by delayed change in the
   real frequency of the clock */
static double slew_error;

/* Minimum offset that the system driver can slew faster than the maximum
   frequency offset that it allows to be set directly */
static double fastslew_min_offset;

/* Maximum slew rate of the system driver */
static double fastslew_max_rate;

/* Flag indicating that the system driver is currently slewing */
static int fastslew_active;

/* ================================================== */

static void handle_end_of_slew(void *anything);
static void update_slew(void);

/* ================================================== */
/* Adjust slew_start on clock step */

static void
handle_step(struct timespec *raw, struct timespec *cooked, double dfreq,
            double doffset, LCL_ChangeType change_type, void *anything)
{
  if (change_type == LCL_ChangeStep) {
    UTI_AddDoubleToTimespec(&slew_start, -doffset, &slew_start);
  }
}

/* ================================================== */

static void
start_fastslew(void)
{
  if (!drv_accrue_offset)
    return;

  drv_accrue_offset(offset_register, 0.0);

  DEBUG_LOG("fastslew offset=%e", offset_register);

  offset_register = 0.0;
  fastslew_active = 1;
}

/* ================================================== */

static void
stop_fastslew(struct timespec *now)
{
  double corr;

  if (!drv_get_offset_correction || !fastslew_active)
    return;

  /* Cancel the remaining offset */
  drv_get_offset_correction(now, &corr, NULL);
  drv_accrue_offset(corr, 0.0);
  offset_register -= corr;
}

/* ================================================== */

static double
clamp_freq(double freq)
{
  if (freq > max_freq)
    return max_freq;
  if (freq < -max_freq)
    return -max_freq;
  return freq;
}

/* ================================================== */
/* End currently running slew and start a new one */

static void
update_slew(void)
{
  double old_slew_freq, total_freq, corr_freq, duration, excess_duration;
  struct timespec now, end_of_slew;

  /* Remove currently running timeout */
  SCH_RemoveTimeout(slew_timeout_id);

  LCL_ReadRawTime(&now);

  /* Adjust the offset register by achieved slew */
  duration = UTI_DiffTimespecsToDouble(&now, &slew_start);
  offset_register -= slew_freq * duration;

  stop_fastslew(&now);

  /* Update the maximum excess duration, decaying even when the slew did
     not time out (i.e. frequency was set or offset accrued), but add a small
     value to avoid denormals */
  slew_excess_duration = (slew_excess_duration + 1.0e-9) * SLEW_EXCESS_DURATION_DECAY;
  excess_duration = duration - slew_duration;
  if (slew_excess_duration < excess_duration &&
      excess_duration <= MAX_SLEW_EXCESS_DURATION)
    slew_excess_duration = excess_duration;

  /* Calculate the duration of the new slew, considering the current correction
     rate and previous delays in stopping of the slew */
  if (fabs(offset_register) < MIN_OFFSET_CORRECTION) {
    duration = MAX_SLEW_DURATION;
  } else {
    duration = correction_rate / fabs(offset_register);
    if (duration < MIN_SLEW_DURATION)
      duration = MIN_SLEW_DURATION;
    if (duration < MIN_SLEW_DURATION_EXCESS_RATIO * slew_excess_duration)
      duration = MIN_SLEW_DURATION_EXCESS_RATIO * slew_excess_duration;
  }

  /* Get frequency offset needed to slew the offset in the duration
     and clamp it to the allowed maximum */
  corr_freq = offset_register / duration;
  if (corr_freq < -max_corr_freq)
    corr_freq = -max_corr_freq;
  else if (corr_freq > max_corr_freq)
    corr_freq = max_corr_freq;

  /* Let the system driver perform the slew if the requested frequency
     offset is too large for the frequency driver */
  if (drv_accrue_offset && fabs(corr_freq) >= fastslew_max_rate &&
      fabs(offset_register) > fastslew_min_offset) {
    start_fastslew();
    corr_freq = 0.0;
  }

  /* Get the new real frequency and clamp it */
  total_freq = clamp_freq(base_freq + corr_freq * (1.0e6 - base_freq));

  /* Set the new frequency (the actual frequency returned by the call may be
     slightly different from the requested frequency due to rounding) */
  total_freq = (*drv_set_freq)(total_freq);

  /* Compute the new slewing frequency, it's relative to the real frequency to
     make the calculation in offset_convert() cheaper */
  old_slew_freq = slew_freq;
  slew_freq = (total_freq - base_freq) / (1.0e6 - total_freq);

  /* Compute the dispersion introduced by changing frequency and add it
     to all statistics held at higher levels in the system */
  slew_error = fabs((old_slew_freq - slew_freq) * max_freq_change_delay);
  if (slew_error >= MIN_OFFSET_CORRECTION)
    lcl_InvokeDispersionNotifyHandlers(slew_error);

  /* Compute the duration of the slew and clamp it.  If the slewing frequency
     is zero or has wrong sign (e.g. due to rounding in the frequency driver or
     when base_freq is larger than max_freq, or fast slew is active), use the
     maximum timeout and try again on the next update. */
  if (fabs(offset_register) < MIN_OFFSET_CORRECTION ||
      offset_register * slew_freq <= 0.0) {
    duration = MAX_SLEW_DURATION;
  } else {
    duration = offset_register / slew_freq;
    if (duration < MIN_SLEW_DURATION)
      duration = MIN_SLEW_DURATION;
    else if (duration > MAX_SLEW_DURATION)
      duration = MAX_SLEW_DURATION;
  }

  /* Restart timer for the next update */
  UTI_AddDoubleToTimespec(&now, duration, &end_of_slew);
  slew_timeout_id = SCH_AddTimeout(&end_of_slew, handle_end_of_slew, NULL);
  slew_start = now;
  slew_duration = duration;

  DEBUG_LOG("slew offset=%e corr_rate=%e base_freq=%f total_freq=%f slew_freq=%e"
            " duration=%f excess=%f slew_error=%e",
            offset_register, correction_rate, base_freq, total_freq, slew_freq,
            slew_duration, slew_excess_duration, slew_error);
}

/* ================================================== */

static void
handle_end_of_slew(void *anything)
{
  slew_timeout_id = 0;
  update_slew();
}

/* ================================================== */

static double
read_frequency(void)
{
  return base_freq;
}

/* ================================================== */

static double
set_frequency(double freq_ppm)
{
  base_freq = freq_ppm;
  update_slew();

  return base_freq;
}

/* ================================================== */

static void
accrue_offset(double offset, double corr_rate)
{
  offset_register += offset;
  correction_rate = corr_rate;

  update_slew();
}

/* ================================================== */
/* Determine the correction to generate the cooked time for given raw time */

static void
offset_convert(struct timespec *raw,
               double *corr, double *err)
{
  double duration, fastslew_corr, fastslew_err;

  duration = UTI_DiffTimespecsToDouble(raw, &slew_start);

  if (drv_get_offset_correction && fastslew_active) {
    drv_get_offset_correction(raw, &fastslew_corr, &fastslew_err);
    if (fastslew_corr == 0.0 && fastslew_err == 0.0)
      fastslew_active = 0;
  } else {
    fastslew_corr = fastslew_err = 0.0;
  }

  *corr = slew_freq * duration + fastslew_corr - offset_register;

  if (err) {
    *err = fastslew_err;
    if (fabs(duration) <= max_freq_change_delay)
      *err += slew_error;
  }
}

/* ================================================== */
/* Positive means currently fast of true time, i.e. jump backwards */

static int
apply_step_offset(double offset)
{
  struct timespec old_time, new_time;
  struct timeval new_time_tv;
  double err;

  LCL_ReadRawTime(&old_time);
  UTI_AddDoubleToTimespec(&old_time, -offset, &new_time);
  UTI_TimespecToTimeval(&new_time, &new_time_tv);

  if (PRV_SetTime(&new_time_tv, NULL) < 0) {
    DEBUG_LOG("settimeofday() failed");
    return 0;
  }

  LCL_ReadRawTime(&old_time);
  err = UTI_DiffTimespecsToDouble(&old_time, &new_time);

  lcl_InvokeDispersionNotifyHandlers(fabs(err));

  return 1;
}

/* ================================================== */

static void
set_sync_status(int synchronised, double est_error, double max_error)
{
  double offset;

  offset = fabs(offset_register);
  if (est_error < offset)
    est_error = offset;
  max_error += offset;

  if (drv_set_sync_status)
    drv_set_sync_status(synchronised, est_error, max_error);
}

/* ================================================== */

void
SYS_Generic_CompleteFreqDriver(double max_set_freq_ppm, double max_set_freq_delay,
                               lcl_ReadFrequencyDriver sys_read_freq,
                               lcl_SetFrequencyDriver sys_set_freq,
                               lcl_ApplyStepOffsetDriver sys_apply_step_offset,
                               double min_fastslew_offset, double max_fastslew_rate,
                               lcl_AccrueOffsetDriver sys_accrue_offset,
                               lcl_OffsetCorrectionDriver sys_get_offset_correction,
                               lcl_SetLeapDriver sys_set_leap,
                               lcl_SetSyncStatusDriver sys_set_sync_status)
{
  max_freq = max_set_freq_ppm;
  max_freq_change_delay = max_set_freq_delay * (1.0 + max_freq / 1.0e6);
  drv_read_freq = sys_read_freq;
  drv_set_freq = sys_set_freq;
  drv_accrue_offset = sys_accrue_offset;
  drv_get_offset_correction = sys_get_offset_correction;
  drv_set_sync_status = sys_set_sync_status;

  base_freq = (*drv_read_freq)();
  slew_freq = 0.0;
  offset_register = 0.0;
  slew_excess_duration = 0.0;

  max_corr_freq = CNF_GetMaxSlewRate() / 1.0e6;

  fastslew_min_offset = min_fastslew_offset;
  fastslew_max_rate = max_fastslew_rate / 1.0e6;
  fastslew_active = 0;

  lcl_RegisterSystemDrivers(read_frequency, set_frequency,
                            accrue_offset, sys_apply_step_offset ?
                              sys_apply_step_offset : apply_step_offset,
                            offset_convert, sys_set_leap, set_sync_status);

  LCL_AddParameterChangeHandler(handle_step, NULL);
}

/* ================================================== */

void
SYS_Generic_Finalise(void)
{
  struct timespec now;

  /* Must *NOT* leave a slew running - clock could drift way off
     if the daemon is not restarted */

  SCH_RemoveTimeout(slew_timeout_id);
  slew_timeout_id = 0;

  (*drv_set_freq)(clamp_freq(base_freq));

  LCL_ReadRawTime(&now);
  stop_fastslew(&now);

  LCL_RemoveParameterChangeHandler(handle_step, NULL);
}

/* ================================================== */