Calibrating An Artemis XTAL On The Cheap

Just for fun, I wrote a program that uses any cheap GPS with a PPS output as the timing source for the Apollo3’s build-in XTAL calibration mechanism. It works great! It means that you don’t have to go buy a frequency counter to calibrate your boards to within the 1 PPM promised by the calibration mechanism.

Here is the output from a sample test run:

The XTAL frequency is measured at 32768.829 Hz
Frequency error is -25.31 PPM
Frequency adjustment value is -26.54 steps
Closest integer step count is -27 steps

The first pic shows that the frequency counter agrees with the GPS about the length of a second to 0.1PPM. The second pic shows the calibrated 16384 Hz clock output from a test Redboard after the calibration completes. The uncalibrated error was 25.31 PPM, and after calibration, it is off by -0.67 PPM. That’s as good as the Apollo3 calibration mechanism can do!

I’ll figure out how to post the code at some point.

Here is another run. I added a test at the end to measure the resulting frequency of the calibrated 16384 Hz signal.

The XTAL frequency is measured at 32768.832 Hz
Frequency error is -25.39 PPM
Frequency adjustment value is -26.63 steps
Closest integer step count is -27 steps
...
16384.008

You can see that the uncalibrated 32KHz clock is 0.832 Hz too fast, and you can see that the calibrated 16384 Hz clock is 100 times better at only .008 Hz fast. One thing to be aware of is that the silicon calibration adjustment mechanism is performed occurs over a 64 second rolling window. In any sub-portion of that 64 second window, the clock might be a bit slow or a bit fast, but over each 64 second window, the average clock speed will be calibrated as desired. This means that an RTC clocked from a calibrated clock source might have a tiny bit of jitter due to the calibration mechanism, but for almost all RTC purposes, it can be ignored.

Nice work Robin - thanks for sharing!

u-blox GNSS modules like the ZED-F9P can capture rising and falling edge events with nanosecond resolution on their INT pin. The timing data is available in the TIM TM2 message.

They can only capture the timing data for one event in each “navigation” period (usually 1Hz, but you can push it to 25Hz). But they can count the total number of rising edges at faster rates. I haven’t tested how fast they can count, but 32kHz shouldn’t be a problem.

If you have time, please take a look at these examples in the shiny new version 2.0 of the GNSS library. You might find them useful.

https://github.com/sparkfun/SparkFun_u- … IM_TM2.ino

https://github.com/sparkfun/SparkFun_u- … IM_TM2.ino

Best wishes,

Paul

Sorry for the late reply, but the Great Pacific NW Windstorm of 2021 took out my power and internet for 3 days.

I was not aware of that capability on the ZED-F9P. That is slick! The only downside is the $220 pricetag. You can get a used HP5316B frequency counter for about half that (which I highly recommend), although the more modern ZED-F9P could probably be made even more capable than the HP frequency counter with a bit of software. I guess one advantage of the HP frequency counter is that it can measure analog signals, or signals at all kinds of voltage levels. I suspect that the ZED-F9P can only handle digital inputs.

The super-simplistic PPS mechanism will work with any $6 GPS module that brings PPS out to a connector. You can even use a GPS module that doesn’t bring PPS out by soldering a wire to its PPS LED, which I have done before in a case of sheer desperation.

Where can I get the code for this RTC xtal calibration on the cheap?

I don’t have a standalone version. Let me work on it…

Here you go.

This code was designed as a FreeRtos task. For a more Arduino-like implementation, it would be trivial to modify the STIMER capture ISR to stick the new reading in a global and set a ‘new-data’ flag. The Arduino mainloop would wait until the new-data flag got set, then read the new data and clear the new-data flag. It would then process the code contained in the task body with the new data reading.

The code contains a discussion on how the capture mechanism works (which is slightly weird, see viewtopic.php?f=170&t=55196). It explains the potential sources of error in the measurements to put an upper and lower bound on the results it generates.

