//: pps_pll.ino for https://arduino.stackexchange.com/questions/67699/stepper-motor-based-clock-movement-with-ds3231
//
// Adapting code from https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
// to do a PPS PLL per https://www.romanblack.com/one_sec.htm
// Simulation: https://wokwi.com/projects/326200705465451092
// DaveX 2020-02-22 CC BY-SA
// Adapted to Wokwi for https://forum.arduino.cc/t/time-delaying-or-time-adancing-a-signal/969389/7
/*
This code monitors a 1 Pulse Per Second (1PPS) input
on pin 2 and multiplies it to a higher rate (1000 pps) output
on pin 13 using a Phase Locked Loop to discipline the phase error.
Since this code is running in Wokwi, I simulated 1PPS signal source
on pin 10 to feed into pin 2. In silicon, you could use a GPS or a
RTC to provide an external clock source.
*/
// Output pulse variables
const byte led_pin = LED_BUILTIN;
volatile byte state = LOW;
const int pps = 10; // output PPS PLL'd to 1PPS
long count = 0; // Initial phase error Try non-zero initializations & see adaptation
unsigned long pulse_interval = (1000000L / pps);
const unsigned long pulse_us = pulse_interval / 20; // 5% duty cycle
unsigned long next_pulse = 0; //
unsigned long pulse_off = 0;
// Input pulse variables:
const byte interrupt_pin = 2; // Choose pin per https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
void sync_ISR(); // interrupt handler for monitoring the input
// simulated input pulse:
void simPulsesPB2(unsigned long base_interval); // simulated 1PPS signal
// Bresenham PLL clock disciplining variables
volatile int count_at_sync = 0; // bresenham sum of the phase difference
volatile bool rose = false;
const int kP = 5; // us/count Proportional control constants
const int kPdiv = 100; // divisor
void setup() {
Serial.begin(115200);
Serial.print("count_at_sync interval;\n");
pinMode(led_pin, OUTPUT);
pinMode(interrupt_pin, INPUT_PULLUP);
pinMode(10, OUTPUT); // simulated input PPS stream
attachInterrupt(digitalPinToInterrupt(interrupt_pin), sync_ISR, RISING);
}
void loop() {
unsigned long loop_micros = micros(); // capture start of loop time
simPulsesPB2(500000); // simulate a 1 PPS for Wokwi
if (loop_micros >= next_pulse) { // mul
// Schedule pulse
state = HIGH;
next_pulse += pulse_interval ;
pulse_off = loop_micros + pulse_us; // schedule off time
// PLL accounting
noInterrupts(); // protect from ISR changes
count++;
interrupts();
}
if (state == HIGH && loop_micros >= pulse_off) {
state = LOW;
}
if (digitalRead(led_pin) != state)
digitalWrite(led_pin, state); // update LED
// adjust interval
if (rose) { // adjust output pulse interval
rose = false; // one shot per pulse
pulse_interval = 1000000UL / pps + (count_at_sync * kP)/kPdiv; // proportional
Serial.print(count_at_sync);
Serial.print(' ');
Serial.print(pulse_interval);
Serial.print("us; ");
}
}
void sync_ISR() { // called by 1pps interrupt attached to pin 2
count_at_sync = (count -= pps); // PLL accounting and record synchronization state
rose = true;
}
/// This is for simulation of an external PPS signal feeding into Wokwi
void simPulsesPB2(unsigned long base_interval) {
// simulate a pulse stream with varying period
//const unsigned long base_interval = 1000;
static unsigned long interval = base_interval; //
static unsigned long last = - interval;
unsigned long now = micros();
if ( now - last > interval) {
last += interval;
//Serial.print('`');
PINB = bit(2); // toggle D10/PB2 per https://content.arduino.cc/assets/Pinout-UNOrev3_latest.pdf
//delayMicroseconds(interval/2);
//PINB = bit(2); // toggle PB2
// simulate different frequencies:
interval = base_interval ;
}
}