Skip to main content

External Interrupts on an ATmega168

This tutorial will teach you how to use external and pin change interrupts on an AVR microcontroller. I will be using an ATmega168. The general principles apply to other AVR microcontrollers, but the specific vary greatly.

What is an Interrupt?

Imagine your are sitting at your computer, reading this post. The phone rings and you answer it. After you hang up the phone (it was a telemarketer trying to sell you a timeshare), you get back to the awesomeness of the post, picking up where you left off.

Microcontroller interrupts are just like that.

  • The microcontroller is executing it’s main routine
  • An event occurs, which raises an interrupt
  • The Interrupt Service Routine (ISR) is run
  • On termination of the ISR, the microcontroller returns to it’s main routine, at the point where it left off

What is an External Interrupt?

The ATmega168 Microcontroller has a set of interrupts that fire off when pin values change. Specifically these are…

Vector No Program Address Source Description
2 0x001 INT0 (PD2) External Interrupt Request 0
3 0x002 INT1 (PD3) External Interrupt Request 1

These are very versatile as you can set them to trigger whenever

  • The pin is low
  • There is any change on the pin
  • When the pin goes from high to low
  • When the pin goes from low to high

The downside is that you can only monitor 2 pins (PD2 and PD3).

What is a Pin Change Interrupt?

In addition to the External Interrupts, the ATmega168 also has 3 interrupts that detect a change in a pin value. These are…

Vector No Program Address Source Description
4 0x006 PCINT0 (PB0 to PB7) Pin Change Interrupt Request 0
5 0x008 PCINT1 (PC0 to PC6) Pin Change Interrupt Request 1
6 0x00A PCINT2 (PD0 to PD7) Pin Change Interrupt Request 2

These can be use to detect a change on any pin. Because each interrupt is for a group of pin, you will also need to do extra work to determine which pin changed and how it changed.

Note: If you look at the pinout diagram on the atmega168 datasheet you will notice that the pins are labelled PCINT0 to PCINT23. PCINT0 to PCINT7 refer to the PCINT0 interrupt, PCINT8 to PCINT14 refer to the PCINT1 interrupt and PCINT15 to PCINT23 refer to the PCINT2 interrupt. This can be a little bit confusing.

Using External Interrupts

The examples in this tutorial are based on the following circuit.

Circuit diagram for external interrupt example

The main routine will run a continual sweep on the 8 red LEDs. When the button (the one connected to PD2) is pressed, we briefly turn on the first green LED, then turn the 2nd green LED on for 2 seconds. After the interrupt is complete we return to the sweep.

The source code for this example is…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>  
 
#define green_led0_on()  PORTC |= _BV(0)
#define green_led0_off()  PORTC &= ~_BV(0)
#define green_led1_on()  PORTC |= _BV(1)
#define green_led1_off()  PORTC &= ~_BV(1)
 
int main (void)
{
    DDRB  = 0b11111111;   // All outputs
    DDRC  = 0b01111111;   // All outputs (Although we will just use PC0 and PC1)
    DDRD  = 0b11111011;   // set PD2 to input 
    PORTD = 0b00000100;   // set PD2 to high
 
    EIMSK |= _BV(INT0);  //Enable INT0
    EICRA |= _BV(ISC01); //Trigger on falling edge of INT0
    sei(); 
 
    while(1)
    {
        sweep();
    }
}
 
void sweep()
{
    PORTB = 0b10000000;
    for (int i=0;i<8;i++)
    {
        _delay_ms(100);
        PORTB >>= 1;
    }
}
 
ISR(SIG_INTERRUPT0) 
{
    green_led0_on();
    _delay_ms(50);
    green_led0_off();
 
    green_led1_on();
    _delay_ms(2000);
	green_led1_off();
}

On line 18 you will see

EIMSK |= _BV(INT0);

In AVR microcontrollers, interrupts are controller by a number of registers. On the ATmega48, 88, 168 & 328 the EIMSK (External Interrupt Mask Register) controls whether the INT0 and INT1 interrupts are enabled.

