Using the TMR0 interrupt to create a real time clock

back to main tutorial page

You should now have an AVR set up on a breadboard. In this section I will lead you through the steps to create a real time clock on the AVR, accurate to a few parts per thousand. This accuracy is needed in many real-time control and interface applications.


Interrupts

Interrupts are easy to understand by a human analogy. Suppose that you are working at your desk and someone calls you on your telephone. That is an interrupt.

The person who calls you on the phone could tell you many different messages, for example,

  • "Your house is on fire!"
  • "Please fill in this paperwork."
  • "Wake up!"

Your response in each case will be rather different. So it is with microcontrollers. Interrupts are messages from the outside world. You can write software to react to the messages if you wish, or you can ignore the messages.

Figure 1 Program flow without, and with, an interrupt

The AVR has 16 different "interrupt sources", meaning that you can write 16 interrupt handlers which can react to 16 different messages. Some are "external" events, for example a voltage change on a special pin. Some are "internal" messages such as "a timer has overflowed" (this is explained below).

Understanding how to use the various interrupts is the key to achieving a good design.


The need for speed

Interrupts are useful because they are very, very fast. The main event loop might take one hundred clock cycles. If we check for a certain event once on each pass through the main event loop - a technique called "polling" - then a very important message could be late by up to

100 * 1/8000000 = 12.5 microseconds.

That could be an unacceptable delay in a high-speed communication system. Also note that a very fast event could be missed altogether.

The interrupt, on the other hand, can begin to produce a response within 4 clock cycles or about 500 nanoseconds, and will not be missed even if it is only a short pulse event.


Nearly parallel programming

An interrupt can happen at (almost) any time. This is a very important concept. It means that the interrupt handler code could start to execute at, or even in the middle of, any line in the main program. The only times that an interrupt cannot happen are

  1. If the source of the interrupt is guaranteed not to "fire" because there is no signal;
  2. If the interrupt is disabled, see below; or
  3. If the AVR is already processing another interrupt.

An interrupt handler cannot itself be interrupted. This means that it is probably a good idea not to do long calculations in an interrupt handler, as this may prevent other functions from working properly. If an interrupt message arrives while another interrupt is being processed, it is effectively "queued up" and will be processed as soon as the main program resumes. Events are not usually missed but they can be delayed.

Nathan's rule for interrupts: keep the interrupt code short and sweet. No long calculations! Set flags, take note of values, get out. Do all long calculations in the main program thread.


Enabling and disabling interrupts

Interrupts can be enabled or disabled by setting some flags in certain registers.

To enable an interrupt you must set the appropriate flag in the appropriate register. This is different for each of the interrupt sources. In this tutorial we will use the TMR0 (timer zero) overflow interrupt. The documentation says that we must set flag TOIE0 in register TIMSK. In the C language this is done like so:

// enable timer 0 overflow interrupt
TIMSK |= BIT(TOIE0);

There is also a single "master switch" which controls whether ALL interrupts will be enabled or disabled. It is called the "Global Interrupt Enable" flag and it can be controlled using these commands:

// enable all interrupts
SEI();
         
// disable all interrupts
CLI();

The timer we wish to use, Timer 0, also has some additional controls. By putting values into them we can control the timer period very accurately.

TCCR0 controls a pre-scaler. This is a system that divides the main oscillator frequency by a power of 2. The main oscillator runs at 8MHz (typically) and this is very fast. If we want a long delay between overflow interrupts, we need to use a slower clock using the prescaler.

TCNT0 is the actual count value. We can read this at any time, and we can also write data to it. See the definition of overflow to understand why this is useful.


Code example tmr0.c

Same hardware setup as for hello.c, only one LED needed on pin 0 of PORTC.

Figure 2 Example program that uses the TMR0 interrupt

  • Open the project tmr0.prj
  • Compile the project and download the hex file to the AVR.
  • The LED should turn on once per second. Can you do an experiment to test whether this is accurate or not, using a stopwatch?

So what?

The previous program, hello.c, had a flashing light but the timing was not precise. The problem was that the delay came from wasted CPU cycles in the main program. In that program, if an interrupt happened, it would cause a delay to the LED period. Not so good. This program has a more accurate form of timing which will remain steady whether there are other interrupts or not (with a few caveats, as usual). So if you need precise timing, it's a good idea to set up TMR0 or one of the other timers to keep time for you.

Exercises

Play with this code until you understand it. Suggestions:
  1. Change the TMR_prescale constant to other values (see environment.h for options) and then make other changes to re-establish the one-second flashes.
  2. Change the program so that the LED flashes 10 times then misses one flash.
  3. Change the program so that the LED flashes quickly when PORTD, pin 0 has a high voltage on it and slowly when PORTD, pin 0 has a low voltage.

Dr Nathan Scott · nscott@mech.uwa.edu.au