If you are building an automation project that needs to detect human presence without draining a battery, a passive infrared (PIR) sensor is the standard choice. The direct answer for the most common setup: an HC-SR501 PIR sensor connects to an Arduino Uno R3 (or R4 Minima) using just three wires—VCC to the Arduino 5V pin, GND to GND, and the OUT pin to Digital Pin 2.

While wiring a PIR sensor is straightforward, getting reliable, noise-free triggers in a real-world environment requires understanding the module's internal timing, managing floating pins, and writing state-change code rather than simple polling. This guide provides the exact hardware specifications, a robust C++ implementation, and a targeted debugging framework for when the sensor misbehaves.

HC-SR501 PIR Sensor Specifications and Operating Modes

Before writing any code, you must understand the hardware you are interfacing with. The HC-SR501 uses a pyroelectric sensor (typically the D203S or similar) hidden behind a multi-faceted plastic dome called a Fresnel lens. This lens focuses infrared radiation from a wide area onto the sensor's dual sensing elements. When a warm body moves across the elements, it creates a differential voltage spike that the onboard LM324 comparator chip translates into a clean digital HIGH signal.

Below are the exact operational parameters for the standard HC-SR501 module. Keep these values in mind when designing your power supply and timing logic.

Parameter Specification / Value Design Implication
Operating Voltage 4.5V to 20V DC Can be powered directly from a 9V battery or 12V lead-acid system, bypassing the Arduino's 5V regulator.
Quiescent Current < 50 µA Extremely low power draw, making it ideal for solar or battery-powered IoT nodes.
Detection Angle < 120° cone The Fresnel lens creates multiple detection 'zones'; motion must cross between zones to trigger.
Delay Time Range 0.3s to 18s (adjustable) Set via the left potentiometer. Dictates how long the OUT pin stays HIGH after motion stops.
Sensitivity Range 3m to 7m (adjustable) Set via the right potentiometer. Higher sensitivity increases susceptibility to RF noise and heat drafts.
Output Signal Level Digital HIGH (VCC dependent) If powered at 5V, OUT is ~3.3V to 5V. Safe for Arduino Uno digital inputs.

Understanding the Trigger Jumper

On the PCB, you will find a 3-pin header with a jumper cap. This selects the trigger mode:

  • H (Retriggerable): The output stays HIGH as long as motion is continuously detected. The delay timer resets with every new movement. This is the default and most useful mode for lighting control.
  • L (Non-retriggerable): The output goes HIGH for the exact delay time, then goes LOW for a brief lockout period (~2.5 seconds), regardless of ongoing motion. Useful for counting distinct entry events.

Required Hardware and Pin Mapping

This build targets the Arduino Uno R3 (ATmega328P) or the newer Arduino Uno R4 Minima (Renesas RA4M1). The logic is identical for both, as the HC-SR501 outputs a standard digital signal well within the 5V tolerance of the AVR and the 5V-tolerant inputs of the R4.

Parts List

  • 1x Arduino Uno R3 or R4 Minima
  • 1x HC-SR501 PIR Motion Sensor Module
  • 1x Standard 5mm LED (any color) for visual feedback
  • 1x 220Ω or 330Ω through-hole resistor (for the LED)
  • 1x 10kΩ through-hole resistor (optional, for pull-down stability)
  • Female-to-Male Dupont jumper wires

Pin Mapping Table

HC-SR501 Pin Arduino Uno Pin Wire Color (Standard) Notes
VCC 5V Red Do not use 3.3V; the module requires at least 4.5V to operate reliably.
OUT Digital Pin 2 Yellow / Orange Pin 2 is chosen to allow hardware interrupt capabilities if you extend the code later.
GND GND Black Ensure a solid ground connection; poor grounding causes phantom triggers.

Step-by-Step Wiring Procedure

⚠️ Hardware Warning: The HC-SR501 requires a 30-to-60-second calibration period upon first receiving power. During this time, it maps the ambient infrared background. Do not move in front of the sensor during this boot sequence, or it will calibrate to your body heat and fail to trigger.
  1. Prepare the Power Rails: Connect the Arduino 5V pin to the VCC pin on the HC-SR501. Connect the Arduino GND to the sensor's GND pin.
  2. Wire the Signal: Connect the sensor's OUT pin to Arduino Digital Pin 2. Pro-tip: If your wires are longer than 12 inches, solder a 10kΩ pull-down resistor between Digital Pin 2 and GND to prevent the pin from floating and causing false triggers during Arduino boot-up.
  3. Wire the Indicator LED: Connect the 220Ω resistor to Arduino Digital Pin 13 (the onboard LED pin, or an external pin if preferred). Connect the other end of the resistor to the anode (long leg) of the LED, and the cathode to GND.
  4. Adjust Potentiometers: Using a small Phillips screwdriver, turn the delay time potentiometer (left) fully counter-clockwise for the minimum ~0.3s delay during testing. Turn the sensitivity potentiometer (right) to the middle position.
  5. Verify Jumper: Ensure the jumper cap is bridging the outermost pin and the middle pin (H mode / Retriggerable).

Complete Arduino Code with State Management

A common mistake beginners make is simply reading the pin state and printing to the Serial monitor inside the loop(). This results in thousands of lines of spam. The code below uses state-change detection and includes a mandatory blocking calibration phase in the setup() function.

Target Board: Arduino Uno R3 / R4 Minima. No external libraries required.

// Pin Definitions
#define PIR_SENSOR_PIN 2
#define LED_INDICATOR_PIN 13

// System Variables
int calibrationTime = 30; // Seconds to wait for PIR to calibrate
bool lastPirState = LOW;  // Track previous state to detect edges
bool currentPirState = LOW;
unsigned long motionStartTime = 0;