#define GPS_PPS_PAD        22     // Where the GPS PPS signal is connected. This is an Apollo3 PAD number, not a PIN number!

#define PPS_CAPTURE_EDGE    0     // '0' means capture on rising edge, '1' means capture on falling edge
				  // My GPS module asserts PPS on the rising edge

bool firstTime;
bool calBufFull;
uint32_t calBufIdx;

// 100 seconds is enough to measure the XTAL frequency to better than 0.5PPM, which means that
// the 1PPM limitation on the calibration mechanism will be the limiting factor.
const uint32_t windowSize_secs = 100;
uint32_t calBuf[windowSize_secs];


// ----------------------------------------------------------------------------------
extern "C" void am_stimer_isr();

// The STIMER capture ISR reads the current count and sends it to the calibration task
void am_stimer_isr()
{
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  
  uint32_t requestStatus = am_hal_stimer_int_status_get(true);
  am_hal_stimer_int_clear(requestStatus);
  
  if (requestStatus & AM_HAL_STIMER_INT_CAPTUREA) {
    uint32_t count = CTIMER->SCAPT0;
    xTaskNotifyFromISR(xtalCal_taskHandle, count, eSetValueWithOverwrite, &xHigherPriorityTaskWoken);
    
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
  }
}

// ----------------------------------------------------------------------------------
// Flush the rolling buffer
void initCal()
{
  memset (calBuf, 0, sizeof(calBuf));
  firstTime = true;
  calBufFull = false;
  calBufIdx = 0;
}


