Full Version : Climber on Interrupts (AVR GCC)
avr >>BEGINNERS & BUTTERFLIES >>Climber on Interrupts (AVR GCC)


AVR_Admin- 05-08-2006
Climber's info on AVR programming - Interrupts

Many people new to the world of microcontrollers are often hesitant to try using interrupts.

Well, phooey on that. I'm not a genius by any stretch of the imagination and if I can figure it out so can you. All of my subsequent examples use interrupts so let's bite the bullet and get on with it.

An interrupt is when the processor immediately stops what it is doing and heads on over to some other code segment when something happens. This "something" can be triggered externally or internally such as when a timer counts down to zero, the analog to digital converter has a result for you or whatever. Each variety of AVR has a set of interrupts it understands and it is up to us to set up the registers and whatnot to enable these interrupts and to add code to handle them.

An interrupt handler is a chunk of code in your program that is called ONLY when the interrupt occurs. That bit of code won't get called from within the "normal" code that starts with your main() function even though it can share the same data structures.

The biggest hurdle to get over when trying to understand interrupts is the fact that when the interrupt occurs the processor will drop whatever it is doing to deal with the interrupt. The only guarantee we get is that any instruction currently being handled will complete before the interrupt handler is called. Now, that's a *machine* instruction. We mustn't forget that a single statement in C may include a number of machine instructions to perform the duty. If you are ever worried about a segment of code that needs to be executed atomically (i.e. all done or not at all) you might want to surround the code segment with cli() and sei(). By the way, these calls compile into a single AVR instruction so any code that is after the cli() will not get interrupted if the cli() actually gets run.

Let's start with a simple example: the old flashing light program.

Instead of using a wait loop to do the timing, we'll use one of the timers to do it because, well, that's what they do. We will set it up so that once per second the LED will go though one complete on-off cycle. By default, the built-in timers ticks up/down at the cpu system clock rate. With an 8MHz clock we need to scale things down a little. We can apply a prescale factor the system clock to generate the timer clock using the built-in facilities.

The prescale stuff for timer0/1 is described on page 72 of the mega8l pdf. Using a prescale is important because the timer has only 16 bits in it. That is, it can only count down from/up to 65535. If we divide 16 million (the max clock speed) by 65536 (counting 0) we get 244.140625. That would mean we could never define a timer period larger than 1/244th of a second at 16 MHz. The prescale simply runs the clock through a counter and divides the frequency by certain values (8, 64, 256 or 1024 for example). When we choose a different prescale by adjusting what we put into the control registers the AVR just taps the clock divider unit and routes the signal to the counter. Both timer 0 and timer 1 use the same prescale unit but, fortunately, we can choose different prescales for each.

So, back to where we were, trying to set up the timer. Dividing 8 million (or system clock) by a prescale of 256 gives us 31250. So, all we need to do then is program the counter to time down from half that (LED is on for 1/2 second then off for 1/2 second) or 15625. That's within the range of the 16 bit timer. We'll program the timer so that it increments 31250 times a second and when it reaches 15625 it will call our interrupt routine which will turn the LED on or off depending on it's current state.

The counter runs independently of our code. So we don't need to worry about resetting anything each time it reaches the end of its cycle.

Because interrupts are so architecture dependent C was designed without provisions in the language to handle them. Consequently, each compiler will likely have a different way of handling interrupts. This page describes how avr-gcc does it. Codevision and other compilers are likely to be a little different and you will need to dust off your compiler manuals if you want to start using interrupts.

The avr-libc reference lists all the interrupts that the software supports. However, all but the largest processors have only a subset of those interrupts avaialable. To get a list, I head on over the iom8.h header file that comes with libc. If you don't know where that is on your computer here is a copy of the part with the interrupts:

CODE
#define SIG_INTERRUPT0       _VECTOR(1)
#define SIG_INTERRUPT1       _VECTOR(2)
#define SIG_OUTPUT_COMPARE2  _VECTOR(3)
#define SIG_OVERFLOW2        _VECTOR(4)
#define SIG_INPUT_CAPTURE1   _VECTOR(5)
#define SIG_OUTPUT_COMPARE1A _VECTOR(6)
#define SIG_OUTPUT_COMPARE1B _VECTOR(7)
#define SIG_OVERFLOW1        _VECTOR(8)
#define SIG_OVERFLOW0        _VECTOR(9)
#define SIG_SPI              _VECTOR(10)
#define SIG_UART_RECV        _VECTOR(11)
#define SIG_UART_DATA        _VECTOR(12)
#define SIG_UART_TRANS       _VECTOR(13)
#define SIG_ADC              _VECTOR(14)
#define SIG_EEPROM_READY     _VECTOR(15)
#define SIG_COMPARATOR       _VECTOR(16)
#define SIG_2WIRE_SERIAL     _VECTOR(17)
#define SIG_SPM_READY        _VECTOR(18)


