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.
| 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) |
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.
| 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.
| 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
- 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.
- Connect Power: Wire the left pin (VCC) to the Arduino 5V pin, and the right pin (GND) to the Arduino GND.
- 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.
- Connect Signal: Wire the middle OUT pin to Arduino Digital Pin 2.
- 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
- 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').
- 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.
- 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
| 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. |
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.






