Skip to Content

SD Card Module with ESP32

SD Card Module with ESP32

In this tutorial, you will learn how to connect a Micro SD card module to the ESP32 and perform essential file operations. We will cover listing directories, creating and deleting files and folders, reading and writing data, and more. By the end, you’ll have a solid foundation to integrate SD card storage into your own projects.

Required Parts

Below you will find the components required for this tutorial. I used an older ESP32 board, which has been deprecated but you can still get it for a very low price. But any other ESP32 is okay too.

Furthermore we need a Micro SD Card Module. Any module with a SPI interface and 3.3V logic will work. If you want to connect the SD Card Module to an Arduino UNO you need a module with 5V logic or a logic converter. I picked a very simple, small module without a logic converter, since we will be connecting it to an ESP32 with 3.3V logic.

Micro SD Card Module

Micro SD Card 8GB

ESP32 lite Lolin32

ESP32 lite

USB data cable

USB Data Cable

Dupont wire set

Dupont Wire Set

Half_breadboard56a

Breadboard

Basics of SD Card Reader Modules

MicroSD card modules serve as an interface between a microcontroller, like the ESP32, and a MicroSD card. They enable the microcontroller to read from and write data to the card, effectively expanding storage capacity for logging, media, or configuration files.

How MicroSD Card Modules Work

At their core, MicroSD cards communicate using the SPI (Serial Peripheral Interface) protocol. The module contains a MicroSD card socket and a level shifter circuit if needed, to match voltage levels between the card and the microcontroller. When connected, the ESP32 acts as the SPI master, sending commands and receiving data from the MicroSD card, which acts as the SPI slave.

The SPI bus uses separate lines for data input (MOSI), data output (MISO), clock (SCK), and chip select (CS). This full-duplex communication allows fast and reliable data transfer, essential for file system operations.

Why SPI and Not I2C?

SPI is preferred over I2C for MicroSD card communication due to its higher data transfer speeds and simpler protocol. I2C is a two-wire bus designed for short-distance, low-speed communication, typically up to 400 kHz or 1 MHz in fast modes. In contrast, SPI can easily operate at several MHz, which is critical for reading and writing large files efficiently.

Moreover, SPI supports full-duplex communication, allowing simultaneous sending and receiving of data, whereas I2C is half-duplex. The MicroSD card specification explicitly supports SPI mode, making SPI the natural choice.

SPI Pins

The following table shows the pins needed for SPI:

SPI PinFunctionDescription
MOSIMaster Out Slave InCarries data from ESP32 to MicroSD card
MISOMaster In Slave OutCarries data from MicroSD card to ESP32
SCKSerial ClockSynchronizes data transmission
CSChip Select (Slave Select)Selects the MicroSD card for communication

The ESP32 typically allows flexible assignment of these pins via its GPIO matrix, but common defaults are GPIO 23 for MOSI, GPIO 19 for MISO, GPIO 18 for SCK, and GPIO 5 for CS.

Types of MicroSD Card Modules: 5V vs 3.3V

There are two main types of MicroSD card modules based on voltage compatibility. Most Arduino boards operate at 5V logic levels, so modules designed for Arduino include onboard level shifters and voltage regulators to safely interface the 3.3V MicroSD cards with 5V signals and power.

The picture below shows the front and back of a 5V MicroSD card module. You can see the Voltage Regulator and Logic Level Shifter IC:

5V MicroSD card module
5V MicroSD card module

However, if your microcontroller already operates at 3.3V logic levels (ESP32), a voltage regulator and level shifter are not required. You can then use a simpler, cheaper SD Card reader module. The following picture shows the front and back of a typical 3.3V MicroSD card module:

3.3V MicroSD card module
3.3V MicroSD card module

If you look at the 3.3V board closely you will see that there are no ICs for voltage regulation or level shifting. But there should be 10K pull-up resistors for the SPI interface:

Pull-up resistors on MicroSD card module
Pull-up resistors on MicroSD card module

Avoid boards that don’t have these pull-up resistors and do not connect a 3.3V MicroSD card module to a 5V Arduino!