bit 7 6 5 4 3 2 1 0
EIMSK INT1 INT0
Read/Write R R R R R R R/W R/W
Initial Value 0 0 0 0 0 0 0 0

The next line (line 19) has

EICRA |= _BV(ISC01);

Again we are setting a register value, the External Interrupt Control Register A (EICRA).

bit 7 6 5 4 3 2 1 0
EICRA ISC11 ISC10 ISC01 ISC00
Read/Write R R R R R/W R/W R/W R/W
Initial Value 0 0 0 0 0 0 0 0

This register determines under which condition INT0 or INT1 should be triggered. Specifically

ISC01 ISC00 Description
0 0 The low level of INT0 (PD2) generates an interrupt request.
0 1 Any logical change on INT0 (PD2) generates an interrupt request.
1 0 The falling edge of INT0 (PD2) generates an interrupt request.
1 1 The rising edge of INT0 (PD2) generates an interrupt request.

ISC11 and ISC10 work in the same way, but on INT1,

The next line (line 20) has

sei();

By default, interrupts are globally switched off. By calling sei() we are enabling them.

Lastly we see at the bottom of our code, the ISR (Interrupt Service Routine). Even though it looks like a normal C function, it is really a macro that defines one of many functions based on the passed in parameters. You can pass in a vector and optionally a list of attributes. The list of interrupt vectors can be found in AVR libc <avr/interrupt.h> Interrupts Documentation under the section labelled “Choosing the vector: Interrupt vector names”

This routine will get called each time the interrupt is triggered. Whilst the ISR is running, interrupts are disabled by default. If you were to press the button multiples times you will notice that the interrupts are queued up and run in succession.

Note: in some tutorials or code examples on the net, you might see ISRs been defined with INTERRUPT or SIGNAL macros. These have been depreciated and are no longer supported.

Pin Change Interrupt Example

The source code for this example is very similar to the previous one. See if you notice the differences.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>  
 
#define green_led0_on()  PORTC |= _BV(0)
#define green_led0_off()  PORTC &= ~_BV(0)
#define green_led1_on()  PORTC |= _BV(1)
#define green_led1_off()  PORTC &= ~_BV(1)
 
int main (void)
{
    DDRB  = 0b11111111;   // All outputs
    DDRC  = 0b01111111;   // All outputs (Although we will just use PC0 and PC1)
    DDRD  = 0b11111011;   // set PD2 to input 
    PORTD = 0b00000100;   // set PD2 to high
 
    PCICR |= _BV(PCIE2);   //Enable PCINT2
    PCMSK2 |= _BV(PCINT18); //Trigger on change of PCINT18 (PD2)
    sei(); 
 
    while(1)
    {
        sweep();
    }
}
 
void sweep()
{
    PORTB = 0b10000000;
    for (int i=0;i<8;i++)
    {
        _delay_ms(100);
        PORTB >>= 1;
    }
}
 
ISR(SIG_PIN_CHANGE2) 
{
    if(bit_is_clear(PIND,2))
    {
    	green_led0_on();
   	 	_delay_ms(50);
    	green_led0_off();
 
    	green_led1_on();
    	_delay_ms(2000);
		green_led1_off();
	}
}

The first change you should notice is the 2 registers we update in lines 18 & 19.

PCICR (Pin Change Interrupt Control Register) is used to determine which of the PCINT interrupts are enabled.

bit 7 6 5 4 3 2 1 0
PCICR PCIE2 PCIE1 PCIE0
Read/Write R R R R R R/W R/W R/W
Initial Value 0 0 0 0 0 0 0 0

PCMSK2 (Pin Change Mask Register 2) determines which pins cause the PCINT2 interrupt to be triggered. As you may have guessed, there are also PCMSK0 and PCMSK1 registers for the PCINT0 and PCINT1 interrupts.

