AVR External Interrupts INT0 and INT1 in C with EICRA and EIMSK

In the GPIO tutorial the program kept checking the button in a loop. That is called polling, and it has two problems: the CPU is busy doing nothing useful, and if the main loop is slow, short pulses can be missed. Interrupts solve both. When a configured event happens on a pin, the hardware pauses the main program, runs a short function called an interrupt service routine (ISR), and then resumes exactly where it left off.

This tutorial uses the two dedicated external interrupt pins of the ATmega328P, INT0 and INT1, to count button presses and toggle an LED. It also explains the equivalent registers on the ATmega16/32 and the rules for writing safe ISRs in avr-gcc.

How AVR Interrupts Work

Every interrupt source has a fixed entry in the interrupt vector table at the start of flash. When an enabled interrupt occurs and the global interrupt flag I in SREG is set, the CPU finishes the current instruction, pushes the return address on the stack, clears the I bit and jumps to the vector. In avr-gcc you never write the table yourself; you define an ISR with the ISR() macro and the vector name, and the linker places a jump to it in the right slot.

Three conditions must all be true for an external interrupt to fire: the trigger condition is selected, the individual interrupt is enabled in its mask register, and global interrupts are enabled with sei().

Trigger options

The Interrupt Sense Control bits select what event causes the interrupt. On the ATmega328P they live in EICRA: ISC01 and ISC00 for INT0, ISC11 and ISC10 for INT1.

ISCx1ISCx0Interrupt is triggered by
00Low level on the pin (repeats while the pin stays low)
01Any logical change
10Falling edge
11Rising edge

Registers and Pins

ItemATmega328PATmega16 / ATmega32
INT0 pinPD2 (DIP pin 4, Uno D2)PD2 (DIP pin 16)
INT1 pinPD3 (DIP pin 5, Uno D3)PD3 (DIP pin 17)
Sense controlEICRAMCUCR (INT2 uses ISC2 in MCUCSR)
Enable maskEIMSK: INT0, INT1GICR: INT0, INT1, INT2
Flag registerEIFR: INTF0, INTF1GIFR: INTF0, INTF1, INTF2
Vector namesINT0_vect, INT1_vectINT0_vect, INT1_vect, INT2_vect

The ATmega328P also offers pin change interrupts on almost every I/O pin through PCICR and PCMSK0 to PCMSK2. They fire on any change and are shared by a whole port, so INT0 and INT1 remain the easier choice when you only need one or two inputs.

Program: Two Buttons on INT0 and INT1

Connect one button from PD2 to ground and a second from PD3 to ground, and keep the LED on PB5 (Uno D13). Button 1 toggles the LED; button 2 increments a counter whose lowest four bits are shown on LEDs at PB0 to PB3, as in the previous tutorial.

INT0 toggles an LED, INT1 counts presses (ATmega328P, 16 MHz)
#define F_CPU 16000000UL

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

/* Shared between ISR and main, so it must be volatile */
volatile uint8_t press_count = 0;

/* INT0: falling edge on PD2 toggles the LED on PB5 */
ISR(INT0_vect)
{
    PINB = (1 << PINB5);        /* writing 1 to PINx toggles the pin */
}

/* INT1: falling edge on PD3 increments the counter */
ISR(INT1_vect)
{
    press_count++;
}

int main(void)
{
    /* Outputs: PB5 LED and PB0..PB3 counter LEDs */
    DDRB |= (1 << DDB5) | 0x0F;

    /* PD2 and PD3 as inputs with pull-ups */
    DDRD  &= ~((1 << DDD2) | (1 << DDD3));
    PORTD |=  (1 << PORTD2) | (1 << PORTD3);

    /* Falling edge on both: ISCx1 = 1, ISCx0 = 0 */
    EICRA = (1 << ISC01) | (1 << ISC11);

    /* Clear any pending flags (write 1 to clear), then enable */
    EIFR  = (1 << INTF0) | (1 << INTF1);
    EIMSK = (1 << INT0)  | (1 << INT1);

    sei();                       /* global interrupt enable */

    while (1) {
        /* Reading one byte is atomic on AVR, so no cli() needed here */
        uint8_t count = press_count;
        PORTB = (PORTB & 0xF0) | (count & 0x0F);
    }
}

Understanding the Code

EICRA = (1 << ISC01) | (1 << ISC11); selects the falling edge for both pins, which matches buttons wired to ground with pull-ups. Before enabling, the code writes ones to EIFR. Interrupt flags on AVR are cleared by writing a logical one, which surprises many beginners, and clearing them prevents an old event from firing the ISR immediately after sei().

The variable press_count is declared volatile. Without this keyword the compiler may keep a copy of the variable in a register inside the main loop and never notice that the ISR changed it in memory. Any variable written in an ISR and read elsewhere, or the other way round, must be volatile.

Here the shared variable is a single byte, which the CPU reads in one instruction. If it were a uint16_t or uint32_t, the main code could read half of the old value and half of the new value if an interrupt arrived in between. In that case wrap the read in ATOMIC_BLOCK(ATOMIC_RESTORESTATE) from <util/atomic.h>, which disables interrupts for the duration of the copy.

Bounce and interrupts

You will notice that the counter sometimes jumps by two or three per press. That is contact bounce: each bounce produces another falling edge, and an interrupt reacts to all of them. Practical fixes are a hardware RC filter (for example 10 kΩ and 100 nF) with a Schmitt input, or ignoring further edges in software for 20 to 50 ms using a timer tick, which the next tutorial will provide. Never put _delay_ms() inside an ISR, because it blocks every other interrupt during the delay.

Rules for Good ISRs

  • Keep ISRs short: set a flag, update a counter or copy a value, and do the heavy work in the main loop.
  • Do not call blocking functions such as _delay_ms() or a polled UART print inside an ISR.
  • Declare all shared variables volatile and protect multi-byte reads with an atomic block.
  • Spell the vector name exactly. A typo such as INTO_vect compiles with only a warning, and when the interrupt fires the default handler jumps to address 0, restarting the program.
  • Do not enable an interrupt without defining its ISR, for the same reason.

Summary

External interrupts let the AVR react to pin events without polling. On the ATmega328P, set the trigger in EICRA, clear stale flags in EIFR, enable the interrupt in EIMSK and call sei(). On the ATmega16/32 the same bits live in MCUCR, GICR and GIFR. Use volatile shared variables, keep ISRs short, and handle switch bounce explicitly.