When building a motion detection sensor Arduino project, the HC-SR501 Passive Infrared (PIR) module is the undisputed baseline for hobbyists. It is cheap, operates at 5V, and outputs a clean digital HIGH when it detects human movement. However, the difference between a reliable security trigger and a ghost-triggering nightmare lies in power rail decoupling and non-blocking code architecture.
This guide targets the Arduino Uno R4 Minima (ABX00080) due to its 48MHz Cortex-M4 processor and robust 5V rail, but the circuit and code are fully backward-compatible with the classic Uno R3 and Nano. We will cover the hardware comparison, exact wiring with decoupling, non-blocking C++ code, and bench-level debugging for when the sensor inevitably misbehaves.
Sensor Technology Comparison
Before soldering or wiring, verify that a PIR sensor is actually what your application needs. Makers often default to the HC-SR501 without considering its limitations regarding field of view (FoV) and false triggers from heat sources. Below is a data-dense comparison of the four most common motion detection modules available in 2026.
| Module | Technology | Max Range | Field of View | Quiescent Current | Typical Price | False Trigger Risk |
|---|---|---|---|---|---|---|
| HC-SR501 | PIR (Pyroelectric) | 7 Meters | ~110° Cone | ~65 µA | $1.50 - $2.50 | High (HVAC, sunlight, pets) |
| RCWL-0516 | Microwave Radar | 9 Meters | 360° (Through walls) | ~2.8 mA | $2.00 - $3.50 | Very High (Moving trees, pipes) |
| HLK-LD2410 | mmWave Radar (24GHz) | 6 Meters | ~60° Cone | ~70 mA | $4.00 - $6.00 | Low (Detects static presence) |
| VL53L1X | Time-of-Flight (ToF) Laser | 4 Meters | ~27° Narrow Beam | ~1.5 mA | $6.00 - $9.00 | Very Low (Highly directional) |
Verdict: Choose the HC-SR501 for basic room entry logging. Upgrade to the HLK-LD2410 mmWave sensor if you need to detect a human sitting perfectly still at a desk (PIR requires movement to trigger). For deeper integration guides, refer to the Adafruit PIR Sensor Guide and the official Arduino Uno R4 Minima documentation.
Parts List and Pin Mapping
Ghost triggers on the HC-SR501 are almost always caused by voltage sag on the 5V rail when the internal BISS0001 signal conditioning chip switches states. To fix this, we add a local decoupling capacitor directly at the sensor.
Bill of Materials (BOM)
- MCU: Arduino Uno R4 Minima (ABX00080)
- Sensor: HC-SR501 PIR Module (v1.2 board with BISS0001 IC)
- Decoupling: 100µF 16V Electrolytic Capacitor
- Pull-down: 10kΩ Resistor (Optional, prevents floating input during MCU boot)
- Wiring: 22 AWG solid-core jumper wires
Pin Mapping Table
| HC-SR501 Pin | Arduino Uno R4 Minima Pin | Notes |
|---|---|---|
| VCC | 5V | Do not use 3.3V; the BISS0001 requires 4.5V-20V. |
| OUT | D2 (Digital Pin 2) | Use an interrupt-capable pin for advanced builds. |
| GND | GND | Ensure a common ground if using external power. |
Step-by-Step Wiring Procedure
- Place the MCU and Sensor: Insert the Arduino Uno R4 Minima and the HC-SR501 header pins into your solderless breadboard, ensuring they are on separate power rails to avoid shorting.
- Wire Power and Ground: Connect the sensor's VCC to the Arduino's 5V pin, and GND to GND.
- Install the Decoupling Capacitor (Crucial): Insert the 100µF electrolytic capacitor as close to the sensor's VCC and GND pins as physically possible. The positive leg (longer lead, marked stripe on negative side) goes to VCC. This local energy reservoir prevents the BISS0001 chip from pulling down the shared 5V rail during state transitions.
- Wire the Signal Line: Connect the sensor's OUT pin to Digital Pin 2 (D2) on the Arduino.
- Add the Pull-down Resistor: Place a 10kΩ resistor between D2 and GND. The HC-SR501 output is push-pull, but during the Arduino's boot sequence (before
pinModeis executed), the pin floats. This resistor prevents phantom interrupts. - Adjust the Potentiometers: On the back of the HC-SR501, turn the Time Delay pot fully counter-clockwise (minimum ~3 seconds) and the Sensitivity pot to the 12 o'clock position for bench testing.
Complete Arduino Code with Non-Blocking Debounce
Beginner tutorials often use delay() to handle the PIR's cooldown period. This blocks the MCU, preventing it from reading other sensors or handling network traffic. The code below uses a millis()-based state machine and includes hardware sanity checks to catch wiring faults at boot.
/*
* Motion Detection Sensor Arduino Project
* Target: Arduino Uno R4 Minima (Compatible with R3/Nano)
* Sensor: HC-SR501 PIR
* Author: ElectricalFlux
*/
// --- Pin Definitions ---
const uint8_t PIR_PIN = 2;
const uint8_t LED_PIN = LED_BUILTIN; // Pin 13 on Uno
// --- Timing Constants ---
const unsigned long DEBOUNCE_MS = 50; // Filter out sub-50ms electrical noise
const unsigned long COOLDOWN_MS = 3000; // Minimum time between valid triggers
// --- State Variables ---
bool motionDetected = false;
bool lastPirState = LOW;
unsigned long lastTriggerTime = 0;
unsigned long lastDebounceTime = 0;
void setup() {
Serial.begin(115200);
while (!Serial && millis() < 2500) {
// Wait for serial monitor, timeout after 2.5s for headless operation
}
Serial.println("[SYS] Booting Motion Detection Sensor...");
pinMode(PIR_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
// Hardware Sanity Check: PIR should not be HIGH immediately on boot
// unless there is active motion or a wiring fault.
delay(100); // Allow BISS0001 to stabilize
if (digitalRead(PIR_PIN) == HIGH) {
Serial.println("[ERR] PIR_LINE_STUCK_HIGH: Check for short to 5V or extreme sensitivity.");
} else {
Serial.println("[SYS] Sensor initialized. Waiting for motion...");
}
}
void loop() {
bool currentPirState = digitalRead(PIR_PIN);
unsigned long currentMillis = millis();
// Non-blocking debounce logic
if (currentPirState != lastPirState) {
lastDebounceTime = currentMillis;
}
if ((currentMillis - lastDebounceTime) > DEBOUNCE_MS) {
// If the state has stabilized and is HIGH, and we aren't in cooldown
if (currentPirState == HIGH && !motionDetected) {
if ((currentMillis - lastTriggerTime) > COOLDOWN_MS) {
motionDetected = true;
lastTriggerTime = currentMillis;
digitalWrite(LED_PIN, HIGH);
Serial.print("[MOTION] Triggered at: ");
Serial.println(currentMillis);
}
}
}
// Auto-reset motion state after cooldown expires
if (motionDetected && (currentMillis - lastTriggerTime) > COOLDOWN_MS) {
motionDetected = false;
digitalWrite(LED_PIN, LOW);
Serial.println("[SYS] Cooldown complete. Sensor armed.");
}
lastPirState = currentPirState;
// Yield to background tasks (WiFi/RTOS on advanced boards)
yield();
}
Debugging: First Three Things to Check When It Fails
When your Serial monitor spits out errors or the LED triggers randomly, do not immediately rewrite your code. Hardware and power issues cause 95% of PIR failures. Here are the first three things to check on the bench.
1. Power Rail Ripple (The Ghost Trigger Culprit)
Symptom: Sensor triggers every 4-5 seconds with no one in the room.
Measurement: Set your multimeter to AC Voltage (mV). Place the probes across the sensor's VCC and GND pins (not the Arduino's).
Threshold: You should read < 20mV AC. If you read > 50mV AC, your 5V rail is noisy. The BISS0001 interprets this ripple as infrared changes.
Fix: Ensure the 100µF capacitor is installed. If ripple persists, power the sensor from a dedicated 5V LDO (like an AP2112K-5.0) rather than the Arduino's USB 5V rail.
2. The "PIR_LINE_STUCK_HIGH" Error
Symptom: Serial monitor outputs [ERR] PIR_LINE_STUCK_HIGH and the LED stays on permanently.
Ranked Causes:
- Time Delay Potentiometer: The delay pot on the back is turned fully clockwise, locking the output HIGH for up to 5 minutes. Turn it fully counter-clockwise.
- Missing Ground: The GND wire is loose, causing the signal pin to float high relative to the MCU. Check continuity from sensor GND to Arduino GND.
- Trigger Jumper: The small jumper on the board is set to "H" (Repeatable Trigger) instead of "L" (Single Trigger). While "H" is usually fine, a noisy environment can lock it. Switch to "L" to force a reset.
3. Voltage Drop Across Jumper Wires
Symptom: Sensor works on the bench, but fails when mounted 3 feet away inside an enclosure.
Measurement: Measure DC voltage at the Arduino 5V pin, then at the sensor VCC pin.
Threshold: The BISS0001 brownout threshold is roughly 4.2V. If your cheap jumper wires drop the voltage below 4.5V under load, the sensor will reboot continuously.
Fix: Use thicker 20 AWG wire for the power run, or step up to a 12V supply at the sensor enclosure and use a local buck converter to drop to 5V.
Extending and Simplifying the Build
Once you have a stable baseline, you can adapt the circuit to fit your specific deployment constraints.
How to Simplify the Build
If you are deploying this in a battery-powered enclosure and need to minimize quiescent current, remove the onboard 5V regulator from the HC-SR501. The module ships with a linear regulator to drop 12V down to 5V, which wastes power. By desoldering the regulator and feeding the BISS0001 chip directly from a 3.3V or 5V battery pack via a high-efficiency buck converter, you can drop the system's sleep current from ~80µA to under 15µA, extending battery life from weeks to months.
How to Extend the Build
To turn this from a local alarm into a smart home node, swap the Arduino Uno R4 Minima for an ESP32-C3 SuperMini. The C3 maintains the same 3.3V logic (use a voltage divider on the PIR OUT pin if your specific HC-SR501 outputs a full 5V HIGH). You can then use the PubSubClient library to publish MQTT payloads to a Home Assistant broker.
For local logging without Wi-Fi, wire an I2C SSD1306 128x64 OLED display to the A4/A5 (SDA/SCL) pins. Update the code to increment a counter variable inside the if (currentPirState == HIGH) block and render the total daily triggers to the screen using the Adafruit_SSD1306 library.






