Using Pin Change Interrupts on AVR® MCUs

Last modified by Microchip on 2023/11/10 11:09

In the last lesson we read the status of an IO port and turned on an LED when we pressed a switch.  If we want to modify our code to be closer to a real world application, we would utilize interrupts so that we can monitor for a change on a pin.  In applications where low power modes are used, this would allow our device to be in a low power mode and "wake up" to perform some task and then go back to its reduced power operation. 

Let's get started learning how to implement an interrupt while keeping the functionality of our application the same.

Overview

In this Lesson:

  • Pin change IRQ's are used in low power board controllers.
  • Alternate functions of PORTB, including pin change IRQs.
  • Navigate the register map of the ATmega328P.
  • Enable an IRQ in the pin change Mask Register.
  • Enable the pin change IRQ in the pin change IRQ control register.
  • Determine the AVR® MCU status register enabling global IRQs.
  • Use <avr/interrupt.h> support for IRQs (the required include, sei() and IRQ vector).
  • Test by hitting a breakpoint in the ISR.

Here's our code as it was at the end of the last lesson.

#include <avr/io.h>


#define LED_ON  PORTB |= (1<<PORTB5)
#define LED_OFF PORTB &= ~(1<<PORTB5)
#define LED_TOGGLE  PINB |= (1<<PINB5)


int main(void) {
    DDRB |= (1 << PB5); // set PB5 as output pin
   DDRB &= ~(1<<DDB7); //set PB7 as an input pin
   
   while (1) {
       
       if(!(PINB & (1<<PINB7))) //If PINB7 is low
       {    
            LED_ON;
        }
       else
        {
            LED_OFF;
        }
        
    }
}

Procedure

PORT B

Take a look at Port B in the datasheet.

Returning to the I/O-Ports section of the datasheet for the ATmega328PB, refer to the Alternate Port Functions section.  Here we will see that PB7 has as an alternate functions, Pin Change Interrupt 7.  This is what we need since, as you recall, our switch is connected to PB7.

ATmega328PB Port B Alternate Functions Table

Now navigate to the Register summary of the datasheet and the search for PCINT7.  Nothing in this section will come up in the search so you should try PCINTn to see if there's a general form shown.  You should land on this section where we see that PCINTn (in our case PCINT7) is associated with PCMSK0.

PCINT7

A search in the datasheet for PCMSK0 will land you on Pin Change Mask Register 0 as shown where we see bit 7

PCMSK0

This information will allow us to start doing some code modifications.  First we will set the bit in PCINT7...

PCMSK0 |= (1<<PCINT7);

Back to Top


PCICR Register

Review the PCICR register.

From the information indicated in red, we also need to set the PCIE0 (Pin Change Interrupt Enable) bit in the PCICR (Pin Change Interrupt Control Register).  This is accomplished with this line of code.

Here's a look at a the PCICR register where we can see the PCIE0 bit.

PCICR

We can set this bit with the following line of code.

PCICR |= (1<<PCIE0);

Back to Top


Review the Status Register

In the notes for each bit in the PCICR register, we see the following information for bit 0.

ATmega328PB PCIE0 Bit Description

This tells us that need to take a look at the Status Register (SREG)

SREG

The SREG indicates that it can be set and cleared with the SEI and CLI instructions.  In order to use these instructions, we'll need to #include a file into our project called <avr/interrupt.h>.  This will allow us to use the functions sei() which enables interrupts by setting the global interrupt mask and cli() which disables them by clearing the global interrupt mask.

Here is the code we have so far...

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

#define LED_ON  PORTB |= (1<<PORTB5)
#define LED_OFF PORTB &= ~(1<<PORTB5)
#define LED_TOGGLE  PINB |= (1<<PINB5)


int main(void) {
    DDRB |= (1 << PB5); // set PB5 as output pin
   DDRB &= ~(1<<DDB7); //set PB7 as an input pin
   
    PCMSK0 |= (1<<PCINT7);
    PCICR |= (1<<PCIE0);
   
    sei();
   
   while (1) {
       
       if(!(PINB & (1<<PINB7))) //If PINB7 is low
       {    
            LED_ON;
        }
       else
        {
            LED_OFF;
        }
        
    }
}

 

Back to Top


Interrupt Vector

Find the Interrupt Vector.

One of the last pieces we need is the memory location that will contain the interrupt service routine. This is referred to as an interrupt vector. When an interrupt occurs, the CPU automatically jumps to the appropriate interrupt vector and executes the ISR.  The datasheet contains a table of these.

ATmega328PB Interrupt Vectors Table

Here is what the code looks for the ISR with the vector...

ISR(PCINT0_vect){
   
   
}

 

Back to Top


Control Logic

Move the control logic.

Now we will move the control logic of our program into this interrupt vector. 

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

#define LED_ON  PORTB |= (1<<PORTB5)
#define LED_OFF PORTB &= ~(1<<PORTB5)
#define LED_TOGGLE  PINB |= (1<<PINB5)

ISR(PCINT0_vect){
   if(!(PINB & (1<<PINB7))) //If PINB7 is low
       {    
            LED_ON;
        }
       else
        {
            LED_OFF;
        }
   
}

int main(void) {
    DDRB |= (1 << PB5); // set PB5 as output pin
   DDRB &= ~(1<<DDB7); //set PB7 as an input pin
   
    PCMSK0 |= (1<<PCINT7);
    PCICR |= (1<<PCIE0);
   
    sei();
   
   while (1) {
       
       
        
    }
}

Back to Top


Readability

One more change to make it little more readable.

We will define a function for when the button is pressed.

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

#define LED_ON  PORTB |= (1<<PORTB5)
#define LED_OFF PORTB &= ~(1<<PORTB5)
#define LED_TOGGLE  PINB |= (1<<PINB5)
#define SWITCH_PRESSED !(PINB & (1<<PINB7))

ISR(PCINT0_vect){
   if(SWITCH_PRESSED) //If PINB7 is low
       {    
            LED_ON;
        }
       else
        {
            LED_OFF;
        }
   
}

int main(void) {
    DDRB |= (1 << PB5); // set PB5 as output pin
   DDRB &= ~(1<<DDB7); //set PB7 as an input pin
   
    PCMSK0 |= (1<<PCINT7);
    PCICR |= (1<<PCIE0);
   
    sei();
   
   while (1) {
       
       
        
    }
}

Back to Top


Program the Device

Select the Program button at the top menu.

MPLAB X IDE Program Icon

You should see the same functionality as the previous lesson where the LED lights up when the switch is pressed.

Back to Top


Breakpoint

Set a Breakpoint.

Let's set a breakpoint inside the Interrupt Service Routine to see that our code is flowing through it when the switch is pressed. The breakpoint has been set for line LED_ON

Set a Breakpoint in MPLAB X IDE

The breakpoint has been reached after pressing the switch.

Breakpoint in MPLAB X IDE

Back to Top

Learn More