esp32 beginner 20 min

ESP32: detect motion with a PIR motion sensor

Wire a PIR motion sensor (HC-SR501) to an ESP32 and detect people moving in a room. The foundation for security, automation, and wake-on-motion projects.

Code available for: ESP32 ArduinoArduino CMicroPythonPython
Published Aug 25, 2026

The PIR motion sensor (HC-SR501 is the most common module) is the cheapest way to detect that someone is in a room. It is a $1 sensor that outputs HIGH when it detects motion and LOW otherwise. Wire it to a GPIO, and you have a motion detector that runs for years on a small battery.

The sensor itself is a passive infrared detector behind a fresnel lens. It detects the change in infrared radiation when a warm body moves across its field of view. It is sensitive to people and large animals; not sensitive to static objects.

This tutorial covers wiring, the two adjustment pots, the warm-up time, and the project patterns (security, automation, wake-on-motion).

What you need

  • ESP32 dev board
  • HC-SR501 PIR motion sensor module (about $1 from anywhere; the variants with three pins labelled VCC, OUT, GND)
  • 3 jumper wires

Wiring

HC-SR501 VCC -- ESP32 5V (the module needs 5V; 3.3V may not work)
HC-SR501 GND -- ESP32 GND
HC-SR501 OUT -- ESP32 GPIO 4

The HC-SR501 runs on 4.5-20V, so the ESP32’s 5V pin works. The output is 3.3V-compatible HIGH/LOW despite running on 5V.

If you must use 3.3V (e.g. running from battery), some HC-SR501 clones work on 3.3V. The original ones do not. Test with your specific module.

The two adjustment pots

The HC-SR501 has two potentiometers on the back:

PotAdjustmentEffect
Sx (left, looking at the back)SensitivityRange of detection, 3-7 meters
Tx (right)Time delayHow long OUT stays HIGH after a trigger, 5 sec to 5 min

Turning the sensitivity pot clockwise increases the range. Turning the time-delay pot clockwise increases how long the output stays HIGH after a trigger.

For most projects, sensitivity maxed out (clockwise) and time delay short (counterclockwise to 5 seconds) is the right starting point.

The “time delay” setting is not a re-trigger delay. It is the minimum time OUT stays HIGH after a single trigger. After the delay, OUT goes LOW and the sensor waits for the next trigger. If you want a longer trigger window, set this higher.

The warm-up time

The HC-SR501 needs about 30-60 seconds to stabilize after power-up. During this time, OUT may randomly go HIGH and LOW. Ignore the readings during warm-up:

void setup() {
  Serial.begin(115200);
  delay(1000);
  pinMode(PIR_PIN, INPUT);
  Serial.println("PIR warming up, ignoring motion for 60 seconds...");
  delay(60000);   // wait for the sensor to stabilize
  Serial.println("PIR ready.");
}

If you do not wait, your project will fire false alarms on startup.

The code

ESP32 (Arduino)

const int PIR_PIN = 4;

unsigned long lastTrigger = 0;
bool motionActive = false;
const unsigned long MOTION_TIMEOUT = 10000;

void setup() {
  Serial.begin(115200);
  delay(1000);
  pinMode(PIR_PIN, INPUT);
  Serial.println("PIR warming up, wait 60s...");
  delay(60000);
  Serial.println("PIR ready.");
}

void loop() {
  bool reading = digitalRead(PIR_PIN);
  if (reading) {
    lastTrigger = millis();
    if (!motionActive) {
      Serial.println("Motion START");
      motionActive = true;
    }
  } else {
    if (motionActive && (millis() - lastTrigger > MOTION_TIMEOUT)) {
      Serial.println("Motion END");
      motionActive = false;
    }
  }
  delay(50);
}

Arduino (Uno, Nano, Mega)

const int PIR_PIN = 2;   // any digital pin works

unsigned long lastTrigger = 0;
bool motionActive = false;
const unsigned long MOTION_TIMEOUT = 10000;

void setup() {
  Serial.begin(9600);   // 9600 is more reliable than 115200 on the Uno's USB bridge
  delay(1000);
  pinMode(PIR_PIN, INPUT);
  Serial.println("PIR warming up, wait 60s...");
  delay(60000);
  Serial.println("PIR ready.");
}

void loop() {
  bool reading = digitalRead(PIR_PIN);
  if (reading) {
    lastTrigger = millis();
    if (!motionActive) {
      Serial.println("Motion START");
      motionActive = true;
    }
  } else {
    if (motionActive && (millis() - lastTrigger > MOTION_TIMEOUT)) {
      Serial.println("Motion END");
      motionActive = false;
    }
  }
  delay(50);
}

