Question re RTC compensation function

Status
Not open for further replies.

Constantin

Well-known member
Hi all,

tonight, I though I'd take a crack at figuring out how to calibrate my RTC using a PPS signal from a GPS receiver. The first step was calculating by how much the MCU clock seems to be off. In this particular instance, about 14.5PPM, BTW. Then wait 10 seconds (as defined by seconds() command from the time library) and see how many µs that took using the MCU as a yard stick. Then multiply the MCU offset (in PPM) by interval(µs)/(time measured by MCU while RTC ticked). Usually the latter is less than 1PPM off relative to the MCU clock, and the multiplier is something like 0.99999 etc.

The product is then used to calculate the offset for the compensate function (about -124 in this case). However, setting this number seems to have no effect. I'm sure there is something obviously wrong with my code, but I'm missing it. Any help would be appreciated!

Code:
//This sketch attempts to calibrate the RTC crystal on a Teensy 3
//using a PPS signal from a GPS receiver

//enables RTC on Teensy 3:
#include <Time.h>  

//determine what Teensy 3 pin the PPS Signal goes to:
const byte PPS = 14; 

void setup() {
  pinMode(PPS,INPUT); //PPS Signal input
  Serial.begin(115200);

  // set the Time library to use Teensy 3.0's RTC to keep time
  setSyncProvider(getTeensy3Time);
  while (!Serial);  // Wait for Arduino Serial Monitor to open	

  delay(100);
  if (timeStatus()!= timeSet) {
    Serial.println("Unable to sync with the RTC");
  } 
  else {
    Serial.println("RTC has set the system time");
  }
}



void loop() {
    if (Serial.available()) {
    time_t t = processSyncMessage();
    if (t != 0) {
      Teensy3Clock.set(t); // set the RTC
      setTime(t);
    }
  }
  
  //how many seconds is the time interval long over which the
  //MCU and RTC will be measured:
  int time_interval = 10; //seconds
  Teensy3Clock.compensate(-124);

  //now determine how many microseconds the MCU takes to count to x
  unsigned long time_interval_mcu = mcu_time_interval_calc(time_interval);

  //now determine how many microseconds the RTC takes to count to x
  unsigned long time_interval_rtc = rtc_time_interval_calc(time_interval);

  long PPM_delta_MCU= time_interval_mcu-1000000L*(long)time_interval;
  Serial.print("i.e. ");
  Serial.print((float)PPM_delta_MCU/(float)time_interval,2);
  Serial.print("PPM for the MCU and ");
  long PPM_delta_RTC= time_interval_rtc-1000000L*(long)time_interval;
  Serial.print((float)PPM_delta_RTC/(float)time_interval,2);
  Serial.println("PPM for the RTC.");

  //because the combination of both formulas cancel out the interval time, omit
  float RTC_cumulative_PPM = (float)PPM_delta_MCU*(float)1000000/(float)time_interval_rtc;

  Serial.print("Taken together, combined RTC offset appears to be ");
  Serial.print(RTC_cumulative_PPM,2);
  Serial.println("PPM for the RTC.");

  int RTC_compensate =(int)(-RTC_cumulative_PPM/.1192);

  Serial.print("This suggests a ");
  Serial.print(RTC_compensate);
  Serial.println(" rtc_compensate factor...");

}

time_t getTeensy3Time()
{
  return Teensy3Clock.get();
}

/*  code to process time sync messages from the serial port   */
#define TIME_HEADER  "T"   // Header tag for serial time sync message

unsigned long processSyncMessage() {
  unsigned long pctime = 0L;
  const unsigned long DEFAULT_TIME = 1357041600; // Jan 1 2013 

  if(Serial.find(TIME_HEADER)) {
    pctime = Serial.parseInt();
    return pctime;
    if( pctime < DEFAULT_TIME) { // check the value is a valid time (greater than Jan 1 2013)
      pctime = 0L; // return 0 to indicate that the time is not valid
    }
  }
  return pctime;
}