MicroSD Card Sizes and Types

MicroSD cards come in various storage capacities and speed classes. The most common sizes range from 2GB up to 1TB, with the following classifications:

TypeNameSize
SDSC Standard CapacityUp to 2GB
SDHC High Capacity4GB to 32GB
SDXC Extended Capacity64GB to 2TB

The ESP32’s SD library supports cards formatted with FAT32 file systems.

MicroSD Card Speed Classes

Speed classes define the minimum sustained write speed of the card, which is critical for applications like video recording or data logging.

Speed ClassMinimum Write SpeedDescription
Class 22 MB/sSuitable for standard definition video
Class 44 MB/sSuitable for high definition video
Class 66 MB/sSuitable for HD video recording
Class 1010 MB/sSuitable for full HD video
UHS-I U110 MB/sUltra High Speed, suitable for HD video
UHS-I U330 MB/sUltra High Speed, suitable for 4K video

For most ESP32 projects, a Class 10 or UHS-I U1 card provides a good balance of speed and cost. Higher speed cards may not always yield faster performance due to microcontroller and SPI bus limitations.

Connecting SD Card Module to ESP32

Connecting your SD Card Module to the ESP32 via SPI is easy. The following picture shows the wiring between an ESP32 Lite and a 3.3V MicroSD Card Module:

Connecting MicroSD Card Module to ESP32 Lite
Connecting MicroSD Card Module to ESP32 Lite

And for convenience here is the pin mapping between the SD Card Module and the ESP32:

SD Card Module PinESP32 Pin
VCC3.3V
GNDGND
MISOGPIO 19
MOSIGPIO 23
SCK/CLKGPIO 18
CS (Chip Select)GPIO 5

Make sure to use the 3.3V pin on the ESP32 to power the SD Card Module. The SD card operates at 3.3V, and applying 5V can damage it.

Formatting the SD Card

Before using your SD card with the ESP32, it’s important to ensure it is properly formatted. Most SD cards come pre-formatted with the FAT32 file system, which is compatible with the ESP32 SD library.

However, if your card is new or has been used with other devices, formatting can be required. You can format the SD card using your computer:

Windows

  1. Insert the SD card into your PC.
  2. Open File Explorer and right-click the SD card drive.
  3. Select Format.
  4. Choose FAT32 as the file system.
  5. Click Start to format.

macOS

  1. Insert the SD card.
  2. Open Disk Utility.
  3. Select the SD card from the sidebar.
  4. Click Erase.
  5. Choose MS-DOS (FAT) as the format.
  6. Click Erase to format.

Linux

Use the mkfs command in the terminal. Replace /dev/sdX1 with your SD card’s device identifier:

  sudo mkfs.vfat -F 32 /dev/sdX1

Important Tips

  • Backup any important data before formatting.
  • Don’t use exFAT or NTFS, as these are not supported by the ESP32 SD library.
  • After formatting, safely eject the card before inserting it into the SD card module.

Maximum SD Card Size

Under Windows the maximum card size you can format via the file explorer is 32GB. It is possible to format higher capacity SD cards as FAT32 and to use them with an ESP32. However, you will need to use different software tools. For instance, there are FAT32Format or guiformat-x64.

SD Class

Most functions related to SD Cards can be found in the SD library and specifically in the SDClass. The code below shows the SDClass with its function declarations. Have a quick look at it and in the next sections you will learn how to use the different functions of the SDClass.

class SDClass {
    private:
      Sd2Card card;
      SdVolume volume;
      SdFile root;

      SdFile getParentDir(const char *filepath, int *indx);
	  
    public:
      // This needs to be called to set up the connection to the SD card
      // before other methods are used.
      bool begin(uint8_t csPin = SD_CHIP_SELECT_PIN);
      bool begin(uint32_t clock, uint8_t csPin);

      //call this when a card is removed. It will allow you to insert and initialise a new card.
      void end();

      // Note that currently only one file can be open at a time.
      File open(const char *filename, uint8_t mode = FILE_READ);
      File open(const String &filename, uint8_t mode = FILE_READ) {
        return open(filename.c_str(), mode);
      }

