Passive Infrared (PIR) sensors are the workhorses of DIY occupancy detection, but they are notoriously noisy. A raw PIR signal will flap, trigger on HVAC drafts, or lock HIGH if the power rail sags. If you are wiring a PIR Arduino project, skipping debounce logic and hardware filtering will result in a system that turns your lights on and off randomly at 3 AM.

This guide cuts through the generic tutorials. We will select the exact module for your use case, wire it with proper pull-downs, and deploy a C++ state machine that handles sensor timeouts and electrical noise.

Target Board: This guide and code are explicitly written and tested for the Arduino Uno R3 (ATmega328P). The logic ports directly to the Nano and Mega, but if you are using a 3.3V board (like the Arduino Nano 33 IoT or ESP32), you must use a logic level shifter or a 3.3V-native sensor like the AM312 to avoid frying the GPIO pin.

The Quick Decision: Which PIR Arduino Module to Buy

Not all PIR modules are identical. The Fresnel lens geometry and the onboard BISS0001 timing capacitors dictate the sensor's behavior. Use this decision path to pick your hardware:

  • IF you need to detect room-scale motion (up to 7 meters) AND you want to physically tune the delay and sensitivity with a screwdriver → Choose HC-SR501.
  • IF you are building a wearable, a battery-powered ESP32 deep-sleep node, OR you have less than 10mm of enclosure depth → Choose AM312.
  • IF you need to detect micro-movements (like typing at a desk) through a plastic enclosure → Choose RCWL-0516 (Microwave radar, not PIR, but solves the PIR blind-spot problem).

Default Pick: For 90% of standard home automation and room-logging projects, buy the HC-SR501. It operates natively at 5V, has adjustable potentiometers, and costs roughly $1.50 per unit in multi-packs.

Hardware Spec Sheet and Pin Mapping

Before soldering, verify your module's voltage tolerance. Feeding 5V into an AM312 data pin connected to a 3.3V microcontroller will destroy the GPIO.

PIR Module Comparison Matrix
Feature HC-SR501 SR602 AM312
Operating Voltage 4.5V - 20V (5V nominal) 3.3V - 5V 2.7V - 12V
Detection Range ~7 meters (120° cone) ~3 meters (100° cone) ~3 meters (100° cone)
Output Logic High VCC (approx 4.8V) VCC (approx 3.3V) VCC (approx 3.0V)
Quiescent Current ~50 µA ~10 µA ~15 µA
Adjustable Pots Yes (Delay & Sensitivity) No (Fixed) No (Fixed)

Parts List

  • 1x HC-SR501 PIR Motion Sensor Module
  • 1x Arduino Uno R3
  • 1x 10kΩ Resistor (for GPIO pull-down)
  • 3x Male-to-Male Jumper Wires (22 AWG stranded)

Pin Mapping Table

HC-SR501 Pin Arduino Uno R3 Pin Notes
VCC 5V Do not use 3.3V; BISS0001 chip will brownout.
OUT D2 Connect 10kΩ resistor from D2 to GND.
GND GND Ensure tight connection; loose ground causes stuck-HIGH.

Step-by-Step Wiring Procedure

