The Quick Answer: Arduino Mega Interrupt Pin Mapping
The Arduino Mega 2560 Rev3 (based on the ATmega2560 microcontroller) features exactly six external hardware interrupt pins. Unlike the Arduino Uno, which only exposes pins 2 and 3, the Mega unlocks pins 2, 3, 18, 19, 20, and 21 for zero-latency hardware interrupts. However, the Arduino IDE pin numbers do not map sequentially to the ATmega2560's internal interrupt vectors (INT0 through INT5). This mismatch is the number one reason makers struggle with Mega interrupt configurations.
Here is the exact mapping you need to reference when configuring your Interrupt Service Routines (ISRs):
| Arduino Digital Pin | ATmega2560 Internal Vector | Hardware Port / Pin | Trigger Modes Supported |
|---|---|---|---|
| 2 | INT4 | PORTE 4 | LOW, CHANGE, RISING, FALLING |
| 3 | INT5 | PORTE 5 | LOW, CHANGE, RISING, FALLING |
| 18 | INT3 | PORTD 3 | LOW, CHANGE, RISING, FALLING |
| 19 | INT2 | PORTD 2 | LOW, CHANGE, RISING, FALLING |
| 20 | INT1 | PORTD 1 | LOW, CHANGE, RISING, FALLING |
| 21 | INT0 | PORTD 0 | LOW, CHANGE, RISING, FALLING |
digitalPinToInterrupt(pin) macro in your code rather than hardcoding the internal vector numbers. The Arduino core handles the translation from Arduino Pin 21 to INT0 automatically, preventing cross-board porting errors.
Decision Tree: Hardware Interrupts vs. Pin-Change vs. Polling
Not every signal deserves a hardware interrupt. Misusing ISRs for slow signals bloats your code and introduces timing jitter. Use this decision path to select the right input method for your sensors:
| Signal Type & Speed | Recommended Method | Concrete Pick / Implementation |
|---|---|---|
| High-speed pulses (<1ms), flow sensors, high-res encoders | Hardware Interrupts | Default Pick: Use Mega Pins 2, 3, 18, 19, 20, 21 with attachInterrupt(). |
| Medium speed (1ms - 10ms), need >6 pins, multiple limit switches | Pin-Change Interrupts (PCINT) | Use the PinChangeInterrupt library by NicoHood on any digital pin. |
| Human-speed inputs (>50ms), standard pushbuttons, slow toggles | Polling in loop() |
Use millis() based state machines. Keep ISRs free for critical tasks. |
The Verdict: If you are reading KY-040 rotary encoders or optical flow sensors, wire them to the 6 dedicated hardware interrupt pins. If you are just reading 10 pushbuttons for a menu interface, do not use interrupts; use a polling library like Button2 or EZButton in your main loop.
Parts List & Wiring for a 6-Channel Encoder Build
To demonstrate the full capacity of the Arduino Mega interrupt pins, we will wire six rotary encoders simultaneously. This setup is common for custom MIDI controllers, multi-axis CNC pendants, or audio mixing desks.
Exact Parts Required
- Microcontroller: Arduino Mega 2560 Rev3 (Genuine or Elegoo MEGA2560 R3)
- Sensors: 6x KY-040 Rotary Encoder Modules (or bare ALPS EC11E encoders)
- Hardware Debounce: 12x 0.1µF (100nF) Ceramic Capacitors (X7R dielectric)
- Pull-up Resistors: Internal
INPUT_PULLUPused (no external 10kΩ required unless cable runs exceed 12 inches) - Wiring: 22 AWG stranded hookup wire
Wiring Procedure
- Power & Ground: Connect the VCC and GND pins of all 6 KY-040 modules to the Mega's 5V and GND rails. Ensure your breadboard power rails are continuous.
- Signal Routing: Connect the CLK (Clock) pins of the encoders to Mega pins 2, 3, 18, 19, 20, and 21. (The DT/Direction pins can go to any standard digital pins; we will read them inside the ISR).
- Hardware Debounce (Critical): Solder a 0.1µF capacitor between the CLK pin and GND on each encoder module. This creates an RC low-pass filter with the internal 20kΩ pull-up, yielding a ~2ms time constant that eliminates contact bounce before it ever reaches the ATmega2560. Software debounce inside an ISR is an anti-pattern; filter it in hardware.
Complete Compilable Code: 6-Channel ISR with Volatile Handling
This code targets the Arduino Mega 2560 Rev3. It attaches ISRs to all 6 hardware interrupt pins, reads the quadrature direction, and safely updates volatile counters without blocking the main loop.
// Target Board: Arduino Mega 2560 Rev3 (ATmega2560)
// Libraries: None required (Standard AVR Core)
#include
// --- PIN DEFINITIONS ---
// Hardware Interrupt Pins on Mega
const uint8_t ENC_CLK_PINS[6] = {2, 3, 18, 19, 20, 21};
// Direction pins mapped to adjacent standard digital pins
const uint8_t ENC_DT_PINS[6] = {22, 23, 24, 25, 26, 27};
// --- VOLATILE STATE VARIABLES ---
// MUST be volatile since they are modified in ISR and read in loop()
volatile long encoderCounts[6] = {0, 0, 0, 0, 0, 0};
volatile bool encoderUpdated[6] = {false, false, false, false, false, false};
// --- ISR GENERATOR MACRO ---
// Generates a unique ISR function for each channel to avoid overhead
#define CREATE_ISR(ch) \
void isr_encoder_##ch() { \
uint8_t dtState = digitalRead(ENC_DT_PINS[ch]); \
if (dtState == HIGH) { \
encoderCounts[ch]++; \
} else { \
encoderCounts[ch]--; \
} \
encoderUpdated[ch] = true; \
}
CREATE_ISR(0)
CREATE_ISR(1)
CREATE_ISR(2)
CREATE_ISR(3)
CREATE_ISR(4)
CREATE_ISR(5)
// Array of function pointers to map loops to the generated ISRs
void (*isr_functions[6])() = {
isr_encoder_0, isr_encoder_1, isr_encoder_2,
isr_encoder_3, isr_encoder_4, isr_encoder_5
};
void setup() {
Serial.begin(115200);
while (!Serial) { ; } // Wait for serial port (Mega native USB behavior)
Serial.println(F("Arduino Mega 6-Channel Hardware Interrupt Init..."));
for (int i = 0; i < 6; i++) {
// Configure pins with internal pull-ups
pinMode(ENC_CLK_PINS[i], INPUT_PULLUP);
pinMode(ENC_DT_PINS[i], INPUT_PULLUP);
// Attach interrupt using the safe macro
int irqPin = digitalPinToInterrupt(ENC_CLK_PINS[i]);
if (irqPin == NOT_AN_INTERRUPT) {
Serial.print(F("ERROR: Pin "));
Serial.print(ENC_CLK_PINS[i]);
Serial.println(F(" is not a valid hardware interrupt pin!"));
} else {
// Trigger on RISING edge (one detent per pulse)
attachInterrupt(irqPin, isr_functions[i], RISING);
}
}
Serial.println(F("All 6 ISRs attached successfully."));
}
void loop() {
// Non-blocking check for updates
for (int i = 0; i < 6; i++) {
if (encoderUpdated[i]) {
// Disable interrupts briefly to safely read the multi-byte volatile variable
noInterrupts();
long currentCount = encoderCounts[i];
encoderUpdated[i] = false;
interrupts();
Serial.print(F("Encoder "));
Serial.print(i);
Serial.print(F(" | Count: "));
Serial.println(currentCount);
}
}
// Main loop is free for other tasks (e.g., motor control, displays)
// delay(10); // Optional: small yield to prevent serial buffer flooding
}
Debugging: "ISR Not Firing" and the Top 3 Failure Modes
When your Arduino Mega interrupt pins fail to register pulses, the issue is rarely the hardware itself. It is almost always a violation of AVR interrupt execution rules. Here is the exact debugging path.
First 3 Things to Check When It Fails
- Missing
volatileKeyword: If your counter variable isn't declaredvolatile, the GCC compiler will optimize the main loop to read the variable from a CPU register rather than RAM, completely ignoring the ISR's updates. - Floating Pins: Did you forget
INPUT_PULLUP? If the pin is configured as standardINPUTwithout an external 10kΩ resistor to 5V, electromagnetic noise will cause phantom interrupts or prevent clean edges. - Using
delay()ormillis()inside the ISR:millis()relies on Timer0, which is itself an interrupt. If your hardware ISR blocks execution, Timer0 cannot fire, andmillis()freezes permanently.
Ranked Causes & Exact Error Strings
error: no matching function for call to 'attachInterrupt(uint8_t&, void (&)(), int)'Cause: You passed the raw pin number (e.g.,
21) directly into attachInterrupt() instead of the interrupt vector number. On the Uno, Pin 2 is INT0, so attachInterrupt(2, ...) accidentally works but targets the wrong vector. On the Mega, Pin 21 is INT0, so passing 21 fails or maps to a non-existent vector.Fix: Always wrap the pin in the macro:
attachInterrupt(digitalPinToInterrupt(21), myISR, RISING);
Rank 2: The Silent Double-Count (Switch Bounce)
You turn the encoder one physical "click" (detent), but the serial monitor shows a count increase of +4 or -4.
Cause: Mechanical contacts bounce for 1-5 milliseconds. The ATmega2560 executes an ISR in roughly 5 microseconds. It will happily register 10 interrupts for a single physical button press.
Fix: Do not add delay() in the ISR. Add a 0.1µF capacitor across the switch contacts (hardware debounce) or implement a state-machine timestamp check using micros() inside the ISR.
Rank 3: Variable Clobbering Warning
warning: variable 'encoderCounts' might be clobbered by 'longjmp' or 'vfork' [-Wclobbered]
Cause: You are modifying a multi-byte variable (like a 32-bit long) in the ISR while the main loop is reading it. An 8-bit AVR chip takes 4 clock cycles to read a 32-bit integer. If the ISR fires between cycle 1 and 2, the main loop reads a corrupted, half-updated value.
Fix: Use the noInterrupts() and interrupts() guard block when copying the volatile variable to a local variable in the main loop, exactly as demonstrated in the code block above.
Extending and Simplifying the Build
Depending on your project scope, you may need to scale this architecture up or down. Here are the concrete engineering paths for both scenarios.
Simplifying: Dropping to the Arduino Uno
If you only need to read two encoders (e.g., a simple 2-axis robot arm), downgrade to the Arduino Uno R3. The Uno only exposes Pins 2 and 3 for hardware interrupts. The exact same code provided above will compile and run on the Uno if you reduce the array sizes from 6 to 2 and remove the extra pin definitions. This saves $15 on the BOM and reduces the physical footprint by 60%.
Extending: Scaling to 16+ Interrupt Channels
What if you are building a 16-channel MIDI sequencer and the Mega's 6 hardware pins aren't enough? Do not attempt to use the PinChangeInterrupt library for high-speed quadrature decoding; the software latency will cause dropped steps at high RPMs.
The Professional Solution: Use an I2C GPIO expander with a dedicated hardware interrupt pin, such as the MCP23017.
Wire the MCP23017's INTA or INTB pin to one of the Mega's hardware interrupt pins (e.g., Pin 2). When any of the 16 inputs on the MCP23017 change state, it pulls Pin 2 LOW. Your Mega ISR fires, reads the MCP23017's interrupt capture register via I2C (or SPI, if using the MCP23S17 variant for faster bus speeds), and determines exactly which of the 16 channels triggered the event. This is how industrial PLCs handle dozens of high-speed inputs without missing a beat.
For deeper reading on AVR interrupt execution cycles and vector tables, refer to the official Microchip ATmega2560 datasheet (Section 15: External Interrupts). For a masterclass on ISR timing and nested interrupt behaviors on AVR chips, Nick Gammon's Interrupts Guide remains the definitive bench reference.