      bool exists(const char *filepath);
      bool exists(const String &filepath) {
        return exists(filepath.c_str());
      }

      bool mkdir(const char *filepath);
      bool mkdir(const String &filepath) {
        return mkdir(filepath.c_str());
      }

      bool remove(const char *filepath);
      bool remove(const String &filepath) {
        return remove(filepath.c_str());
      }

      bool rmdir(const char *filepath);
      bool rmdir(const String &filepath) {
        return rmdir(filepath.c_str());
      }

    private:
      int fileOpenMode;

      friend class File;
      friend bool callback_openPath(SdFile&, const char *, bool, void *);
  };

  extern SDClass SD;
};

Initializing SD Card Module

Before you can work with files on your SD card, you need to initialize the SD card module with your ESP32. Initialization sets up the communication between the ESP32 and the SD card, ensuring the card is ready for file operations. The following code shows you how to do that:

#include "FS.h"
#include "SD.h"
#include "SPI.h"

void setup() {
  Serial.begin(115200);
  if(!SD.begin()) {
    Serial.println("Card Mount Failed");
    return;
  }
  Serial.println("SD Card Initialized");
}

void loop() { }

The SD.begin() function returns true if the card is successfully initialized. If it returns false, the card might not be connected properly, or it could be formatted incorrectly. Also check the wiring of the SD Card module with the ESP32.

Note that SD.begin() has parameters you can set, if the default is not working for you:

SD.begin(uint8_t ssPin=SS, 
         SPIClass &spi=SPI, 
         uint32_t frequency=4000000, 
         const char * mountpoint="/sd", 
         uint8_t max_files=5, 
         bool format_if_empty=false);

Most importantly, you can change the pins for the SPI interface here. The SD library uses the VSPI SPI pins (23, 19, 18, 5) by default.

To change the pins, redefine the pin constants, create an instance of the SPIClass and pass this instance to the SD.begin() method. Here is an example:

#include "FS.h"
#include "SD.h"
#include "SPI.h"

#define SCK  17
#define MISO 19
#define MOSI 23
#define CS   5

SPIClass spi = SPIClass(VSPI);

void setup() {
  spi.begin(SCK, MISO, MOSI, CS);
  if (!SD.begin(CS, spi)) {
    Serial.println("Card Mount Failed");
  }
}

void loop() {}

Once initialized, the SD card is ready for file operations like reading, writing, and directory management.

Reading SD Card Type

After initializing the SD card module, you can programmatically identify the type of SD card connected. The sd.cardType() function returns an integer representing the card type. Here is a code example that prints the card type to the Serial Monitor:

#include "FS.h"
#include "SD.h"
#include "SPI.h"

void setup() {
  Serial.begin(115200);

  if (!SD.begin()) {
    Serial.println("Card Mount Failed");
    return;
  }

  uint8_t cardType = SD.cardType();
  if (cardType == CARD_NONE) {
    Serial.println("No SD card attached");
  } else if (cardType == CARD_MMC) {
    Serial.println("MMC Card");
  } else if (cardType == CARD_SD) {
    Serial.println("Standard SD Card");
  } else if (cardType == CARD_SDHC) {
    Serial.println("SDHC Card");
  } else {
    Serial.println("Unknown Card Type");
  }
}

void loop() { }

We start by initializing the SD card with SD.begin(). Then, we call SD.cardType() to get the card type and finally, we print the card type to the Serial Monitor.

Getting SD Card information

The SD library also has functions to retrieve the size of an SD card, the total available space and the used space. See the following code:

#include "FS.h"
#include "SD.h"
#include "SPI.h"

void setup() {
  Serial.begin(115200);
  SD.begin();

  uint64_t MB = 1024 * 1024;

  uint64_t cardSize = SD.cardSize();
  Serial.printf("SD Card Size: %lluMB\n", cardSize / MB);

  uint64_t totalBytes = SD.cardSize();
  Serial.printf("Total space: %lluMB\n", totalBytes / MB);

  uint64_t usedBytes = SD.usedBytes();
  Serial.printf("Used space: %lluMB\n", usedBytes / MB);
}