unsigned long mcu_time_interval_calc(int time_interval)
{
  unsigned long time_begin, time_end, delta;
  while(digitalRead(PPS)==LOW){
  } //wait until it goes high but ignore the first one
  delay(200); //delay 200ms to let PPS signal die  
  while(digitalRead(PPS)==LOW){
  } //wait until it goes high, second time is a charm
  time_begin=micros();  
  for (int i=0;i<time_interval;i++)
  {
    delay(200); //delay 200ms to let PPS signal die
    while(digitalRead(PPS)==LOW){
    } //wait x rounds
  } 
  time_end=micros();
  delta=time_end-time_begin;

  Serial.print("Over a ");
  Serial.print(time_interval);
  Serial.print(" second interval, the MCU took ");
  Serial.print(delta);
  Serial.println(" 'MCU micro-seconds'.");   
  return (delta); //return the interval that the MCU will run
}

unsigned long rtc_time_interval_calc(int time_interval)
{
  unsigned long time_begin, time_end, delta;
  byte current_second=second();
  while(current_second==second()){
  } //wait until it changes
  time_begin=micros(); // start clock
  current_second=second();
  for (int i=0;i<time_interval;i++)
  {
    while(current_second==second()){
    } //wait x rounds
    current_second=second();//each time increment the seconds
  } 
  time_end=micros();
  delta=time_end-time_begin;

  Serial.print("Over a ");
  Serial.print(time_interval);
  Serial.print(" second interval, the RTC took ");
  Serial.print(delta);
  Serial.println(" 'MCU micro-seconds'.");   
  return (delta); //return the mcu us the RTC took
}
 
Last edited:
The syncronisation to the RTC is done every 300 seconds as seen in the now() function (in Time.cpp).

Code:
static uint32_t syncInterval = 300;  // time sync will be attempted after this many seconds

Between the synchronisations the clock, and seconds(), runs off millis() so for a few ten second intervals you will compare millis()
to millis()
 
Hi mlu, and many thanks for your observation - silly me!

For some reason I thought seconds() would be interrupt driven from the Teensy 3 RTC core. Seems like a simpler way to do it than to use the MCU clock, even if the latter is synchronized from time to time. I shall regroup.
 
Perhaps its as simple as using

Code:
second(Teensy3Clock.get())

to read the RTC second without any resync issues.
 
Many thanks, I will try that!

Long term, for the teensy it would seem to make sense to re-write the now() section of the library to use the RTC interrupt function? That is, have an interrupt handler increment sysTime++ every time the RTC jumps instead of the while statement lifted from the time.cpp file below. The syncing can then still happen.

Code:
time_t now() {
  while (millis() - prevMillis >= 1000){      
    sysTime++;
    prevMillis += 1000;        
#ifdef TIME_DRIFT_INFO
    sysUnsyncedTime++; // this can be compared to the synced time to measure long term drift     
#endif
  }
  .
  .
  .
return (time_t)sysTime;
}

However, for my code, it might make more sense to focus on the registers themselves and not incorporate the time() library at all. That is, I see on page 867 of the manual, RTC_TSR, the register for seconds. A simple compare would establish an increment while allowing for any compensation to be automatically incorporated, no?

Code:
unsigned long rtc_time_interval_calc(int time_interval)
{
  unsigned long time_begin, time_end, delta;
  int current_second=RTC_TSR;
  while(current_second==RTC_TSR){
  } //wait until it changes
  time_begin=micros(); // start clock
  current_second=RTC_TSR;
  for (int i=0;i<time_interval;i++)
  {
    while(current_second==RTC_TSR){
    } //wait x rounds
    current_second=RTC_TSR;//each time increment the seconds
  } 
  time_end=micros();
  delta=time_end-time_begin;
  return (delta); //return the mcu us the RTC took
}

The only downside is that the MCU is used as a reference time keeper. I haven't figured out yet how to use the PPS signal directly to compensate the RTC clock, so I use this interim method, where the error of the MCU clock is measured first, then the time is measured for the RTC to go through x seconds and finally the compensated time is measured. The key setting up front is setting zero compensation for the MCU clock, ensuring that the MCU is not re-calibrated mid-measurement (for long measurements).

