Integrating a PIR and Arduino setup is the standard starting point for home automation, security logging, and smart lighting. However, the internet is flooded with basic blink-an-LED tutorials that fail the moment you put the sensor in a real room. You get phantom triggers from your HVAC vent, the sensor locks HIGH and never resets, or your battery drain is completely unacceptable.

This guide cuts the fluff. We will make a hard decision on which sensor module to buy, wire it correctly to an Arduino Uno R4 Minima (backward compatible with the Uno R3), deploy production-grade debounced code, and troubleshoot the exact serial errors that plague PIR deployments.

The Verdict: Which PIR Sensor to Choose

Do not buy a sensor until you have run your project requirements through this decision path. The two dominant modules on the market—the standard HC-SR501 and the miniature AM312—solve entirely different engineering problems.

Decision Tree: HC-SR501 vs AM312
Project Constraint If your project requires... Then choose...
Power Source Battery / Coin cell / Deep sleep IoT AM312 (Draws ~12µA)
Mains / USB / Always-on 5V rail Proceed to Range ↓
Detection Range Wide room (up to 7 meters, 120° cone) HC-SR501 (Buy this)
Tight hallway / Desk proximity (< 3 meters) AM312 (Buy this)
Adjustability Need hardware dials for delay/sensitivity HC-SR501 (Buy this)
Logic Voltage Strict 3.3V logic (ESP32 / RP2040) AM312 (Native 3.3V)
Default Recommendation: If you are building a standard room-occupancy sensor powered by a USB wall adapter, buy the HC-SR501 (approx. $1.50 USD). Its integrated BISS0001 timing chip handles the analog signal processing, saving your microcontroller from heavy math.

Hardware Spec Sheet & Parts List

Before wiring, verify you have the exact components listed below. Substituting the Arduino board or the sensor variant will change the voltage logic and pin mappings.

Bill of Materials (BOM)
Component Exact Variant Key Spec
Microcontroller Arduino Uno R4 Minima (or Uno R3) 5V Logic, 48MHz (R4) / 16MHz (R3)
PIR Sensor HC-SR501 (with Fresnel lens) 5V-20V Input, 3.3V TTL Output
Decoupling Cap 100nF (0.1µF) Ceramic Capacitor For power rail noise suppression
Wiring 22 AWG Solid Core / Dupont Jumper Keep PIR data wire under 2 meters
Indicator 5mm LED + 220Ω Resistor Visual debug indicator

Pin Mapping & Wiring Steps

The HC-SR501 outputs a clean 3.3V HIGH signal when motion is detected, even when powered by 5V. This makes it safe for 5V Arduinos and 3.3V boards alike. However, it is highly susceptible to RF interference and power rail ripple.

Pin Mapping: HC-SR501 to Arduino Uno R4
HC-SR501 Pin Arduino Uno R4 Pin Notes
VCC (Left) 5V Do not use 3.3V out; the onboard LDO needs 5V in.
OUT (Middle) D2 (Digital Pin 2) Use an interrupt-capable pin for advanced builds.
GND (Right) GND Must share a common ground with the Arduino.

Numbered Wiring Procedure

  1. Set the Trigger Mode: Look at the bottom right corner of the HC-SR501 board. There is a 3-pin header with a jumper cap. Move the jumper to the top two pins (H). This enables 'Retriggerable' mode, meaning the output stays HIGH as long as motion continues. The bottom position (L) is non-retriggerable and will cause premature timeouts in your code.
  2. Connect Power: Wire the left pin (VCC) to the Arduino 5V pin, and the right pin (GND) to the Arduino GND.
  3. Add Decoupling: Solder or plug a 100nF ceramic capacitor directly across the VCC and GND pins on the PIR module. This is not optional if your sensor is more than 30cm from the Arduino. It prevents voltage droop from causing phantom triggers.
  4. Connect Signal: Wire the middle OUT pin to Arduino Digital Pin 2.
  5. Adjust Potentiometers: Using a small Phillips screwdriver, turn the Delay Time (Tx) potentiometer fully counter-clockwise (minimum ~3 seconds). Turn the Sensitivity (Sx) potentiometer to the 12 o'clock position. We will fine-tune these after the code is verified.

