A Passive Infrared (PIR) motion detector Arduino project detects changes in infrared radiation caused by human movement. The most common module for this is the HC-SR501, which pairs a pyroelectric sensor with a BISS0001 processing chip. When wired to an Arduino Uno R3 (ATmega328P) or Nano v3, it provides a simple 5V HIGH digital signal upon detecting motion.

This guide skips the generic overviews and goes straight into the bench-tested specifics: exact wiring, a non-blocking state-machine code block, and a debugging framework for the notorious "stuck HIGH" and false-trigger failure modes.

HC-SR501 vs. RCWL-0516: Choosing Your Motion Sensor

Before wiring, verify you have the right sensor for your environment. PIR sensors detect heat signatures, while microwave radar sensors detect physical movement through obstacles. Here is how the standard HC-SR501 compares to the popular RCWL-0516 microwave module for embedded projects.

Specification HC-SR501 (PIR) RCWL-0516 (Microwave Radar)
Detection Principle Passive Infrared (Heat delta) Doppler Microwave (5.8 GHz)
Max Range ~7 meters (120° cone) ~9 meters (360° omnidirectional)
Wall Penetration None (Line of sight required) Yes (Penetrates drywall/plastic)
False Trigger Rate Low (Immune to fans/pets if mounted high) High (Triggers from moving pipes/fans)
Operating Voltage 4.5V to 20V (5V recommended) 2.8V to 12V
Typical Price (2026) $1.50 - $2.50 USD $1.80 - $3.00 USD

Source reference: For a deeper look into the physics of the pyroelectric effect inside the HC-SR501, see the All About Circuits breakdown of PIR sensor operation.

Parts List & Pin Mapping

This build assumes you are using a 5V logic Arduino. If you are using a 3.3V board (like an Arduino Due or ESP32), you must use a logic level shifter or voltage divider on the sensor's OUT pin to avoid damaging your microcontroller's GPIO.

Required Components

  • Microcontroller: Arduino Uno R3 (Rev3) or Nano v3 (ATmega328P)
  • Sensor: HC-SR501 PIR Module (v1.2 with BISS0001 chip)
  • Wiring: 22 AWG solid-core jumper wires (Male-to-Male)
  • Indicator: 5mm LED with 220Ω current-limiting resistor
  • Power: 5V/1A USB power supply (PIR calibration draws ~65mA, but brownouts cause false triggers)

Pin Mapping Table

HC-SR501 Pin Arduino Uno R3 Pin Wire Color (Standard) Notes
VCC 5V Red Do NOT use 3.3V; the onboard LDO needs headroom.
OUT Digital Pin 2 Yellow Pin 2 allows hardware interrupts if needed later.
GND GND Black Ensure common ground with the Arduino.
Bench Tip: The HC-SR501 has an onboard 3.3V LDO regulator. While the module can technically accept up to 20V on the VCC pin, feeding it exactly 5V from the Arduino's 5V rail minimizes thermal noise on the BISS0001 chip, reducing phantom triggers.

Step-by-Step Wiring & Assembly

  1. De-energize the board: Unplug the Arduino USB cable before making connections.
  2. Connect Power: Route the red wire from the HC-SR501 VCC pin to the Arduino 5V pin. Route the black wire from GND to GND.
  3. Connect Signal: Route the yellow wire from the HC-SR501 OUT pin to Arduino Digital Pin 2.
  4. Wire the Indicator: Connect the 220Ω resistor to Digital Pin 13, then to the LED anode. Connect the LED cathode to GND.
  5. Set the Jumper Cap: Locate the 3-pin header on the bottom edge of the HC-SR501. Move the plastic jumper cap to the H position (closest to the diode). This enables "Repeat Trigger" mode, keeping the output HIGH as long as motion is continuously detected.
  6. Adjust Potentiometers: Using a small Phillips screwdriver, turn the Delay Time pot (right side) fully counter-clockwise (minimum ~3 seconds). Turn the Sensitivity pot (left side) to the 12 o'clock position for medium range.
  7. Attach the Fresnel Lens: Snap the white plastic dome onto the sensor. Never test the sensor without this lens; it focuses the IR beams and creates the detection zones.

