ESP32 S3 Camera - Save Photo to SD Card When Motion Detected

This tutorial instructs you how to build a small security camera with the ESP32 S3. A PIR motion sensor watches the room. When somebody walks in front of it, the ESP32 S3 takes a photo and saves it to a microSD card. The board works alone. It does not need WiFi, and it does not need a computer.

ESP32 S3 Camera - Save Photo to SD Card When Motion Detected

What you'll build:

  1. An ESP32 S3 camera with a PIR motion sensor
  2. A photo saved to the microSD card every time the sensor sees a movement
  3. A quiet time, so one person walking does not fill your card with 50 photos
  4. A camera that works without internet

Hardware Preparation

1×ESP32 S3 N16R8 OV5640 Camera
1×Alternatively, ESP32 S3 N16R8 OV2640 Camera
1×USB Cable Type-A to Type-C (for USB-A PC)
1×USB Cable Type-C to Type-C (for USB-C PC)
1×HC-SR501 Motion Sensor
1×Alternatively, AM312 Mini Motion Sensor
1×microSD Card
1×Jumper Wires
1×Optionally, DC Power Jack
1×Optionally, ESP32 S3 44-Pin Screw Terminal Block
1×Optionally, ESP32 S3 44-Pin Breakout Board
1×Recommended: Screw Terminal Expansion Board for ESP32 S3
1×Recommended: Breakout Expansion Board for ESP32 S3
1×Recommended: Power Splitter for ESP32 S3

Or you can buy the following kits:

1×DIYables Sensor Kit (18 sensors/displays)
Disclosure: Some of the links provided in this section are Amazon affiliate links. We may receive a commission for any purchases made through these links at no additional cost to you.
Additionally, some of these links are for products from our own brand, DIYables .

※ NOTE THAT:

The ESP32 S3 camera board has 40 pins, but the screw terminal block and the breakout board have 44 pins. They still work together. Put the camera board in the center of the 44-pin board, so 2 pins stay free on the left side and 2 pins stay free on the right side.

The screw terminal block lets you connect wires with a screwdriver, with no soldering. The breakout board gives you easy pin headers for a breadboard.

Overview of PIR Motion Sensor

A PIR motion sensor sees the heat of a human body. PIR means "Passive InfraRed". The sensor does not send anything out. It only listens to the infrared light (heat) around it. When a warm body moves in front of it, the heat pattern changes, and the sensor tells the ESP32 S3.

PIR Motion Sensor HC-SR501

For more detail about the motion sensor, see the ESP32 S3 - Motion Sensor tutorial.

Key Specifications

ItemValue
Power5V
Output3.3V HIGH when it sees a movement, LOW when it sees nothing
Distance3 to 7 meters (you can change it)
Angleabout 110°
Warm-up time30 to 60 seconds after power on

※ NOTE THAT:

The output of the HC-SR501 is 3.3V, so you can connect it directly to an ESP32 S3 pin. It is safe.

PIR Motion Sensor Pinout

The sensor has 3 pins:

  1. VCC pin: connect this pin to 5V
  2. GND pin: connect this pin to GND
  3. OUT pin: goes HIGH when the sensor sees a movement
PIR Motion Sensor Pinout

The Two Orange Screws

The HC-SR501 has two small orange screws. Turn them with a small screwdriver:

ScrewWhat it changesTurn right (clockwise)
SensitivityHow far the sensor seesSees further (up to 7 m)
Time delayHow long the output stays HIGHStays HIGH longer (up to 5 minutes)

※ NOTE THAT:

For this project, turn the time delay screw all the way to the left. The output then stays HIGH for only about 3 seconds, so the sensor is ready again quickly.

The Jumper with Two Positions

Next to the pins there is a small jumper:

PositionNameWhat happens
HRepeatThe output stays HIGH while the movement continues
LSingleThe output goes HIGH one time, then LOW

Both positions work with this code. H is the usual choice.

How The Project Works

The code watches the OUT pin all the time. It does not look for "the pin is HIGH". It looks for the moment the pin changes from LOW to HIGH:

