ESP32 cam pictures to SD card

ESP32 cam pictures to SD card cover

Now that you know how to capture a picture with your ESP32 camera, you may want to store those pictures to a permanent medium.

The ESP32 camera ships with an internal storage (up to 16MB on some boards) that you can in part fill with files. Most of the times, though, you will insert an external SD card for both larger storage space and easier accessibility.

This page will show you how to easily interact with the SD card of your ESP32 camera to save images on it in 3 different ways:

  1. manually setting the filename
  2. using an always incrementing counter that survives reboots
  3. using NTP (Network Time Protocol) to use the current timestamp as filename

As an added bonus, when using the NTP timestamping, I will show you how easy it is to create a nested folder structure to save pictures under the current date folder (e.g. /20231001/20231001T100000.jpg, /20231001/20231001T110000.jpg, ...) to keep your files organized.

Hardware requirements

This project works with S3 and non-S3 boards.

If you don't have a board with SD card slot, you can still run the below sketches by replacing each occurrence of sdmmc with spiffs.

#include <eloquent_esp32cam/extra/esp32/fs/sdmmc.h>
// becomes
#include <eloquent_esp32cam/extra/esp32/fs/spiffs.h>

if (sdmmc.save(camera.frame).to(filename).isOk()) {}
// becomes
if (spiffs.save(camera.frame).to(filename).isOk()) {}
1 2 3 4 5 6 7

Software requirements

This project is tested on EloquentEsp32Cam version 2.0.5

Arduino IDE Tools configuration for ESP32S3

!!! Install ESP32 core version 2.x, version 3.x won't work !!!

Board ESP32S3 Dev Module
Upload Speed 921600
USB Mode Hardware CDC and JTAG
USB CDC On Boot Disabled
USB Firmware MSC On Boot Disabled
USB DFU On Boot Disabled
Upload Mode UART0 / Hardware CDC
CPU Frequency 240MHz (WiFi)
Flash Mode QIO 80MHz
Flash Size 4MB (32Mb)
Partition Scheme Huge APP (3MB No OTA/1MB SPIFFS)
Core Debug Level Info
PSRAM OPI PSRAM
Arduino Runs On Core 1
Events Run On Core 1
Erase All Flash Before Sketch Upload Disabled
JTAG Adapter Disabled

Save frame with manual filename

If you're manually interacting with the ESP32 camera or you want to use your own naming scheme, you can easily choose the name of your file with the following code.

See source

Filename: Manual_Name.ino

/**
 * SDMMC file storage: manual filename
 * 
 * This sketch shows how to save a picture on the SD Card
 * filesystem by specifying the filename manually
 *
 * Open the Serial Monitor and enter 'capture' (without quotes)
 * to capture a new image and save it to SD
 *
 * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO"
 * to turn on debug messages
 */

#include <eloquent_esp32cam.h>
#include <eloquent_esp32cam/extra/esp32/fs/sdmmc.h>

using namespace eloq;


void setup() {
    delay(3000);
    Serial.begin(115200);
    Serial.println("___SAVE PIC TO SD CARD___");

    // camera settings
    // replace with your own model!
    camera.pinout.freenove_s3();
    camera.brownout.disable();
    camera.resolution.vga();
    camera.quality.high();

    // you can configure each pin of SDMMC (if needed)
    // (delete these lines if you are not sure)
    sdmmc.pinout.clk(39);
    sdmmc.pinout.cmd(38);
    sdmmc.pinout.d0(40); 
    // or shorter
    sdmmc.pinout.freenove_s3();

    // init camera
    while (!camera.begin().isOk())
        Serial.println(camera.exception.toString());

    // init SD
    while (!sdmmc.begin().isOk())
        Serial.println(sdmmc.exception.toString());

    Serial.println("Camera OK");
    Serial.println("SD card OK");
    Serial.println("Enter the filename where to save the picture");
}


