T4 and phase lock to a 60 KHz Time Signal

TelephoneBill

Well-known member
One of my current experiments concerns revisiting the UK MSF Time Signal on 60 KHz and investigating what advantages (if any) a Teensy 4 can offer in respect of achieving phase lock with its carrier (for original post, see - https://forum.pjrc.com/threads/35397-Teensy3-1-as-a-precision-Frequency-Standard).

The first write up uses a T3.1 and employs digital capacitors for frequency control which the T4 does not possess. So I have had to find a new method of very fine frequency control - and this is not as easy as before due to the "isolation" of the 24 MHz crystal module and lack of "timing influence" from the main chip. I have found a solution. It employs the higher peripheral clock rate of the T4 at 150 MHz, coupled with the fact that the 24 MHz crystal runs slightly slower than its nominal value at room temperatures by about 2 ppm.

Each of the T4 quad timers have direct access to the 150 MHz clock. The period for one clock pulse therefore ought to be a precise 6.667 nS. So to generate an exact 60 KHz square wave (with overall period 16.667 uS) that would require a total of 2500 clocks. Split this into two "half periods" of mark/space respectively and each would need 1250 clocks. The OFLAG output signal from a quad timer can employ "toggle mode", so using the COMPARE1 register set for 1250 counts, this should generate one cycle of the required 60 KHz square wave every two passes. Now the COMPARE1 register starts at 0 and will not trigger the OFLAG until the count rolls over from 1249, so the actual value held in the register needs to be 1249.

Recalling the fact that the clock module runs slow, if the COMPARE1 register held 1249 for two consecutive passes, this would generate a sq wave slightly lower than 60 KHz by about 2 parts per million. In order to increase that frequency slightly, one pass (say the "space" one) needs to be 1248 instead of 1249. But if that happened on every two passes, the output period would be under 16.667 uS by almost 6.667 nS - and this is too big by a long chalk. So this principle of using the 1249 and 1248 (mark and space) combination can only happen "once in a blue moon" if the measured frequency is going to end up very close to 60 KHz. How "infrequent" that switch from 1249/1249 to 1249/1248 happens is the actual method of frequency control that I found works very well. By using another simple internal count variable in the Interrupt Service Routine for the quad timer, I can strictly control when the switch to the faster rate occurs. And I can make it either less frequent or more frequent at will.

The net effect on the output frequency, of switching from one slower rate to another rate faster by just one clock period, causes the output signal to have one "clock's worth" of jitter. But because the peripheral clock rate is a super 150 MHz, this is limited to a mere 6.667 nS. That is far less jitter than that found in many of the GPS synthesised square wave signals.

The attached picture shows a variety of the waveforms from my experiment. Trace 2 (Light Blue) is the new T4 synthesised 60 KHz sq wave. Trace 1 (Yellow) is a 10 KHz reference TRIGGER signal from an ultra stable double oven 10 MHz OCXO divided down by 74HC390 decade counters. Trace 3 (Purple) is the same 60 KHz time signal as used in the T3.1 experiment, but this time I have fed the RF signal into one of the T4 ACMP comparators, and it is the output from that comparator that you see on the screen. Trace 4 (Dark Blue) is a 60 KHz reference signal from a GPS module and I have adjusted the Trace1 falling edge to co-incide with the rising edge of Trace 4. The advantage of using both the GPS and still triggering from OCXO is that I have an accurate UTC time reference but without the jitter that is inherent in my GPS module. This way, I can observe how well the T4 performs in an absolute sense (and that's very well, as you can see from the picture).

NewFile8.jpg

In a separate post, I will offer the code details of my sketch employed for this illustration and discuss some further refinements to the design.
 
Phase Comparison

A second design consideration with this experiment was to engineer a new mechanism for "phase comparison" between two 60 KHz waveforms - that is, between Trace 2 and Trace 3 in the previous picture.

Using the Quad Timer components in T4, again, this cannot follow the original T3.1 design because only ONE external pin can be referenced by any timer, at any given moment. If you use an external pin for the OFLAG output, you cannot then simultaneously attach another external pin directly to the same timer in order to CAPTURE the timer count from an external signal transition. You can, however, use the external pin associated with a second timer (in the same Quad Group of four timers) to capture the count of the first timer, but that is more tricky to arrange (and I could not immediately see how). Fortunately, I discovered a slight variation to this arrangement which I found simpler to configure and understand.

My alternative was to use TWO timers simultaneously (again in the same Quad Group) and set them both counting at the same rate and with the same initial zero setting. I then use one external pin connected with the first timer as an OFLAG output pin, whilst the external pin for the second timer is configured as its own COUNT CAPTURE. Because both timers are clocking at the same count rate, they will maintain synchronism of their count values. So a CAPTURE at any moment on the second timer will reflect the same reading as would be reached by the first timer. Therefore this second timer's captured value will also reflect the current phase of the first timer signal (the one whose output is made available as an external signal). This does mean that BOTH timers will have to be frequency adjusted when a control action is being applied (and at the same time) in order that they always remain in step, but that is a relatively simple thing to code for.

This arrangement is illustrated in the following diagram.
Timers01.jpg

In order to "lock" the phase of the OFLAG output (Trace 2) with that of the MSF Time Signal Carrier (Trace 3), the sequence of events has two distinct parts. First, the "frequency" of the OFLAG output has to brought into very close proximity with that of the Carrier. Once that has been achieved, then secondly, the "phase" of them has to be aligned. When the T4 chip is first switched on, the frequency of OFLAG will be close to the Carrier but slightly different (set by the initial condition of the LOAD register, and by ambient temperature). One simple method of frequency determination is to measure the phase of Timer 1 (represented by the capture value on a rising transition of the Carrier) and then measure it again a short time later (on the next falling transition of the carrier). The difference between these two values will be inversely proportional to the frequency difference twixt the two. When the measured difference is sufficiently small, then the phase alignment can be accomplished by nudging the control values to push the two signals into a defined alignment. The phase alignment I chose for this experiment is 90 degrees - this makes the maths simpler by avoiding a sudden large change in captured count value that happens when Timer 1 rolls over from maximum to zero. You can see this 90 degree alignment in the scope picture.
 
Temperature Stabilisation

(... that should read "directly proportional to the frequency difference" in the last paragraph, not "inversely proportional".)

As the 24 MHz crystal module warms up from switch-on, I discovered that the frequency tends to drift downwards (slower). This gets worse with an increase in ambient temperature, suggesting that the point of inflexion for the frequency/temp curve is somewhat below normal room temp (the curve is considered an inverted quadratic). The phase control described in post #2 will compensate for this, but the overall stability performance can be improved if the module could be assisted to reach a steady temperature and to remain fixed there.

One idea to try keep a steady module temperature is to employ the main chip itself as a form of "heating element". The body of the chip encapsulation has a larger thermal "inertia" than just the crystal module on its own, so the theory is that this will be less liable to temperature fluctuations. There is also a temperature sensor available for the chip core and the external body temperature should be proportional to any reading from it. Such a reading might then be used as a compensation factor in frequency control.

To get a close thermal coupling of the crystal can with the chip body, I have used a small piece of "1 cm. wide Copper Foil tape" - the type of sticky tape used by hobby enthusiasts - cut into an "L" shape so that it covers the chip surface but overhangs on one side and adheres to the top of the crystal module. Copper is an excellent thermal conductor, so it should transfer some of the heat energy over the top of the crystal can. This is illustrated in this next picture.

CopperFoil01.jpg

The wrinkles seen are difficult to avoid because the foil is only 40 thou. thick but they don't interfere with the principle of heat transfer. And care needs to be taken to bridge over the gap so as not to make contact with the two small oscillator capacitors underneath in the middle.

It goes without saying that the T4 board itself needs to be kept out of any air drafts. I use a small thin perspex cover during the testing.
 
Sketch Code and Description

Below is my current sketch to generate a precision 60 KHz. This is still a work in progress. When fine control is in operation, the slow drift back and forth (wrt GPS) of signal edge transitions is around 200 nS. I hope to improve on this later.

There are some interesting features of this design, including the frequency control mechanism - first described in post #1 - to compensate the 24 MHz clock and produce an output waveform who's long term accuracy is "parts per billion". Another feature is the use of the ACMP3 comparator and XBAR1 to bring the comparator output to a topside pin. This output is then hard wired to QT1 Timer2 input to provide a "capture count", which in turn provides the phase reference to stabilise the output signal.

Code:
//TestT4017 - QTIMER TEST PROGRAM for T4 (Phase Lock 60 KHz with UK MSF Time Signal)
//==================================================================================
//Author: TelephoneBill
//Date: 07 OCT 2019

//NOTES:
//TestT4017 - Teensy 4.0 - uses ACMP3, QT1 Timer1, QT1 Timer2.
//Phase Lock of T4 generated 60 KHz sq wave (Timer1 output on Pin 12) with MSF Radio Receiver 60 KHz input via 0.1 mfd capacitor (on pin 23).
//ACMP3 analogue comparator employed to convert MSF Receiver signal to a sq wave. ACMP3 input on Pin 23 is biased to mid voltage from wiper of
//a 10 turn pot (across 3v3 and GND). Adjusted so that "no signal" input can just create noise on ACMP3 output (this ensures comparator threshold
//in mid position). Output from ACMP3 is Pin 2 and this is wire jumpered to Pin 11 input of Timer2, so that sq wave triggers capture of Timer2
//value (150 MHz clock).

//Pins:
//Pin0 = ISR Strobe
//Pin1 = Action Strobe (timing when action happens)
//Pin2 = ACMP3_OUT (output) via XBAR1 and EMC_04
//Pin11 = Capture input for QT1 Timer2
//Pin12 = Output OFLAG for QT1 Timer1
//Pin13 = Diagnostic LED (One Second Flash)
//Pin18 = Comparator CMP1 input 60 KHz from radio receiver

//IDE Monitor Commands:
//a - Set ActionTime (a120000)
//aa - increment ActionTicks (bring forward by 12000 counts)
//B - Set Crystal Bias Current (0,1,2,3)
//c - Toggle ControlOn
//C - Set Comp11Val1/Comp11Val2
//d - Increment Comp11Val1 (d123) - decrease frequency
//D - Increment LSET - decrease frequency fine
//f - Toggel FineControlOn
//i - Invert output waveform
//p - Print TCVAverage/Comp11Val1/LSET value every second
//P - Print all TCVHist array
//r - Toggle RecordOn
//s - Decrement HCycleTicks by 1234
//T - Print CORE Temp
//u - Decrement Comp11Val1 (u123) - increase frequency
//U - Decrement LSET - increase frequency fine
//v - Print current ISRTicks
//w - Increments HCycleTicks by 1234
//z - Toggle Out60KHzOn

//definitions
#define TCVArraySize 1250     //size of history array for TCValues
#define ItemsPerLine 20       //printed output per line (TCValue history array)
byte Byte1, Byte2, Byte3, Byte4, Byte5, Byte6, Byte7;
volatile int a, b, c, N1Ptr;
volatile uint32_t ISRTicks, ISRComp1Ticks, ISRComp2Ticks, ISRCapt2Ticks, Bias, BiasValue, HCycleTicks, Secs, PrevSecs, T1, Increment, Time1, Time2, ElapsedTime;
volatile uint32_t ActionTicks, ActionTime, TCVHist[TCVArraySize];
volatile uint16_t TMR1Capt2Value, Count1, ISRControlTicks, Comp11Val1, Comp11Val2;
uint32_t TCVAverage, PrevTCVAverage;
int LSET, Diff;
float CPUTemp;
boolean ControlOn, ActionRequired, FineControlOn, PrintControlOn, RecordOn, InvertOn, Out60KHzOn, Out60KHzState;

//SETUP
//=====
void setup() {
  //initialise general hardware
  Serial.begin(9600);                   //setup serial port
  pinMode(0, OUTPUT);                   //pin 0 as digital output (ISR timing)
  pinMode(1, OUTPUT);                   //pin 1 as digital output (action timing strobe)
  pinMode(13, OUTPUT);                  //pin 13 as digital output
  FlashLED(4);                          //confidence boost on startup

  //turn on clocks for Timer QT1 (CG13)
  CCM_CCGR6 |= CCM_CCGR6_QTIMER1(CCM_CCGR_ON);

//===================================================================
  //Disable all timers for QT1
  TMR1_ENBL = 0;
  
  //intialise QT1 Timer1 registers (QT1_Timer1 = 60 KHz Output signal)
  TMR1_CTRL1 = 0;                       //stop all functions of QT1 Timer1 (Timers are known as 0,1,2,3)

  //status and control register
  TMR1_SCTRL1 = 0;                      //clear prior to setting
  TMR1_SCTRL1 |= 0b0000000000000001;    //no compare interrupt, no overflow, input edge flag enabled, rising capture, no forcing, no invert, external pin is OFLAG  
  
  TMR1_LOAD1 = 0;                       //counter starts counting from zero
  TMR1_COMP11 = 1250-1;                 //16.7uS - count up to this value
  TMR1_CMPLD11 = 1250-1;                //load compare register with value from this register

  //comparator status and control register
  TMR1_CSCTRL1 = 0;                     //clear prior to setting
  TMR1_CSCTRL1 |= 0b0000001001000001;   //no debug, no fault, no altload, no reload, no trigger, count up, TCF1EN, no COMP2, COMP1
  
  //configure Teensy pin Capture pin
  IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_01 = 1; //sets up pin 12 as the "external pin" for QT1 Timer1 (see R.M. page 309).

//===================================================================
  //intialise QT1 Timer2 registers (QT1_Timer2 = Internal Sync 60 KHz for capturing phase)
  TMR1_CTRL2 = 0;                       //stop all functions of QT1 Timer2 (Timers are known as 0,1,2,3)

  //status and control register
  TMR1_SCTRL2 = 0;                      //clear prior to setting
  TMR1_SCTRL2 |= 0b0000010011000000;    //no compare interrupt, no overflow, input edge flag enabled, rising/falling capture, no forcing, no invert, external pin is input  
  
  TMR1_LOAD2 = 0;                       //counter starts counting from zero
  TMR1_COMP12 = 1250-1;                 //16.7uS - count up to this value
  TMR1_CMPLD12 = 1250-1;                //load compare register with value from this register

  //comparator status and control register
  TMR1_CSCTRL2 = 0;                     //clear prior to setting
  TMR1_CSCTRL2 |= 0b0000001001000001;   //no debug, no fault, no altload, no reload, no trigger, count up, TCF1EN, no COMP2, COMP1
  
  //configure Teensy pin Capture pin
  IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_02 = 1; //sets up pin 11 as the "external pin" for QT1 Timer2 (see R.M. page 309).

//===================================================================
  //start QT1 Timer1
  TMR1_CTRL1 = 0b0011000000100011;      // 001(Count rising edges Primary Source),1000(IP Bus Clock),00 (Secondary Source = 0), 
                                        // 0(Count Once),1(Count up to Compare),0(Count Up),0(Co Channel Init),011(Toggle OFLAG on Compare)
  //start QT1 Timer2
  TMR1_CTRL2 = 0b0011000100100011;      // 001(Count rising edges Primary Source),1000(IP Bus Clock),10 (Secondary Source = Timer2 input pin), 
                                        // 0(Count Once),1(Count up to Compare),0(Count Up),0(Co Channel Init),011(Toggle OFLAG on Compare)
  //enable Timer1 and Timer2 simultaneously for QT1
  TMR1_ENBL = 0b0000000000000110;
  
//===================================================================
  //prepare interrupt parameters
  attachInterruptVector(IRQ_QTIMER1, QT1_isr);
  NVIC_ENABLE_IRQ(IRQ_QTIMER1);

//===================================================================

  //enable ACMP3 to threshold radio receiver output (0.1 mfd into pin 23 and 10 Turn pot)
  //ACMP3_OUT (output) is on T4 pin 2 (via XBAR1 and EMC_04)
  //input is pin 18 (ACMP3_IN0 = AD_B1_01 : Default - no need for muxing) - 0.1 mfd Capacitor from radio
  //voltage reference (INM7) is 6-bit internal DAC, VIN2 = VDD (3.3)
  //comparator output state available - read CMP1_SCR LSBit
  CCM_CCGR2 |= CCM_CCGR2_XBAR1(CCM_CCGR_ON);  //turn clock on for xbara1
  XBARA1_SEL3 = 0x001C;                       //this connects XBAR1_INOUT6 to XBAR1_IN28 (ACMP3_OUT)
  IOMUXC_SW_MUX_CTL_PAD_GPIO_EMC_04 = 3;      //ALT3 connects XBAR1_INOUT6 to T4 Pin 2
  IOMUXC_GPR_GPR6 |= 0b00000000000001000000000000000000; //IOMUXC_XBAR_DIR_SEL_6 = 1 (direction select for XBAR_INOUT6)
  CCM_CCGR3 |= 0x03000000;                    //enable clocks to CG12 of CGR3 for ACMP3
  CMP3_CR0 = 0b00000000;                      //FILTER_CNT=0; HYSTCTR=0
  CMP3_CR1 = 0b00010111;                      //SE=0, high power, COUTA, output pin, enable; mode #2A
  CMP3_DACCR = 0b11011111;                    //Set DAC = 1/2 of VIN2 (3.3v) 
  CMP3_MUXCR = 0b00000111;                    //CMP_MUX_PSEL(0) | CMP_MUX_MSEL(7) Input pins select; plus = IN0 (pin 18), minus = DAC (code 7). PSTM = 0 (Pass Through Mode Disabled)
  
  //initialise variables
  Count1 = 0;
  ISRTicks = 0;
  ISRControlTicks = 0;
  HCycleTicks = 0;
  Secs = 0;
  PrevSecs = 0;
  Comp11Val1 = 250;
  Comp11Val2 = Comp11Val1 + 1;
  LSET = 5;
  T1 = 1000;                                  //time T1 set for 1000
  N1Ptr = 0;
  ActionTicks = 0;
  ActionTime = 120000;                        //every second
  ActionRequired = false;
  RecordOn = true;
  Out60KHzOn = true;
}
  

//ISR ROUTINE FOR QT1
//===================
void QT1_isr() {                          //ISR for QT1
  //test Timer2 flags (60 KHz capture timer)
  if (TMR1_CSCTRL2 & TMR_CSCTRL_TCF1) {   //test if COMP1 flag set Timer2
    TMR1_CSCTRL2 &= ~(TMR_CSCTRL_TCF1);   //clear COMP1 flag
    ISRComp2Ticks++;
  }
  if (TMR1_SCTRL2 & TMR_SCTRL_IEF) {      //test if input edge capture event Timer2
    TMR1_SCTRL2 &= ~(TMR_SCTRL_IEF);      //clear input edge flag
    ISRCapt2Ticks++;
    TMR1Capt2Value = TMR1_CAPT2;          //read the capture value Timer2
    TMR1_CAPT2 = 0;                       //reset the capture register
  }

  //test Timer1 flags (60 KHz output timer)
  if (TMR1_CSCTRL1 & TMR_CSCTRL_TCF1) {   //test if COMP1 flag set Timer1
    digitalWriteFast(0,1);
    TMR1_CSCTRL1 &= ~(TMR_CSCTRL_TCF1);   //clear COMP1 flag
    ISRComp1Ticks++;

    //TIMER1 ACTIONS
    //==============
    //Output frequency adjustments for 60 KHz. Compare Register holds no. of clocks required for a toggle of the output state. More clocks - slower toggle (and freq).
    //Normally, the CMPLoad register uses 1249 clocks per toggle. Every now and again (ValX seconds), it will use 1248 (which makes it incrementally faster by 6.6667 nS).
    //So ValX controls the output frequency in a very fine way - bigger ValX then lower the freq. Val1 is always 1 count below Val2 - so always marginally faster freq
    //when Val1 in use than when Val2 is in use. LSET controls the proportion of time using either Val1 or Val2. If Count1 (0 to 9) is above or equal LSET then it uses Val1,
    //otherwise it will use Val2 (so a higher value of LSET makes the output lower frequency, because Val2 in use more than Val1).  
    Count1++;
    if (Count1>=10) {
      Count1 = 0;
    }
    ISRControlTicks++;                    //when ISRControlTicks reaches Val1 or Val2, then ISRControlTicks is reset zero
    if (Count1>=LSET) {                   //LSET = 0 to 9 - determines the fractional ratio of Val1 to Val2 (higher LSET = lower frequency)
      if (ISRControlTicks>=Comp11Val1) {  //Val1 = Val2 - 1, so this code is higher frequency (faster trace)
        //more ISRControlTicks used (slower freq)
        ISRControlTicks = 0;
        TMR1_CMPLD11 = 1248;              //count for just above 60 KHz (higher than 60 KHz)
        TMR1_CMPLD12 = 1248;
      }
      else {
        TMR1_CMPLD11 = 1249;              //count for just below 60 KHz (lower than 60 KHz)
        TMR1_CMPLD12 = 1249;
      }
    }
    else {
      if (ISRControlTicks>=Comp11Val2) {  //Val2 = Val1 + 1, so this code is lower frequency (slower trace)
        //less ISRControlTicks used (higher freq)
        ISRControlTicks = 0;
        TMR1_CMPLD11 = 1248;              //count for just above 60 KHz (higher than 60 KHz)
        TMR1_CMPLD12 = 1248;
      }
      else {
        TMR1_CMPLD11 = 1249;              //count for just below 60 KHz (lower than 60 KHz)
        TMR1_CMPLD12 = 1249;
      }
    }
  
    //test for record time (make N1Tot consecutive TCV measurements in total)
    if (RecordOn) {
      TCVHist[N1Ptr] = TMR1Capt2Value; //save Capture (phase) value in history
      N1Ptr++;
      if (N1Ptr>=TCVArraySize) {
        N1Ptr = 0;
      }
    }

    //update Half Cycle ticks (each tick is 8.3333 uS - 120,000 needed for one second)
    HCycleTicks++;                        //this updates every 8.3333 uS - uniquely identifies every ISR pass
    if (HCycleTicks>=120000) {            //one second has now elapsed
      digitalWriteFast(13,1);             //flash the LED on
      HCycleTicks = 0;                    //rollover of Timer1 ticks
      Secs++;                             //update seconds counter
    }
    if (HCycleTicks==2000) {
      digitalWriteFast(13,0);             //flash the LED off
    }

    //invert the output if desired (double time for Compare1 and Compare2 registers)
    if (InvertOn) {
      InvertOn = false;
      TMR1_CMPLD11 = 2499;
      TMR1_CMPLD12 = 2499;
    }

    //update control action required timer
    ActionTicks++;
    if (ActionTicks>=ActionTime) {        //action is required at ActionTime
      digitalWriteFast(1,1);              //action timing strobe (for scope use)
      ActionRequired = true;
      ActionTicks = 0;
    }
        
  }//end of Timer1 Actions

  //update count
  ISRTicks++;

  asm volatile ("dsb");                   // wait for clear memory barrier
  digitalWriteFast(0,0);
}


//MAIN LOOP
//=========
void loop() {
  int i, j;
  //call KeyInput() routine
  KeyInput();

  //test for control action needed
  if (ActionRequired) {
    ActionRequired = false;                 //action only once until next action time arrives

    //compute TCVAverage for most recent TCVArraySize values
    i = N1Ptr; j = 0; TCVAverage = 0;
    while (j<TCVArraySize) {
      TCVAverage += TCVHist[i-j];
      j++;
      if ((i-j)<0) {i += TCVArraySize;}  
    }
    TCVAverage = TCVAverage/TCVArraySize;
    digitalWriteFast(1,0);

    //compute change in TCVAverage   
    Diff = TCVAverage - PrevTCVAverage;     //measure the change of TCValue average since last second
    PrevTCVAverage = TCVAverage;            //update previous value of TCVAverage
    
    //test if automatic frequency control activated
    if ((ControlOn)&&(!FineControlOn)) {
      //Coarse Frequency/Phase Control
      if (Diff>=20) {                       //positive means 60 KHz Timer 1 too fast
          Comp11Val1+=5;                    //slow down
      }
      if (Diff<=-20) {                      //negative means 60 KHz Timer 1 too slow
          Comp11Val1-=5;                    //speed up
      }
      if ((Diff<20)&&(Diff>-20)) {
        if (TCVAverage>625) {               //nudge the phase to centre (625)
          Comp11Val1++;                     //slow down
        }
        if (TCVAverage<625) {               //nudge the phase to centre (625)
          Comp11Val1--;                     //speed up
        }
      }
    }
    if ((ControlOn)&&(FineControlOn)) {
      //Fine Frequency/Phase Control
      if (Diff>5) {                        //positive means 60 KHz Timer 1 too fast
          Comp11Val1++;                     //slow down
      }
      if (Diff<-5) {                       //negative means 60 KHz Timer 1 too slow
          Comp11Val1--;                     //speed up
      }
      if (TCVAverage>625) {                 //nudge the phase to centre (625)
        LSET++;                             //slow down
        if (LSET>9) {LSET=0; Comp11Val1++;}  
      }
      if (TCVAverage<625) {                 //nudge the phase to centre (625)
        LSET--;                             //speed up  
        if (LSET<0) {LSET=9; Comp11Val1--;}  
      }
    }
    Comp11Val2 = Comp11Val1 + 1;            //update the second compare value in line with first            
  }

  //print results of control action
  if ((Secs>PrevSecs)&&(PrintControlOn)) {
    Serial.print("Secs = "); Serial.print(Secs);
    Serial.print(", Comp11Val1 = "); Serial.print(Comp11Val1);
    Serial.print(", LSET = "); Serial.print(LSET);
    Serial.print(", TCVAverage = "); Serial.print(TCVAverage);
    Serial.print(", Diff = "); Serial.print(Diff);
    Serial.println();
    PrevSecs = Secs;
  }
}


//SUBROUTINES
//===========
//Flash LED routine
void FlashLED(int m) {
  for (int n=0;n<m;n++) {
    digitalWriteFast(13, 1);          //set pin 13 high
    delay(100);
    digitalWriteFast(13, 0);          //set pin 13 low
    delay(100);
  }
}

//Print Time1 and Time2 routine
void PrintTimes() {
  ElapsedTime = Time2 - Time1;
  Serial.print("Time1 = "); Serial.print(Time1);
  Serial.print(", Time2 = "); Serial.print(Time2);
  Serial.print(", ElapsedTime = "); Serial.println(ElapsedTime);
}

//KeyInput routine
void KeyInput() {
  //process any keystrokes available
  if (Serial.available()>0) {
    //read the incoming byte
    Byte1 = Serial.read();
    if (Byte1>0x20) {
      switch (Byte1) {
      case 'B':  //set Bias Current for 24 MHz oscillator
        //task goes here...
        if (Serial.available()>=3) {
          Byte2 = Serial.read();
          BiasValue = Byte2-0x30;
          Bias = BiasValue << 12;
          XTALOSC24M_MISC0 = (XTALOSC24M_MISC0 & 0xFFFF9FFF) | Bias;
        }
        Serial.print("Bias = "); Serial.println(Bias);
        break;
      case 'C':  //count value
        //task goes here...
        if (Serial.available()>=5) {
          Byte2 = Serial.read();
          Byte3 = Serial.read();
          Byte4 = Serial.read();
          Comp11Val1 = ((Byte2-0x30) * 100) + ((Byte3-0x30) * 10) + ((Byte4-0x30) * 1);
          Comp11Val2 = Comp11Val1 + 1;
        }
        Serial.print("Comp11Val1 = "); Serial.print(Comp11Val1); Serial.print(", Comp11Val2 = "); Serial.println(Comp11Val2);
        break;
      case 'D':  //increment LSET - lower frequency fine
        //task goes here...
        LSET++;
        if (LSET>9) {
          LSET = 0;
          Comp11Val1++;
          Comp11Val2 = Comp11Val1 + 1;            
        }
        Serial.print("LSET = "); Serial.print(LSET);
        Serial.print(", Comp11Val1 = "); Serial.print(Comp11Val1);
        Serial.print(", Comp11Val2 = "); Serial.println(Comp11Val2);
        break;
      case 'P':  //print TCV history array
        //task goes here...
        c = TCVArraySize/20;
        for (a=0; a<c; a++) {
          Serial.printf("%04d  ", a+1);
          for (b=0; b<ItemsPerLine; b++) {
            Serial.printf(" %04d", TCVHist[(ItemsPerLine*a)+b]);
          }
          Serial.println();
        }
        break;
      case 'T':  //CPU core temp
        //task goes here...
        CPUTemp = tempmonGetTemp();
        Serial.print("CPUTemp (deg C) = "); Serial.println(CPUTemp);
        break;
      case 'U':  //decrement LSET - raise frequency fine
        //task goes here...
        LSET--;
        if (LSET<0) {
          LSET = 9;
          Comp11Val1--;
          Comp11Val2 = Comp11Val1 + 1;            
        }
        Serial.print("LSET = "); Serial.print(LSET);
        Serial.print(", Comp11Val1 = "); Serial.print(Comp11Val1);
        Serial.print(", Comp11Val2 = "); Serial.println(Comp11Val2);
        break;
      case 'a':  //set ActionTime
        //task goes here...
        if (Serial.available()>=8) {
          Byte2 = Serial.read();
          Byte3 = Serial.read();
          Byte4 = Serial.read();
          Byte5 = Serial.read();
          Byte6 = Serial.read();
          Byte7 = Serial.read();
          ActionTime = ((Byte2-0x30) * 100000) + ((Byte3-0x30) * 10000) + ((Byte4-0x30) * 1000) + ((Byte5-0x30) * 100) + ((Byte6-0x30) * 10) + ((Byte7-0x30) * 1);
        }
        Serial.print("ActionTime = "); Serial.println(ActionTime);
        break;
      case 'c':  //toggle ControlOn
        //task goes here...
        ControlOn = !ControlOn;   //toggle ControlOn
        Serial.print("ControlOn = "); Serial.println(ControlOn);
        break;
      case 'd':  //increment count value (freq down)
        //task goes here...
        Comp11Val1++;
        Comp11Val2 = Comp11Val1 + 1;
        Serial.print("Comp11Val1 = "); Serial.print(Comp11Val1); Serial.print(", Comp11Val2 = "); Serial.println(Comp11Val2);
        break;
      case 'f':  //toggle FineControlOn
        //task goes here...
        FineControlOn = !FineControlOn;   //toggle ControlOn
        Serial.print("FineControlOn = "); Serial.println(FineControlOn);
        break;
      case 'i':  //set InvertOn
        //task goes here...
        InvertOn = true;
        Serial.print("InvertOn = "); Serial.println(InvertOn);
        break;
      case 'p':  //toggle PrintControlOn
        //task goes here...
        PrintControlOn = !PrintControlOn;   //toggle ControlOn
        Serial.print("PrintControlOn = "); Serial.println(PrintControlOn);
        break;
      case 'r':  //toggle RecordOn
        //task goes here...
        RecordOn = !RecordOn;   //toggle RecordOn
        Serial.print("RecordOn = "); Serial.println(RecordOn);
        break;
      case 's':  //decrement HCycleTicks by 1234 (S1234)
        //task goes here...
        if (Serial.available()>=6) {
          Byte2 = Serial.read();
          Byte3 = Serial.read();
          Byte4 = Serial.read();
          Byte5 = Serial.read();
          Increment = ((Byte2-0x30) * 1000) + ((Byte3-0x30) * 100) + ((Byte4-0x30) * 10) + ((Byte5-0x30) * 1);
          HCycleTicks = HCycleTicks - Increment;
          if (HCycleTicks>=120000) {HCycleTicks -= 120000;}
        }
        Serial.print("HCycleTicks = "); Serial.println(HCycleTicks);
        break;
      case 't':  //print Times
        //task goes here...
        PrintTimes();
        break;
      case 'u':  //decrement count value (freq up)
        //task goes here...
        Comp11Val1--;
        Comp11Val2 = Comp11Val1 + 1;
        Serial.print("Comp10Val1 = "); Serial.print(Comp11Val1); Serial.print(", Comp11Val2 = "); Serial.println(Comp11Val2);
        break;
      case 'v':  //print ISRTicks, ISRComp2Ticks, ISRCapt2Ticks
        //task goes here...
        Serial.print("ISRTicks = "); Serial.println(ISRTicks);
        Serial.print("ISRComp1Ticks = "); Serial.println(ISRComp1Ticks);
        Serial.print("ISRComp2Ticks = "); Serial.println(ISRComp2Ticks);
        Serial.print("ISRCapt2Ticks = "); Serial.println(ISRCapt2Ticks);
        break;
      case 'w':  //increment HCycleTicks by 1234 (S1234)
        //task goes here...
        if (Serial.available()>=6) {
          Byte2 = Serial.read();
          Byte3 = Serial.read();
          Byte4 = Serial.read();
          Byte5 = Serial.read();
          Increment = ((Byte2-0x30) * 1000) + ((Byte3-0x30) * 100) + ((Byte4-0x30) * 10) + ((Byte5-0x30) * 1);
          HCycleTicks = HCycleTicks + Increment;
          if (HCycleTicks>=120000) {HCycleTicks -= 120000;}
        }
        Serial.print("HCycleTicks = "); Serial.println(HCycleTicks);
        break;
        TMR1_SCTRL1 |= 0b0000000000000001;
      case 'z':  //toggle Out60KHzOn
        //task goes here...
        Out60KHzOn = !Out60KHzOn;   //toggle RecordOn
        Serial.print("Out60KHzOn = "); Serial.println(Out60KHzOn);
        if (Out60KHzOn) {
          TMR1_SCTRL1 = 0;                      //clear prior to setting
          TMR1_SCTRL1 |= 0b0000000000000001;
        }
        else {
          TMR1_SCTRL1 = 0;            
        }
        break;
      }
    }
  }
}

The first part of SETUP configures QT1 Timer 1 as the output signal source (available on Pin12). Then QT1 Timer2 is configured as the "capture count" phase reference. Both timers are finally enabled with the TMR1_ENBL command to ensure that they start "clocking" at exactly the same moment. A serial monitor "v" command prints out the cumulative ticks of each timer as they fire the ISR in turn to demonstrate that both counts track each other precisely in time. So reading the "capture" of Timer2 is equivalent to reading the same value in Timer1, and hence determining the phase of Timer1 wrt the edge from the ACMP3 comparator (MSF radio signal). The last statements in SETUP configure ACMP3 and XBAR1 and a number of variables are initialised.

The next code creates the ISR. This ISR can be fired by three discrete events (in the order listed)... (1) Timer2 reaching its COMPARE1 register and reloading, (2) Timer2 detecting a "capture" edge on its external pin, (3) Timer1 reaching its COMPARE1 register and reloading. Most of the action in the ISR happens as a result of this last event.

Now comes the frequency control statements, which are a little tricky to decipher. There are in fact two control ideas. One, which relies on "Count1" reaching a level set by the LSET variable, is a FINE FREQUENCY control (LSET varies from 0 to 9 and provides "tenths" of the COARSE control). The other is the COARSE CONTROL provided by "ISRControlTicks" which reaches either a level set by "Comp11Val1" or "Comp11Val2" (which one is used depends on LSET). Once the level of Comp11Val1/Comp11Val2 is reached, then the COMPARE1 value changes from 1249 to 1248. This is effectively dropping one count, but not on every pass of the ISR (that is, not for every value of "ISRControlTicks"). The count is only dropped once infrequently, and how infrequent is decided by the value of Comp11Val1/Comp11Val2 (whichever is in operation at the time).

Comp11Val2 is always "1" value higher than Comp11Val1. And how many times Comp11Val2 is used, rather than Comp11Val1, depends on LSET. As a rule, when LSET is fixed at midpoint "5" (for example) then the output frequency goes "faster" with a lower value for Comp11Val1. This is an illustration of COARSE control. A "C123" serial monitor command will set Comp11Val1 manually to "123", which is a faster setting than C250 (the nominal on program startup).

Notice also that both Timer1 and Timer2 COMPARE registers need to be changed at the same time, or else they won't track each other exactly (TMR1_CMPLD11 and TMR1_CMPLD12).

What the program illustrates in operation is the fact that the frequency of the 24 MHz crystal module changes with temperature. Such changes are very small - parts per billion rather than parts per million - as the crystal reaches its nominal operating temperature. But such is the precision achieved by this 60 KHz output waveform over time that these changes make all the difference to achieving "phase lock" to the MSF radio signal, or not doing so.

The next ISR code after frequency control is a mechanism for deciding if "Recording" of the Timer2 captured value is performed or not. This was a diagnostic section during early development (not now significant). RecordOn is normally true in practise, so 1250 historical values are stored in a circular buffer. These are average later in the MAIN LOOP to smooth out noise on the radio signal.

Then a count is maintained of the number of "Half Cycle Ticks" (each cycle of 60 KHz has two half cycles created by the rising or falling transitions). After 120,000 ticks this counter is reset to zero and the Teensy4 LED is flashed on and off. So the LED will flash precisely once every second.

The 60 KHz output waveform (Timer1 OFLAG) toggles state on each Half Cycle Tick. Which state happens at any given moment (compared to the radio signal state) will depend on the startup time. The next few statements allow a manual "i" command to cause an inversion of this output signal, if that is desired for any reason. They do that by simply adding an additional "1250 counts" to the COMPARE registers for one tick.

The final statements within the ISR determine the ACTION TIME that a corrective control action takes place in the MAIN LOOP. This is nominally set to once every second but the "a240000" monitor command (or another six digit value) can change that. The value is 120000 for every second, so 240000 causes this to change to once every two seconds, or "012000" would be ten times per second.

In the MAIN LOOP, first the "KeyInput" routine checks to see if any manual commands have been entered, and then carries these out accordingly. Then, the next test is to see if ACTION TIME has been reached. If it has then action is required. The first action is to compute the average value for Timer2 CAPTURE (TCVAverage) over the last 1250 values (to remove noise as mentioned). This averaged variable can then be used as a reliable phase reference for controlling the output waveform.

The latter statements in "action required" decide how frequency control is going to automatically be performed. If just "ControlOn" is true (set by a "c" command") then COARSE control is enacted by adjusting the level for "Comp11Val1". If "FineControlOn" is true (set by an "f" command) then a more subtle control is performed by testing the phase reference "TCVAverage" against the phase midpoint of the radio signal, which should be "625" (1250 divided by 2, so a 90 degree lag). Both "c" and "f" commands are toggle on/off commands.

The final part of the MAIN LOOP prints out diagnostic information in the serial monitor window.

===

What this program achieves is a precision 60 KHz output that is phase locked (90 degree lag) with the MSF radio time signal using Teensy4. The ISR routine is also triggered with the same precision, so further code could be included here for any additional purpose. The waveforms are illustrated in the picture of post #1.

This precision of "parts per billion" is achieved even though the 24 MHz module crystal frequency itself has an accuracy of about 2ppm, and that does not change.

If anyone wishes to try out this sketch but does not have access to the MSF signal, a GPS module can be used as a substitute (set the GPS output to 60 KHz and wire
as though it were the MSF signal).
 
The DS3231 looks to be an excellent device. It quotes +/- 2ppm so looks fine for mcros() and millis() as standalone. Has a few other interesting features. Great value.

Have you tried receiving DCF77 via a ferrite loopstick? I guess there will be 77.5 KHz crystals available locally. Like my MSF signal, the best idea is to put a crystal in a high impedance amp to get the maximum Q. Then if this is tuned by a small trimmer cap to be spot on frequency (crystal might need selecting), just a very small amount of stray coupling from the received signal will set the crystal resonating at the precision 77.5 KHz transmitted signal. This can then be squared up using the traditional methods.

I have been playing with a simple Pierce crystal oscillator recently and included an RC in the feedback (often needed for small low freq crystals). Then by adjusting the R to get it working at a fraction under 60 KHz exact, again, stray coupling from the MSF receiver output can pull the Pierce into exact lock at the transmitted frequency. Wonderful to see such a precision signal.

A problem that cannot be avoided with "over the air transmissions" is phase drift caused by mixing of the ground wave with reflections from the underside of the D layer. These add vectorally for a resultant phase, but the D layer height varies a bit with electron production (UV rays from the sun) and this wobbles the phase at a slow rate.

I don't see any easy way to alter the response of the DS3231 so not sure the code above will be of use, but feel free to use bits as you think appropriate.
 
Just noticed that the data sheet describes an AGE register which looks useful for modifying frequency. It suggests that at 25 deg C, a 1 bit change alters the frequency by 0.1ppm. This can only be changed relatively slowly, but when calibrated against a good reference like radio time signals (or GPS), it would provide an excellent "standard" that should be "phase shift free". I'll try get hold of one.
 
I got mine for cheap as "Chronodot 2.0" - Mine is most likely a clone of the Adafruit board (mine was <5€ - I did not know that it is a Adafruit product)
You can use Paul's DS1307 library for the basic functions (time), it is partly compatible.
 
It looks like the teensy4 can be switched from a 24 Mhz crystal derived oscillator to a RTC crystal derived RC oscillator. Which appears to be tunable with COUNT_RC_TRG (or RC_OSC_PROG?) The result might be tunable (1.37 ppm) accuracy that could be dithered for long term perfect.
 
Back
Top