IR counter: one arrival, one event

Connect a reflective IR module, filter its digital output and count new detections only after an accepted clear state.

Sensors and measurement displaysBeginner75 min

What you will learn

  • Connect a 3.3 V reflective IR module to GPIO27 and an indicator LED to GPIO25 through 220 Ω.
  • Verify the module's active level and configure DETECTED_LEVEL.
  • Explain 50 ms qualification for both detection and clearance.
  • Count only a new accepted transition after clearance, with no repeats while held.
  • Test startup with an object present and explain the limits of digital detection counts.

Before you start

Complete courses 03 and 07: button events, digital sensor outputs, polarity and time-based stability checks.

Reference setup

Board in Arduino IDE
ESP32 Dev Module
Arduino-ESP32
3.3.12
Serial Monitor
115200 baud

This is a reference profile, not an identification of the pictured board. Adapt the GPIO mapping and verify the circuit before using ESP32-C3/S2/S3 or a differently labelled board.

What we will build

Build an infrared detection counter. Introducing an object adds one; keeping it in place adds no more counts. An LED shows accepted detection and Serial Monitor displays the count.

We now recognise events as well as states. Testing reset with an object already present checks that startup creates no imaginary arrival.

Learning objectives

  • Connect a reflective IR module and verify its active output level.
  • Accept a changed signal after 50 ms of stability.
  • Count only transitions from a clear area to a detected object.
  • Explain why an accepted clear state is required before another count.
  • Distinguish detection counts from distance and numbers of unique objects.

Prerequisites

Complete courses 03 and 07: button events, digital-sensor polarity and input stability. A sensor replaces the button, while an event count replaces toggling the LED.

Equipment

ComponentQuantitySpecification and notes
Development board with a classic ESP32-WROOM-32 module1Match the reference GPIO labels; check physical header positions on the actual board.
USB data and power cable1Use the connector fitted to your board; the pictured kit lists Micro-USB.
Solderless breadboard1The kit lists 830 tie points. Check whether the power rails are split.
Infrared obstacle detection module1Reflective IR module verified for 3.3 V; VCC/GND/OUT. Reference OUT is active LOW.
Red LED1Anode A and cathode K; identify polarity on the actual part.
220 Ω resistor1One current-limiting resistor per LED branch; use the kit’s 220 Ω resistors.
Jumper wires6Use male-to-male or female-to-male leads to suit the board headers.

Approximate quantity, depending on layout and connector types.

Prepare cardboard for repeatable experiments. Use a reflective IR module documented for 3.3 V operation with VCC, GND and OUT terminals. Verify its model and terminal labels separately. The reference board is a classic ESP32/WROOM-32; other ESP32 families require adaptation.

What the sensor actually reports

An emitter produces infrared light and a receiver responds to its reflection. The SunFounder reference module supports 3.3–5 V and outputs HIGH without detection and LOW when an object is detected. Its trimmer changes the triggering conditions. The sketch therefore starts with DETECTED_LEVEL = LOW.

This output is digital: the detection condition is either satisfied or not. It cannot provide a distance in centimetres. Each increment also does not necessarily mean a different object: removing and returning the same cardboard produces another event. Two objects with no clear interval between them produce one continuous detection.

Reflection also depends on the surface. SunFounder's reflective IR module documentation describes reduced useful detection range for darker objects. We will compare objects instead of treating a trimmer position as a universal distance setting.

Wiring and assembly

Wiring diagram

The reference IR module works at 3.3 V with an active-LOW OUT signal. Verify the actual module polarity. This detects reflected infrared light; it does not measure distance.

Wiring diagram — electrical connections listed in the table below
Wiring diagramEnlargeSVGPNG
Connections · Wiring diagram
FromToConnection
ESP32.3V3U1.VCCModule supply; identify the VCC terminal.
ESP32.GNDU1.GNDCommon ground for the board and sensor.
U1.OUTESP32.GPIO273.3 V logic signal to GPIO27.
ESP32.GPIO25R1.1Digital output for the LED.
R1.2D1.AR1, 220 Ω, in series with the LED anode.
D1.KESP32.GNDCathode to common ground.
  1. Disconnect USB and find the ESP32's 3V3, GND, GPIO27 and GPIO25 labels.
  2. Connect IR module VCC to 3V3 and GND to common ground.
  3. Connect module OUT to GPIO27. Follow your own module's labels rather than the terminal order in another photograph.
  4. Connect GPIO25 through R1, a 220 Ω resistor, to D1's anode; connect the cathode to GND.
  5. Point the emitter and receiver towards clear space. Check the wiring before reconnecting USB.

GPIO27 uses INPUT_PULLUP, documented in the Espressif GPIO reference. Powering this module from 3V3 keeps the experiment in the 3.3 V domain; do not connect an output from a 5 V powered module directly to a GPIO.

Arduino environment

In Arduino IDE 2.x, select ESP32 Dev Module, the board's port and esp32 by Espressif Systems 3.3.12. The Espressif installation guide explains setup. No additional libraries are required.

Extract the Arduino example and open ir_counter/ir_counter.ino. Keep its folder named ir_counter. Run Verify, then Upload, and open Serial Monitor at 115200 baud. Remove the cardboard from the module's view for the initial test.

Complete Arduino sketch