void loop() {
    // await for filename from the Serial Monitor
    if (!Serial.available())
        return;

    String filename = Serial.readStringUntil('\n');
    filename.trim();

    if (!filename.endsWith(".jpg") && !filename.endsWith(".jpeg"))
        filename = filename + ".jpg";

    // capture picture
    if (!camera.capture().isOk()) {
        Serial.println(camera.exception.toString());
        return;
    }

    // save to SD
    if (sdmmc.save(camera.frame).to(filename).isOk()) {
      Serial.print("File written to ");
      Serial.println(sdmmc.session.lastFilename);
    }
    else Serial.println(sdmmc.session.exception.toString());

    // you can also save under a nested folder
    if (sdmmc.save(camera.frame).inside("myfolder").to(filename).isOk()) {
      Serial.print("File written to ");
      Serial.println(sdmmc.session.lastFilename);
    }
    else Serial.println(sdmmc.session.exception.toString());

    // restart the loop
    Serial.println("Enter another filename");
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87

The code may seem lengthy, but it is for the most part comments and configuration lines.

Here is the breakdown.

Configure SD MMC

The EloquentEsp32Cam has a driver for the SD MMC library. I chose this library because it requires no configuration in most of the cases, it just works out of the box.

In the cases where it does not (eg. Freenove S3 camera), you can configure the pins with the following lines.

// replace with your board actual pins
sdmmc.pinout.clk(39);
sdmmc.pinout.cmd(38);
sdmmc.pinout.d0(40); 
// or shorter
sdmmc.pinout.freenove_s3();
1 2 3 4 5 6

When using the AiThinker camera, for example, you can just delete these lines.

Save frame under root directory

After you input your desired filename in the Serial monitor, this is the line that saves the current frame under the root directory of the SD card.

if (sdmmc.save(camera.frame).to(filename).isOk()) {}
1

Save frame under nested directory

If you want to keep your files organized under a tree structure, you can nest a file under your desired folder by adding inside(folder-name).

if (sdmmc.save(camera.frame).inside("myfolder").to(filename).isOk()) {}
1

Here is the Serial Monitor log for this sketch.

sd-manual-name.jpg 781.16 KB

Save frame with incremental filename

Many times you will use the ESP32 camera in a autonomous deployment (e.g. a timelapse setup). In this case you may not want to bother with manually choosing a filename for each frame. An incremental filename may be all you need.

By incremental naming I mean that your pictures will be saved as 0000001.jpg, 0000002.jpg and so on.

Getting this result is as easy as re-using most of the above sketch and replacing

if (sdmmc.save(camera.frame).to(filename).isOk()) {}
1

with

if (sdmmc.save(camera.frame).to("", "jpg").isOk()) {}
1

In the first case, the to function accepted the complete name of the file to write. 

In the second case, it accepts a file name and file extension. If the filename is empty, it will fill it with the incremental counter. The best part is that the counter survives reboots, so you will never risk to overwrite existing images.

As before, you can nest this file under a specific folder.

if (sdmmc.save(camera.frame).inside("myfolder").to("", "jpg").isOk()) {}
1

This same approach works with any kind of file, really, not just images.

// save text to .txt file with incremental name
if (sdmmc.save("Hello world!").to("", "txt").isOk()) {}
1 2

Here is the Serial Monitor.

sd-incremental.jpg 617.01 KB
And here's the full sketch.

See source

Filename: Incremental_Name.ino

/**
 * SDMMC file storage: incremental filename
 * 
 * This sketch shows how to save a picture on the SD Card
 * filesystem by using an incremental filename that
 * persists across reboots
 *
 * Open the Serial Monitor and enter 'capture' (without quotes)
 * to capture a new image and save it to SD
 *
 * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO"
 * to turn on debug messages
 */

#include <eloquent_esp32cam.h>
#include <eloquent_esp32cam/extra/esp32/fs/sdmmc.h>

using namespace eloq;


void setup() {
    delay(3000);
    Serial.begin(115200);
    Serial.println("___SAVE PIC TO SD CARD___");

    // camera settings
    // replace with your own model!
    camera.pinout.freenove_s3();
    camera.brownout.disable();
    camera.resolution.vga();
    camera.quality.high();

    // you can configure each pin of SDMMC (if needed)
    // (delete these lines if you're not sure)
    sdmmc.pinout.clk(39);
    sdmmc.pinout.cmd(38);
    sdmmc.pinout.d0(40);

    // init camera
    while (!camera.begin().isOk())
        Serial.println(camera.exception.toString());

    // init SD
    while (!sdmmc.begin().isOk())
        Serial.println(sdmmc.exception.toString());

    Serial.println("Camera OK");
    Serial.println("SD card OK");
    Serial.println("Enter 'capture' to capture a new picture");
}


void loop() {
    // await for "capture" from the Serial Monitor
    if (!Serial.available())
        return;

    if (Serial.readStringUntil('\n') != "capture") {
        Serial.println("I only understand 'capture'");
        return;
    }

    // capture picture
    if (!camera.capture().isOk()) {
        Serial.println(camera.exception.toString());
        return;
    }

    // save under root folder
    if (sdmmc.save(camera.frame).to("", "jpg").isOk()) {
      Serial.print("File written to ");
      Serial.println(sdmmc.session.lastFilename);
    }
    else Serial.println(sdmmc.session.exception.toString());

    // save under nested folder
    if (sdmmc.save(camera.frame).inside("myfolder").to("", "jpg").isOk()) {
      Serial.print("File written to ");
      Serial.println(sdmmc.session.lastFilename);
    }
    else Serial.println(sdmmc.session.exception.toString());

    // restart the loop
    Serial.println("Enter 'capture' to capture a new picture");
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85

Save frame with NTP timestamping

In the most demanding scenarios, you may want to keep track of when the picture was taken.

If you have access to WiFi, you can leverage NTP (Network Time Protocol) to keep track of time and name your files based on current date and time. This requires a bit of configuration before you can use it. The full sketch is listed below.

See source

Filename: NTP_Name.ino

/**
 * SDMMC file storage: NTP filename
 * 
 * This sketch shows how to save a picture on the SD Card
 * filesystem by generating a filename using current time
 *
 * Open the Serial Monitor and enter 'capture' (without quotes)
 * to capture a new image and save it to SD
 *
 * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO"
 * to turn on debug messages
 */

// if you define WIFI_SSID and WIFI_PASS before importing the library
// you can call wifi.connect() instead of wifi.connect(ssid, password)
#define WIFI_SSID "SSID"
#define WIFI_PASS "PASSWORD"

#include <eloquent_esp32cam.h>
#include <eloquent_esp32cam/extra/esp32/ntp.h>
#include <eloquent_esp32cam/extra/esp32/fs/sdmmc.h>

using namespace eloq;


void setup() {
    delay(3000);
    Serial.begin(115200);
    Serial.println("___SAVE PIC TO SD CARD___");

    // camera settings
    // replace with your own model!
    camera.pinout.freenove_s3();
    camera.brownout.disable();
    camera.resolution.vga();
    camera.quality.high();

    // if connected to the internet, try to get time from NTP
    // you can set your timezone offset from Greenwich
    ntp.offsetFromGreenwhich(0);
    // or any of
    ntp.cst();
    ntp.ist();
    ntp.eest();
    ntp.cest();
    ntp.bst();
    ntp.west();
    ntp.cet();
    ntp.gmt();
    ntp.edt();
    ntp.pdt();

    // enable/disable daylight saving
    ntp.isntDaylight();
    ntp.isDaylight();
    
    ntp.server("pool.ntp.org");

    // you can configure each pin of SDMMC (if needed)
    // (delete these lines if not sure)
    sdmmc.pinout.clk(39);
    sdmmc.pinout.cmd(38);
    sdmmc.pinout.d0(40);

    // init camera
    while (!camera.begin().isOk())
        Serial.println(camera.exception.toString());

    // init SD
    while (!sdmmc.begin().isOk())
        Serial.println(sdmmc.exception.toString());

    // connect to WiFi to sync NTP
    while (!wifi.connect().isOk())
      Serial.println(wifi.exception.toString());

    // get NTP time
    while (!ntp.begin().isOk())
      Serial.println(ntp.exception.toString());

    Serial.println("Camera OK");
    Serial.println("SD card OK");
    Serial.println("NTP OK");
    Serial.print("Current time is ");
    Serial.println(ntp.datetime());
    Serial.println("Enter 'capture' to capture a new picture");
}


void loop() {
    // await for "capture" from the Serial Monitor
    if (!Serial.available())
        return;

    if (Serial.readStringUntil('\n') != "capture") {
        Serial.println("I only understand 'capture'");
        return;
    }

    // capture picture
    if (!camera.capture().isOk()) {
        Serial.println(camera.exception.toString());
        return;
    }

    // save under root directory
    if (sdmmc.save(camera.frame).to(ntp.datetime(), "jpg").isOk()) {
      Serial.print("File written to ");
      Serial.println(sdmmc.session.lastFilename);
    }
    else Serial.println(sdmmc.session.exception.toString());

    // save under nested directory
    String date = ntp.date();
    String datetime = ntp.datetime();

    if (sdmmc.save(camera.frame).inside(date).to(datetime, "jpg").isOk()) {
      Serial.print("File written to ");
      Serial.println(sdmmc.session.lastFilename);
    }
    else Serial.println(sdmmc.session.exception.toString());
    
    // restart the loop
    Serial.println("Enter 'capture' to capture a new picture");
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125

In this case, we added a new section to configure the NTP server.

// if connected to the internet, try to get time from NTP
// you can set your timezone offset from Greenwich
ntp.offsetFromGreenwhich(0);
// or any of
ntp.cst();
ntp.ist();
ntp.eest();
...

// enable/disable daylight saving
ntp.isntDaylight();
ntp.isDaylight();
    
ntp.server("pool.ntp.org");
1 2 3 4 5 6 7 8 9 10 11 12 13 14

NTP needs to know where you're located to give the correct time. This is achieved by setting your offset from the Greenwich time (in hours). A few helper methods are provided for easier setup.

Then you need to configure if you're under daylight saving time or not.

The line

ntp.server("pool.ntp.org");
1

lets you configure a specific NTP server. If you don't have any specific reason to change it, you can delete this line since pool.ntp.org is the default server.

Timestamped filename

To use the current timestamp as filename, it is as easy as

if (sdmmc.save(camera.frame).to(ntp.datetime(), "jpg").isOk()) {}
1

If you remember from above, to accepts a filename and an extension. In this case we're using the current datetime as filename and jpg as extension. The filename will look something like 20231003T102005.jpg, where 20231003 is the date in YYYYMMDD format, T is the time separator, and 102005 is the time in HHMMSS format.

If you prefer to categorize the images under a folder structure that tracks the date, it couldn't be easier.

if (sdmmc.save(camera.frame).inside(ntp.date()).to(ntp.datetime(), "jpg").isOk()) {}
// or, if you want to name the file with the time only
if (sdmmc.save(camera.frame).inside(ntp.date()).to(ntp.time(), "jpg").isOk()) {}

This will generate a folder structure like below:
|- 20231001
   |- 20231001T100000.jpg
   |- 20231001T110000.jpg
   |- 20231001T120000.jpg
|- 20231002
   |- 20231002T100000.jpg
   |- 20231002T110000.jpg
   |- 20231002T120000.jpg
...
1 2 3 4 5 6 7 8 9 10 11 12 13 14

It feels like magic, doesn't it?

sd-ntp.jpg 756.64 KB

Conclusion

In this page you learned 3 ways to save the current frame of your ESP32 camera to an external SD card:

  1. manually setting the filename
  2. using an always incrementing counter that survives reboots
  3. using NTP (Network Time Protocol) to use the current timestamp as filename

After configuring the SD and (optionally) the NTP server, it only takes 1 line of code to actually save the frame. 

This gives you complete freedom over file naming scheme and folder structuring. One thing less to worry about in your next project!

Become an ESP32-CAM EXPERT

Subscribe to my newsletter

Join 1181 businesses and hobbysts skyrocketing their Arduino + ESP32 skills twice a month