Hardware vs. Software: Why Arduino Interrupt Pins Matter for High-Speed Signals
The classic Arduino Uno R3 has exactly two dedicated hardware interrupt pins: Digital 2 (INT0) and Digital 3 (INT1). If you are polling a sensor in your main loop() using digitalRead(), you are limited by the execution time of your other code. If your loop takes 5ms to run, you will entirely miss a 2ms pulse from a high-speed rotary encoder or a hall effect sensor. Hardware interrupts bypass the main loop, immediately pausing your code to execute an Interrupt Service Routine (ISR) the microsecond a pin changes state.
While modern boards like the ESP32 DevKit V1 support hardware interrupts on almost all GPIOs (except 6-11, and 34-39 which are input-only), the ATmega328P architecture on the Uno R3 remains the baseline for learning strict ISR discipline. According to the official Arduino attachInterrupt() reference, relying on hardware interrupts is mandatory for signals exceeding 100Hz if your main loop handles blocking tasks like Serial.print() or I2C display updates.
Parts List and Pin Mapping for the RPM Tachometer Build
To demonstrate interrupt mechanics, we are building a non-contact RPM tachometer. This reads a magnet passing by a hall effect sensor, calculating revolutions per minute without bogging down the main processor.
| Component | Exact Variant / Spec | Estimated Cost (2026) |
|---|---|---|
| Microcontroller | Arduino Uno R3 (ATmega328P DIP or SMD) | $24.00 - $28.00 |
| Sensor | A3144 Hall Effect Sensor (TO-92 package) | $0.50 (pack of 10) |
| Pull-up Resistor | 10kΩ 1/4W Carbon Film | $0.02 |
| Bypass Capacitor | 100nF (0.1µF) Ceramic (X7R) | $0.05 |
| Target Magnet | Neodymium N42 10x3mm disc | $1.50 |
Pin Mapping Table
| Arduino Uno R3 Pin | Function | Wire Color | Connection Notes |
|---|---|---|---|
| 5V | VCC Power | Red | Feeds A3144 Pin 1 |
| GND | Ground | Black | Feeds A3144 Pin 3 & 100nF cap |
| Digital 2 (INT0) | Hardware Interrupt Input | Yellow | Feeds A3144 Pin 2. Requires 10kΩ pull-up to 5V. |
Complete Compilable Code with ISR Error Handling
This code targets the Arduino Uno R3 (ATmega328P). It uses the FALLING edge trigger because the A3144 pulls the line LOW when a magnetic south pole is detected. Notice the strict use of the volatile keyword and the critical section (noInterrupts() / interrupts()) used to safely copy the ISR variable into the main loop.
// Target Board: Arduino Uno R3 (ATmega328P)
// Project: Hardware Interrupt RPM Tachometer
#define HALL_SENSOR_PIN 2 // Must be D2 or D3 on Uno R3
#define MAGNET_COUNT 1 // Number of magnets on the rotating shaft
#define PULSES_PER_REV 1 // Pulses per revolution (MAGNET_COUNT * 1)
// CRITICAL: Variables shared between ISR and main loop MUST be volatile
volatile uint32_t pulseCount = 0;
volatile uint32_t lastPulseMicros = 0;
void setup() {
Serial.begin(115200);
// Configure pin with internal pull-up as a fallback safety measure
pinMode(HALL_SENSOR_PIN, INPUT_PULLUP);
// Attach the hardware interrupt
// digitalPinToInterrupt() translates the physical pin to the AVR interrupt vector
attachInterrupt(digitalPinToInterrupt(HALL_SENSOR_PIN), magnetDetectISR, FALLING);
Serial.println("Tachometer Initialized. Waiting for pulses...");
}
// --- INTERRUPT SERVICE ROUTINE (ISR) ---
// Keep this as short as physically possible. No Serial.print(), no delay().
void magnetDetectISR() {
pulseCount++;
lastPulseMicros = micros();
}
void loop() {
// Local variables for safe data extraction
uint32_t localCount;
uint32_t localLastPulse;
// CRITICAL SECTION: Disable interrupts to read multi-byte volatile variables safely
noInterrupts();
localCount = pulseCount;
localLastPulse = lastPulseMicros;
pulseCount = 0; // Reset counter for the next sampling window
interrupts();
// Calculate RPM based on a 1-second sampling window
// (Assuming this loop runs roughly every 1000ms, or adapt with millis())
static uint32_t lastCalcTime = 0;
uint32_t currentTime = millis();
if (currentTime - lastCalcTime >= 1000) {
lastCalcTime = currentTime;
// Stall Detection / Error Handling
uint32_t timeSinceLastPulse = currentTime - (localLastPulse / 1000);
if (localCount == 0 && timeSinceLastPulse > 2000) {
Serial.println("STATUS: Motor stalled or sensor disconnected.");
} else if (localCount > 0) {
// RPM = (Pulses / PulsesPerRev) * 60
float rpm = (float)localCount / PULSES_PER_REV * 60.0;
Serial.print("RPM: ");
Serial.println(rpm, 1);
}
}
// Simulate heavy main-loop blocking task to prove ISR independence
delay(250);
}
Debugging Interrupt Failures: The First Three Things to Check
When your interrupt-driven circuit behaves erratically, freezes, or fails to compile, follow this ranked diagnostic path.
1. Compiler Error: 'digitalPinToInterrupt' was not declared in this scope
The Cause: You are attempting to use a pin that does not support hardware interrupts on your specific board variant, or you are using an outdated core. On the Uno R3, passing 4 or A0 into digitalPinToInterrupt() will throw this exact error because those pins lack dedicated external interrupt vectors (INT0/INT1).
The Fix: Verify your pin against the board's datasheet. For the Uno R3, strictly use 2 or 3. If you need more pins, you must switch to Pin Change Interrupts (PCINT) using a library like EnableInterrupt, or upgrade to an ESP32 DevKit V1 where Espressif's GPIO matrix allows routing almost any pin to an interrupt.
2. Erratic Counts or System Freezes (The 'Volatile' Bug)
The Cause: You omitted the volatile keyword on pulseCount. Without it, the GCC compiler optimizes the main loop by caching the variable in a CPU register, completely ignoring the updates made by the ISR. Alternatively, you used delay() or Serial.print() inside the ISR, which relies on interrupts to function, causing a deadlock.
The Fix: Audit every variable touched by the ISR. If the main loop reads it, it must be declared volatile. Strip all function calls out of the ISR except for microsecond timing and basic arithmetic.
3. Ghost Triggering and Multiplied RPM Readings
The Cause: Switch bounce or electromagnetic interference (EMI). Mechanical switches and some hall sensors exhibit microsecond-level signal ringing when transitioning states. A single magnet pass might trigger the FALLING edge interrupt 4 or 5 times.
The Fix: Implement a software debounce mask inside the ISR using micros().
void magnetDetectISR() {
static uint32_t lastISRTime = 0;
uint32_t currentTime = micros();
// Ignore pulses that arrive within 2000µs (2ms) of each other
if (currentTime - lastISRTime > 2000) {
pulseCount++;
lastISRTime = currentTime;
}
}
Extending and Simplifying the Build
To Simplify: If you are only measuring slow-moving mechanisms (like a wind-speed anemometer spinning at 1Hz), strip out the interrupts entirely. Use a standard digitalRead() polling loop with a 10ms delay(). This removes the complexity of volatile variables and critical sections, making the code much easier for beginners to debug.
To Extend: To measure rotational direction (clockwise vs. counter-clockwise), replace the single A3144 sensor with a KY-040 Rotary Encoder or a dual-channel quadrature encoder. You will wire Channel A to D2 (INT0) and Channel B to D3 (INT1). Inside the Channel A ISR, read the state of Channel B; if B is HIGH, the shaft is moving forward; if B is LOW, it is moving in reverse. This requires both of the Uno R3's hardware interrupt pins and is the standard method for CNC machine position tracking.
Frequently Asked Questions About Arduino Interrupt Pins
Can I use analog pins as interrupt pins on the Arduino Uno?
No, not for dedicated hardware interrupts. The ATmega328P only maps INT0 to Digital 2 and INT1 to Digital 3. However, the analog pins (A0-A5) share physical ports with the digital pins and do support Pin Change Interrupts (PCINT). PCINTs are more complex to configure because they trigger on any state change (both rising and falling) and group multiple pins into a single interrupt vector, requiring you to manually check which pin actually changed state inside the ISR.
What is the difference between hardware interrupts and pin change interrupts (PCINT)?
Hardware interrupts (External Interrupts) have dedicated vectors for specific pins, can be configured to trigger on specific edges (RISING, FALLING, CHANGE, LOW), and have the highest priority in the AVR architecture. Pin Change Interrupts group up to 8 pins per port (e.g., PORTB, PORTC, PORTD). When any pin in that group changes state, a single shared ISR fires. PCINTs are useful when you run out of dedicated hardware pins, but they require more CPU cycles to decode the source of the trigger and cannot natively filter for just a RISING or FALLING edge without software logic.
Why does my I2C OLED display freeze when an interrupt triggers?
I2C communication relies on its own internal interrupts and precise timing to clock data over the SDA/SCL lines. If your custom hardware interrupt ISR takes too long to execute (typically >50µs), or if you attempt to use Wire.beginTransmission() inside the ISR, you will collide with the I2C state machine, causing the bus to lock up and the display to freeze. Always defer I2C display updates to the main loop() by setting a simple volatile boolean displayNeedsUpdate = true; flag inside the ISR, and let the main loop handle the actual rendering.






