# Autotune

With the Crystal Frequency enhancement of my library I can of course fix the biggest issue of my clock library: it does not keep time well if the DCF77 signal is lost. Although this happens rarely due to its extreme noised tolerance, it still will happen (e.g. because DCF77 goes down for maintenance).

First I considered adding a DS3232 RTC but then came to the conclusion that temperature drift is not a big issue as my clock is usually sitting at room temperature. So if the adjustment is roughly good to 1 ppm I would trust this for several hours. The only thing is how to ensure that it is almost always close to 1 ppm.

My solution is to compare the phase of the decoder stage with the elapsed time. The point is that the detector stage (and thus the 1 second tick) is phase locked to the demodulated DCF77 signal. On the other hand the millisecond interrupts and thus the 1/100 second ticks are driven by the crystal clock. Thus it becomes possible to compare the phase of the divided crystal clock with the phase of the demodulated DCF77 signal. This in turn allows me to infer the necessary frequency adjustments.

Lets have a look at the new design and see how it works. If you compare this with the previous design you notice the new box “frequency control”. This box is implemented in the namespace DCF77_Frequency_Control. As I already explained the basic idea is very simple. The tricky part is the question: how long should I measure? I call the measurement period length “tau”. The unit of measure is 1/100 s. This is because the phase lock in the decoder resolves 1/100 s. It follows that the precision that I can achieve (in ppm) is 1 000 000 * 1/100 s / tau = 10 000 s / tau. Or with to put it another way in order to get down to 1 Hz I would need to measure 160 000 s or almost two days. Unfortunately this implies that under normal conditions this is almost impossible to do. The issue is that crystal frequency depends on temperature and temperature varies some centigrades between day and night. As it turns out this is enough to shift the crystal frequency by some Hertz. On the other hand to short measurements will not allow me to tune to a reasonable precision.

So I experimented quite a lot and came to the conclusion that “bisection” seems a reasonable approach. I start with tau = 2500 s (for a precision of 64 Hz or 4 ppm). After this period I will adjust the crystal in steps of 64 Hz. If the phase deviates by 5 or more ticks (= 1/20 s) then I will immediately adjust and restart the measurement. Eventually it will require only 0 or 1 step adjustment. Once this happens I will double time and adapt the resolution acoordingly.
In case of a 0 step adjustment I will also pretend that half of the (new) measurement period is already elapsed. This way the frequency control will settle faster. This will go on till either tau_max is reached (and thus frequency is controlled to within 1 Hz) or if it does not manage to get better precision anymore (e.g. due to frequency variations caused by the temperature shift between night and day).

In case the phase drifts a lot then the algorithm check if less than tau/2 have elapsed and decreases tau accordingly. If this does not help and the error becomes still bigger it will immediately terminate the measurement and decrease tau.

The constants when to switch are somewhat deliberate but I fiddled and tested quite a lot with them. Testing is pretty time consuming since the time constants are so large though.

