Arduino Pro: Ceramic Resonator Frequency Compensation

The Arduino Pro gets its 16-MHz CPU clock from a ceramic resonator, rather than a quartz crystal, which means the frequency accuracy is ±0.5% rather than pretty much spot on. I’m building one into a WWVB-based clock, so it knows the exact elapsed time between synch events.

My clock uses a 20-ms timebase: 16 MHz prescaled by 8, then divided by (nominally) 40000 using Timer1.

Knowing the exact time between WWVB updates, the firmware compares that with the local time interval to find the offset, finds the fractional error, and then tweaks the Timer1 period to make the answer come out right the next time.

Here’s what three days in the life of that algorithm look like:

Drift: TS   5268489 UTC 10006.040959 Elapsed 13920 Offset 0 Corr +0 ICR1 39840
Drift: TS   5268805 UTC 10006.092559 Elapsed 18960 Offset 1 Corr +2 ICR1 39842
Drift: TS   5269711 UTC 10007.003159 Elapsed 54360 Offset 0 Corr +0 ICR1 39842
Drift: TS   5269966 UTC 10007.044659 Elapsed 15300 Offset 0 Corr +0 ICR1 39842
Drift: TS   5270079 UTC 10007.063959 Elapsed  4920 Offset -1 Corr -8 ICR1 39834
Drift: TS   5271157 UTC 10008.003759 Elapsed 61440 Offset 12 Corr +7 ICR1 39841
Drift: TS   5271833 UTC 10008.115359 Elapsed 39780 Offset 1 Corr +1 ICR1 39842

The UTC field is YYDDD.HHMMSS. The TS value is a simple monotonic timestamp: UTC brutally converted to minutes assuming a year is 365.25 days.

I set ICR1 to 39840 when the program starts, having already determined the actual oscillator frequency for this particular Arduino Pro. That’s not necessary, because the firmware will adjust it automatically, but it does eliminate the first big step that would compensate the resonator’s -0.4% initial frequency error.

As nearly as I can tell, the corrections are tracking room temperature changes, as it’s been really cold around here lately and the clock is atop a bookcase in an outside corner of the room.

After the first +2 change, it ran for 19 hours with less than one second of error: 14 ppm. The -8 change was probably an overcorrection, as the synch interval was just over an hour, but so it goes. That caused 195 ppm error over the next 17 hours, then it’s back on track.

There’s an obvious conflict between getting quick updates as conditions change and minimizing long-term free-run drift. The firmware currently insists on a minimum of 60 minutes between synchs, but (given an initial preset) I think I can dramatically increase that without losing anything.

This code does the Timer1 setup:

#define TIMER1COUNTS            39841l

TCCR1B    = B00011000;            // Timer1: CTC mode = 12 high bits, TOP=ICR1, stopped with no clock source
TCNT1 = 0;            // force count to start from scratch, CTC mode low bits
TCCR1A = 0;            // no compare outputs to OC1A OC1B, WGM1 1:0 = 00
TCCR1C = 0;            // no forced compares
TIMSK1 = 1 << ICIE1;            // allow interrupt on capture event (TCNT == ICF)
SetICR1(TIMER1COUNTS - 1);            // total counts - 1, start running

The SetICR1 function makes sure the new ICR1 isn’t below the current TCNT1 value, which would cause a horrible timekeeping blip. As it is, there’s a microsecond (more or less) glitch during the update.


void SetICR1(word NewICR1) {
TCCR1B &= ~B00000111;     // turn off Timer1 by removing the clock source
ICR1 = NewICR1;
 if (TCNT1 > NewICR1) {     // force counter below new TOP value
 TCNT1 = NewICR1 - 1;
 }
TCCR1B |= B00000010;     // turn on clock with prescaler
}

When the firmware does a WWVB synch, it then checks to see if enough time has passed since the last synch and, if so, tweaks ICR1. The variables hold what you’d expect and are all long ints to hold the expected values…

if ((UTCRightNow.SyncAge != SYNC_UNSYNC) && (UTCRightNow.SyncAge > SYNC_MINDRIFT)) {
 WWVB_Elapsed = 60l * (WWVBToMinutes(&WWVB_Time_Predicted) - WWVBToMinutes(&WWVB_Time_Sync));
 TimeOffset = (60l * (long int)(UTCRightNow.SyncAge - 1)) + (long int)UTCRightNow.Second - WWVB_Elapsed;
 DriftTicks = (int)((FetchICR1() * TimeOffset) / WWVB_Elapsed);
 if (DriftTicks) {
  SetICR1(FetchICR1() + DriftTicks);
 }
}

The FetchICR1 function reads ICR1 without disabling interrupts, doing it twice to be sure nothing’s whacked the magic hardware that allows atomic two-byte register reads.

One failure mode: if something goes badly wrong, ICR1 can become so far off the correct value that the clock will never synch again. I must add a bit of defensive code to SetICR1 that ensures the new value is never more than, say, 1% off the nominal value.

All in all, this works a whole lot better than I expected…

The catch is that most Arduino applications don’t know the exact time interval and, without that, there’s no way to tweak the oscillator on an ongoing basis. However, for any particular Arduino Pro, I think you could very accurately compensate the initial frequency error by measuring the actual oscillator frequency and then hardcoding the adjustment value.

About these ads

,

  1. #1 by david on 2010-01-22 - 19:06

    I’d love to see an actual correlation plot between time and temperature, since I know you’ve got the data loggers hanging around… :)

    • #2 by Ed on 2010-01-22 - 19:56

      Yeah, but not snuggled right up against the Arduino Pro on the back of the clock… which is now near the living-room window for better reception.

      I’m accumulating a much longer time series even as we type. As nearly as I can tell, after the resonator stabilizes it doesn’t do much drifting. It’s pretty much rock solid from morning to late evening and I know the temperature changes by quite a bit during those hours.

      My back of the envelope puts the drift under 13 ppm over the daylight hours, so things are looking good. More plots to come!