Seem reasonable?
 
Last edited:
For measuring the quotient between GPS PPS and RTC seconds the MCU clock, miilis()
should give you better than 0.5 ppm resolution, including digitisation errors and loop initialisation and such,
even if the MCU clock has a 15ppm error, as long as that error stays stable.

When you know that the two clocks give the same number of milliseconds over your calibration interval
then you can use the Cortex-M4 systick counter to measure fractions of milliseconds, it counts down from
48000-1 or 96000-1 to 0 every milliseconds.
 
For measuring the quotient between GPS PPS and RTC seconds the MCU clock, miilis()
should give you better than 0.5 ppm resolution, including digitisation errors and loop initialisation and such,
even if the MCU clock has a 15ppm error, as long as that error stays stable.

If mills() is good, wouldn't micros() be better?

FWIW, the two clocks have been running for more than 5 minutes now and the compensate function does not seem to affect the RTC_TSR register. That is, RTC_TSR continues to increment at the same rate with the same offset as it did before I used Teensy3Clock.compensate(-125). Seems to me that the compensation seems to happen downstream from the RTC_TSR register?

Here is some output, unchanged over 5 minutes:

Code:
Over a 10 second interval, the MCU took 10000150 'MCU micro-seconds'.
Over a 10 second interval, the RTC took 10000144 'MCU micro-seconds'.
i.e. 15.00PPM for the MCU and 14.40PPM for the RTC.
Taken together, combined RTC offset appears to be 15.00PPM for the RTC.
This suggests a -125 rtc_compensate factor...

Neither metric has budged by more than 1PPM over that period… I am re-uploading the code to see if I missed something.
 
Last edited:
Of course you are right micros() is correct.

Now if the GPS PPS and the RTC give the same number of MCU micros
over 10 sec then RTC and GPS are synchronized and the error is 0.
Your measurements gives -6 micros over 10 seconds relative to GPS PPS
if I read your data correctly. This would mean RTC runs 0.6 ppm to fast.

How do you calculate the compensation values ?
What is the value of the RTC_TCR ?
 
Hi mlu,

I agree that the crystal seems to run very nicely. However, the compensation function I'd like to write is still giving me fits.

It's curious, no matter how I adjust RTC_TCR (i.e. make it 0x0100), the TSR register seems to tick up as if nothing happened. My guess is that one has to pay attention to the prescaler register and adjust the other registers somehow whenever it exceeds a certain threshold. I will have to take another look at the manual, and the teensy core files to perhaps come to a better understanding how Paul wrote the compensate function.

As for the compensation ppm calculator, wonder if the following would be correct -(1000000/interval)*(usMCU-usRTC)/usMCU.

That is, if the RTC is running faster than the MCU, the result should be negative, and vice versa.
 
Last edited:
your measurement intervall in seconds should be a multiple of the value of the compensation
intervall, CIR+1, this will ensure that the compensations are aligned with your measurements.

If I am not mistaken the compensation is TCR/((CIR+1)*32768).
Using TCR = -5 and CIR =253 gives -0.6 ppm

That is every 254 seconds 5 RTC clock ticks is added.

RTC_TCR = (253<<8) | ((-5)&0xFF);

What you call usMCU is really usGPS (or usPPS)
 
your measurement intervall in seconds should be a multiple of the value of the compensation
intervall

That is a very good point. In order to see the benefit of compensation, the measurement intervals should be at least 2x of the prescaler time interval. However, I would prefer not to be tied to a fixed interval like 256 seconds for productivity reasons. For the crystal I'm using, the allowable error tolerance is quite small (+/- 5PPM) so what might make more sense is to fraction the error:

30.2PPM / (error in PPM) = interval (s).

So, for this crystal, I'm calculating a 0.4PPM error which corresponds to a 74 second time interval per extra TCR being applied. Today, BTW, the compensation works (I used 0x080 to test), I evidently used a bum interval / correction factor in the last RTC_TCR setting.
 