```namespace DCF77_Frequency_Control {
// tau = length of measurement period in ticks (@ 100 Hz)
volatile uint32_t tau = tau_min;
volatile int8_t   precision = precision_at_tau_min;
volatile int8_t   confirmed_precision = 0;

// indicator if data may be persisted to EEPROM
volatile boolean  data_pending = false;

volatile uint32_t elapsed;
volatile int16_t  tick;
volatile int16_t  start_tick;

volatile int16_t  deviation = 0;

void restart_measurement() {
start_tick = tick;
elapsed = 0;
}

bool calibration_running = false;
void start_calibration() {
calibration_running = true;
restart_measurement();
}

void cancel_calibration() {
calibration_running = false;
}

// get the adjust step that was used for the last adjustment
//   if there was no adjustment or if the phase drift was poor it will return 0
//   if the adjustment was from eeprom it will return the negative value of the
int8_t get_confirmed_precision() {
return confirmed_precision;
}

// this is always something in between the adjust step contants
int8_t get_target_precision() {
return precision;
}

// 1 tick = 1/100s but this does not matter, the only relevant informaiton is the ratio
//   deviation / elapsed_ticks
// this is undefined if elapsed_ticks == 0, it is also pretty meaningless if elapsed_ticks is very small
// elapsed ticks == 0 occurs immediately at the start of a new measurement run,
// it also occurs if the clock is out of sync
void get_phase_deviation(int16_t &deviation, uint32_t &elapsed_ticks) {
if (calibration_running) {
const uint8_t prev_SREG = SREG;
cli();
elapsed_ticks = elapsed;
deviation = deviation;
SREG = prev_SREG;
sei();
} else {
elapsed_ticks = 0;
}
if (elapsed_ticks = 0) {
// necessary even if calibration_running in order
// to avoid glitches right at the start of a run
deviation = 0;
}
}

void debug() {
Serial.println(F("confirmed_precision ? target_precision, total_adjust, deviation(3), elapsed/tau: "));
Serial.print(confirmed_precision);
Serial.print(' ');
Serial.print(calibration_running? '@': '.');
Serial.print(' ');
Serial.print(precision);
Serial.print(F(", "));
Serial.print(F(" Hz, "));
Serial.print(deviation);
Serial.print(F(" Ticks, "));
const int16_t deviation_Hz = deviation * precision;
Serial.print(deviation_Hz);
Serial.print(F(" Hz, "));
const int16_t deviation_ppm = deviation_Hz / 16;
Serial.print(deviation_Hz / 16);
const int8_t remainder = deviation_Hz - 16*deviation_ppm;
if (remainder) {
if (remainder>0) {
Serial.print(F("+"));
}
Serial.print(remainder);
Serial.print(F("/16"));
}
Serial.print(F(" ppm "));
Serial.print(F(", "));
Serial.print(elapsed);
Serial.print('/');
Serial.println(tau);
}

bool increase_tau() {
if (precision > precision_at_tau_max) {
tau <<= 1;
precision >>= 1;
return true;
} else {
return false;
}
}

bool decrease_tau() {
if (precision < precision_at_tau_min) {
tau >>= 1;
precision <<= 1;
return true;
} else {
return false;
}
}

}

void process_1_Hz_tick() {
const int16_t good_deviation = 1;  // maximum absolute deviation to increase tau
const int16_t poor_deviation = 3;  // deviation to decrease tau
const int16_t bad_deviation  = 5;  // deviation to decrease tau immediately

if (calibration_running) {
//const int16_t deviation = tick - start_tick;
deviation = tick - start_tick;
const int16_t absolute_deviation = deviation>0 ? deviation : -deviation;

if (elapsed >= tau) {
// measurement finished
if (absolute_deviation == 0 &&
precision > precision_at_tau_max) {
//    absolute_deviation == 0
// precision not at maximum resolution
//   --> next measurement period will be doubled

// however since no adjustment was done we can
// just continue with the current measurement period
// and thus get faster to the next better resolution
} else {
// adjustment done --> new measurement period required
restart_measurement();
}

confirmed_precision = precision;
data_pending = true;

if (absolute_deviation <= good_deviation) {
increase_tau();
} else
if (absolute_deviation <= poor_deviation) {
// stay with current precision
} else {
if (!decrease_tau()) {
confirmed_precision = -precision;
data_pending = false;
}
}
} else {  // elasped < tau
if (absolute_deviation >= poor_deviation) {
while (elapsed + elapsed < tau && decrease_tau()) {
// decrease_tau as much as reasonable, notice that decrease_tau()
// will return true if it decreased tau
// also notice that it allows for coarser adjustments
}
}
// tau was already decreased but we drifted > 4/100 s this measurement period
// --> drift is way to high, terminate the measurement immediately and
//     adjust as if the measurement was completed
// --> adjustment is probably not sufficient but we get a new measurement
//     period and thus the change to react earlier, also since tau will
//     be decreased larger adjustment steps become possible
restart_measurement();

decrease_tau();
confirmed_precision = -precision;
data_pending = false;
}
}
tick -= 100;
}
}

void process_1_kHz_tick() {
static uint8_t divider = 0;
if (divider < 9) {
++divider;
}  else {
divider = 0;

// ticks will increase every 10 ms = @ 100 Hz
++tick;
++elapsed;
}
}

// ID constants to see if EEPROM has already something stored
const char ID_u = 'U';
const char ID_k = 'P';
void persist_to_eeprom(const int8_t precision, const int16_t adjust) {
// this is slow, do not call during interrupt handling
uint16_t eeprom = eeprom_base;
eeprom_write_byte((uint8_t *)(eeprom++), ID_u);
eeprom_write_byte((uint8_t *)(eeprom++), ID_k);
eeprom_write_byte((uint8_t *)(eeprom++), (uint8_t) precision);
eeprom_write_byte((uint8_t *)(eeprom++), (uint8_t) precision);
eeprom += 2;
Serial.println(F("persisted to eeprom"));
}

uint16_t eeprom = eeprom_base;
if (eeprom_read_byte((const uint8_t *)(eeprom++)) == ID_u &&
eeprom_read_byte((const uint8_t *)(eeprom++)) == ID_k) {
uint8_t ee_precision = eeprom_read_byte((const uint8_t *)(eeprom++));
if (ee_precision == eeprom_read_byte((const uint8_t *)(eeprom++))) {
eeprom += 2;
precision = (int8_t) ee_precision;
return;
}
}
}
precision = 0;
}

int8_t ee_precision;
tau = tau_max / precision;
}

// do not call during ISRs
void auto_persist() {
// ensure that reading of data can not be interrupted!!
// do not write EEPROM while interrupts are blocked
int8_t  precision;
cli();
if (data_pending && confirmed_precision > 0) {
precision = confirmed_precision;
} else {
data_pending = false;
}
sei();
if (data_pending) {
int8_t  ee_precision;

if (confirmed_precision < abs(ee_precision) ||        // precision better than it used to be
( abs(ee_precision) < 8 &&                        // precision better than 8 Hz or 0.5 ppm @ 16 MHz
abs(ee_adjust-adjust) > 8 )           ||        // deviation worse than 8 Hz (thus 16 Hz or 1 ppm)
( confirmed_precision == precision_at_tau_max &&  // tau_max > 1 day thus it is
{
cli();
const int8_t  new_ee_precision = precision;
sei();
}
data_pending = false;
}
}

void setup() {
uint32_t ee_tau;
int8_t ee_precision;

if (ee_precision < precision_1_ppm && ee_precision > 0) {
// we assume that at startup we will never be better than 1 ppm,
// no matter how good the previous calibration was
ee_precision = precision_1_ppm;
} else {
ee_precision = precision_at_tau_min;
}
ee_tau = tau_max / ee_precision;

cli();
tau = ee_tau;
precision = ee_precision;
sei();
// so we know the absolute value of the precision but
// still have an indicator that it was read from EEPROM
confirmed_precision = precision > 0? -precision: precision;