In this post, we will see how to use Pin Change interrupts on Arduino. This, for example, will allow us to have interrupts on all pins on boards based on the Atmega328P.
But wait a minute, Sacrilege, Arduinos only have 2 interrupt pins! Well, the story is not exactly like that.
In this post, we will see what Pin Change interrupts (PCINT) are and how they work, which are different from the standard interrupts (INT) that we are used to.
Of course, we will also see some code examples. However, we will usually use a library to manage the PCINT. We will see this more practical approach at the end of the article, and you will see that using Pin Change interrupts is very simple.
What are PCINTs
Processors like Atmel have different types of interrupts both internal and external. In our case, we are interested in external interrupts, i.e., those triggered by changing the state of one of the pins.
We have two types of external interrupts:
- INT, hardware external interrupts.
- PCINT, Pin Change interrupts.
Usually, when we talk about interrupts, we refer to the external interrupts of type INT, which we already saw in this post. And it’s true that we have a very limited number of pins with INT interrupts.
Much less known are the Pin Change interrupts (PCINT), which operate similarly, but act on a much higher number of processor pins.
Logically, not everything was going to be so nice and PCINTs also have some disadvantages compared to the usual INTs. But nothing that we cannot overcome or prevent them from being used.
First, unlike the INT interrupts that act on a single pin, the PCINTs act on a group of pins simultaneously (usually on a port).
If we have a single pin associated with each PCINT, we can simply deduce that it has acted on this pin. But, in general, we will have more than one pin and we will have to make a subsequent query to a register to know the pin on which it has acted.
Secondly, unlike the INT interrupts that allow configuring the triggering of CHANGE, FALLING, RISING, LOW, and HIGH, the INT interrupts only distinguish CHANGE events.
If we want to detect rising or falling edges, we must save the state of the register in a variable and perform the comparison with the previous state in the ISR.
Finally, for the aforementioned reasons, they are slightly slower than the INT interrupts. But in general, it is not something we should worry about; it is an irrelevant difference except in very extreme cases.
How to use PCINTs
There are several registers involved in the activation and use of Pin Change interrupts. Let’s go step by step through the process, using the Atmega 328p as a reference, as it is the most used in Arduino Uno and Nano. Although later we will see how to extrapolate it to other Atmel processors.
Enable or disable PCINTs
First, we can enable or disable the PCINTs associated with a group of pins with the PCICR (Pin Change Interrupt Control Register) register.
Here we have 3 bits, which control the activation or deactivation of the PCINTs for each group of pins. PCICR
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
– | – | – | – | – | PCIE2 | PCIE1 | PCIE0 |
Enable or disable for a pin
Once the PCINT is enabled for a group of pins, we must specify which pins of the group can trigger the interrupt. For that, we have the PCMSK0, PCMSK1, and PCMSK2 (Pin Change Mask) registers, in which each bit indicates if the pin triggers the PCINT or not. PCMSK0
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
– | PCINT7 | PCINT6 | PCINT5 | PCINT4 | PCINT3 | PCINT2 | PCINT1 |
PCMSK1
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
– | PCINT14 | PCINT13 | PCINT12 | PCINT11 | PCINT10 | PCINT9 | PCINT8 |
PCMSK2
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
PCINT23 | PCINT22 | PCINT21 | PCINT20 | PCINT19 | PCINT18 | PCINT17 | PCINT16 |
Clear the flag register
On the other hand, we have the PCIFR (Pin Change Interrupt Flag Register) register. The bits of this register are activated each time a change occurs on a pin of the group. PCIFR
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|---|---|
– | – | – | – | – | PCIF2 | PCIF1 | PCIF0 |
To reset it, we have to set a ‘1’ in the corresponding register. The flags are reset automatically when the associated ISR is triggered.
Define the ISRs
Finally, in the code, we have to associate the ISRs that we want to use. So, in the case of the Atmega 328p, we have the functions
- ISR (PCINT0_vect) for pin group D8 to D13
- ISR (PCINT1_vect) for pin group A0 to A5
- ISR (PCINT2_vect) for pin group D0 to D7
These ISRs are associated, respectively, with each of the indicated groups.
Operation of PCINT
We now have all the components to explain the operation of the Pin Change interrupts. In summary, when a pin change is triggered in one of the groups, the corresponding flag is activated in PCIFR.
If this group is enabled in the PCICR, the pin that originated the trigger is enabled in its PCMSKx, and the group has its appropriate ISR defined in the code, the ISR is triggered.
After the execution of the ISR, the PCIFR flag register is cleared, leaving the system ready to receive another pin change event.
PCINT code example
Let’s put all of the above together in a code with a simple example of using Pin Change interrupts. For now, we will continue using the Atmega389p as a reference.
The following example shows how to activate the three available ISRs for the three groups and how to associate them with certain pins of each group.
// Enable PCINT on a PIN
void pciSetup(byte pin)
{
*digitalPinToPCMSK(pin) |= bit (digitalPinToPCMSKbit(pin)); // activate pin in PCMSK
PCIFR |= bit (digitalPinToPCICRbit(pin)); // clear interrupt flag in PCIFR
PCICR |= bit (digitalPinToPCICRbit(pin)); // enable interrupt for the group in PCICR
}
// Define ISR for each port
ISR (PCINT0_vect)
{
// manage for PCINT for D8 to D13
}
ISR (PCINT1_vect)
{
// manage PCINT for A0 to A5
}
ISR (PCINT2_vect)
{
// manage PCINT for D0 to D7
}
void setup()
{
// Enable PCINT for different pins
pciSetup(7);
pciSetup(8);
pciSetup(9);
pciSetup(A0);
}
void loop()
{
}
In this example, we have only enabled the three ISRs, but we do not distinguish in which pin has triggered, nor the type of event. You have a complete example with the management at this link.
The resulting code is, let’s say, not very intuitive. Fortunately, the community has developed several libraries that save us the trouble of having to handle this code ourselves. We will see them at the end of the post.
PCINT on other processors
In the examples, we have used the Atmega 328p processor, but what about the other Atmega models? Well, in general, it is very similar but each one has its own pin definition.
Below, you have tables with the INTs and PCINTs of some of the most common Atmega processors.
Atmega 128/328p (Arduino Uno and Nano)
Pin | Port | INT | Arduino Pin |
---|---|---|---|
2 | PD2 | INT0 | 2 |
3 | PD3 | INT1 | 3 |
Pin | Port | PCINT | Pin | Port | PCINT | Pin | Port | PCINT |
---|---|---|---|---|---|---|---|---|
2 | PD2 | PCINT18 | 8 | PB0 | PCINT0 | A0 | PC0 | PCINT8 |
3 | PD3 | PCINT19 | 9 | PB1 | PCINT1 | A1 | PC1 | PCINT9 |
4 | PD4 | PCINT20 | 10 | PB2 | PCINT2 | A2 | PC2 | PCINT10 |
5 | PD5 | PCINT21 | 11 | PB3 | PCINT3 | A3 | PC3 | PCINT11 |
6 | PD6 | PCINT22 | 12 | PB4 | PCINT4 | A4 | PC4 | PCINT12 |
7 | PD7 | PCINT23 | 13 | PB5 | PCINT5 | A5 | PC5 | PCINT13 |
Atmega 32u4 (Arduino Leonardo and Micro)
Pin | Port | INT |
---|---|---|
0 | PD2 | INT2 |
1 | PD3 | INT3 |
2 | PD1 | INT1 |
3 | PD0 | INT0 |
7 | PE6 | INT6 |
Pin | Port | PCINT |
---|---|---|
SCK/15 | PB1 | PCINT1 |
MOSI/16 | PB2 | PCINT2 |
MISO/14 | PB3 | PCINT3 |
8/A8 | PB4 | PCINT4 |
9/A9 | PB5 | PCINT5 |
10/A10 | PB6 | PCINT6 |
11 | PB7 | PCINT7 |
Atmega2560 (Arduino Mega)
Pin | Port | INT | Arduino Pin |
---|---|---|---|
2 | PE4 | INT4 | 6 |
3 | PE5 | INT5 | 7 |
21 | PD0 | INT0 | 43 |
20 | PD1 | INT1 | 44 |
19 | PD2 | INT2 | 45 |
18 | PD3 | INT3 | 46 |
n/c | PE6 | INT6 | 8 (fake 75) |
n/c | PE7 | INT7 | 9 (fake 76) |
Pin | Port | PCINT | Pin | Port | PCINT | Pin | Port | PCINT |
---|---|---|---|---|---|---|---|---|
10 | PB4 | PCINT4 | SS | PCINT0 | PB0 | A8 | PK0 | PCINT16 |
11 | PB5 | PCINT5 | SCK | PCINT1 | PB1 | A9 | PK1 | PCINT17 |
12 | PB6 | PCINT6 | MOSI | PCINT2 | PB2 | A10 | PK2 | PCINT18 |
13 | PB7 | PCINT7 | MISO | PCINT3 | PB3 | A11 | PK3 | PCINT19 |
14 | PJ1 | PCINT10 | A12 | PK4 | PCINT20 | |||
15 | PJ0 | PCINT9 | A13 | PK5 | PCINT21 | |||
A14 | PK6 | PCINT22 | ||||||
A15 | PK7 | PCINT23 |
We can adapt our code for the different processors or, much better, use a library that takes care of it and avoids the headaches as we will see next.
For more information, consult the Datasheet of the processor
PCINT in a library
As we said, there are many libraries to manage the Pin Change interrupts available in the library manager. Some examples are Sodaq_PcInt, PinChangeInterrupt, EnableInterrupt, PciManager.
Personally, I like the library YetAnotherArduinoPcIntLibrary because it is easy to use, the code is small and efficient, and it is well written. Also, it distinguishes between RISING/FALLING/CHANGE modes and allows passing variables to the ISR callback functions.
The truth is that it is a wonderful library and makes using Pin Change interrupts as comfortable as a normal INT. And here we have an example of how to use the library.
#define PCINT_PIN A5
#include <YetAnotherPcInt.h>
void pinChanged(const char* message, bool pinstate) {
Serial.print(message);
Serial.println(pinstate ? "HIGH" : "LOW");
}
void setup() {
Serial.begin(115200);
pinMode(PCINT_PIN, INPUT_PULLUP);
PcInt::attachInterrupt(PCINT_PIN, pinChanged, "Pin has changed to ", CHANGE);
}
void loop() {}
It can’t be more comfortable! That’s how easy it is to use Pin Change interrupts in our projects, which allows, in the case of the Atmega 328p, to have interrupts on all pins.
Download the code
All the code from this post is available for download on Github.