Extending Arduino Wake Sources with AVR Pin Change Interrupts
Standard attachInterrupt() only reaches two pins on an ATmega328P. Pin change interrupts let any port pin wake the CPU, but they require direct register access and careful port-level handling.
25 Nov 2025, 22:22 UTC

The Two-Pin Bottleneck
If you have tried to build a battery-powered Arduino that sleeps until a button, sensor, or reed switch changes state, you have probably hit the same wall: the standard attachInterrupt() function on an ATmega328P-based board (Uno, Nano, Pro Mini) only maps to two hardware external interrupt pins, INT0 and INT1. On a typical Arduino Uno, those are digital pins 2 and 3. Every other pin is invisible to the interrupt system. That is a problem when your wake source is on pin 7, or when you need to monitor several inputs without polling.
How Pin Change Interrupts Work on ATmega328P
The ATmega328P has a second interrupt mechanism called pin change interrupts (PCI). Instead of dedicating a pin to a specific vector, PCI groups pins into ports. Any logical change on any enabled pin in that port triggers a single shared interrupt service routine (ISR) for that port. On the ATmega328P, the ports map to Arduino pins roughly as follows:
- Port B: digital pins 8–13 (plus crystal pins)
- Port C: analog pins A0–A5
- Port D: digital pins 0–7
To enable this, you touch three register families:
PCMSK0/PCMSK1/PCMSK2: the pin change mask registers. Setting a bit here enables PCI for the corresponding physical pin.PCICR: the pin change interrupt control register. SettingPCIE0,PCIE1, orPCIE2enables the interrupt for the entire port group.- The ISR itself uses the vector names
PCINT0_vect,PCINT1_vect, orPCINT2_vect.
Because the Arduino core does not expose PCI through attachInterrupt(), you configure these registers directly in your sketch.
A Minimal Register-Level Wake Sketch
Upload the following sketch in the Arduino IDE, targeting an ATmega328P board such as the Uno or Pro Mini. No special permissions are required beyond normal USB access to the board. The example puts the MCU into idle sleep and wakes it when Arduino pin 4 (physical PD4 on Port D) changes.
#include <avr/sleep.h>
const uint8_t wakePin = 4; // Arduino pin 4 = PD4 on Port D
volatile bool woke = false;
ISR(PCINT2_vect) {
woke = true;
}
void setup() {
Serial.begin(9600);
pinMode(wakePin, INPUT_PULLUP);
PCICR |= _BV(PCIE2); // enable PCI for Port D
PCMSK2 |= _BV(PCINT20); // enable PCI for PD4 (Arduino pin 4)
PCIFR |= _BV(PCIF2); // clear any stale flag before enabling
}
void loop() {
woke = false;
Serial.println("Entering sleep");
delay(50); // allow serial buffer to drain
set_sleep_mode(SLEEP_MODE_IDLE);
sleep_enable();
sleep_mode(); // halts until PCI fires
sleep_disable();
if (woke) {
Serial.println("Woke by pin change");
delay(50);
}
}
In idle mode, peripherals and timers keep running, so millis() and delay() continue to function after wake. If you move to deeper sleep modes later, remember that the main clock stops and those timing functions pause until an interrupt restores them.
Shared Vectors and Other Gotchas
The biggest limitation is architectural: every enabled pin on a port shares one ISR vector. If you enable PCI on pins 4 and 5 of Port D, a change on either pin runs the same PCINT2_vect handler. Your ISR must read the port state or set a flag so the main loop can determine which pin actually woke the CPU. With many active pins on a busy port, shared ISR overhead can add latency.
A second risk is unintended wakeups. Because PCI triggers on any logical change, noise on an unconnected or floating pin within the same port can wake the processor repeatedly. Always use INPUT_PULLUP or an external pull resistor for wake pins, and disable PCI on pins you do not need.
A third issue is flag management. The pin change interrupt flag (PCIFR) can be set before you enable the interrupt. If you enable PCICR while a stale flag is pending, the CPU enters the ISR immediately. Clearing the flag before enabling the interrupt—as shown in the example—avoids this.
Verifying and Choosing Your Approach
Before trusting a PCI wake in the field, verify it on the bench. Toggle the wake pin with a known signal (a jumper to GND or a function generator) and confirm the serial message appears. If you have a logic analyzer or oscilloscope, probe the wake pin and a debug pin toggled inside the ISR; the delay between the pin edge and the debug toggle confirms ISR entry timing. If the sketch never wakes, check that PCMSK and PCICR bits match the physical port and pin, and that no other code is clearing the registers.
Use pin change interrupts when your wake source is not on pins 2 or 3, or when you need to monitor multiple pins on the same port without polling. If your wake source is already on an external interrupt pin and you only need one or two sources, attachInterrupt() remains simpler and gives you per-pin vectors. PCI is the tool for expanding the wake surface, but it demands careful register hygiene and port-level thinking.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.