The Uno’s ATmega328 has no ADC2-vs-Wi-Fi trap, so any GPIO pin works.

MicroPython (ESP32 or Pico)

from machine import Pin
import time

PIR_PIN = 4
pir = Pin(PIR_PIN, Pin.IN)
motion_active = False
last_trigger = 0
TIMEOUT_MS = 10_000

print('PIR warming up, wait 60s...')
time.sleep_ms(60_000)
print('PIR ready.')

while True:
    reading = pir.value()
    now = time.ticks_ms()
    if reading:
        last_trigger = now
        if not motion_active:
            print('Motion START')
            motion_active = True
    else:
        if motion_active and (time.ticks_diff(now, last_trigger) > TIMEOUT_MS):
            print('Motion END')
            motion_active = False
    time.sleep_ms(50)

Raspberry Pi Python (with gpiozero)

import gpiozero
import time

pir = gpiozero.MotionSensor(4)   # BCM pin 4 (physical pin 7)
print('PIR warming up, wait 60s...')
time.sleep(60)
print('PIR ready.')

pir.when_motion = lambda: print('Motion START')
pir.when_no_motion = lambda: print('Motion END')

while True:
    time.sleep(1)

gpiozero.MotionSensor does the warm-up, debouncing, and event handling for you. The when_motion and when_no_motion callbacks fire automatically.

What you should see

The 10-second timeout is a tradeoff. The HC-SR501’s OUT stays HIGH for the time-delay setting (5 sec by default), so we set our timeout higher than that. Adjust based on your time-delay setting.

The retrigger jumper

The HC-SR501 has a 3-pin jumper on the back labeled H and L. The default is H (single trigger mode). In single trigger mode, the OUT goes HIGH once, then ignores new triggers until the time delay expires. In L (repeatable trigger mode), every motion event resets the time delay, keeping OUT HIGH as long as motion continues.

For most projects, H is fine. For “motion just ended” detection, you need L mode and a longer time delay.

Project patterns

Security alarm

Trigger a buzzer or send an MQTT alert when motion is detected while the system is armed:

bool armed = false;

void loop() {
  if (motionActive && armed) {
    mqtt.publish("ctrlaltbrian/security/motion", "ALARM");
    digitalWrite(BUZZER_PIN, HIGH);
    delay(1000);
    digitalWrite(BUZZER_PIN, LOW);
  }
}

Wake-on-motion for battery projects

The PIR can wake the ESP32 from deep sleep. Wire the OUT pin to a GPIO that supports esp_sleep_enable_ext0_wakeup() (any GPIO except 6-11):

#define BUTTON_PIN_BITMASK (1ULL << PIR_PIN)

void setup() {
  esp_sleep_enable_ext0_wakeup(PIR_PIN, 1);   // wake when PIR goes HIGH
  // ...
  esp_deep_sleep_start();
}

When the ESP32 wakes from deep sleep, it knows motion happened. Read the PIR, take action, go back to sleep. Battery life is measured in months.

Lighting automation

Turn on a relay or smart bulb when someone enters a room:

void loop() {
  if (motionActive) {
    digitalWrite(LIGHT_PIN, HIGH);
  } else if (!motionActive && (millis() - lastTrigger > 60000)) {
    digitalWrite(LIGHT_PIN, LOW);
  }
}

The 60-second “no motion” timeout is the right delay for a room. If the person is reading, they need the light to stay on.

What you learned

  • The HC-SR501 PIR sensor detects motion of warm bodies via a fresnel lens.
  • Wiring is 3 pins: VCC (5V), GND, OUT.
  • Wait 60 seconds after power-up for the sensor to stabilize.
  • Two adjustment pots control sensitivity (range) and time delay (how long OUT stays HIGH).

When something breaks

  • False triggers constantly. Sensitivity too high, or sensor is pointed at a window (sunlight), a heating vent, or a busy area.
  • No triggers at all. Wiring wrong, or the warm-up period is not over. Check the OUT pin with a multimeter; it should toggle between 0V and 3.3V when motion happens.
  • Sensor triggers when nothing is moving. Sensitivity too high, or a draft is moving a curtain. Reposition or reduce sensitivity.
  • OUT stays HIGH forever. Time delay is set too high, or the sensor is in single-trigger mode and a person is constantly moving.

What to build next

  • The HC-SR04 tutorial adds distance sensing to a PIR for projects that need to know if someone is close, not just present.
  • The deep sleep + PIR wake tutorial covers the wake-on-motion battery pattern in depth.
  • The book ESP32 Smart Home covers building a complete home security system with multiple PIR sensors and a dashboard.