Safety Note: While this is a low-voltage (5V DC) build, always disconnect the USB or barrel jack power before stripping wires or modifying jumper connections to prevent shorting the 5V rail to ground, which can trip your PC's USB overcurrent protection or fry the Arduino's onboard 5V regulator.
  1. Set the Trigger Mode: Locate the 3-pin header and the small plastic jumper cap on the HC-SR501. Move the cap to the H (High) position. This enables "retriggerable" mode, meaning the output stays HIGH as long as motion is continuously detected. The L (Low) mode locks the output for a fixed time and ignores motion, which is useless for most Arduino polling loops.
  2. Tune the Potentiometers: Using a small Phillips screwdriver, turn the Delay Time pot (usually on the right) fully counter-clockwise. This sets the hardware lockout to the minimum (~0.3 seconds). We will handle timing in software. Turn the Sensitivity pot (left) to the 12 o'clock position.
  3. Install the Pull-Down Resistor: Insert one leg of the 10kΩ resistor into the Arduino's GND pin, and the other leg into Digital Pin 2. This prevents the pin from floating and reading phantom motion when the PIR is booting up.
  4. Connect Power and Data: Wire the PIR VCC to Arduino 5V, PIR GND to Arduino GND, and PIR OUT to Arduino Digital Pin 2 (sharing the node with the pull-down resistor).
  5. Mount the Sensor: Keep the PIR away from HVAC vents, direct sunlight, and the tops of fluorescent light fixtures. Heat sources and electromagnetic ballasts are the primary causes of false PIR triggers.

Bulletproof Arduino Code (With Debounce and Error Handling)

The biggest mistake makers make with PIR sensors is treating the digital output as a clean switch. PIR outputs ring, flap, and occasionally lock HIGH if the BISS0001 chip experiences a voltage sag. This code implements a software debounce filter and a watchdog timer to catch stuck sensors.

Target Board: Arduino Uno R3. No external libraries required.

/*
 * Robust PIR Motion Logger with Debounce and Error Handling
 * Target: Arduino Uno R3 (ATmega328P)
 * Sensor: HC-SR501 (Retriggerable Mode)
 */

// --- PIN DEFINITIONS ---
#define PIR_PIN 2
#define STATUS_LED_PIN LED_BUILTIN

// --- TIMING CONSTANTS (Milliseconds) ---
const unsigned long DEBOUNCE_TIME = 150;       // Ignore flaps < 150ms
const unsigned long STUCK_TIMEOUT = 600000;    // 10 minutes max continuous HIGH
const unsigned long SERIAL_BAUD = 9600;

// --- STATE VARIABLES ---
int currentPirState = LOW;
int lastPirState = LOW;
unsigned long lastDebounceTime = 0;
unsigned long motionStartTime = 0;
bool isMotionActive = false;
bool errorFlag = false;

void setup() {
  pinMode(PIR_PIN, INPUT);
  pinMode(STATUS_LED_PIN, OUTPUT);
  Serial.begin(SERIAL_BAUD);
  
  // Allow PIR sensor to calibrate its internal baseline (takes 10-30s)
  Serial.println("SYS: Calibrating PIR sensor. Stand clear for 15 seconds...");
  digitalWrite(STATUS_LED_PIN, HIGH);
  delay(15000);
  digitalWrite(STATUS_LED_PIN, LOW);
  Serial.println("SYS: Calibration complete. Monitoring motion.");
}

void loop() {
  int reading = digitalRead(PIR_PIN);

  // --- DEBOUNCE LOGIC ---
  if (reading != lastPirState) {
    lastDebounceTime = millis();
  }

  if ((millis() - lastDebounceTime) > DEBOUNCE_TIME) {
    if (reading != currentPirState) {
      currentPirState = reading;

      if (currentPirState == HIGH) {
        // Motion detected
        isMotionActive = true;
        motionStartTime = millis();
        digitalWrite(STATUS_LED_PIN, HIGH);
        Serial.println("EVT: MOTION_DETECTED");
      } else {
        // Motion cleared
        isMotionActive = false;
        digitalWrite(STATUS_LED_PIN, LOW);
        Serial.println("EVT: MOTION_CLEARED");
      }
    }
  }

  // --- ERROR HANDLING: STUCK SENSOR ---
  if (isMotionActive && !errorFlag) {
    if ((millis() - motionStartTime) > STUCK_TIMEOUT) {
      errorFlag = true;
      Serial.println("ERR: PIR_STUCK_HIGH_TIMEOUT - Check ground wire or BISS0001 power rail.");
      // Blink LED rapidly to indicate hardware fault
    }
  }

  // Reset error flag if sensor recovers
  if (!isMotionActive && errorFlag) {
    errorFlag = false;
    Serial.println("SYS: Sensor recovered from stuck state.");
  }

  // Error LED indicator (rapid blink if stuck)
  if (errorFlag) {
    digitalWrite(STATUS_LED_PIN, (millis() / 100) % 2);
  }

  lastPirState = reading;
  delay(10); // Small yield for stability
}

