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.
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
Hey, thanks for this explanation and example. I’m just starting out with AVR gear, and this helps a lot.
Thanks again
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.
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.
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
include arv/io.h
util/dealy.h
avr/interrupt.h
instead of CHECKBIT(PORTB, 0) you need to read the Pins with
CHECKBIT(PINB, 0)
have fun
Thankyou very much! This had the two missing lines of code I needed: 18 and 19.
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 🙂
I have the same problem, I see a lot of people using this but I don’t understand what it does
_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)
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?
hello i want to interface 4 push buttons using interrupts with my atmega328p which has just 2 external interrupt !! how i can do it.