if (lastPirState == LOW && pirState == HIGH) { // the movement has just started }

This is important. If the code only looked for HIGH, it would take a new photo hundreds of times per second while a person stands in front of the camera.

Wiring Diagram between PIR Motion Sensor and ESP32 S3

Connect the PIR motion sensor to your ESP32 S3 with three jumper wires.

The wiring diagram between ESP32 S3 PIR Motion Sensor

This image is created using Fritzing. Click to enlarge image

PIR Sensor PinESP32 S3 Pin
VCC5V
GNDGND
OUTGPIO21

Then put the microSD card into the slot on the board.

Safety Notes

The PIR sensor needs 5V on its VCC pin, not 3.3V. With 3.3V it looks like it works, but it gives wrong signals. Its OUT pin gives 3.3V, which is correct for the ESP32 S3.

Do not use a camera pin for the sensor. The camera already uses GPIO4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17 and 18. The card slot uses GPIO38, 39 and 40. GPIO21 is free, so we use it.

Arduino IDE Settings for ESP32 S3 Camera

The camera does not work with the default settings of the Arduino IDE. You must change some items in the Tools menu before you upload the code.

Tools Menu ItemSelect This
BoardESP32S3 Dev Module
Flash Size16MB (128Mb)
PSRAMOPI PSRAM
Partition SchemeHuge APP (3MB No OTA/1MB SPIFFS)
CPU Frequency240MHz (WiFi)
USB ModeHardware CDC and JTAG
Upload ModeUART0 / Hardware CDC
Upload Speed921600
USB CDC On BootEnabled

※ NOTE THAT:

PSRAM must be OPI PSRAM, and Partition Scheme must be Huge APP. Change Flash Size first, because the list of Partition Scheme options changes after that.

How To Program the Motion Camera

  • Set the sensor pin as an input.
#define PIR_PIN 21 pinMode(PIR_PIN, INPUT);
  • Wait for the sensor after you start the board. A PIR sensor needs 30 to 60 seconds to learn the heat of the room. Before that, it gives false alarms.
Serial.println("The PIR sensor is warming up. Please wait 30 seconds and do not move."); delay(30000);
  • Look for the moment the pin changes from LOW to HIGH.
int pirState = digitalRead(PIR_PIN); if (lastPirState == LOW && pirState == HIGH) { Serial.println("Motion detected!"); takeAndSavePhoto(); } lastPirState = pirState; // remember the state for the next time
  • Add a quiet time. Without it, one person walking slowly makes ten photos.
if (millis() - lastPhotoTime >= QUIET_TIME) { takeAndSavePhoto(); lastPhotoTime = millis(); }
camera_fb_t *fb = esp_camera_fb_get(); File file = SD_MMC.open("/motion_00001.jpg", FILE_WRITE); file.write(fb->buf, fb->len); file.close(); esp_camera_fb_return(fb);

ESP32 S3 Code

The following code saves a photo named motion_00001.jpg, motion_00002.jpg, and so on, every time the sensor sees a movement. The quiet time is 5 seconds. To change it, change the line #define QUIET_TIME 5000.

/* * This ESP32 S3 code was developed by newbiely.com * * This ESP32 S3 code is made available for public use without any restriction * * For comprehensive instructions and wiring diagrams, please visit: * https://newbiely.com/tutorials/esp32-s3/esp32-s3-camera-save-photo-to-sd-card-when-motion-detected */ #include "esp_camera.h" #include "FS.h" #include "SD_MMC.h" #define PIR_PIN 21 // the PIR motion sensor is connected to GPIO21 #define QUIET_TIME 5000 // wait 5 seconds before the next photo (5000 milliseconds) // Camera pins of the ESP32-S3-N16R8 camera board. // The same pins work for the OV2640, OV3660 and OV5640 versions. #define PWDN_GPIO_NUM -1 #define RESET_GPIO_NUM -1 #define XCLK_GPIO_NUM 15 #define SIOD_GPIO_NUM 4 #define SIOC_GPIO_NUM 5 #define Y9_GPIO_NUM 16 #define Y8_GPIO_NUM 17 #define Y7_GPIO_NUM 18 #define Y6_GPIO_NUM 12 #define Y5_GPIO_NUM 10 #define Y4_GPIO_NUM 8 #define Y3_GPIO_NUM 9 #define Y2_GPIO_NUM 11 #define VSYNC_GPIO_NUM 6 #define HREF_GPIO_NUM 7 #define PCLK_GPIO_NUM 13 // Pins of the microSD card slot on the board #define SD_CLK_GPIO_NUM 39 #define SD_CMD_GPIO_NUM 38 #define SD_D0_GPIO_NUM 40 int photoNumber = 1; int lastPirState = LOW; unsigned long lastPhotoTime = 0; // Returns the name of the camera sensor that is installed on the board const char *getSensorName(int pid) { switch (pid) { case OV2640_PID: return "OV2640 (2 MP, max UXGA 1600x1200)"; case OV3660_PID: return "OV3660 (3 MP, max QXGA 2048x1536)"; case OV5640_PID: return "OV5640 (5 MP, max QSXGA 2592x1944)"; default: return "Unknown sensor"; } } bool initCamera() { camera_config_t config; config.ledc_channel = LEDC_CHANNEL_0; config.ledc_timer = LEDC_TIMER_0; config.pin_d0 = Y2_GPIO_NUM; config.pin_d1 = Y3_GPIO_NUM; config.pin_d2 = Y4_GPIO_NUM; config.pin_d3 = Y5_GPIO_NUM; config.pin_d4 = Y6_GPIO_NUM; config.pin_d5 = Y7_GPIO_NUM; config.pin_d6 = Y8_GPIO_NUM; config.pin_d7 = Y9_GPIO_NUM; config.pin_xclk = XCLK_GPIO_NUM; config.pin_pclk = PCLK_GPIO_NUM; config.pin_vsync = VSYNC_GPIO_NUM; config.pin_href = HREF_GPIO_NUM; config.pin_sccb_sda = SIOD_GPIO_NUM; config.pin_sccb_scl = SIOC_GPIO_NUM; config.pin_pwdn = PWDN_GPIO_NUM; config.pin_reset = RESET_GPIO_NUM; config.xclk_freq_hz = 20000000; config.pixel_format = PIXFORMAT_JPEG; config.frame_size = FRAMESIZE_UXGA; // 1600x1200 - a big, sharp photo config.jpeg_quality = 12; // 0 to 63, a lower number means a better image config.fb_count = 1; config.fb_location = CAMERA_FB_IN_DRAM; config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; if (psramFound()) { config.fb_location = CAMERA_FB_IN_PSRAM; config.jpeg_quality = 10; config.fb_count = 2; config.grab_mode = CAMERA_GRAB_LATEST; } else { config.frame_size = FRAMESIZE_QVGA; Serial.println("WARNING: PSRAM not found. Please set PSRAM to OPI PSRAM in the Tools menu."); } esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.printf("Camera init failed with error 0x%x\n", err); return false; } sensor_t *s = esp_camera_sensor_get(); Serial.printf("Camera sensor: %s\n", getSensorName(s->id.PID)); // Each sensor needs slightly different settings switch (s->id.PID) { case OV2640_PID: s->set_vflip(s, 0); s->set_hmirror(s, 0); break; case OV3660_PID: s->set_vflip(s, 1); // the OV3660 image is upside down by default s->set_brightness(s, 1); // make the image a little brighter s->set_saturation(s, -2); // make the colors a little softer break; case OV5640_PID: s->set_vflip(s, 0); s->set_hmirror(s, 0); break; default: break; } return true; } bool initSDCard() { SD_MMC.setPins(SD_CLK_GPIO_NUM, SD_CMD_GPIO_NUM, SD_D0_GPIO_NUM); // true means 1-bit mode. The camera uses the other pins, so we cannot use 4-bit mode. if (!SD_MMC.begin("/sdcard", true)) { Serial.println("SD card not found. Please check the card and push it in again."); return false; } if (SD_MMC.cardType() == CARD_NONE) { Serial.println("No SD card inside the slot"); return false; } Serial.printf("SD card size: %llu MB\n", SD_MMC.cardSize() / (1024 * 1024)); return true; } // Makes a file name like /motion_00001.jpg String makeFileName(int number) { char name[32]; sprintf(name, "/motion_%05d.jpg", number); return String(name); } // Looks for the first free file name, so old photos are never lost void findFirstFreeNumber() { while (SD_MMC.exists(makeFileName(photoNumber))) photoNumber++; Serial.printf("The next photo will be motion_%05d.jpg\n", photoNumber); } void takeAndSavePhoto() { camera_fb_t *fb = esp_camera_fb_get(); if (!fb) { Serial.println("Photo capture failed"); return; } String fileName = makeFileName(photoNumber); File file = SD_MMC.open(fileName.c_str(), FILE_WRITE); if (file) { file.write(fb->buf, fb->len); file.close(); Serial.printf("Saved %s (%u bytes)\n", fileName.c_str(), fb->len); photoNumber++; } else { Serial.printf("Cannot write the file %s\n", fileName.c_str()); } esp_camera_fb_return(fb); // give the memory back to the camera } void setup() { Serial.begin(115200); delay(1000); pinMode(PIR_PIN, INPUT); if (!initCamera()) { Serial.println("Stopped"); while (true) delay(1000); } if (!initSDCard()) { Serial.println("Stopped"); while (true) delay(1000); } findFirstFreeNumber(); Serial.println("The PIR sensor is warming up. Please wait 30 seconds and do not move."); delay(30000); Serial.println("Ready. Waiting for motion..."); } void loop() { int pirState = digitalRead(PIR_PIN); // LOW to HIGH means the sensor has just seen a movement if (lastPirState == LOW && pirState == HIGH) { if (millis() - lastPhotoTime >= QUIET_TIME) { Serial.println("Motion detected!"); takeAndSavePhoto(); lastPhotoTime = millis(); } } lastPirState = pirState; }

Detailed Instructions

  1. New to ESP32 S3? Complete our Getting Started with ESP32 S3 guide first.
  2. Format your microSD card as FAT32 on your computer.
  3. Unplug the USB cable. Put the card into the slot, and wire the PIR sensor as in the table above.
  4. Turn the time delay screw of the sensor all the way to the left.
  5. Copy the above code and paste it into the Arduino IDE.
  6. Change the settings in the Tools menu. See the table above.
  7. Compile and upload the code to the ESP32 S3 board by clicking the Upload button in Arduino IDE.
Arduino IDE Upload Code
  1. Open the Serial Monitor in Arduino IDE.
How to open serial monitor on Arduino IDE
  1. Press the RESET button one time, then leave the room for 30 seconds. The sensor is warming up.
  2. Come back and walk in front of the camera.
  3. Look at the Serial Monitor. You see Motion detected! and the name of the photo.
  4. Pro Tip: Point the sensor away from windows, heaters and air conditioners. Moving warm air makes false alarms.

Serial Monitor

The following readings were captured in 2026:

Newbiely | Arduino IDE 2.3.8
──
File
Edit
Sketch
Tools
Help
ESP32S3 Dev Module
Newbiely.ino
···
8 Serial.println("Hello World!");
Output
Serial Monitor
Message (Enter to send message to 'ESP32S3 Dev Module' on 'COM15')
New Line
9600 baud
[2026-09-23 12:00:01] Camera sensor: OV5640 (5 MP, max QSXGA 2592x1944) [2026-09-23 12:00:01] SD card size: 15193 MB [2026-09-23 12:00:01] The next photo will be motion_00001.jpg [2026-09-23 12:00:01] The PIR sensor is warming up. Please wait 30 seconds and do not move. [2026-09-23 12:00:31] Ready. Waiting for motion... [2026-09-23 12:01:12] Motion detected! [2026-09-23 12:01:13] Saved /motion_00001.jpg (184320 bytes) [2026-09-23 12:01:45] Motion detected! [2026-09-23 12:01:46] Saved /motion_00002.jpg (183104 bytes)
Ln 11, Col 1
ESP32S3 Dev Module on COM15
2

Troubleshooting

The sensor sees a movement all the time, even in an empty room.

Try these, one by one:

  • Wait one full minute after you start the board. The sensor is still warming up.
  • Turn the sensitivity screw a little to the left.
  • Point the sensor away from windows, heaters, air conditioners and lamps.
  • Check that VCC is on 5V, not on 3.3V. This is the most common mistake.

The sensor never sees anything.

  • Check the wire on the OUT pin. It must go to GPIO21.
  • Turn the sensitivity screw a little to the right.
  • Walk across the front of the sensor, not straight towards it. A PIR sensor sees side movement much better.

One person makes five or six photos.

Increase QUIET_TIME from 5000 to 15000 (15 seconds).

The Serial Monitor shows SD card not found.

Format the card as FAT32, use a card of 32 GB or smaller, and push the card in again.

The photo is dark.

A PIR sensor also works in the dark, but the camera does not. Add a light, or a lamp that switches on with the movement.

The Serial Monitor shows nothing after the boot messages.

If your USB cable is in the port named UART, set USB CDC On Boot to Disabled. If it is in the port named USB, set it to Enabled.

Applications

This camera keeps everything on the microSD card, so it works with no WiFi, no internet and no monthly payment. Here are practical projects you can build with this code:

  1. Offline security camera: See who came into your room while you were away, with no internet connection at all.
  2. Package delivery camera: Take a photo when the delivery person arrives at your door.
  3. Wildlife and trail camera: Photograph cats, birds, foxes or deer in your garden or in the forest at night.
  4. Shop alarm record: Keep a picture of every person who enters after closing time.
  5. Garage and bicycle watcher: Watch your bicycle, your car or your motorbike in a garage with no WiFi.
  6. Holiday home camera: Leave it in an empty house for weeks, and read the card when you come back.
  7. Building site camera: Keep a photo of everybody who enters the site outside working hours.
  8. Farm and field camera: Watch a gate or a barn far away from any router.
  9. Classroom science project: Show students how a sensor, a camera and a memory card work together.
  10. Backup for a WiFi camera: Keep photos on the card even when the internet is down.

Video Tutorial

Watch the step-by-step video walkthrough for this ESP32 S3 project below.

Challenges

  1. Beginner: Add an LED that turns on for two seconds every time a photo is saved.
  2. Intermediate: Save three photos in a row on every movement, so you see the direction of the person.
  3. Advanced: Write a text file on the card with one line per event, for example the photo name and the time since the board started.

※ OUR MESSAGES

  • As freelancers, We are AVAILABLE for HIRE. See how to outsource your project to us
  • Please feel free to share the link of this tutorial. However, Please do not use our content on any other websites. We invested a lot of effort and time to create the content, please respect our work!