Learn about what interrupts are and how they can be used in your next AVR microcontroller project!

So far, we have learned how to use some peripherals on the Atmega168, including I/O, ADC, and timers. In this tutorial, we will learn what interrupts are and how they can be used to make real-time programs.

Schematic

image5.jpg

What are Interrupts?

Simple programs written for microcontrollers can usually be all done inside the main function and require little use of peripherals. However, most other microcontroller programs are more complex and require large amounts of code. When this happens, interrupts can become very helpful, but what exactly are interrupts?

Imagine a scenario where our microcontroller needs to do two things simultaneously: keep track of time accurately and blink an LED. Our program could start by resetting a timer, incrementing a counter, and then waiting for the timer to overflow. Once this is done, our code can blink the LED. While this gets the job done somewhat, there are two issues with this. The CPU spends most of its time sitting in delay loops, which is a waste of CPU time, and the execution time of the LED can be difficult to calculate.

So, how can we fix this scenario? We can use an interrupt on the timer! So instead of incrementing a counter in the main code, we shift the code to an interrupt service routine, which handles timing. 

Normally, the microcontroller will run the LED blink code, but as soon as the timer generates an interrupt request, the microcontroller stops the LED blink code, executes the timer interrupt service routine, and then returns back to the LED blink code. This way, the LED blink code cannot interfere with our timer code and it can more accurately (and easily) track time. 

Interrupts on the AVR Core

The AVR has a vector table where each interrupt source jumps to a unique address. This is incredibly advantageous since we no longer need to perform comparisons to see which interrupt has fired, which can take time. 

The table below shows the different interrupts available on the Atmega168 and which addresses they jump to in the program memory. However, several interrupt options have to be configured before we can use them. 

image1.jpg

Extract from the ATmega168 datasheet

Table Location

The Atmega168 has a bootloader area of memory that allows it to re-write its own program memory on the fly, which can be useful for firmware updates. Therefore, where the ISR vector table will sit in memory is important. If the table sits in the bootloader area, it can never be updated (not recommended) if the bootloader is enabled. 

Therefore, if there is no bootloader, you should put the vector table at the bottom of memory (near address 0x0000), but if you use the bootloader then you should move the vector table above the bootloader. This can easily be done by changing several bits in the MCUCR Register.

image4.jpg

  • If IVSEL = 0 then ISR is at the start of vector table else ISR resides in the bootloader. For now, leave this at 0 since we are not using the bootloader
  • If IVCE = 1 then the ISR switchover is performed. Leave this at 0 for now

Interrupt Enable Bits

Each interrupt source (I/O pin, peripheral, etc.) has an associated interrupt enable bit. Like the PIC, there is a global interrupt enable bit found in the STATUS register, which needs to be set to allow interrupts to work. To find out where these interrupt flags reside, you need to consult the specific peripheral chapter on the datasheet. 

For example, we will be using the overflow interrupt on timer 0, so if we look at the timer 0 chapter, we find that the interrupt enable bit is found in register TIMSK0 (page 89), and is called TOIE0. This bit needs to be set to 1 for the timer overflow to fire. This register also has two other interrupt sources, overflow on A match and overflow on B match, which can be useful for PWM functions (this will be covered in the future).

image2.jpg

image3.jpg

Note, setting the I bit in the SREG is not done using the SREG itself, instead, use the function sei(); to set the I bit and cei(); to clear the I bit.

Writing the ISR in WinAVR

So we now understand that interrupts need to be enabled to fire, but how do we write one using C and the WINAVR compiler? The answer is simple: We use the special reserved word ISR and pass the interrupt name argument to tell the compiler which interrupt each function handles. Note that we need to include the interrupt header file, otherwise interrupt functions won’t work!

    #include <avr/interrupt.h>

ISR(TIMER0_OVF_vect)
{
	// Interestingly, the AVR automatically clears interrupt flags....unlike the PIC
	// Put your code here
}

Simple Blink Example

In this example, the ATmega168 will make an LED connected to PD0 blink every so often, where the rate of blinking is controlled by timer 0. However, you may notice that the main function is empty and the LED is blinked inside the timer overflow interrupt service routine (ISR). This means that we can put any code we want in the while loop and that code will not prevent the interrupt from running.

    /*
 * AVR Interrupt.c
 *
 * Created: 09/01/2018
 * Author : RobinLaptop
 */ 

// These are really useful macros that help to get rid of unreadable bit masking code
#define setBit(reg, bit) (reg = reg | (1 << bit))
#define clearBit(reg, bit) (reg = reg & ~(1 << bit))
#define toggleBit(reg, bit) (reg = reg ^ (1 << bit))
#define clearFlag(reg, bit) (reg = reg | (1 << bit))


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

ISR(TIMER0_OVF_vect)
{
	// Interestingly, the AVR automatically clears interrupt flags =) ....unlike the PIC =(
	
	// Toggle the LED (PD0 , Pin 2)
	toggleBit(PORTD, PD0);
}



int main(void)
{
	// Initialize Registers

	clearBit(TCCR0A, WGM00);			// Configure WGM to be 0x00 for normal mode
	clearBit(TCCR0A, WGM01);
	clearBit(TCCR0B, WGM02);

	setBit(TCCR0B, CS00);				// Configure clock source to be clock io at 1024 pre-scale
	clearBit(TCCR0B, CS01);
	setBit(TCCR0B, CS02);

	DDRD = 0xFF;						// Make PORT D and output

	sei();								// Enable interrupts
	setBit(TIMSK0, TOIE0);				// Enable the timer interrupt
		
    while (1) 
    {
		// Put any code you want here
		// It should not affect the interrupt service routine!
    }
}

Conclusion

This tutorial only covers a single interrupt, the timer 0 overflow interrupt, but it clearly demonstrates that interrupts are incredibly powerful. If used properly, you can have a system that can respond to signals as soon as they arrive and have the main code suspended. This can be used to do many things, including multitasking, multiple handling of different peripherals, and creating real-time code!

Robin Mitchell
Graduated from the University Of Warwick in Electronics with a BEng 2:1 and currently runs MitchElectronics.

Maker Pro Logo
Continue to site
Quote of the day

-