bit 7 6 5 4 3 2 1 0
PCMSK2 PCINT23 PCINT22 PCINT21 PCINT20 PCINT19 PCINT18 PCINT17 PCINT16
Read/Write R/W R/W R/W R/W R/W R/W R/W R/W
Initial Value 0 0 0 0 0 0 0 0

Next you should notice that the ISR uses a different vector. The PCINT interrupts use vectors SIG_PIN_CHANGE0, SIG_PIN_CHANGE1 and SIG_PIN_CHANGE2. We are using SIG_PIN_CHANGE2 because we want to handle the PCINT2 interrupt.

The PCINT2 interrupt triggers when the button is pressed and a second time when it is released. I’ve added an if statement to the ISR so the LED’s only turn on when the button is pressed.

Words of caution

Now it is time to highlight 3 common issues with interrupts..

  • Timing
  • Compiler optimisation
  • Non atomic operations

The timing problem relates to what you may be doing with your main routine. If you are doing something that is time sensitive (like I/O for instance) and your ISR take a long time to run you could get yourself in trouble. In general it is recommended that you keep ISRs short.

Next we have the Compiler optimisation issue. When you share variables between your main routine and your ISR, you need to mark them as volatile. This lets the compiler code optimiser know that the variable can change for reasons it is not aware of. This is best explained with a code example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
volatile int someone_pressed_the_button;
 
int main (void)
{
    setup_stuff(); 
    someone_pressed_the_button=0;
 
    while(1)
    {
        if (someone_pressed_the_button!=0)
        {
        	turn_led_on();
        	someone_pressed_the_button = 0;
        }
    }
}
 
ISR(SIG_INTERRUPT0) 
{
    someone_pressed_the_button = 1;
}

If the shared variable was not set to volatile, the code optimizer might optimise away the whole if statement. After all if the variable is non volatile, we would never expect the “then” part of that statement to be executed.

The last problem has to do with the shared variable. ints are a 4 byte data type and we are working with an 8 bit microcontroller. Consider what happens if we were part way through updating the shared variable when the interrupt was triggered. We deal with this by putting the sensitive code within an atomic block.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
volatile int someone_pressed_the_button;
 
int main (void)
{
    setup_stuff(); 
    someone_pressed_the_button=0;
 
    while(1)
    {
        if (someone_pressed_the_button!=0)
        {
        	turn_led_on();
        	ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
        	{
        		someone_pressed_the_button = 0;
        	}
        }
    }
}
 
ISR(SIG_INTERRUPT0) 
{
    someone_pressed_the_button = 1;
}

More Information

AVR libc <avr/interrupt.h> Interrupts Documentation
ATmega48/88/168/328 datasheet

Related News

ATmega8 breadboard circuit – Part 2 of 3 – The Microcontroller

This tutorial continues on from ATmega8 Breadboard Circuit - Part 1 where we build a...

Breadboards – 101

Breadboards are invaluable for experimenting with electronic circuits. They allow you to create temporary circuits...

Introduction to 74HC595 shift register – Controlling 16 LEDs

This tutorial shows you how to control 16 LEDs with just 3 control lines. We...