void setup() {
  Serial.begin(9600);
  pinMode(PIR_SENSOR_PIN, INPUT);
  pinMode(LED_INDICATOR_PIN, OUTPUT);
  
  // Optional: Internal pull-down is not natively supported on standard Uno pins 
  // in a way that helps here, so we rely on the external 10k resistor or 
  // the module's internal output stage which drives HIGH/LOW actively.
  
  Serial.println("Calibrating PIR sensor...");
  Serial.print("Please clear the sensor's field of view for ");
  Serial.print(calibrationTime);
  Serial.println(" seconds.");
  
  // Visual feedback during calibration
  for (int i = 0; i < calibrationTime; i++) {
    Serial.print(".");
    delay(1000);
  }
  Serial.println("\nCalibration complete. System active.");
  
  // Initialize state tracking
  lastPirState = digitalRead(PIR_SENSOR_PIN);
}

void loop() {
  currentPirState = digitalRead(PIR_SENSOR_PIN);
  
  // State-Change Detection (Edge Triggering)
  if (currentPirState != lastPirState) {
    if (currentPirState == HIGH) {
      // Motion Just Started
      motionStartTime = millis();
      Serial.println("[EVENT] Motion Detected!");
      digitalWrite(LED_INDICATOR_PIN, HIGH);
    } else {
      // Motion Just Stopped
      unsigned long duration = millis() - motionStartTime;
      Serial.print("[EVENT] Motion Ended. Duration: ");
      Serial.print(duration / 1000.0);
      Serial.println(" seconds.");
      digitalWrite(LED_INDICATOR_PIN, LOW);
    }
    
    // Update last known state
    lastPirState = currentPirState;
    
    // Basic hardware debounce delay
    delay(50); 
  }
}

Debugging: First Three Things to Check When It Fails

When your PIR sensor setup misbehaves, the issue is almost always traceable to one of three specific failure modes. Follow this decision tree to isolate the fault.

1. Compilation Error: 'D2' was not declared in this scope

The Symptom: The Arduino IDE throws the exact error string: Compilation error: 'D2' was not declared in this scope.

The Cause: You copied code from an ESP32 or ESP8266 tutorial. On Espressif chips, pins are often defined as D2, D4, etc. On AVR-based Arduinos (Uno, Nano, Mega), the digital pins are strictly integers.

The Fix: Change your pin definition from #define PIR_SENSOR_PIN D2 to #define PIR_SENSOR_PIN 2. Ensure you are using the integer 2 without the 'D' prefix.

2. Runtime: Serial Monitor Spamming "Motion Detected" Continuously

The Symptom: The sensor is sitting on an empty desk, but the Serial monitor is flooding with motion events, or the LED is flickering rapidly.

The Cause: This is rarely a hardware fault. It is usually caused by polling the pin state without tracking state changes, or a missing common ground between the Arduino and an external power supply powering the sensor.

The Fix: First, verify your code uses the state-change logic provided above (comparing currentPirState to lastPirState). Second, if you are powering the HC-SR501 from a separate 5V wall adapter, you must connect the GND of the wall adapter to the GND of the Arduino. Without a shared ground reference, the Arduino's input pin will float, reading random electromagnetic noise as HIGH signals.

3. Hardware: Random Triggers When No One is in the Room (Phantom Motion)

The Symptom: The code is correct, but the sensor triggers every few minutes in an empty room.

The Cause: The HC-SR501 is notoriously susceptible to Radio Frequency (RF) interference and thermal drafts. If you have a Wi-Fi router, a 2.4GHz cordless phone, or a cellular IoT modem (like a SIM800L) within 3 feet of the PIR sensor, the RF envelope will induce a voltage in the high-impedance pyroelectric element, tricking the comparator. Additionally, an HVAC vent blowing across the sensor will cause rapid ambient temperature shifts.

The Fix: Move the sensor away from RF transmitters. If space is constrained, wrap the PIR module's PCB (not the plastic dome) in aluminum foil and connect the foil to the GND pin to create a Faraday cage. Lower the sensitivity potentiometer slightly.

Extending and Simplifying the Build

Once you have a reliable baseline, you can adapt this circuit to fit specific project constraints.

How to Simplify (No Microcontroller Required)

If your goal is simply to turn on a 12V LED strip or a small fan when someone walks by, you do not need the Arduino at all. The HC-SR501's OUT pin can source roughly 10mA to 15mA when HIGH. This is enough to directly drive the gate of a logic-level N-channel MOSFET (like the IRLZ44N) or a small 5V signal relay. Connect the PIR VCC and OUT to your 12V source and the MOSFET gate, respectively, and you have a standalone, ultra-low-power motion switch.

How to Extend (Adding Environmental Context)

A PIR sensor is blind to ambient light; it will trigger your lights at 2:00 PM just as easily as at 2:00 AM. To fix this, add a Light Dependent Resistor (LDR) in a voltage divider configuration to an Arduino Analog Pin (e.g., A0). In your C++ code, read the analog value before acting on the PIR trigger. If the LDR reads above a certain threshold (indicating daylight), ignore the PIR's HIGH signal.

For IoT applications, swap the Arduino Uno for an ESP32 DevKit V1. The ESP32's deep sleep current is roughly 10µA. You can wire the PIR's OUT pin to an ESP32 RTC GPIO pin (like GPIO 4), configure it as a wake-up source using esp_sleep_enable_ext0_wakeup(), and push an MQTT message to Home Assistant only when motion occurs, allowing the system to run for months on a single 18650 lithium cell.