ESP32-CAM PIR Motion-Triggered Camera with Deep Sleep Wake-Up

Streaming video all day wastes power and storage when nothing is happening. A better design for a wildlife camera, a shop entrance or a store-room monitor is to let a passive infrared (PIR) sensor watch the scene and wake the ESP32-CAM only when something warm moves. The camera takes a photo, saves it and goes back to deep sleep within a couple of seconds.

This tutorial focuses on the motion-trigger part: how a PIR sensor behaves, why it should not be connected straight to GPIO 13 when the SD card is in use, how ext0 wake-up works, and how to avoid endless wake loops. The photo is stored on a microSD card in 1-bit mode with a file number that survives power loss.

How the HC-SR501 PIR Sensor Behaves

The HC-SR501 contains a pyroelectric sensor behind a Fresnel lens and a small controller. It runs from about 5 V and its output pin goes to roughly 3.3 V when motion is detected, which is safe for ESP32 logic. Two potentiometers set the sensitivity (range) and the time the output stays HIGH after motion. A jumper selects single-trigger or repeat-trigger mode; repeat-trigger keeps the output HIGH as long as movement continues.

After power-up the sensor needs up to about a minute to stabilise, during which it may trigger by itself. It also has a dead time of a few seconds after each pulse when it ignores motion. Set the time potentiometer to its minimum, so the output returns LOW quickly and the camera can go back to sleep.

Circuit: Why a Transistor Is Used

With the SD card in 1-bit mode, GPIO 12 and 13 are released by the SD driver, but GPIO 13 is still physically connected to the card's DAT3 pin. SD cards pull this line up, so GPIO 13 tends to sit HIGH and cannot be used reliably as an active-HIGH wake input. The usual solution is an NPN transistor that inverts the PIR signal: motion pulls GPIO 13 LOW, and the ESP32 wakes on a LOW level.

PartConnection
HC-SR501 VCC5V
HC-SR501 GNDGND
HC-SR501 OUT2.2 kilo-ohm resistor to the base of a BC547 or 2N2222
Transistor emitterGND
Transistor collectorGPIO 13, plus 10 kilo-ohm pull-up to 3.3V
microSD cardOn-board slot, used in 1-bit mode
Power5 V supply rated 2 A, or a 2S battery through a 5 V buck converter
PIR OUT --[2.2k]--+ B   (NPN)
                     C ---+--- GPIO 13
                     E    |
                     |   [10k]
                    GND   |
                         3.3V

No motion: transistor off, GPIO 13 = HIGH
Motion:    transistor on,  GPIO 13 = LOW  -> wake

Complete Code

The program runs from top to bottom once per wake-up and never reaches loop(). It uses the Preferences library to store the photo number in flash, so numbering continues after the battery is changed.

PIR wake-up from deep sleep, capture, save to microSD (1-bit mode) and sleep again
#include "esp_camera.h"
#include "FS.h"
#include "SD_MMC.h"
#include <Preferences.h>
#include "driver/rtc_io.h"

// AI-Thinker ESP32-CAM pin map
#define PWDN_GPIO_NUM  32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM   0
#define SIOD_GPIO_NUM  26
#define SIOC_GPIO_NUM  27
#define Y9_GPIO_NUM    35
#define Y8_GPIO_NUM    34
#define Y7_GPIO_NUM    39
#define Y6_GPIO_NUM    36
#define Y5_GPIO_NUM    21
#define Y4_GPIO_NUM    19
#define Y3_GPIO_NUM    18
#define Y2_GPIO_NUM     5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM  23
#define PCLK_GPIO_NUM  22

#define WAKE_PIN GPIO_NUM_13     // LOW = motion (through transistor)
#define FLASH_PIN GPIO_NUM_4

Preferences prefs;

bool initCamera() {
  camera_config_t c = {};
  c.ledc_channel = LEDC_CHANNEL_0;
  c.ledc_timer   = LEDC_TIMER_0;
  c.pin_d0 = Y2_GPIO_NUM;  c.pin_d1 = Y3_GPIO_NUM;
  c.pin_d2 = Y4_GPIO_NUM;  c.pin_d3 = Y5_GPIO_NUM;
  c.pin_d4 = Y6_GPIO_NUM;  c.pin_d5 = Y7_GPIO_NUM;
  c.pin_d6 = Y8_GPIO_NUM;  c.pin_d7 = Y9_GPIO_NUM;
  c.pin_xclk = XCLK_GPIO_NUM;   c.pin_pclk = PCLK_GPIO_NUM;
  c.pin_vsync = VSYNC_GPIO_NUM; c.pin_href = HREF_GPIO_NUM;
  c.pin_sccb_sda = SIOD_GPIO_NUM; c.pin_sccb_scl = SIOC_GPIO_NUM;
  c.pin_pwdn = PWDN_GPIO_NUM;   c.pin_reset = RESET_GPIO_NUM;
  c.xclk_freq_hz = 20000000;
  c.pixel_format = PIXFORMAT_JPEG;
  c.frame_size   = FRAMESIZE_SVGA;
  c.jpeg_quality = 10;
  c.fb_count     = 1;
  c.fb_location  = CAMERA_FB_IN_PSRAM;
  c.grab_mode    = CAMERA_GRAB_LATEST;
  return esp_camera_init(&c) == ESP_OK;
}

