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, ×tamp, 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