void loop() {}

List Directory

The SD library unfortunately has no function to recursively list all the files and folders stored on an SD card. You will have to implement this function yourself. Here is the code for a printDir() function that prints the folder and file structure to the Serial Monitor:

#include "FS.h"
#include "SD.h"
#include "SPI.h"

void printDir(const char *dirname, uint8_t levels = 0) {
  File root = SD.open(dirname);
  if (!root || !root.isDirectory()) return;

  File file = root.openNextFile();
  while (file) {
    for (int i = 0; i < levels; i++) Serial.print("  ");

    if (file.isDirectory()) {
      Serial.printf("[%s]\n", file.name());
      printDir(file.path(), levels + 1);
    } else {
      Serial.printf("%s (%d)\n", file.name(), file.size());
    }
    file = root.openNextFile();
  }
}

void setup() {
  Serial.begin(115200);
  SD.begin();
  printDir("/");
}

void loop() {}

If you just need the files within a specific folder you can use the following code:

void printDir(const char *dirname) {
  File dir = SD.open(dirname);
  File file = dir.openNextFile();
  while (file) {
    if (!file.isDirectory()) {
      Serial.printf("%s (%d)\n", file.name(), file.size());
    }
    file = dir.openNextFile();
  }
}

Create or Delete Directories

To create or remove directories the SD library offers the functions mkdir() and rmdir(). Here is a code example that first creates a directory and then deletes it:

#include "FS.h"
#include "SD.h"
#include "SPI.h"

void setup() {
  Serial.begin(115200);
  SD.begin();

  const char *path = "/data";
  if (!SD.mkdir(path)) {
    Serial.println("mkdir failed");
  }

  if (!SD.rmdir(path)) {
    Serial.println("rmdir failed");
  }
}

void loop() {}

Note that you need to create and delete nested folders individually and that folders need to be empty before they can be deleted. Here an example on how to create and delete a nested folder structure:

void setup() {
  SD.begin(); 

  SD.mkdir("/data");
  SD.mkdir("/data/temp");
  SD.mkdir("/data/temp/kitchen");


  SD.rmdir("/data/temp/kitchen");
  SD.rmdir("/data/temp");
  SD.rmdir("/data");
}

File Functions

You can find all file related functions in the File.cpp file of the SD library. Below a quick overview of the most important functions:

File file = SD.open(path, FILE_WRITE);
File file = SD.open(path, FILE_APPEND);

file.write(buf, size); 
file.print(text);

file.read(buf, nbyte); 
file.read();
file.available();

uint32_t pos = file.position();
file.seek(pos);

file.size();

file.flush();
file.close();

SD.exists(path);
SD.remove(path);
SD.rename(path1, path2);

In the next sections we will discuss these functions in more detail.

Write Data to File

When writing data to a file, you first must open the file, write the data and then close the file. Here an example without error checking:

File file = SD.open("/readme.txt", FILE_WRITE);
file.print("This is the readme");
file.close();

I found that the filename must start with a slash (‘/’) or the path from the root, e.g. “/text/readme.txt”. Otherwise I was not able to create a file.

Note that there is also a printf() method that allows you to write formatted text to a file, for instance:

File file = SD.open("/log.txt", FILE_WRITE);
file.printf("Temperature =  %.2f\n", 21.3);
file.close();

To make things simpler, you typically want to wrap the code for writing text to a file in a function:

void writeFile(const char *path, const char *text) {
  File file = SD.open(path, FILE_WRITE);
  if (!file) {
    Serial.println("Failed to open file for writing");
    return;
  }
  file.print(text);
  file.close();
}

Also note that you can write binary instead of text data to a file:

File file = SD.open("/data.bin", FILE_WRITE);
uint8_t buffer[4] = { 0xDE, 0xAD, 0xBE, 0xEF };

size_t bytesWritten = file.write(buffer, sizeof(buffer));

if (bytesWritten != sizeof(buffer)) {
  Serial.println("Write error!");
} 

