The Decision Path: Polling vs. Arduino Pin Interrupt

When building embedded systems that read digital pulses—like anemometers, flow meters, or rotary encoders—the first architectural decision is whether to poll the pin in the main loop or delegate it to a hardware pin interrupt. Polling is simpler, but it silently drops pulses the moment your main loop gets bogged down by display updates, serial printing, or network requests.

Use this decision tree to select the right approach for your specific sensor frequency and board constraints. This path terminates in a concrete recommendation for standard 8-bit AVR builds.

Signal FrequencyMain Loop LoadRecommended ApproachConcrete Pick
< 100 HzLight (no blocking delays)Standard Polling (digitalRead())Use standard polling; interrupts add unnecessary overhead.
100 Hz - 5 kHzModerate (LCD, basic logic)External Hardware Interrupt (INT0/INT1)Use attachInterrupt() on Pin 2 or 3. (Default pick for YF-S201 flow sensors).
5 kHz - 20 kHzHeavy (WiFi, SD logging)Pin Change Interrupts (PCINT) or Hardware CounterUse the PinChangeInterrupt library, or switch to an ESP32.
> 20 kHzAnyDedicated Hardware Counter ICUse a PCF8583 or switch to a 32-bit MCU with dedicated pulse counters.
Bench Insight: The YF-S201 water flow sensor maxes out around 450 Hz at 120 L/min. While 450 Hz is theoretically slow enough to poll, adding a 16x2 I2C LCD update (which takes ~5ms per refresh) to your main loop will cause you to miss up to 40% of your pulses. For any fluid metering project, always default to a pin interrupt.

Hardware Selection and ATmega328P Pin Mapping

This guide targets the Arduino Nano V3 (ATmega328P, 16MHz). The ATmega328P has only two dedicated External Interrupt pins: INT0 (Digital Pin 2) and INT1 (Digital Pin 3). If you wire your sensor to Pin 4 and try to use attachInterrupt(), it will fail silently at runtime.

Parts List

  • MCU: Arduino Nano V3 (ATmega328P, 16MHz crystal variant)
  • Sensor: YF-S201 Hall Effect Water Flow Sensor (1/2' NPT threads)
  • Pull-up Resistor: 10kΩ (The YF-S201 has an open-collector NPN output; it requires a pull-up to VCC)
  • Debounce Capacitor: 0.1µF (104) ceramic capacitor (placed between Signal and GND to filter contact bounce)
  • Wiring: 22 AWG stranded silicone wire

Pin Mapping Table

Arduino Nano PinATmega328P Hardware FunctionYF-S201 Sensor WireNotes
D2INT0 (External Interrupt 0)Yellow (Signal)Must use Pin 2 or 3 for external interrupts.
5VVCC OutputRed (VCC)Sensor operates 4.5V to 18V.
GNDGroundBlack (GND)Shared ground with Nano and capacitor.

Step-by-Step Wiring and Compilable ISR Code

Follow these steps to wire the circuit and deploy the Interrupt Service Routine (ISR). The code below includes atomic read blocks to prevent variable tearing—a critical failure mode on 8-bit AVRs that most basic tutorials miss.

  1. Wire Power: Connect the sensor Red wire to Nano 5V, and Black wire to Nano GND.
  2. Install Hardware Filter: Connect the 10kΩ resistor between Nano D2 and 5V. Connect the 0.1µF capacitor between Nano D2 and GND. This RC network eliminates the 2-5µs contact bounce inherent in the sensor's internal Hall switch.
  3. Wire Signal: Connect the sensor Yellow wire to Nano D2.
  4. Upload Code: Flash the following sketch to your Nano.
/*
 * High-Speed Flow Sensor Pulse Counter
 * Target Board: Arduino Nano V3 (ATmega328P, 16MHz)
 * Sensor: YF-S201 Water Flow Sensor
 */

// Pin Definitions
const int FLOW_SENSOR_PIN = 2; // Must be INT0 (Pin 2) on Nano
const int STATUS_LED_PIN = 13;

// Volatile variables for ISR communication
volatile unsigned long pulseCount = 0;
volatile unsigned long lastPulseTime = 0;

// Flow calculation constants (YF-S201: ~4.5mL per pulse)
const float CALIBRATION_FACTOR = 4.5; 
const unsigned long DEBOUNCE_MICROS = 1000; // 1ms software debounce backup

// The Interrupt Service Routine (ISR)
void pulseISR() {
  unsigned long currentTime = micros();
  // Software debounce check (hardware RC filter does the heavy lifting)
  if ((currentTime - lastPulseTime) > DEBOUNCE_MICROS) {
    pulseCount++;
    lastPulseTime = currentTime;
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(FLOW_SENSOR_PIN, INPUT_PULLUP); // Enable internal pull-up as backup
  pinMode(STATUS_LED_PIN, OUTPUT);

  // Attach interrupt: Trigger on FALLING edge for open-collector Hall sensor
  attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN), pulseISR, FALLING);
  Serial.println("Flow sensor initialized. Monitoring pulses...");
}

