Accurate Interval Timer for
AVR MCU
(Last
updated 5 Jan 2013)
Updated 5 Jan 2013
This page made it on Hackaday (here's the
link) and got quite a few comments. Here is a summary of some
of the better comments:
Arlet posted a simple integer math solution for use within the ISR that
accumulates cycles and converts to milliseconds. As he explains,
each interrupt is 19968 cycles and each millisecond is 20000 cycles, so
this code:
a += 19968;
if (a >= 20000)
{
msec++;
a -= 20000;
}
performs the conversion.
Mathew, reboots, and lwatcdr all suggested a 20.48 MHz crystal, rather
than the 20 MHz crystal I used. This means a binary divisor will
give an even integer multiple for the number of cycles per
interrupts. For this example, a prescaler of /256 yields a reload
value of 80, which removes the need to muck about with the reload
adjustments.
Alex Rossie provided a link to this earlier Hackaday
entry describing a solution similar to mine for creating a TI
Chronos wristwatch that keeps Martian time.
Martin and WitchDoc suggested setting the prescaler to /1 and using a
reload value of 19999 (actually, 20000). This works if you have a
16-bit timer available.
Daid and hat offered suggestions for improving the stability of the
AVR's crystal oscillator using various temperature compensation ideas.
Tom the Brat suggested counting the numbe of milliseconds (ticks),
multiplying by 1000, then dividing by 78125 to get the number of
seconds for display.
jc and Hack Man recommended an external clock source of greater
accuracy than the AVR's crystal. In particular, Hack Man
suggested the Maxim DS3234 SPI-compatible RTC with integrated crystal.
Julian Skidmore offered an interesting twist on my technique.
Rather than use CTC mode, which resets the timer to 0 on each OCR
match, his technique does a simple calculation within the ISR to
compute the next OCR value in free-run mode, adjusting for the lost
cycles with fractional arithmetic. I'm won't go into the details
here, but strongly suggest you read through his comment in the Hackaday
link above.
Odokemon (Danny Chouinard) posted a link to a page on his site where he
ovenized
a crystal oscillator, temperature sensor, and tiny heater (made
from a couple of resistors), and packed all of this into a tiny
insulated box. The concept is to keep the crystal at a fixed
temperature, which reduces or eliminates frequency variations due to
temperature changes. It is really a clever project and quite well
done. I did not find a mention of it on Hackaday, but it deserves
one.
The bottom line for all of this: There are many different ways to
achieve accurate interval timers with small MCUs like the
ATmega168. Depending on how much freedom you have over crystal
frequency, 8-vs-16-bit timers, use of external timing sources,
environmental controls, and other factors, one or more of the above
suggestions may be well suited for your project.
Many microcontroller projects need an interval timer of some kind,
often for accurate timing of intervals from a few seconds to several
hours or days. MCUs like the ATmega168 have timer/counter
subsystems that can act as interval timers, but using these
timer/counters runs into two snags. First, the MCU's timing
reference, usually a crystal, has built-in tolerance issues.
Second, MCUs such as the ATmega168 use a binary prescaler that does not
permit an even division of common crystal frequencies down to even
multiples of milliseconds.
In order to create an accurate interval timer with such an MCU, you
need to address both of these issues. The following design shows
one method for creating such an interval timer, using C in AVRStudio 4.
The design
I'm basing this design on an ATmega168 with a 20 MHz crystal and
suitable caps connected as a timing reference. The actual
schematic isn't really important; you can find such a design all over
the web. I'm focusing on the firmware design here.
The firmware consists of a simple main() function that sets up Timer1
(16-bit timer/counter) in CTC mode, with an interrupt on OC1A
match. The compare register, OCR1A, is loaded with a value that,
with a prescaler of /256, gives an interrupt approximately 1000 times
per second.
On each interrupt, a local msecs counter is incremented. The
counter is
then checked; if it has hit 1000, it is immediately zeroed and a global
seconds counter is decremented if that counter is not yet zero.
This design creates a down-counting timer that starts at some initial
value and counts down once per second until it hits zero, where it
stays until written with another value.
The difficulty lies with the reload value stored in OCR1A and the
associated prescaler. There is no combination of prescalers and
reload value that provides an accurate one millisecond interval.
(Edit 5 Jan 2013: As pointed
out by Martin and WitchDoc above, this is, stictly speaking, not
true. I should have said that such combination does not exist for
8-bit timers. I was focused on making a solution that was general
across both types of timers. If you are using a 16-bit timer, you
can use a prescaler of /1 and an OCR value of 20000 and forget about
all of the following reload calcs. Yes, I did choose a 16-bit
timer for this example; I should have used an 8-bit instead; it would
have been less confusing. Sorry about that.)
For example, using a prescaler of /256 gives:
ClocksPerSecond / MillisecondsPerSecond / Prescaler =
InterruptsPerSecond
(20000000 / 1000) / 256 = 78.125
Put another way, if you use a 20 MHz crystal, a /256 prescaler, and a
reload value of 78.125, you will get one interrupt every millisecond.
Obviously, you don't get to choose a reload value of 78.125; the
closest you can get is 78. The problem is that after 1000
interrupts, you will not have timed one second, you will have timed a
shorter interval. To be precise, you will have timed:
1000 * (78 * 256) cycles or 19968000 cycles
This means that your one-second interval will be 0.9984 seconds, and
your minute will be 59.904 seconds and your hour will be 3594.24
seconds, or nearly six seconds off.
The simplest way to fix this issue is to modify the OCR1A reload value
inside the ISR, adjusting the value occasionally so that after 1000
interrupts, you have used precisely 20000000 cycles.
Given the above conditions, you would need to use a reload of 78 for
seven interrupt intervals, but switch to a reload of 79 for the eighth
interrupt interval. This would give you:
1000 * ((78 * 7) + 79) * 32 cycles or 20000000 cycles
The implementation
It's easy to implement the above design, but it won't work, in the
sense that it will give you exactly 20000000 cycles per 1000 interrupt
intervals, but you still won't have an accurate second. The
problem here is that crystals drift and have their own frequency
tolerance. A typical Fox 20 MHz crystal from Digikey shows a
frequency stability of +/- 50ppm and a frequency tolerance of +/-
30ppm. That's pretty darned good for a 42-cent part, but your
seccond could still be off by up to 1600 cycles, which adds up over an
hour.
Rather than hard-code the use of seven reloads of 78 and one reload of
79, I opted to use named literals for how often I modified the reload
value; I call this trimming in my code and the literals define a trim
value used to modify the reload value in the ISR.
The final step is to test against a reference clock, then adjust the
named literals MSEC_ISR_TRIM_VALUE and (if necessary) RELOAD_1MSEC, and
recompile. Note that your trim value (MSEC_ISR_TRIM_VALUE) should
be a whole divisor of 1000. If it isn't, you will miss the final
trim at the 1000th interrupt. Note also that lower trim values
cause longer intervals, as the greater reload value is used more often;
a trim value of 5 means the greater reload value is used 200 times, but
a trim value of 20 only uses the greater value 50 times.
In my case, I did not have a NIST-traceable interval timer, so I ended
up using a Radio Shack interval timer. With repeated testing and
tweaking, I now have an interval timer that matches the Radio Shack
timer. (Yes, I know, that's not absolutely accurate, but it's
relatively accurate, and that's what I was shooting for.) If you
duplicate this work and have a NIST-traceable timer, I would be
interested to know how closely your final trim values match the nominal
values of 78 for RELOAD_1MSEC and 8 for MSEC_ISR_TRIM_VALUE.
Here is the final code that I'm using in my project:
========================================================================
#define RELOAD_1MSEC
(((F_CPU/1000)/256)-1)
#define MSEC_ISR_TRIM_VALUE 5
/*
* ISR for Timer1 OCR1A compare
*
* Control reaches here each time Timer1 reaches its OCR1A
counter
* value. This interrupt is used to define one
millisecond.
*
* However, there is an issue. The MCU is driven with
a 20 MHz
* crystal but the MCU uses a binary divider chain to
determine
* the interrupt rate. This means that there is no
accurate
* divider; you can't divide 20 MHz by a binary prescaler
and get
* an even multiple of cycles for one msec.
*
* For example, using a prescaler of 256 gives:
*
* 20000000 / 1000 / 256 = 78.125 per msec
*
* If you load OCR1A with 78 and set the prescaler to 256,
you will
* get an interrupt every (78 * 256) cycles, or 19,968
cycles. This
* means your msec will be off by 32 cycles and your second
will be
* off by (32 * 1000) or 32,000 cycles. This is really
inaccurate.
*
* The key is to add back in the 0.125 per msec that is
missing from
* the reload value in the above equation. Since 0.125
= 1/8, we
* need to use 79 as our our OCR1A load value once every
eight msecs.
* For one full second, this will give:
*
* (78 * 256 * 875) + (79 * 256 * 125) = 20000000 cycles
*
* which is perfect; our one-second clock will be as
accurate as
* our crystal.
*
* However, this isn't necessarily accurate, since the
crystal
* has tolerances of its own. So the code below uses a
named
* literal, MSEC_ISR_TRIM_VALUE, to provide a selected trim
value
* The trim value is the number of msecs that must elapse
before
* an additional count is added to the OCR1A value (79 vs 78
in
* the above example).
*
* For example, on one of my projects, the above trim value
of
* 8 gave too much delay and the clock would lose time
versus a
* reference clock. After repeated tests, I settled on
a trim
* value of 5 and a base reload value of 77, which gave a
more
* accurate clock.
*
* Note that the trim value should be an even divisor of
1000 or
* you won't get the last adjustment when hitting msec #999.
*
* Note that you don't normally reload the OCR1A value
inside the
* ISR, as you see below. The only reason I'm doing
this is to
* ensure the OCR1A value adjusts for the missing fractions
of
* seconds. If you don't need the accuracy and can
live with a
* constant reload value, you don't need to modify OCR1A
here.
*/
ISR(TIMER1_COMPA_vect)
{
static unsigned int
msecs = 0;
OCR1A = RELOAD_1MSEC;
// this is correct most of the
time
if ((msecs % MSEC_ISR_TRIM_VALUE) ==
MSEC_ISR_TRIM_VALUE-1) // but on each trim msec...
{
OCR1A = RELOAD_1MSEC +
1; // we adjust for the missing cycles
}
msecs++;
// count the msec that just passed
if (msecs == 1000)
// if we have done a
full second...
{
msecs = 0;
// rewind the msecs counter
if
(ElapsedTimeSecs) // if
big timer is active and non-zero...
{
ElapsedTimeSecs--; // count the second
that just passed
}
}
}
===================================================
Home