// ----------------------------------------------------------------------------------
// This task uses a GPS PPS signal as a precision timing reference to measure the actual
// frequency of the 32KHz XTAL oscillator.  The PPS signal has two very useful characteristics:
//  - it is extremely precise over the span of 1 second (within tens of nanoseconds)
//  - the errors in PPS timing over long timespans do not accumulate
//
// The calibration process is very simple: a counter is set up to count at the XTAL rate.
// By measuring the total number of XTAL beats over a long timespan, the XTAL frequency can be 
// determined with an accuracy that increases as the length of the timespan increases.
// 
// A rolling buffer is used to store the current XTAL count at each successive second.  
// The buffer needs to be long enough so that the timespan between the oldest and newest
// XTAL counts will provide the desired level of accuracy.
// It can be shown that a 100 second span is long enough to measure the XTAL frequency
// to better than 0.5 PPM. Since the Artemis XTAL calibration mechanism can only compensate
// the XTAL to 1 PPM, the measurement system will not benefit from a longer measurement span.
// 
// The measurement/calibration mechanism is designed to run continuously. Each new measurment
// replaces the oldest measurement in the rolling buffer.  If environmental effects cause
// the XTAL frequency to drift, the measurement mechanism value will continuously track the drift.
// 
// GPS reception is not always completely reliable.  If PPS cuts out for a second or two
// here and then, there is no problem at all. Since the rolling buffer stores counts, it just means
// that the actual timespan contained in the rolling buffer gets longer by 1 second for each second
// of missed PPS reception.
// If the PPS disappears for longer periods of time (arbitrarily chosen to be 60 seconds),
// the system is designed to retain its last-known-good XTAL trim setting, but it restarts
// collecting data within the rolling buffer.
// When the PPS comes back, the system will resume measurement and trimming.
//
// Finally: this whole mechanism depends on the ability of the STIMER to count accurately.
// Sadly, this is not always the case. If some other part of the system is reading
// any of the CTIMER STIMER current-value registers, there is a statistical liklihood
// of that read operation causing the STIMER counter to double-increment. This will make the XTAL
// appear to run slightly faster than it actually is running.
// For more info on that issue, see:  https://forum.sparkfun.com/viewtopic.php?f=170&t=55246
// This program was tested in a system that made sure to never trigger the double-count bug.
//
void xtalCal_task(void *pvParameters)
{
  static int32_t cal_factor_prev = INT32_MAX;
  
  uint32_t timestamp, oldest;
  
  // Set up the PPS capture, init the interrupt mechanisms, start the timer   
  am_hal_stimer_capture_start(0, GPS_PPS_PAD, PPS_CAPTURE_EDGE);
  am_hal_stimer_int_clear(AM_HAL_STIMER_INT_CAPTUREA);
  NVIC_SetPriority(STIMER_IRQn, NVIC_configKERNEL_INTERRUPT_PRIORITY);
  NVIC_ClearPendingIRQ(STIMER_IRQn);
  NVIC_EnableIRQ(STIMER_IRQn);
  am_hal_stimer_int_enable(AM_HAL_STIMER_INT_CAPTUREA);
  
  initCal();
  
  while (1) {
    // Wait for the capture ISR to send us the next PPS timestamp
    // In theory, the capture will occur within 1 second (the next PPS event).
    // However, it might be an arbitrarily long time if there is no GPS reception
    // If we lose reception for a long enough time, we start over from scratch.
    BaseType_t xResult = xTaskNotifyWait(0,~0, &timestamp, pdMS_TO_TICKS(1000 * 60));
    
    if (xResult != pdPASS) {
      // We should have gotten a PPS by now
      printf("xtalCal_task: No PPS observed for 60 seconds\n");
      
      // At this point, we flush our calibration history.
      // The actual cal_factor retains is most recent update though. We remain
      // as accurately calibrated as possible during this disruption in satellite reception.
      initCal();
    }
    else {
      if (calBufFull) {
        oldest = calBuf[calBufIdx];
      }
      else {
        oldest = calBuf[0];
      }
      calBuf[calBufIdx] = timestamp;
      calBufIdx++;
      if (calBufIdx >= windowSize_secs) {
        calBufIdx = 0;
        calBufFull = true;
      }
      
      if (firstTime) {
        firstTime = false;
      }
      else {
        // The span is the number of intervals (periods of the XTAL clock) between the two time marks
        uint32_t span = timestamp - oldest;  
      
        // The actual number of seconds between count and prevCount is extracted from the span information itself.
        // This is how we compensate for the possibility that PPS reception dropped out and we didn't actually
        // get 1 measurement per second.
        // We round the seconds calculation to take care of situation where the XTAL is slow
        // This takes care of situations where maybe the PPS reception was missing on the second that it
        // was supposed to be present.
        uint32_t secs = ((span+0x4000)>>15);
      
        // Don't bother doing a calibration calculation until we have at least 10 seconds of measurement span
        if (secs >= 10) {
          // The average frequency over that span is the number of XTAL periods divided by the measurement period
          double freq = double(span) / secs;
      
          // Calculate the potential sources of error in our measurements.
          // Notes on the diagram below:
          //   - The '|' represents the clock event on STIMER when its value increments.
          //   - A capture event anywhere within an STIMER clock gets captured on the *next* STIMER clock.
          //     (See https://forum.sparkfun.com/viewtopic.php?f=170&t=55196 for more info on that)
          //     Therefore, a capture anywhere within the period where the STIMER count is 1 will get captured as 
          //     a 2 on the next clock/increment event.
          //   - Captures marking the start of a timing measurement are marked as 'S', ends with an 'E'.
          //
          // Worst case effects will occur when the 'S' and 'E' events are placed to occur just barely before or
          // after the XTAL clock event that they are adjacent to.  For the purposes of calculating the Actual Span,
          // the S and E events can be considered to occur coincident to the clock event.
          // There are four potential scenarios to consider, labeled "A" through "D":
          //
          //     |     |     |     |     |     |     |     |
          //     |  0  |  1  |  2  |  3  |  4  |  5  |  6  |   STIMER count
          //     |     |     |     |     |     |     |     |
          //  A: |     |    S|     |     |E    |     |     |   Actual Span=2:  S=2, E=5 -> Measured Span=(5-2)=3    error = +1
          //  B: |     |    S|     |     |    E|     |     |   Actual Span=3:  S=2, E=5 -> Measured Span=(5-2)=3    error =  0
          //  C: |     |     |S    |     |E    |     |     |   Actual Span=2:  S=3, E=5 -> Measured Span=(5-3)=2    error =  0
          //  D: |     |     |S    |     |    E|     |     |   Actual Span=3:  S=3, E=5 -> Measured Span=(5-3)=2    error = -1
          //
          // The bottom line is that our measurement may be as far off as 1 period too few or 1 period too many.
          // Note that these errors do not accumulate.  It does not matter how many periods occured in between
          // the S and E sampling events.  The errors are introduced due to the relationship between the timing
          // of S & E in relationship to the STIMER clock/increment operation.
          double minFreq_worstCase = double(span-1) / secs;   // Scenario A: our span measurement might contain an extra clock
          double maxFreq_worstCase = double(span+1) / secs;   // Scenario D: our span measurement might be missing a clock
      
          // Calculate the CALXT trim value as per the Artemis data sheet:
          double error_ppm = (((double)32768.0 - freq) * 1000000.0) / freq;
          double error_adj = error_ppm / 0.9535;
          int32_t cal_factor = (int32_t)((error_adj >= 0) ? (error_adj + 0.5) : (error_adj - 0.5));
          CLKGEN->CALXT_b.CALXT = cal_factor & 0x7FF;
        
          if (1 || (cal_factor != cal_factor_prev)) {
            cal_factor_prev = cal_factor;
          
            printf("%3u, %08X, %08X, %08X, %10.3f, %10.3f, %10.3f, %4ld\n",
                  calBufFull ? windowSize_secs : calBufIdx,
                  oldest,
                  timestamp,
                  span,
                  minFreq_worstCase,
                  freq,
                  maxFreq_worstCase,
                  cal_factor
                  );
          }
        }
      }
    }
  }
}