void loop() {
  // CRITICAL: Atomic read block to prevent 32-bit variable tearing on 8-bit AVR
  unsigned long currentCount;
  noInterrupts();          // Disable interrupts temporarily
  currentCount = pulseCount; // Copy the volatile variable
  interrupts();            // Re-enable interrupts immediately

  // Calculate flow rate (Liters per minute)
  // This is a simplified calculation; production code should track time deltas
  static unsigned long lastPrintTime = 0;
  if (millis() - lastPrintTime >= 1000) {
    lastPrintTime = millis();
    
    float totalLiters = (currentCount * CALIBRATION_FACTOR) / 1000.0;
    Serial.print("Total Pulses: ");
    Serial.print(currentCount);
    Serial.print(" | Volume: ");
    Serial.print(totalLiters, 2);
    Serial.println(" L");
    
    digitalWrite(STATUS_LED_PIN, !digitalRead(STATUS_LED_PIN));
  }
}

Debugging: First 3 Checks and Exact Error Strings

When your interrupt fails, the main loop usually runs perfectly fine while the pulse count stays at zero. Here is the exact decision path to diagnose the failure, ranked from most common to most obscure.

The First 3 Things to Check

  1. Pin Capability: Did you wire the sensor to Pin 4, 5, or 6? On the Nano/Uno, attachInterrupt() only works on Pins 2 and 3. Move the signal wire to Pin 2.
  2. Variable Tearing (Count Freezes/Drops): If your count randomly drops or freezes at high RPM, you are reading a 32-bit unsigned long on an 8-bit CPU without an atomic block. The ISR updates the upper bytes while the main loop reads the lower bytes. You must wrap the variable copy in noInterrupts() and interrupts() as shown in the code above.
  3. Missing Pull-up: The YF-S201 output transistor pulls the line to ground, but it cannot drive it HIGH. If you forgot the 10kΩ pull-up resistor (and disabled INPUT_PULLUP), the pin will float, causing erratic counts or none at all.

Exact Compiler Error Strings

If your code fails to compile, check for these exact error strings:

Error: error: 'pulseISR' was not declared in this scope
Cause: You defined the ISR function after the setup() function, or you misspelled the function name in the attachInterrupt() call.
Fix: Ensure the ISR function is defined above setup(), or add a function prototype at the top of the sketch.
Error: error: 'digitalPinToInterrupt' was not declared in this scope
Cause: You are using a very old third-party core (pre-1.5.0) or a non-standard ATTiny board definition that lacks this macro.
Fix: Update your board manager to the official Arduino AVR Boards package, or replace the macro with the raw integer 0 (since Pin 2 maps to INT0 on the ATmega328P).

Extending the Build: External vs. Pin Change Interrupts

The standard attachInterrupt() function uses the ATmega328P's External Interrupts (INT0 and INT1). These are fast, support specific edge triggering (RISING, FALLING, CHANGE), and are ideal for 90% of hobbyist sensor projects. However, you only get two of them.

If your project requires reading three rotary encoders (6 pins) or multiple flow meters, you will run out of INT pins. Here is how to extend or simplify the build based on your exact constraints:

When to Extend: Pin Change Interrupts (PCINT)

The ATmega328P has 23 pins capable of Pin Change Interrupts. Unlike INT0/INT1, PCINTs do not support edge detection in hardware—they fire on any logic change (both rising and falling edges), and you must read the pin state in software to determine what happened. Furthermore, PCINTs are grouped into three ports (PORTB, PORTC, PORTD); an interrupt on any pin in PORTD fires the same ISR vector.

  • How to implement: Do not write raw PCINT registers unless you are optimizing for flash size. Use the PinChangeInterrupt library by NicoHood. It abstracts the port masking and provides a clean attachPCINT() API.
  • Trade-off: PCINTs have slightly higher latency and require more CPU cycles to decode which pin actually changed state. They are not suitable for signals above 5 kHz.

When to Simplify: Hardware Counter ICs

If you are building an industrial-grade flow meter or a high-RPM tachometer where the signal exceeds 10 kHz, offload the counting entirely. Use a dedicated I2C hardware counter like the PCF8583. The PCF8583 can count up to 32,767 pulses completely independently of the Arduino, requiring zero interrupts and zero main-loop overhead. You simply query the I2C bus once per second to read the accumulated register.

The Final Recommendation

For standard fluid metering, anemometers, and single rotary encoders under 5 kHz, stick to the external pin interrupt on Pin 2 or 3. It offers the best balance of hardware edge-detection reliability and code simplicity. Reserve PCINTs for low-speed UI buttons, and reserve hardware counters for high-frequency industrial telemetry. For further reading on AVR interrupt mechanics, consult the ATmega328P Datasheet (Section 11: Interrupts) and the definitive Nick Gammon Interrupt Guide.