Complete Compilable Code (Arduino Uno R3 / Nano v3)

This code targets the Arduino Uno R3 / Nano v3 (ATmega328P). It avoids the common beginner mistake of using blocking delay() functions in the main loop. Instead, it uses a millis()-based state machine to handle the mandatory 30-second PIR calibration period and track motion timeouts without freezing the microcontroller.

/*
 * PIR Motion Detector State Machine
 * Target Board: Arduino Uno R3 / Nano v3 (ATmega328P)
 * Sensor: HC-SR501 (Jumper set to 'H' - Repeat Trigger)
 */

// --- Pin Definitions ---
#define PIR_SENSOR_PIN  2    // Digital pin connected to HC-SR501 OUT
#define STATUS_LED_PIN  13   // Built-in or external LED

// --- Timing Constants ---
#define CALIBRATION_TIME_MS 30000  // HC-SR501 requires 30s to calibrate on boot
#define SERIAL_BAUD_RATE    9600

// --- State Variables ---
bool isCalibrated = false;
bool lastMotionState = LOW;
unsigned long bootTime = 0;
unsigned long lastPrintTime = 0;

void setup() {
  pinMode(PIR_SENSOR_PIN, INPUT);
  pinMode(STATUS_LED_PIN, OUTPUT);
  
  Serial.begin(SERIAL_BAUD_RATE);
  Serial.println(F("SYS: Booting PIR State Machine..."));
  
  // Visual indicator for calibration phase
  digitalWrite(STATUS_LED_PIN, LOW);
  bootTime = millis();
}

void loop() {
  unsigned long currentMillis = millis();
  
  // --- Phase 1: Calibration ---
  // The BISS0001 chip needs ~30 seconds to stabilize its baseline IR reading.
  if (!isCalibrated) {
    if (currentMillis - bootTime < CALIBRATION_TIME_MS) {
      // Blink LED rapidly during calibration
      if ((currentMillis / 250) % 2 == 0) {
        digitalWrite(STATUS_LED_PIN, HIGH);
      } else {
        digitalWrite(STATUS_LED_PIN, LOW);
      }
      
      // Print countdown every 5 seconds
      if (currentMillis - lastPrintTime >= 5000) {
        unsigned long remaining = (CALIBRATION_TIME_MS - (currentMillis - bootTime)) / 1000;
        Serial.print(F("SYS: Calibrating... "));
        Serial.print(remaining);
        Serial.println(F("s remaining. Do not move."));
        lastPrintTime = currentMillis;
      }
      return; // Exit loop early, do not read sensor yet
    } else {
      isCalibrated = true;
      Serial.println(F("SYS: Calibration complete. Monitoring active."));
      digitalWrite(STATUS_LED_PIN, LOW);
    }
  }

  // --- Phase 2: Active Monitoring ---
  bool currentMotionState = digitalRead(PIR_SENSOR_PIN);
  
  // Detect state change (Edge Detection)
  if (currentMotionState != lastMotionState) {
    if (currentMotionState == HIGH) {
      Serial.println(F("EVT: Motion DETECTED (Pin went HIGH)"));
      digitalWrite(STATUS_LED_PIN, HIGH);
    } else {
      Serial.println(F("EVT: Motion ENDED (Pin went LOW)"));
      digitalWrite(STATUS_LED_PIN, LOW);
    }
    lastMotionState = currentMotionState;
  }
  
  // --- Phase 3: Hardware Fault Detection ---
  // If the sensor is stuck HIGH for more than 2 minutes continuously, flag an error.
  // This catches brownouts or faulty H/L jumper configurations.
  static unsigned long highStartTime = 0;
  if (currentMotionState == HIGH) {
    if (highStartTime == 0) highStartTime = currentMillis;
    if (currentMillis - highStartTime > 120000) {
      Serial.println(F("ERR: Sensor stuck HIGH. Check VCC or H/L jumper."));
      highStartTime = currentMillis; // Reset to avoid spamming serial
    }
  } else {
    highStartTime = 0;
  }
}

