The Direct Answer: What is attachInterrupt() and When Do You Need It?
The attachInterrupt() function configures a microcontroller's hardware interrupt pin to immediately pause the main loop() and execute an Interrupt Service Routine (ISR) when a specific voltage transition (RISING, FALLING, or CHANGE) occurs. You need it when polling a pin in the main loop is too slow to catch fast state changes, such as reading a rotary encoder at high RPMs or capturing a microsecond pulse from a flow sensor.
Unlike polling, which requires the MCU to constantly ask "is the pin high yet?", a hardware interrupt is an event-driven hardware signal. If your main loop is busy driving a Neopixel strip or waiting on a blocking I2C sensor read, polling will miss encoder clicks. An interrupt guarantees the event is captured within microseconds, regardless of what the main code is doing.
delay() or blocking library calls (like Wire.requestFrom()), you must use hardware interrupts.
Hardware Spec Sheet & Pin Mapping
For this build, we are using a standard KY-040 rotary encoder. Mechanical encoders are notorious for contact bounce, making them the perfect stress-test for interrupt logic. The target board for the code below is the Arduino Uno R3 (ATmega328P).
Parts List
- MCU: Arduino Uno R3 (ATmega328P) or compatible clone
- Sensor: KY-040 Rotary Encoder Module (includes breakout board)
- Resistors: 2x 10kΩ through-hole resistors (for hardware pull-ups if your module lacks them)
- Capacitors (Optional but recommended): 2x 0.1µF ceramic capacitors for hardware debouncing
- Wiring: Solderless breadboard and 22 AWG solid core jumper wires
Pin Mapping Table
| KY-040 Pin | Arduino Uno R3 Pin | Notes |
|---|---|---|
| CLK (Clock) | Digital Pin 2 | Must be an INT pin (INT0) |
| DT (Data) | Digital Pin 3 | Read inside ISR to determine direction |
| SW (Switch) | Digital Pin 4 | Polled in main loop (active LOW) |
| + (VCC) | 5V | Do not use 3.3V on a 5V Uno |
| GND | GND | Common ground required |
The Build: Wiring the KY-040 Encoder
- De-energize the board: Unplug the Arduino USB cable before wiring.
- Connect Power and Ground: Route 5V and GND from the Uno to the breadboard power rails. Connect the KY-040 VCC and GND to these rails.
- Wire the Quadrature Pins: Connect the encoder CLK pin to Uno Pin 2, and DT to Uno Pin 3.
- Add Pull-up Resistors: While the Uno has internal pull-ups, mechanical encoders benefit from external 10kΩ pull-up resistors tied from CLK and DT to 5V for cleaner signal edges. If your KY-040 module already has surface-mount resistors on the breakout board, you can skip this step.
- Hardware Debounce (Pro-Tip): Solder or breadboard a 0.1µF ceramic capacitor between CLK and GND, and another between DT and GND. This creates a low-pass RC filter that physically eliminates contact bounce before it reaches the MCU, saving CPU cycles in your ISR.
- Wire the Pushbutton: Connect the SW pin to Uno Pin 4.
Complete Compilable Code (Target: Arduino Uno R3)
This code uses attachInterrupt() on the CLK pin. It reads the DT pin to determine rotation direction and includes a software debounce timer to reject phantom interrupts caused by mechanical bounce. Note the use of the volatile keyword and the absolute absence of Serial.print() inside the ISR.
/*
* Target Board: Arduino Uno R3 (ATmega328P)
* Sensor: KY-040 Rotary Encoder
* Author: ElectricalFlux Bench Team
*/
// Pin Definitions
const byte ENCODER_CLK = 2; // Hardware interrupt pin (INT0)
const byte ENCODER_DT = 3; // Direction data pin
const byte ENCODER_SW = 4; // Pushbutton switch pin
// Volatile variables shared between ISR and main loop
volatile long encoderPos = 0;
volatile unsigned long lastInterruptTime = 0;
// Debounce threshold in microseconds (2000us = 2ms)
const unsigned long DEBOUNCE_THRESHOLD = 2000;
void setup() {
Serial.begin(115200);
// Configure pins with internal pull-ups as a fallback
pinMode(ENCODER_CLK, INPUT_PULLUP);
pinMode(ENCODER_DT, INPUT_PULLUP);
pinMode(ENCODER_SW, INPUT_PULLUP);
// Attach the interrupt to the CLK pin, triggering on FALLING edge
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), readEncoderISR, FALLING);
Serial.println("Encoder initialized. Rotate the shaft.");
}
void loop() {
// Main loop can do heavy work without missing encoder steps
// Example: Read the pushbutton (polled)
if (digitalRead(ENCODER_SW) == LOW) {
Serial.println("Button Pressed! Resetting position.");
// Disable interrupts briefly to safely read/write multi-byte volatile vars
noInterrupts();
encoderPos = 0;
interrupts();
delay(500); // Simple debounce for the button
}
// Print position (done in main loop, NEVER in ISR)
static long lastPrintedPos = -1;
noInterrupts();
long currentPos = encoderPos;
interrupts();
if (currentPos != lastPrintedPos) {
Serial.print("Position: ");
Serial.println(currentPos);
lastPrintedPos = currentPos;
}
}
// --- INTERRUPT SERVICE ROUTINE ---
void readEncoderISR() {
unsigned long currentTime = micros();
// Software debounce: ignore interrupts that happen too quickly
if (currentTime - lastInterruptTime < DEBOUNCE_THRESHOLD) {
return;
}
lastInterruptTime = currentTime;
// Read the DT pin to determine direction
// If DT is HIGH, we are moving one way; if LOW, the other
if (digitalRead(ENCODER_DT) == HIGH) {
encoderPos++;
} else {
encoderPos--;
}
}
Debugging: First 3 Things to Check & Common Compiler Errors
When your interrupt setup fails—either silently ignoring rotations or throwing compiler errors—run through this diagnostic sequence.
The First 3 Things to Check When It Fails
- Is the pin actually interrupt-capable? On the Uno R3, only Pins 2 and 3 support hardware interrupts. If you wired CLK to Pin 4,
digitalPinToInterrupt(4)returns-1, and the interrupt will silently fail to attach. Always consult the official Arduino attachInterrupt reference for your specific board's interrupt map. - Are shared variables marked
volatile? If you forgetvolatile, the GCC compiler will optimize your code by cachingencoderPosin a CPU register. The ISR will update the RAM value, but the main loop will only ever read the stale register value. The position will appear frozen. - Is your ISR blocking? If you put
Serial.print(),delay(), orWire.requestFrom()inside the ISR, the microcontroller will lock up. Interrupts disable global interrupts while executing; since Serial relies on the UART interrupt to empty its buffer, calling Serial inside an ISR creates a deadlock.
Exact Compiler Error Strings & Ranked Causes
If your code won't compile, look for these exact GCC error strings in the Arduino IDE console:
Error 1: error: invalid use of void expression
- Cause: You included parentheses in the function name inside the attach call:
attachInterrupt(pin, readEncoderISR(), FALLING). - Fix: Remove the parentheses. You must pass the function pointer, not execute the function:
attachInterrupt(pin, readEncoderISR, FALLING).
Error 2: error: redefinition of 'void __vector_1()'
- Cause: You are mixing the Arduino
attachInterrupt()API with raw AVR GCC interrupt vectors (e.g.,ISR(INT0_vect)) on the same pin. - Fix: Pick one method. The Arduino API is a wrapper around the raw vectors. Using both causes a namespace collision at compile time.
Error 3: warning: variable 'encoderPos' might be clobbered by 'longjmp' or 'vfork'
- Cause: The compiler detected a variable shared with an ISR that lacks the
volatilequalifier. - Fix: Add
volatileto the variable declaration.
Extending and Simplifying the Build
How to Simplify: If you are out of hardware interrupt pins (e.g., you need to read three encoders on an Uno), abandon attachInterrupt() and use the PinChangeInterrupt library. Pin Change Interrupts (PCINT) allow you to trigger an ISR on any digital pin, though the ISR must then poll the port registers to figure out which specific pin triggered the event. Nick Gammon's interrupt guide remains the definitive resource for understanding AVR PCINT vectors.
How to Extend: For high-RPM applications (like CNC spindle encoders outputting 10,000+ pulses per second), digitalRead() inside the ISR is too slow (it takes ~3-4 microseconds). Extend the build by using Direct Port Manipulation. Replace digitalRead(ENCODER_DT) with (PIND & (1 << 3)). This reads the entire Port D register in a single CPU clock cycle, reducing ISR execution time from ~8µs to under 1µs.
Frequently Asked Questions
Can I use Serial.print() inside an Arduino attachInterrupt ISR?
No. Never use Serial.print(), Serial.println(), or any UART functions inside an ISR. When an ISR executes, global interrupts are disabled. The Arduino Serial library relies on the UART hardware interrupt to move data from the software buffer to the hardware transmit register. If the buffer fills up while inside your ISR, Serial.print() will block and wait for the UART interrupt to fire. Since global interrupts are disabled, the UART interrupt can never fire, resulting in a permanent hard-lock of the microcontroller. Always use flags or volatile variables to pass data to the main loop for printing.
Which Arduino pins support attachInterrupt on the Uno and Mega?
Hardware interrupt pins vary strictly by microcontroller architecture. On the Arduino Uno (ATmega328P), only Pins 2 and 3 support hardware interrupts (INT0 and INT1). On the Arduino Mega 2560, you have six hardware interrupt pins: 2, 3, 18, 19, 20, and 21. On 3.3V boards like the ESP32 DevKit V1, almost all GPIO pins (except 34-39 which are input-only) support interrupts via the GPIO matrix, but you must still use digitalPinToInterrupt() to map the GPIO number to the internal interrupt vector.
Why is my rotary encoder skipping steps with attachInterrupt?
Step skipping usually stems from one of three physical or logical bottlenecks. First, contact bounce: mechanical switches physically vibrate when closing, generating dozens of microsecond-level phantom pulses. Fix this with 0.1µF hardware capacitors or a software micros() debounce timer. Second, ISR execution time: if your main loop is disabling interrupts for long periods (using noInterrupts() for extended I2C transactions), the MCU will miss the physical edge transition. Keep noInterrupts() blocks under 50 microseconds. Third, wrong trigger mode: using CHANGE on a noisy signal will double-count edges. Stick to FALLING or RISING on the CLK pin and read the DT pin state to determine direction.