Complete Arduino Code (Uno R4 / R3)

This code targets the Arduino Uno R4 Minima (and is 100% backward compatible with the Uno R3). Unlike basic tutorials, this implementation includes non-blocking state management, a cooldown lockout to prevent relay chatter, and an active error-handling routine that detects if the sensor hardware has locked up.

/*
 * PIR Motion Sensor with Debounce and Error Handling
 * Target Board: Arduino Uno R4 Minima / Uno R3
 * Sensor: HC-SR501 (Jumper set to 'H' - Retriggerable)
 */

// --- PIN DEFINITIONS ---
const int PIR_PIN = 2;
const int LED_PIN = 13; // Onboard LED for visual feedback

// --- TIMING CONSTANTS (Milliseconds) ---
const unsigned long DEBOUNCE_TIME = 500;      // Ignore triggers < 500ms (RF noise)
const unsigned long COOLDOWN_TIME = 5000;     // Force 5s off-time after motion clears
const unsigned long STUCK_HIGH_TIMEOUT = 30000; // Flag error if HIGH > 30s

// --- STATE VARIABLES ---
int pirState = LOW;
unsigned long lastTriggerTime = 0;
unsigned long lastClearTime = 0;
unsigned long highStartTime = 0;
bool isCoolingDown = false;
bool errorState = false;

void setup() {
  Serial.begin(115200);
  pinMode(PIR_PIN, INPUT);
  pinMode(LED_PIN, OUTPUT);
  
  // Allow the HC-SR501 BISS0001 chip to calibrate its baseline
  Serial.println("[SYS] Calibrating PIR sensor... wait 30 seconds.");
  delay(30000); 
  Serial.println("[SYS] Calibration complete. Monitoring...");
}

void loop() {
  int currentRead = digitalRead(PIR_PIN);
  unsigned long currentMillis = millis();

  // --- ERROR HANDLING: STUCK HIGH DETECTION ---
  if (currentRead == HIGH) {
    if (highStartTime == 0) highStartTime = currentMillis;
    
    if (currentMillis - highStartTime > STUCK_HIGH_TIMEOUT && !errorState) {
      Serial.println("[ERR] PIR STUCK HIGH > 30s");
      errorState = true;
      digitalWrite(LED_PIN, HIGH); // Solid LED indicates hardware fault
    }
  } else {
    highStartTime = 0;
    if (errorState) {
      Serial.println("[SYS] PIR recovered from stuck state.");
      errorState = false;
    }
  }

  // --- MAIN LOGIC (Ignored if in error state or cooldown) ---
  if (!errorState && !isCoolingDown) {
    if (currentRead == HIGH && pirState == LOW) {
      // Rising edge detected - apply debounce
      if (currentMillis - lastTriggerTime > DEBOUNCE_TIME) {
        pirState = HIGH;
        lastTriggerTime = currentMillis;
        digitalWrite(LED_PIN, HIGH);
        Serial.println("[EVT] MOTION DETECTED");
      }
    } 
    else if (currentRead == LOW && pirState == HIGH) {
      // Falling edge detected - motion stopped
      pirState = LOW;
      lastClearTime = currentMillis;
      digitalWrite(LED_PIN, LOW);
      isCoolingDown = true;
      Serial.println("[EVT] MOTION CLEARED - Entering Cooldown");
    }
  }

  // --- COOLDOWN MANAGER ---
  if (isCoolingDown && (currentMillis - lastClearTime >= COOLDOWN_TIME)) {
    isCoolingDown = false;
    Serial.println("[SYS] Cooldown finished. Sensor armed.");
  }
}

Debugging: First 3 Things to Check & Error Strings

