EXTERNAL INTERRUPTS ON THE ATmega168/328


INTRODUCTION:

In the previous section I talked about the basics of interrupts. In this section, we will talk about the first type of device interrupts called external interrupts. These interrupts are basically called on a given status change on the INTn pin. This is essentially an input interrupt and is great to use for applications when you might need to react quickly to an outside source, such as a bumper of your robot hitting the wall or to detect a blown fuse.


HARDWARE:

Hardware wise there is not difference between External Interrupts and Inputs so don't be afraid to reread the Digital Input Tutorial if you need a refresher.

If you look at the AVR pinout diagram you will see the INTx which are used for External Interrupts and PCINTx pins that are used for Pin Change Interrupts.


EXTERNAL INTERRUPTS:
ATmega328 - INTx

Figure 1: ATmega168/328 - External Interrupt Pins

The ATmega8 and the ATmega88/168/328 are backwards compatible when it comes to the pinouts however, they are programmed slightly different and while external interrupts work the same way on both types of microcontrollers they do require different code to run.

External interrupts are fairly powerful, they can be configured to trigger on one of 4 states. Low level will trigger whenever the pin senses a LOW (GND) signal. Any Logic Change trigger at the moment the pin changes from HIGH (Vcc) to LOW (GND) or from LOW (GND) to HIGH(Vcc). On Falling Edge will trigger at the moment the pin goes from HIGH (Vcc) to LOW (GND). On Rising Edge will trigger at the moment the pin goes from LOW (GND) to HIGH (Vcc). The best part is that you can configure each INTx independently.

External interrupts use the below 3 registers. Which you could find under the "External Interrupts section of the datasheet.

7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 bit
EICRA - - - - ISC11 ISC10 ISC01 ISC00

External Interrupt Control Register A

ISCx1 ISCx0 DESCRIPTION
0 0 Low level of INTx generates an interrupt request
0 1 Any logic change on INTx generates an interrupt request
1 0 The falling edge of INTx generates an interrupt request
1 1 The rising edge of INTx generates an interrupt request

ISC Bits Settings

7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 bit
EIMSK - - - - - - INT1 INT0

External Interrupt Mask Register

7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 bit
EIFR - - - - - - INTF1 INTF0

External Interrupt Flag Register

ATmega168/328 Code:

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

    int main(void)
    {
        DDRD &= ~(1 << DDD2 );     // Clear the PD2 pin
        // PD2 (PCINT0 pin) is now an input

        PORTD |= (1 << PORTD2);    // turn On the Pull-up
        // PD2 is now an input with pull-up enabled

        EICRA |= (1 << ISC00);    // set INT0 to trigger on ANY logic change
        EIMSK |= (1 << INT0);     // Turns on INT0

        sei();                    // turn on interrupts

        while(1)
        {
            /* main program loop here */
        }
    }

    ISR (INT0_vect)
    {
        /* interrupt code here */
    }

PIN CHANGE INTERRUPTS:
ATmega328 - PCINTx

Figure 2: ATmega168/328 - Pin Change Interrupt Pins

One important thing to note, on the older ATmega8 does not have any PCINT pints, therefore, this section of the tutorial only applies to ATmega88 through ATmega328.

7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 bit
PCICR - - - - - PCIE2 PCIE1 PCIE0

Pin Change Interrupt Control Register

7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 bit
PCIFR - - - - - PCIF2 PCIF1 PCIF0

Pin Change Interrupt Flag Register

7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 bit
PCMSK2 PCINT23 PCINT22 PCINT21 PCINT20 PCINT19 PCINT18 PCINT17 PCINT16

Pin Change Mask Register 2

7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 bit
PCMSK1 - PCINT14 PCINT13 PCINT12 PCINT11 PCINT10 PCINT09 PCINT08

Pin Change Mask Register 1

7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 bit
PCMSK0 PCINT7 PCINT6 PCINT5 PCINT4 PCINT3 PCINT2 PCINT1 PCINT0

Pin Change Mask Register 0

The PCIEx bits in the PCICR registers enable External Interrupts and tells the MCU to check PCMSKx on a pin change state. When a pin changes states (HIGH to LOW, or LOW to HIGH) and the corresponding PCINTx bit in the PCMSKx register is HIGH the corresponding PCIFx bit in the PCIFR register is set to HIGH and the MCU jumps to the corresponding Interrupt vector.

ATmega168/328 Code:

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

    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

        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)
        {
            /* main program loop here */
        }
    }

    ISR (PCINT0_vect)
    {
        /* interrupt code here */
    }

Now there are 2 limits to the PCINTx interrupt. The first is that ANY change to the pin state will trigger in interrupt, if you remember you can control what state change triggers an INTx interrupt (rising pulse, falling pulse, Low level or any level change). Since a pin has to change states in order to trigger the interrupt we can read the state of the pin and if it's currently HIGH(Vcc) we know that a rising edge event triggered the interrupt, like wise if the pin state is currently LOW(GND) we know that a falling edge interrupt triggered the interrupt.

ATmega168/328 Code:

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

    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

        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)
        {
            /* main program loop here */
        }
    }

    ISR (PCINT0_vect)
    {
        if( (PINB & (1 << PINB0)) == 1 )
        {
            /* LOW to HIGH pin change */
        }
        else
        {
            /* HIGH to LOW pin change */
        }
    }

Now for the 2nd problem is that up to 8 pins share the same PCINTx vector. So when the interrupt fires you will have detect what event triggered the change. So what we might need to do is take a snapshot of the Input states and compare them to the state from the previous time the interrupt triggered. If you take a look at the datasheet pinouts PCMSK0 matches up with PORTB, PCMSK1 with PORTC and PCMSK2 with PORTD.

ATmega168/328 Code:

    #include <avr/io.h>
    #include <stdint.h>            // has to be added to use uint8_t
    #include <avr/interrupt.h>     // Needed to use interrupts

    volatile uint8_t portbhistory = 0xFF;     // default is high because the pull-up

    int main(void)
    {
        DDRB &= ~((1 << DDB0) | (1 << DDB1) | (1 << DDB2)) ; // Clear the PB0, PB1, PB2 pin
        // PB0,PB1,PB2 (PCINT0, PCINT1, PCINT2 pin) are now inputs

        PORTB |= ((1 << PORTB0) | (1 << PORTB1) | (1 << PORTB2)); // turn On the Pull-up
        // PB0, PB1 and PB2 are now inputs with pull-up enabled

        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)
        {
            /* main program loop here */
        }
    }

    ISR (PCINT0_vect)
    {
        uint8_t changedbits;

        changedbits = PINB ^ portbhistory;
        portbhistory = PINB;

        if (changedbits & (1 << PINB0))
        {
            /* PCINT0 changed */
        }

        if (changedbits & (1 << PINB1))
        {
            /* PCINT1 changed */
        }

        if (changedbits & (1 << PINB2))
        {
            /* PCINT2 changed */
        }
    }

Ok ok, I know, the bit math is a bit funny. So I'll be nice and explain it. When we XOR ( ^ ) the PORTB register with the portbhistory register we will get a 0's on the bits that are the same, and 1's in the bits that are different (yes a practical use for the XOR operation). In the IF statements we AND ( & ) the operation with a bitmask that we created using the bit shift register (1 << PBx) in order to isolate specific bit. Lastly, notice how I defined porthistory as volatile? Like I said before, if you want to pass a global variable to an Interrupt make it volatile so that it doesn't cause obsolete data due to compiler optimization.


Now finally, what if you wanted to put both together? Well I got to leave a bit of fun for you guys.


Cheers
Q