ir_counter.ino Arduino / C++
Download .ino

#include <Arduino.h>

// Reflective obstacle module: 3.3 V supply and 3.3 V-compatible OUT.
constexpr uint8_t INPUT_PIN = 27;
constexpr uint8_t LED_PIN = 25;
constexpr int DETECTED_LEVEL = LOW;  // Change only after checking your module.
constexpr uint32_t FILTER_MS = 50;
constexpr uint32_t REPORT_MS = 500;

int candidateLevel = HIGH;
uint32_t candidateSince = 0;
uint32_t lastReportAt = 0;
uint32_t detectionCount = 0;
bool acceptedKnown = false;
bool acceptedDetected = false;
bool armed = false;

void setup() {
  Serial.begin(115200);
  pinMode(INPUT_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  candidateLevel = digitalRead(INPUT_PIN);
  candidateSince = millis();
  lastReportAt = candidateSince;
  Serial.println("IR counter: first qualify CLEAR, then each new detection counts once.");
}

void loop() {
  const uint32_t now = millis();
  const int raw = digitalRead(INPUT_PIN);
  if (raw != candidateLevel) {
    candidateLevel = raw;
    candidateSince = now;
  }
  if (static_cast<uint32_t>(now - candidateSince) >= FILTER_MS) {
    const bool detected = (candidateLevel == DETECTED_LEVEL);
    if (!acceptedKnown || detected != acceptedDetected) {
      acceptedKnown = true;
      acceptedDetected = detected;
      if (!acceptedDetected) {
        armed = true;
      } else if (armed) {
        ++detectionCount;
        armed = false;
      }
    }
  }
  digitalWrite(LED_PIN, acceptedKnown && acceptedDetected ? HIGH : LOW);
  if (static_cast<uint32_t>(now - lastReportAt) >= REPORT_MS) {
    lastReportAt = now;
    Serial.print("raw="); Serial.print(raw);
    Serial.print(" detected=");
    Serial.print(!acceptedKnown ? "WAITING" : (acceptedDetected ? "YES" : "NO"));
    Serial.print(" count="); Serial.print(detectionCount);
    Serial.print(" armed="); Serial.println(armed ? "YES" : "NO");
  }
}

The displayed and downloadable sketches match. Observe their default behaviour before changing polarity.

From raw level to event

FILTER_MS requires 50 ms of stability. candidateLevel holds the candidate input and candidateSince records when it last changed. Every raw transition restarts qualification. acceptedKnown indicates that at least one state has passed initial qualification.

acceptedDetected represents accepted detection. The LED follows this state, while detectionCount stores events as a uint32_t. armed means that a clear area was previously accepted and the next detection may increment the count. Counting clears armed; another accepted clear phase sets it again.

The program does not count every pass through loop() with an active input. It increments only when a newly accepted state means detection and counting is armed. Even a ten-second hold is one event. Every 500 ms, REPORT_MS schedules raw (0/1 for LOW/HIGH), detected (YES/NO or initial WAITING), count, and armed (YES/NO).

Startup state and polarity

After reset, the count is zero, the LED is off and armed is false. If an object is already present, its qualified detection turns on the LED but leaves the count at zero. Remove it long enough for a clear state to pass the filter; the following arrival then produces the first count.

Compare the raw level with no object and with cardboard in place. If the levels do not differ, adjust the trimmer gradually and repeat the experiment. If your module's documentation and observations confirm active HIGH, set DETECTED_LEVEL = HIGH, upload again and restart the experiment. With the default active LOW, a disconnected pulled-up input can appear clear, so this report alone does not prove correct wiring.

Running the experiment

Hold each phase for two seconds. Start by resetting with no object present and waiting for an accepted clear state.

SequenceCountLED and readiness for another event
Clear area after reset0Off; armed.
Introduce the cardboard1On; awaiting clearance.
Keep the same cardboard in place1On; no additional counting.
Remove the cardboard1Off; armed again.
Introduce the cardboard again2On; awaiting clearance.

Next reset with cardboard already in front of the module. Expect zero counts and an illuminated LED after qualification. Record actual observations separately. Reset clears the count; it is not stored persistently.

Troubleshooting

SymptomCheck and action
The count stays at zeroFirst leave the area clear long enough; check OUT, polarity and threshold.
The count grows while an object is stationaryCheck whether the module actually returns to clear; stabilise position, threshold and connections.
The LED is active with no intended objectCheck DETECTED_LEVEL and whether the module sees the desk or another object.
Fast passes are missedDetection and clearance must each remain stable for at least 50 ms.
Different objects produce different resultsRepeat under the same position and lighting; different reflection changes detection.

Independent challenge

Make five slow cardboard arrivals with a clearly empty interval between them. Then hold it in place for five seconds and check that the count remains unchanged. Repeat with a darker object without moving the trimmer. Record successful detections; retain the observed result even if it differs from your prediction.

Questions and worked guidance

Why does an object present at reset not produce a count of one? No clear state has yet been accepted, so the program has no evidence of a new transition. The LED still reports the accepted detection.

Five separate qualified arrivals produce five events. Holding does not add events. If the gap between two arrivals fails qualification, they merge into one accepted detection. The count cannot establish direction, distance, object identity or numbers of people: it records only the specified transitions of one digital signal.

Downloads

Primary references