When a PIR build fails, it is almost never a code logic error; it is an environmental or electrical fault. If your serial monitor throws an error, follow this ranked troubleshooting path.

The First 3 Things to Check When It Fails

  1. The Jumper Position: 80% of 'weird timing' bugs happen because the HC-SR501 jumper is in the 'L' (bottom) position. Verify it is on the top two pins ('H').
  2. Power Rail Ripple: If you are powering the Arduino via a cheap USB phone charger, the switching noise will couple into the PIR's high-gain op-amp. Switch to a linear power supply or a high-quality USB hub.
  3. Thermal Drafts: PIR sensors do not detect 'motion'; they detect changes in infrared thermal signatures. An HVAC vent blowing across the Fresnel lens will trigger it continuously. Move the sensor away from heat sources.

Decoding Serial Error Strings

Error String Troubleshooting Matrix
Exact Error String Ranked Causes (Most Likely First) Hardware Fix
[ERR] PIR STUCK HIGH > 30s 1. Jumper in 'L' mode.
2. Severe RF interference (WiFi router < 1ft away).
3. Missing ground connection.
Move jumper to 'H'. Add 100nF decoupling cap. Keep away from 2.4GHz antennas.
[EVT] MOTION DETECTED (Firing every 3s with no human present) 1. Sunlight/Heat lamp in field of view.
2. Sensitivity pot (Sx) turned fully clockwise.
3. Pets in the room.
Turn Sx pot counter-clockwise by 3 full turns. Apply electrical tape to block lower lens facets.
Sensor never triggers (Always LOW) 1. VCC < 4.5V (Brownout).
2. Code didn't wait for 30s calibration.
3. Dead BISS0001 IC.
Measure VCC with a multimeter. Ensure delay(30000) is in setup(). Replace module.
Warning on RF Interference: The HC-SR501 acts as an accidental antenna for 2.4GHz signals. If you are pairing this with an ESP32 or placing it near a WiFi router, the RF envelope will induce a voltage on the PIR traces, causing false triggers. Always use shielded cable for the OUT pin if the run exceeds 50cm, or place the WiFi antenna at least 30cm away from the sensor dome.

Extending and Simplifying the Build

Once your baseline PIR and Arduino circuit is stable, you will likely need to adapt it for a specific deployment. Here is how to scale the design up or down without rewriting your core logic.

How to Simplify (For Battery / Wearable Projects)

If you are migrating this logic to a battery-powered ATTiny85 or ESP32 deep-sleep node, the HC-SR501 is the wrong tool. Its onboard voltage regulator and timing IC draw ~50µA continuously.
The Fix: Swap to the AM312 module. It lacks the potentiometers and the BISS0001 chip, outputting raw, short motion bursts. To use it with the code above, simply increase the DEBOUNCE_TIME to 1500 (1.5 seconds) to stitch the short AM312 bursts into a single logical 'HIGH' state in your microcontroller.

How to Extend (For Smart Home / MQTT Integration)

To turn this into a networked occupancy sensor for Home Assistant:
The Fix: Replace the Uno R4 with an ESP32-DevKitC V4. Wire the PIR OUT pin to GPIO 4. Add the PubSubClient library to your sketch. Instead of toggling an LED in the [EVT] MOTION DETECTED block, publish a JSON payload to your MQTT broker:

// Inside the MOTION DETECTED logic block:
char payload[50];
snprintf(payload, sizeof(payload), "{\"state\": \"ON\", \"lux\": %d}", analogRead(LDR_PIN));
client.publish("home/livingroom/occupancy", payload);

By adding a simple GL5528 Photoresistor (LDR) to an analog pin, you can gate the MQTT publish event so that motion is only reported when the room is actually dark, saving network traffic and preventing daylight false-alarms.

For deeper theory on how the pyroelectric crystals inside these sensors generate charge from infrared photons, refer to the All About Circuits PIR physics breakdown. For official wiring and safety practices regarding digital inputs, consult the Arduino Digital Input Documentation.