For more on handling PIR sensor timing and hardware interrupts, refer to the Adafruit PIR Sensor Guide.

Debugging: First Three Checks & Common Failures

PIR sensors are notorious for behaving erratically on the bench. If your serial monitor outputs ERR: Sensor stuck HIGH. Check VCC or H/L jumper. or the sensor never triggers, run through these first three diagnostic checks.

1. Verify Power Rail Voltage (The 3.3V Trap)

Symptom: Sensor output is erratic, triggers randomly, or stays permanently HIGH.
Cause: Powering the HC-SR501 from the Arduino's 3.3V pin. The module has an onboard LDO that drops voltage to 3.3V for the BISS0001 chip. If you feed it 3.3V, the LDO starves the chip, causing logic faults.
Fix: Measure the VCC pin with a multimeter. It must read between 4.8V and 5.2V. Move the red jumper to the Arduino 5V pin.

2. Check the H/L Jumper Cap Position

Symptom: The sensor triggers, goes LOW immediately, and refuses to trigger again for 3-5 seconds even if you keep waving your hand.
Cause: The jumper cap is set to 'L' (Single Trigger mode). In this mode, the output goes HIGH for the potentiometer-set duration, then forces a hardware lockout period.
Fix: Move the jumper cap to the 'H' position (Repeat Trigger). This allows the output to stay HIGH as long as the pyroelectric element detects continuous IR changes.

3. Inspect Potentiometer Settings and Fresnel Lens

Symptom: Serial monitor shows calibration complete, but waving your hand yields no EVT: Motion DETECTED output.
Cause: The sensitivity potentiometer is turned fully counter-clockwise (minimum), or the white Fresnel lens dome is missing.
Fix: Snap the lens on. Turn the sensitivity pot clockwise by 3 full turns to maximize the detection cone. Verify the delay pot is not set to the maximum 200-second hold time, which can mask the LOW transition during bench testing.

Safety Note: Never attempt to open the metal can of the pyroelectric sensor element itself. While it operates at low voltage, the internal FET amplifier is highly sensitive to ESD (Electrostatic Discharge) and touching the pins will permanently destroy the element.

Extending and Simplifying the Build

Once the baseline circuit is stable on your workbench, you can adapt the design for specific deployment scenarios.

How to Simplify: Hardware-Only Relay Drive

If you do not need serial logging, MQTT integration, or complex logic, remove the Arduino entirely. The HC-SR501's OUT pin can source up to 200mA at 3.3V (post-LDO).
The Build: Connect the OUT pin directly to the base of a 2N2222 NPN transistor via a 1kΩ base resistor. Wire the transistor's collector to a 5V relay coil, and the emitter to GND. Add a 1N4007 flyback diode across the relay coil. This creates a standalone, hardware-only motion-activated switch for under $3.00 in parts.

How to Extend: ESP32 MQTT & Daylight Gating

To integrate this into a smart home network, swap the Arduino Uno for an ESP32-WROOM-32 DevKit v1.

  • Voltage Warning: The ESP32 is strictly 3.3V logic. You must power the HC-SR501 with 5V, but route the OUT pin through a simple voltage divider (e.g., 2kΩ and 3.3kΩ resistors) to drop the 3.3V sensor output to a safe ~2.0V for the ESP32 GPIO.
  • Daylight Gating: Add an LDR (photoresistor) in a voltage divider circuit to an ESP32 ADC pin (e.g., GPIO 34). Read the ADC value in code; if the ambient light is above 2000 lux, bypass the motion alert to save battery and network traffic.
  • Network Integration: Use the PubSubClient library to publish a JSON payload {"motion": true, "lux": 450} to an MQTT broker like Mosquitto, allowing Home Assistant to trigger automations.

For reliable ESP32 wiring practices and pinout constraints, consult the SparkFun PIR Hookup Guide, which covers logic-level shifting in detail.