void goToSleep() {
  // Wait until motion has ended, otherwise we wake up immediately
  unsigned long t0 = millis();
  while (digitalRead(WAKE_PIN) == LOW && millis() - t0 < 30000) delay(100);

  pinMode(FLASH_PIN, OUTPUT);
  digitalWrite(FLASH_PIN, LOW);
  rtc_gpio_hold_en(FLASH_PIN);            // keep flash LED off in sleep

  esp_sleep_enable_ext0_wakeup(WAKE_PIN, 0);  // wake when GPIO 13 is LOW
  Serial.println("Going to sleep");
  Serial.flush();
  esp_deep_sleep_start();
}

void setup() {
  Serial.begin(115200);
  rtc_gpio_hold_dis(FLASH_PIN);           // release hold from last sleep
  pinMode(WAKE_PIN, INPUT);               // external 10k pull-up

  esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
  if (cause != ESP_SLEEP_WAKEUP_EXT0) {
    Serial.println("Power-on: waiting 60 s for PIR to settle");
    delay(60000);
    goToSleep();
  }

  Serial.println("Motion detected");
  if (!initCamera()) {
    Serial.println("Camera init failed");
    goToSleep();
  }
  // Let auto exposure adjust: discard a few frames
  for (int i = 0; i < 3; i++) {
    camera_fb_t *f = esp_camera_fb_get();
    if (f) esp_camera_fb_return(f);
  }
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Capture failed");
    goToSleep();
  }

  // 1-bit mode: uses only CLK 14, CMD 15, D0 2 and frees GPIO 4, 12, 13
  if (SD_MMC.begin("/sdcard", true)) {
    prefs.begin("pircam", false);
    uint32_t n = prefs.getUInt("count", 0) + 1;
    char path[32];
    snprintf(path, sizeof(path), "/motion_%05lu.jpg", (unsigned long)n);
    File file = SD_MMC.open(path, FILE_WRITE);
    if (file) {
      file.write(fb->buf, fb->len);
      file.close();
      prefs.putUInt("count", n);          // only count saved photos
      Serial.printf("Saved %s (%u bytes)\n", path, (unsigned)fb->len);
    } else {
      Serial.println("File open failed");
    }
    prefs.end();
    SD_MMC.end();
  } else {
    Serial.println("SD card mount failed");
  }
  esp_camera_fb_return(fb);
  goToSleep();
}

void loop() {
  // never reached: every wake-up restarts setup()
}

How the Code Works

  • esp_sleep_get_wakeup_cause() separates a power-on reset from a motion wake-up. After power-on the code waits a minute for the PIR to settle before arming the wake-up.
  • esp_sleep_enable_ext0_wakeup(GPIO_NUM_13, 0) uses the RTC domain to watch one pin during deep sleep. GPIO 13 is an RTC-capable pin, which is required for ext0.
  • Before sleeping, the code waits until GPIO 13 has returned HIGH. Without this, a still-active PIR output would wake the board again immediately, creating a loop of photos.
  • The flash LED pin is driven LOW and latched with rtc_gpio_hold_en(), because an undriven GPIO 4 can let the flash glow during sleep.
  • Three frames are discarded after init so exposure and white balance settle; the first frame from a cold sensor is often dark or green.

The photo count is written to flash only after the file has been saved successfully. Flash memory has a limited number of erase cycles, but one small write per motion event is far within what the Preferences (NVS) storage is designed for.

Troubleshooting and Tips

  • Continuous wake-ups: the PIR time potentiometer is set too long, or the transistor stage is wired wrongly. Measure GPIO 13: it should read 3.3 V without motion.
  • False triggers: PIR sensors react to sunlight patches, heaters and air from fans. Aim the sensor away from them and lower the sensitivity.
  • Photo shows an empty scene: wake-up and camera start take a moment, so fast subjects may already be gone. Point the PIR slightly ahead of where the subject enters the frame.
  • SD card mount failed: format the card as FAT32 and check it is fully inserted.
  • Battery drains quickly: the board's regulator and other parts draw current even in deep sleep. Measure the sleep current with a multimeter before choosing a battery size.

Summary

A PIR sensor and ext0 deep-sleep wake-up turn the ESP32-CAM into an event-driven camera that only works when something moves. An NPN transistor inverts the PIR output so GPIO 13 can share its line with the SD card in 1-bit mode, the code waits for the PIR to go idle before sleeping, and Preferences keeps the photo numbering across power cycles.