To build a reliable pulse detector Arduino setup for reading spinning utility meters, tachometers, or encoders, use a TCRT5000 IR reflective sensor paired with an Arduino Nano V3. By routing the sensor's digital output through a hardware interrupt (INT0) and adding a 0.1µF ceramic capacitor for hardware debouncing, you eliminate false triggers from contact bounce and ambient light, achieving accurate counts even at high RPMs.
Component Selection & Sensor Specifications
Most beginner tutorials rely entirely on software debouncing (using delay() or millis() checks inside the loop). This fails miserably on fast-spinning targets because polling blocks the microcontroller, and software debounce windows often swallow legitimate high-speed pulses. The correct approach is hardware debouncing combined with interrupt service routines (ISRs).
Required Parts List
- Microcontroller: Arduino Nano V3 (ATmega328P, 16MHz, 5V logic). Do not use the Nano 33 IoT or Nano ESP32 for this specific 5V code without level shifting.
- Sensor Module: TCRT5000 Reflective Optical Sensor Module (must include the LM393 comparator chip for digital output).
- Hardware Debounce: 0.1µF (100nF) ceramic capacitor (X7R dielectric).
- Pull-up Resistor: 10kΩ through-hole resistor (optional if using internal pull-ups, but external is cleaner for noisy environments).
- Target Marker: 3M Scotchlite reflective tape or white electrical tape.
The TCRT5000 pairs a 950nm infrared emitting diode with a phototransistor. The onboard LM393 comparator converts the analog phototransistor current into a clean 5V/0V digital square wave. Below are the critical operating parameters you need to design your circuit around, sourced directly from the Vishay TCRT5000 datasheet.
| Parameter | Symbol | Min | Typ | Max | Unit |
|---|---|---|---|---|---|
| Forward Voltage (IR Emitter) | V_F | - | 1.25 | 1.50 | V |
| Peak Wavelength | λ_p | - | 950 | - | nm |
| Collector Dark Current | I_CEO | - | - | 200 | nA |
| Collector Light Current (Target at 5mm) | I_C | 0.1 | 0.5 | 1.0 | mA |
| Rise / Fall Time | t_r / t_f | - | 10 | - | µs |
| Optimal Sensing Distance | d | 1 | 2.5 | 25 | mm |
Wiring Diagram & Pin Mapping
The physical wiring of a pulse detector is where most noise issues originate. Long wires act as antennas for 50/60Hz mains hum. Keep the leads between the TCRT5000 module and the Arduino under 15cm (6 inches).
| TCRT5000 Module Pin | Arduino Nano Pin | Wire Color (Std) | Notes & Requirements |
|---|---|---|---|
| VCC | 5V | Red | Requires stable 5V; do not use 3.3V. |
| GND | GND | Black | Common ground with Nano. |
| DO (Digital Out) | D2 (INT0) | Yellow | Must be D2 or D3 for hardware interrupts on ATmega328P. |
| AO (Analog Out) | Not Connected | - | Leave floating; we only use the LM393 digital output. |
Step-by-Step Wiring & Hardware Debounce
- Power the Module: Connect the module VCC to the Nano's 5V pin and GND to GND. The power LED on the module should illuminate.
- Route the Signal: Connect the DO pin to Arduino Digital Pin 2 (D2). This pin maps to hardware interrupt INT0 on the ATmega328P.
- Apply Hardware Debounce: Solder or clip the 0.1µF ceramic capacitor directly across the DO pin and the GND pin on the sensor module itself. This creates a low-pass RC filter (combined with the module's internal pull-up) that physically shunts high-frequency contact bounce and EMI spikes to ground before they ever reach the microcontroller.
- Set the Threshold: Turn the blue trimpot (potentiometer) on the module with a small Phillips screwdriver. Point the sensor at your target material. Adjust the pot until the module's output LED toggles cleanly when the reflective tape passes, but stays off when looking at the dark background.
Interrupt-Driven Pulse Detection Code
This firmware targets the Arduino Nano V3 (ATmega328P, 5V/16MHz). It uses attachInterrupt() to catch pulses without blocking the main loop. We implement a secondary software debounce inside the ISR using micros() to guarantee we don't double-count a single passing marker, even if the hardware capacitor is slightly undersized.
/*
* Optical Pulse Detector Arduino - Tachometer / Meter Reader
* Target Board: Arduino Nano V3 (ATmega328P, 16MHz, 5V)
* Sensor: TCRT5000 with LM393 Comparator
*/
// Pin Definitions
const uint8_t SENSOR_PIN = 2; // INT0 hardware interrupt pin
const uint8_t STATUS_LED = 13; // Onboard Nano LED
// Volatile variables modified inside ISR
volatile unsigned long pulseCount = 0;
volatile unsigned long lastPulseMicros = 0;
volatile bool isrOverflowError = false;
// Debounce window in microseconds (2000µs = 2ms = max 500Hz / 30,000 RPM)
const unsigned long DEBOUNCE_WINDOW_US = 2000;
// Main loop timing
unsigned long lastPrintMillis = 0;
const unsigned long PRINT_INTERVAL_MS = 1000;
void setup() {
Serial.begin(115200);
while (!Serial) { ; } // Wait for serial port (native USB boards, safe on Nano)
pinMode(SENSOR_PIN, INPUT_PULLUP);
pinMode(STATUS_LED, OUTPUT);
// Attach interrupt on FALLING edge (LM393 pulls low when target detected)
attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), pulseISR, FALLING);
Serial.println(F("Pulse Detector Initialized. Waiting for target..."));
}
void pulseISR() {
unsigned long currentMicros = micros();
// Software debounce fallback
if (currentMicros - lastPulseMicros >= DEBOUNCE_WINDOW_US) {
pulseCount++;
lastPulseMicros = currentMicros;
// Basic overflow protection for 32-bit unsigned long
if (pulseCount == 0) {
isrOverflowError = true;
}
}
}
void loop() {
unsigned long currentMillis = millis();
// Non-blocking serial output every 1 second
if (currentMillis - lastPrintMillis >= PRINT_INTERVAL_MS) {
lastPrintMillis = currentMillis;
// Critical section: disable interrupts briefly to read 32-bit volatile safely
noInterrupts();
unsigned long safeCount = pulseCount;
bool safeError = isrOverflowError;
interrupts();
if (safeError) {
Serial.println(F("ERR: ISR Overflow - Pulse count exceeded 4.29 billion."));
isrOverflowError = false; // Reset flag
} else {
Serial.print(F("Total Pulses: "));
Serial.println(safeCount);
}
// Blink LED to show main loop is alive
digitalWrite(STATUS_LED, !digitalRead(STATUS_LED));
}
}
Debugging: First Three Checks & Common Failures
When your serial monitor shows Total Pulses: 0 despite a spinning target, do not immediately rewrite your code. Hardware and optical physics are usually the culprits. Here are the first three things to check when it fails:
- Verify the LM393 Threshold Potentiometer: If the trimpot is turned too far, the comparator will either latch HIGH (ignoring the target) or latch LOW (triggering continuously). Use a multimeter on the DO pin; it should read ~5V at rest and drop to ~0.2V when you place reflective tape in front of it.
- Confirm Interrupt Pin Mapping: The code uses
digitalPinToInterrupt(SENSOR_PIN). On the Nano V3, D2 is INT0 and D3 is INT1. If you accidentally wired the sensor to D4, the hardware interrupt will never fire, and the count will remain zero. - Inspect Ambient IR Saturation: Sunlight and incandescent bulbs emit massive amounts of 950nm IR light. If the sensor is flooded with ambient IR, the phototransistor saturates, and the LM393 cannot detect the small delta from your reflective tape. Shield the sensor with a piece of heat-shrink tubing or black electrical tape to block peripheral light.
Ranked Causes for Specific Serial Errors
| Exact Serial Output / Symptom | Root Cause | Fix / Measurement Threshold |
|---|---|---|
Serial: No state change on D2 (Count stuck at 0) |
Sensor wired to non-interrupt pin, or LM393 pot misadjusted. | Move wire to D2. Adjust pot until DO pin measures < 0.5V on reflective target. |
| Count increments by 2 or 3 per single pass | Missing hardware debounce capacitor; software window too short. | Install 0.1µF cap across DO-GND. Increase DEBOUNCE_WINDOW_US to 5000. |
ERR: ISR Overflow |
Variable rolled over, or severe EMI causing thousands of false triggers per second. | Check for 50/60Hz mains coupling. Route sensor cable away from AC lines. Read < 1 ohm across sensor GND to Nano GND. |
| Count drops at high RPM (>5000) | Target marker is too small; sensor rise/fall time is too slow. | Widen the reflective tape. Ensure distance is < 10mm (optimal per datasheet). |
Scaling the Build: Extend or Simplify
Depending on your end goal, you may need to alter the architecture of this pulse detector. Here is how to adapt the build for different use cases.
How to Simplify the Build
If you are only reading a slow-moving utility meter (e.g., 1 pulse per second) and don't care about high-speed tachometry, drop the interrupt entirely. Replace the ISR with a simple polling loop using digitalRead() and a state-change flag. This frees up the hardware interrupt lines for other peripherals and makes the code easier to read for beginners. However, you must poll at least 10x faster than your expected pulse rate (e.g., every 50ms) to avoid missing the pulse window.
How to Extend the Build
For advanced home automation or industrial logging, migrate from the Arduino Nano to an ESP32 DevKit V1. The ESP32 features a dedicated hardware Pulse Counter (PCNT) peripheral. Unlike the ATmega328P, which requires the CPU to wake up and execute an ISR for every single pulse, the ESP32's PCNT module counts pulses entirely in the background hardware, even while the chip is in deep sleep. To extend this project:
- Use the ESP32's
pcnt.hlibrary to handle counting. - Add an MQTT client (using the PubSubClient library) to push the pulse count to Home Assistant every 60 seconds.
- Power the ESP32 with a 3.7V 18650 Li-ion cell and a TP4056 charging module, utilizing
esp_deep_sleep_start()between reads to achieve months of battery life on a single cell.
By mastering the intersection of optical hardware tuning and interrupt-driven firmware, you transform a $2 sensor module into a precision measurement tool capable of handling everything from smart-metering to motor control feedback.