We don't need to worry about the VECTOR stuff but the names of the interrupts right after the #define bit is something we DO care about. Those are the names we give to our routines if we want to handle that type of interrupt. For example, if we wrote a chunk of code to do something when EEPROM is ready we will label it SIG_EEPROM_READY. For this example, we will use SIG_OUTPUT_COMPARE1A. If enabled, an interrupt of this type will occur when the counter for timer1A reaches a certain point. That "point" we care about depends on how we set up the counter itself.

Before we can the interrupt it we have to set up the timer. We need to define it parameters (prescale factor, what type of timer, etc) and enable the interrupts. By default, interrupts won't be called. We have to specifically enable them. Both within the configuration for the timer and globally.

The particular interrupt we are interested in using SIG_OUTPUT_COMPARE1A. So, we need to make a routine that does something whenever the interrupt occures. Here it is:

CODE
SIGNAL(SIG_OUTPUT_COMPARE1A)
{
 static uint8_t ledon;

 if (ledon)
 {
   ledon = 0;
   cbi(PORTD, PD4);
 }
 else
 {
   ledon = 1;
   sbi(PORTD, PD4);
 }
}


For now, we'll use the SIGNAL macro. It's a macro, not a function but we can treat it like one for our purposes here. We will make use of this macro once for each interrupt that we want to handle. When the program is compiled the linker will deal with putting the interrupt handler in the right place. That's one of the things I love about working in C.

How it works is pretty simple. The handler just checks to see if the LED is on by checking the "on" status variable. If it is, turn it off and vice versa. The on variable must remain in existance and keep its last assigned value for this to work. That's why I defined it as static. I could also have defined this near the top of the program as a global variable. You'll also note I didn't initialize the variable. ANSI C specifies that static variables are guaranteed to have 0 in them when the program starts.

Now, I know what you are thinking. What happens to the interrupt handler if another interrupt arrives? Well, that depends on whether you used the INTERRUPT macro or the SIGNAL macro like we did above. They are the same except that the INTERRUPT macro can be interrupted while the SIGNAL macro can't without us adding in special code to change their behaviour. If an interrupt arrives while we are in the SIGNAL macro then it will wait until the macro ends. The ONLY priority that interrupts have within the AVR is if they arrive or are pending at the same time. In that case, the one with a lower entry vector table will come first. I.E. in the same order that they appear in the interrupt table found in iom8.h or earlier in this web page.

Now let's go over setting up the 16 bit timer.

CODE

 TIMSK = _BV(OCIE1A);
 TCCR1B = _BV(CS12)        // 256 prescale
        | _BV(WGM12);      // CTC mode, TOP = OCR1A
 OCR1A = 1250;


TIMSK is the Timer Interrupt Mask Register. A "1" in certain bit positions tell the processor to enable interrupts on certain timer events. In this case we want an interrupt to happen on the "output compare A match." When the counter reaches a certain value an interrupt will occur. The value in question depends on how we set up the rest of the counter.

TCCR1A and TCCR1B are the two registers to set up the rest. You will see I am putting a "1" into CS12. The CSxx bits control the clock source that is used to change the counter. If you look on page 98 of the manul there is a little table that defines what the 3 bits (CS12, 11 and 10) do. In this case, the clock source is CLKio with a 256 prescaler. In other words the frequency of the square wave that increments the counter is 1/256th the clock we use to drive the whole processor.

The WGM bits (Waveform Generation Mode) tell us HOW the timer is going to function. Note that the bits are spread across the two TCCR1A/B registers.

What we want our counter to do is simple. Start from 0, count up to what we have in the OCR1A register, call an interrupt and reset the counter back to 0. That means we want CTC mode where TOP is OCR1A. If you look at table 39 that means we only want WMG12 set. So, if we only need CS12 and WGM12 set then we don't even need to bother with TCCR1A at all.

So, here is the whole program.

CODE
// interrupt based light flashing program for atmel mega8l
//
// Craig Limber, July 2004
//

#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/signal.h>

//-----------------------------------------------------------------------------
SIGNAL(SIG_OUTPUT_COMPARE1A)
{
 static uint8_t ledon;

 if (ledon)
 {
   ledon = 0;
   cbi(PORTD, PD4);
 }
 else
 {
   ledon = 1;
   sbi(PORTD, PD4);
 }
}

//-----------------------------------------------------------------------------
void main()
{
 DDRD = _BV(DDD4);     // enable output

 TIMSK = _BV(OCIE1A);
 TCCR1B = _BV(CS12)    // 256 prescale
        | _BV(WGM12);  // CTC mode, TOP = OCR1A
 OCR1A = 15625;        // count up to TOP   1hz with 8 meg system clock
 while (1)
   asm volatile("nop" ::);  // we spin!  Better if put processor to sleep
}


Link: http://members.shaw.ca/climber/avrinterrupts.html


Forumer™ is Voted #1 Free Forum Hosting provider
Build your own community today with the largest message board hosting company.