For measuring the time error, with zero compensation, you can use any intervall, like 10s
or 100 to get some more resolution.

For checking the precision after calibration you must either use an interval that is syched to the
compensation, or use a VERY long time to even out the fact that correction is applied in 'burst'
quite many seconds apart.
 
Yes, that's why I want to use a multiple approach that minimizes 'lumpiness' by only applying RTC_TCR corrections in single 30ppm iterations vs 5 in a lump at once but spaced 5x further apart. Ie many little bumps vs bigger ones spaced further apart.

Either approach gets you there and I can certainly appreciate why Paul chose a single metric with a default 256s time period.
 
For checking the precision after calibration you must either use an interval that is syched to the
compensation, or use a VERY long time to even out the fact that correction is applied in 'burst'
quite many seconds apart.

I'm using 2x of the time compensation interval. Here is a sample output. I seem to be done.

Code:
Welcome. Prepare to wait 7 seconds for CIC register to run out.
Now waiting for PPS signal acquisition.
Done. Now calculating what RTC PPM should be at 25*C
Over a 10 second interval, the MCU took 10000155 'MCU micro-seconds'.
Over a 10 second interval, the RTC took 10000087 'MCU micro-seconds'.
i.e. 15.50PPM for the MCU and 8.70PPM for the RTC, i.e. a 6.80PPM difference.
It is 22.44 *C on board, which suggests a 0.04 PPM correction per the crystal spec. sheet. 
So, at 25*C, the crystal correction factor should be: 6.76 PPM
.
.
.
Over a 18 second interval, the MCU took 18000279 'MCU micro-seconds'.
Over a 18 second interval, the RTC took 18000279 'MCU micro-seconds'.
i.e. 15.50PPM for the MCU and 15.50PPM for the RTC, i.e. a 0.00PPM difference

Not that the code is always this close but it's usually to within less than 0.1PPM. I do get the odd malfunction and the initial offset calculation is garbage. I'll re-write that section to detect odd output and repeat the measurements.

The advantage of storing the crystal error at 25*C is that you can then easily compensate for ambient temperatures later. Now is this approach 100% accurate? Of course not. The inversion point for crystals may vary from 20-30*C. So without further testing, there is no way of knowing whether 25*C is the actual inversion point. It may be practical enough to apply the ambient temperature correction factor once a minute and still end up with a device whose accuracy exceeds 1PPM.

The only downside to this approach (vs. using a TXO RTC) is that the TXO RTC will do all this work for you, whether the MCU is running and/or paying attention to it, or not. The above crystal approach requires the MCU to be powered 100% of the time and to be able to make periodic adjustments. Additionally, in order to discover the initial calibration factor, some testing is required. A TXO RTC like the DS3231 will save you all that work but will take up more board space and require two I2C lines to communicate with your MCU.

The good news is that it's winter, so I will test the periodic temperature correction factor tomorrow in freezing ambients. Wish me luck!
 
Last edited:
The good news is that it's winter, so I will test the periodic temperature correction factor tomorrow in freezing ambients. Wish me luck!

Had some fun today and logged a whole lot of data points while the board and the crystal were adjusting to outdoor temperatures and back again.
ppm_error.png
As you can see, the data describes the parabola that is expected. The formula is totally different from what I expected per the specifications, but that's not a particular worry, as the data also nicely shows where the turnover temperature is for the crystal. So, this correction factor is arguably better since it captures the parabola data better.

The next step was implementing a least-squares approach to calculating the factors for the 2nd power approximation. Via MS Excel, I also confirmed that the resulting factors are correct. My next task is burning the factors into the EEPROM, preferably as floats. However, I am failing in this regard as the EEPROMEx library I'd like to be using will not compile on the Teensy 3 (works just fine on AVRs…).
 

Attachments

  • ppm_error.png
    ppm_error.png
    23.8 KB · Views: 295
Last edited:
Status
Not open for further replies.
Back
Top