Implementing AVR interrupts

Before we go to the code part, let’s what is needed to run interrupts successfully. These conditions apply to all AVR interrupts. First of all, we need to enable global interrupts by setting global Interrupts enable bit (I) in the SREG register. This is crucial as this bit enables or disables all interrupts in the AVR microcontroller. So each time you are setting up an interrupt, be sure this bit is enabled. The next thing is enabling individual interrupt bits on a particular peripheral control register. Without setting this bit – wanted interrupt won’t work either. And the last thing is to be sure that there are required conditions for an interruption to occur. For instance, if you set timer overflow interrupt, be sure this overflow will occur.

Defining AVR interrupts

Now we can get to some code as the practice is the best teacher. If you look at various C compilers, you’ll find that interrupt defining syntax may differ across them. This is mainly because compilers stay away from hardware-specific details, so it is up to the software developer how the hardware interacts. This is why we have to use particular libraries to make it possible. So let’s stick with AVR-GCC. As I mentioned, the gcc compiler also needs a library that will understand interrupt syntax. So be sure your code includes one:

#include <avr/interrupt.h>

During AVR-GCC development, interrupt defining syntax has evolved into a very compact form. Now you can use the very generic macro function ISR(isr_vector). All you need is to provide this function with the proper ISR vector name, and you are set. Let say you want to implement the External Interrupt 0 handling function you write:

ISR(INT0_vect)
{
//your  code
}

So far, we just defined interrupt service routine. Now it’s time to set up the interrupt itself, remembering that we need to enable global and peripheral interrupts. According to the datasheet, pin change interrupts generate software interrupts as pin-change interrupts can be generated even if the pin is configured as input or output. This will give us the advantage to simulate without any intrusion. Let’s write a simple program that will generate an interrupt on pin change of PCINT0 of AVR. Interrupt service routine will toggle PC0 pin every time will be called. To generate the interrupt, we will toggle PCINT0 (PB0) pin that will be configured as an output to generate software like an interrupt.

#include <avr/io.h>
#include <avr/interrupt.h>
//Set PB0 and PC0 pins as output
void MCUInit(void)
{
DDRB |=(1<<PB0);
DDRC |=(1<<PC0);
//Enable pin change interrupt bits
PCICR |=(1<<PCIE0);
//Set PCINT0 bit in pin change mask register PCMSK0
PCMSK0 |=(1<<PCINT0);
//Enable global interrupts
sei();
}
//pin change interrupt ISR
ISR(PCINT0_vect)
{
    PINC =(1<<PC0);
}

int main(void)
{
MCUInit();
    while(1)
    {
        //toggling PB0 pin
        PINB =(1<<PB0);
    }
}

Common issues with interrupts

Interrupts aren’t so innocent as they may look. They may become a real pain in the butt when your program grows bigger. So better take precaution from the start before it’s too late. First of all, be sure your interrupt routine occupies as little time as possible. Keep ISR short, especially if you are doing intensive I/O operations like data transfer. If your ISR takes too long to process, you can miss the next byte coming. Better put data in a buffer and give resources to the main routine. Instead of doing something in ISR, better raise a flag indicating the event’s presence and leave ISR.

ISR usually shares the same variables with the main routines. For instance, the variable can indicate that button has been pressed. So these variables usually are declared as global. So mark these values with the volatile keyword. This way compiler won’t optimize out this variable, knowing that it may be used somehow.

And last, probably the most dangerous issue is atomic operations. Imagine a situation when the program has to fetch 16-bit data byte by byte and interrupt. It occurs between these two bytes, and interrupts share the same variable with the main routine – this leads to serious errors that may be hard to detect sometimes. So, in this case, use atomic operation code blocks. Use this is a special macroblock that disables global interrupts until the atomic operation is complete.

ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
    SomeValue = SharedValue;
}

You can do more with interrupts

Don’t limit yourself to things you see in front. Don’t afraid to open interrupt.h file and see what else you can do with interrupts. For instance, you can define empty interrupts for some interrupts that will return without doing anything:

EMPTY_INTERRUPT(ADC_vect);

Another great feature is that you can create an ISR alias. For example, you have established an ISR for one interrupt and have another interrupt that should perform the same, then put them in the alias, so you don’t need to write another ISR.

ISR(INT0_vect)
{
    PORTB = 42;
}
ISR_ALIAS(INT1_vect, INT0_vect);

Or you can take full control of ISR by yourself with ISR_NAKED(). Using this function, register values won’t be stored in the stack automatically when jumping to ISR – it is all left for you. If you need a faster ISR response, you can choose which registers to push to stack and restore. And you will not get away with this if you get hands-on RTOS. Multitasking is practically impossible without naked ISRs.

And yes you can have interrupts inside interrupts. These are called “nested interrupts” – use these if you really know what you’re doing.

2 Comments:

  1. As we’ve discussed elsewhere, the modifications to the PIN registers must be only writes, since the read and write functionality on PINx is completely different and independent, so a read-modify-write is going to cause all sorts of problems.

  2. Yep. I have to go through all code examples and locate if there is any of these left. Thanks.

Comments are closed