Architecting High-Frequency Pulse Counting with Arduino External Interrupts
Learn how to implement high-frequency pulse counting on Arduino using external interrupts, volatile variables, and atomic access to prevent data corruption and pulse loss.
18 May 2026, 14:24 UTC

The Problem: Pulse Loss in Polled Loops
When counting high-frequency pulses—such as those from a rotary encoder or a flow sensor—using digitalRead() in a standard loop() often leads to missed events. Because the CPU must execute other instructions between checks, any pulse that rises and falls while the processor is busy is lost forever. The solution is an Interrupt Service Routine (ISR), which forces the CPU to pause its current task and execute a specific function immediately upon a hardware trigger.
Requirements for Precise Counting
- Low Latency: The system must respond to a signal edge within a few clock cycles.
- Non-Blocking Execution: The counting mechanism cannot halt the rest of the application logic.
- Data Integrity: The count must remain accurate even when the main loop is reading the value for calculations.
The Smallest Suitable Design
For AVR-based boards like the Arduino Uno or Nano, the most efficient design utilizes the hardware external interrupt pins (Digital Pin 2 for INT0 and Digital Pin 3 for INT1). This offloads the signal detection to the hardware, removing the need for software polling.
// Required for 8-bit AVR architectures to prevent compiler optimization
volatile unsigned long pulseCount = 0;
void setup() {
pinMode(2, INPUT_PULLUP);
// Trigger ISR on the falling edge of the signal
attachInterrupt(digitalPinToInterrupt(2), countPulse, FALLING);
Serial.begin(9600);
}
void loop() {
unsigned long currentCount = 0;
// Atomic block: disable interrupts while reading multi-byte variables
noInterrupts();
currentCount = pulseCount;
interrupts();
Serial.println(currentCount);
delay(1000);
}
void countPulse() {
pulseCount++; // Minimal logic to reduce CPU overhead
}
Trust and Data Boundaries
The ISR operates in a privileged hardware context. This creates a boundary between the Asynchronous Context (the ISR) and the Synchronous Context (the main loop). To manage this, two rules are mandatory:
- The
volatileKeyword: Variables shared between the ISR and the loop must be declaredvolatile. This tells the compiler that the variable can change unexpectedly, preventing it from caching the value in a register. - Atomic Access: On 8-bit AVRs, an
unsigned long(4 bytes) requires multiple clock cycles to read. If an interrupt occurs halfway through the read, the resulting value will be corrupted. Wrapping the read innoInterrupts()andinterrupts()ensures the read is atomic.
Operational Checks and Failure Modes
Incorrect ISR implementation can lead to system instability or inaccurate data. Consider these failure modes:
| Failure Mode | Cause | Mitigation |
|---|---|---|
| Bounce Triggers | Mechanical switch noise causing multiple interrupts per press. | Use a hardware RC filter (10kΩ resistor / 100nF capacitor). |
| Main Loop Starvation | ISR execution time exceeds the interval between pulses. | Keep ISRs extremely short; move calculations to the loop. |
| System Hang | Using delay() or Serial.print() inside the ISR. |
Never use functions that rely on interrupts within an ISR. |
Verification and Testing
To verify the implementation, connect the interrupt pin to a known signal generator. Compare the pulseCount against the generator's frequency over a 10-second window. If the count is consistently lower than the expected value, the CPU is likely overwhelmed by the interrupt overhead.
Diagnostic Check: To confirm the ISR is triggering, temporarily add a line to toggle the onboard LED (Pin 13) inside the ISR. If the LED flickers or stays dimly lit, the hardware trigger is functioning.
Conditions for Design Change
The external interrupt approach is suitable for frequencies up to a few kilohertz. However, you must transition to a Hardware Timer/Counter peripheral if:
- The pulse frequency exceeds the CPU's ability to enter and exit the ISR (typically above 10-20kHz for complex loops).
- The application requires nanosecond-precision timestamps for each pulse.
- The CPU load becomes too high, causing other time-sensitive tasks to fail.
Rollback: To revert to polling, remove the attachInterrupt() call and move the counting logic back into a digitalRead() conditional within the loop().
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.