Using Pin Change Interrupts on AVR® MCUs
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.
#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.
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.
A search in the datasheet for PCMSK0 will land you on Pin Change Mask Register 0 as shown where we see bit 7
This information will allow us to start doing some code modifications. First we will set the bit in PCINT7...
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.
We can set this bit with the following line of code.
Review the Status Register
In the notes for each bit in the PCICR register, we see the following information for bit 0.
This tells us that need to take a look at the Status Register (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/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;
}
}
}
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.
Here is what the code looks for the ISR with the vector...
}
Control Logic
Move the control logic.
Now we will move the control logic of our program into this interrupt vector.
#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) {
}
}
Readability
One more change to make it little more readable.
We will define a function for when the button is pressed.
#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) {
}
}
Program the Device
Select the Program button at the top menu.
You should see the same functionality as the previous lesson where the LED lights up when the switch is pressed.
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
The breakpoint has been reached after pressing the switch.