file.close();

A common use case for this is the writing of sensor data. You can create a struct with your sensor data and then write or append this struct as a data block to a binary file:

struct SensorData {
  uint32_t timestamp;
  int16_t temperature;
  int16_t humidity;
};

SensorData data = { millis(), 234, 81 };
File file = SD.open("/log.bin", FILE_WRITE);
file.write((uint8_t *)&data, sizeof(data));
file.close();

Once written, you can then read the binary sensor data as follows:

SensorData data;

File file = SD.open("/log.bin");
file.read((uint8_t *)&data, sizeof(data));
file.close();

Serial.printf("timestamp:    %d\n", data.timestamp);
Serial.printf("temperature:  %d\n", data.temperature);
Serial.printf("humidity:     %d\n", data.humidity);

If you have appended multiple blocks of sensor data to a file and want to read a specific block back, you can use the seek() function:

SensorData data;

int n = 4; // Read 5ths sample (0...4)
File file = SD.open("/log.bin");
file.seek(n * sizeof(data));
file.read((uint8_t *)&data, sizeof(data));
file.close();

If you need to know the current position of the file pointer while writing to a file use the position() function.

Append Data to File

If you want to append data to a file, instead of overwriting its contents, use the FILE_APPEND flag:

File file = SD.open("/readme.txt", FILE_APPEND);
file.print("This is more to read");
file.close();

And here again a convenience function with error checking for appending text to a file:

void appendFile(const char* path, const char* text) {
  File file = SD.open(path, FILE_APPEND);
  if (!file) {
    Serial.println("Failed to open file for appending");
    return;
  }
  if (!file.print(text)) {
    Serial.println("Append failed");
  }
  file.close();
}

Read File Contents

You can read the contents of a file by using the read() method:

File file = SD.open("/readme.txt");
while (file.available()) {
  Serial.write(file.read());
}
file.close();

The read() method returns an integer and you have to call it repeatedly to read the complete file. Typically you do this in a while-loop and use the function available() to determine, when to stop. But you could also use a for-loop to read the first n characters of a file, for instance.

Below is a conveniency function that prints the contents of a file to the Serial Monitor:

void printFile(const char* path) {
  File file = SD.open(path);
  if (!file) {
    Serial.println("Failed to open file for reading");
    return;
  }

  while (file.available()) {
    Serial.write(file.read());
  }
  file.close();
}

Flush File

If you keep a file open for a longer period of time, for instance, to periodically append sensor data to a log file, make sure to flush the file buffer frequently. In case of a power loss this reduces the risk of data loss and data corruption.

File file = SD.open("/log.txt", FILE_WRITE);
for(int i=0; i<10; i++) {
  file.print("temp = %d", i);
  file.flush();
  delay(1000);
}
file.close();

Delete File

To delete a file, call the remove() function with the file path. You can also check upfront if the file exists:

if (SD.exists("/log.txt")) {
  SD.remove("/log.txt");
}

Rename File

Renaming a file is simple. Just call the rename() function:

if (!SD.rename("/logs/old_log.txt", "/logs/new_log.txt")) { 
  Serial.println("Could not rename file");
}

And that’s it. Now you are familiar with all the functions you need to read and write data to an SD card.

Conclusion

In this guide, you learned how to connect a Micro SD Card Module with the ESP32, format the card, and initialize communication. You also explored how to list directories, create and delete folders, and handle files by reading, writing, appending, renaming, and deleting them. For more applications, see also examples folder of the SD library.

Remember to close and flush your files to avoid data corruption. This is especially important for data loggers, which may keep a file open for longer periods of time and are prone to power losses.

If you have any questions feel free to leave them in the comment section.

Happy Tinkering!

Rich

Thursday 26th of February 2026

Many thanks. I've tried following many tutorials with my ESP32-C6 and got nowhere. Only change I needed to make with yours (for the C6) was to change SPIClass(VSPI) to SPIClass(FSPI). Hope this helps someone else too.

Stefan Maetschke

Thursday 26th of February 2026

Great, thanks for the tip. Much appreciated!