As the program runs, it fills up the rolling buffer and the measured frequency gets more and more accurate. When it hits 100 samples, the rolling buffer starts overwriting itself. The last number (-44) is the calculated CALXT trim value based on the latest frequency measurement. You can see that even though the frequency is moving around a bit, the CALXT has the same value. That’s because we can calculate the frequency more precisely (less than 0.5PPM) than CALXT can fix it (1PPM). Of course, it is possible that your specific crystal’s frequency is precisely on the hairy edge of being half-way between needing one of two adjacent CALXT values. In that case, the system will bounce around between those two adjacent CALXT values depending on the outcome of each specific frequency measurement.

 96, 0000F1BD, 00307240, 002F8083,  32769.368,  32769.379,  32769.389,  -44
 97, 0000F1BD, 0030F242, 00300085,  32769.375,  32769.385,  32769.396,  -44
 98, 0000F1BD, 00317243, 00308086,  32769.371,  32769.381,  32769.392,  -44
 99, 0000F1BD, 0031F244, 00310087,  32769.367,  32769.378,  32769.388,  -44
100, 0000F1BD, 00327246, 00318089,  32769.374,  32769.384,  32769.394,  -44
100, 0000F1BD, 0032F247, 0032008A,  32769.370,  32769.380,  32769.390,  -44
100, 000171BF, 00337248, 00320089,  32769.360,  32769.370,  32769.380,  -44
100, 0001F1C0, 0033F24A, 0032008A,  32769.370,  32769.380,  32769.390,  -44
100, 000271C1, 0034724B, 0032008A,  32769.370,  32769.380,  32769.390,  -44
100, 0002F1C3, 0034F24D, 0032008A,  32769.370,  32769.380,  32769.390,  -44

One last thing, while that wad of output data was being produced, my HP Freq counter was reporting an XTAL frequency of 32769.383 Hz. That’s right there with the measurement system.

Thanks for the code and the links. Will keep me busy for a while.