Debugging: First 3 Things to Check When It Fails

When your serial monitor starts throwing errors or the sensor refuses to trigger, follow this ranked troubleshooting path. Do not swap the sensor until you have checked these three physical layer issues.

1. Symptom: Serial prints ERR: PIR_STUCK_HIGH_TIMEOUT

What it means: The OUT pin has been HIGH for over 10 minutes without dropping. The software watchdog caught it.

  • Cause A (Most Likely): Missing or loose GND connection. The BISS0001 chip loses its reference ground and the output transistor saturates. Fix: Reseat the GND jumper and measure continuity from the PIR GND pin to the Arduino GND pin (should be < 1 ohm).
  • Cause B: The Delay Time potentiometer is turned fully clockwise. Fix: Turn it fully counter-clockwise to rely on software timing.
  • Cause C: Power supply brownout. If powering the Arduino via a weak USB hub, the 5V rail might be sagging to 4.2V under load, confusing the PIR's internal voltage regulator. Fix: Use a dedicated 5V 2A wall adapter.

2. Symptom: Serial prints rapid EVT: MOTION_DETECTED / EVT: MOTION_CLEARED flapping

What it means: The sensor is triggering every few seconds even in an empty room.

  • Cause A (Most Likely): Environmental thermal noise. HVAC vents, sunlight moving across the floor, or a nearby PC exhaust fan are hitting the Fresnel lens. Fix: Relocate the sensor or tape over the bottom facets of the lens.
  • Cause B: Electromagnetic interference (EMI). Fluorescent light ballasts or unshielded AC wiring within 6 inches of the PIR data wire. Fix: Route the data wire away from AC mains and add a 0.1µF ceramic capacitor across the PIR VCC and GND pins.

3. Symptom: Sensor never triggers (No serial output)

What it means: The OUT pin is permanently LOW.

  • Cause A (Most Likely): The jumper cap is set to "L" (Non-retriggerable) and the hardware lockout timer is stuck, or the sensor is still in its 30-second boot calibration phase. Fix: Move jumper to "H" and wait 30 seconds after power-on.
  • Cause B: You are powering the HC-SR501 with 3.3V. The BISS0001 requires a minimum of 4.5V to operate the internal charge pumps. Fix: Connect VCC to the Arduino 5V pin.

Extending and Simplifying the Build

Once the baseline logger is stable, you can scale the project up for home automation or down for ultra-low power.

How to Extend (Scale Up)

To integrate this into a smart home, add an ESP-01S module or swap the Uno for an Arduino Nano ESP32. Use the MQTT protocol to publish the EVT: MOTION_DETECTED string to a broker like Mosquitto, allowing Home Assistant to trigger routines. When doing this, add a hardware interrupt (attachInterrupt) to the PIR pin so the ESP32 can sleep between motion events, saving power and CPU cycles.

How to Simplify (Scale Down)

If you just need to turn on a 12V LED strip when someone walks into a closet, drop the Arduino entirely. Wire the HC-SR501 VCC to a 12V-to-5V buck converter, and connect the OUT pin directly to the gate of an IRLZ44N Logic-Level N-Channel MOSFET. The PIR's 3.3V HIGH output is sufficient to open the MOSFET gate and power the LED strip, creating a purely analog, zero-code motion light for under $4 in parts.

For deeper reading on PIR physics and BISS0001 timing calculations, refer to the Adafruit PIR Sensor Guide and the official Arduino Language Reference for digital I/O handling.