12 Comments

  1. Brad

    Hey, thanks for this explanation and example. I’m just starting out with AVR gear, and this helps a lot.
    Thanks again

  2. Dr. Bob Bob

    Good post. I am a PIC guy myself but I know enough about microcontrollers to know that interrupts are very important. I rarely use external interrupts because of signal bounce. You could write a pretty good article about that issue alone.

    Another article that would be interesting would be on other peripheral interrupts (ex timers and serial data). I find those interrupts to be much more interesting than external interrupts.

  3. Oscar

    Great article.

    I come from PIC and I see some coincidences: INT0 for pin RB0 and PCINT0 for pin RB4-RB7.

    Thanks to you I can understand the power of AVR interrupts.

  4. srbombaywala

    Hello there
    i am using this code for toggling the leds.
    ________________________________________________
    #include
    #include

    #include

    #define SETBIT(ADDRESS,BIT) (ADDRESS |= (1<<BIT))
    #define CLEARBIT(ADDRESS,BIT) (ADDRESS &= ~(1<<BIT))
    #define FLIPBIT(ADDRESS,BIT) (ADDRESS ^= (1<<BIT))
    #define CHECKBIT(ADDRESS,BIT) (ADDRESS & (1<<BIT))
    #define WRITEBIT(RADDRESS,RBIT,WADDRESS,WBIT) (CHECKBIT(RADDRESS,RBIT) ? SETBIT(WADDRESS,WBIT) : CLEARBIT(WADDRESS,WBIT))
    volatile int i=1;
    int main(void)
    {
    DDRB &= ~(1 << DDB0); // Clear the PB0 pin
    // PB0 (PCINT0 pin) is now an input

    PORTB |= (1 << PORTB0); // turn On the Pull-up
    // PB0 is now an input with pull-up enabled
    DDRD = 0xFF;
    PORTD = 0xF9;

    PCICR |= (1 << PCIE0); // set PCIE0 to enable PCMSK0 scan
    PCMSK0 |= (1 << PCINT0); // set PCINT0 to trigger an interrupt on state change

    sei(); // turn on interrupts

    while(1)
    {

    if(i==0)
    {
    //i=0;
    SETBIT(PORTD,0);
    CLEARBIT(PORTD,1);
    CLEARBIT(PORTD,2);
    }
    else
    if(i==1)
    {
    SETBIT(PORTD,1);
    CLEARBIT(PORTD,2);
    CLEARBIT(PORTD,0);
    }
    else
    {
    SETBIT(PORTD,2);
    CLEARBIT(PORTD,0);
    CLEARBIT(PORTD,1);
    }
    }

    }

    ISR (PCINT0_vect)
    {

    /*if(PORTB & (1 << PB0))
    {
    i++;
    if (i==3)
    i=0;
    }
    else
    {
    i=i;
    }*/
    if(!CHECKBIT(PORTB,0))
    {

    i++;
    if (i==3)
    i=0;
    }
    else
    {
    i=i;

    }

    }
    _________________________________________________
    the switch is connected to PCINT0 (i.e. PortB0) and the led's are connected to PORTD0,1,2.
    Since the switch press gives two interrupts (one high to low when pressed and other low to high when released ) the led's toggle by 2 instead of one.
    I have used the for loop in the ISR but nothing seems to work
    Kindly help

  5. srbombaywala

    include arv/io.h
    util/dealy.h
    avr/interrupt.h

  6. MHeU

    instead of CHECKBIT(PORTB, 0) you need to read the Pins with
    CHECKBIT(PINB, 0)
    have fun

  7. Dave

    Thankyou very much! This had the two missing lines of code I needed: 18 and 19.

  8. samim

    i can’t understand _BV() !!! what it mean? plz explain it . don’t take the question any way because i’m a novice in AVR system 🙂

    1. Benjamin Hondorp

      I have the same problem, I see a lot of people using this but I don’t understand what it does

      1. protostack

        _BV is a macro that creates a bit value. Best explained with a few examples

        _BV(0) gives you 0b0000001
        _BV(1) gives you 0b0000010
        _BV(2) gives you 0b0000100

        so if you wanted to put a 1 in position 0 & 3 you would do something like

        somevalue = _BV(0) | _BV(3)

        or if you had an existing value and you wanted to set position 3 to a 1 you would do

        somevalue |= _BV(3)

  9. Sohil Mehta

    Hi. Thanks for the tutorial. I found it better than the other ones on the web.
    I am having trouble understanding the “issues with interrupts” part. I understood timing but couldn’t understand compiler optimisation and non-atomic operations. Can someone help me with this?

  10. Yassine

    hello i want to interface 4 push buttons using interrupts with my atmega328p which has just 2 external interrupt !! how i can do it.

Leave a reply

Shopping Cart