Ever started a project with your ESP32 and found yourself running out of room for your data? Whether you’re building a weather station that needs to log years of readings, a smart home device that requires a massive configuration file, or even a tiny music player that needs to store your favorite tunes, the ESP32’s built-in memory can quickly become a bottleneck.
That’s where the humble microSD card comes to the rescue! It’s the perfect solution for expanding your project’s storage capacity without breaking the bank.
In this tutorial, you’ll learn how to connect a microSD card module to the ESP32, and how to read from and write to files stored on the card. You’ll also learn how to handle folders, create and delete files, append data, and even check the card’s size and speed.
Let’s get started!
MicroSD Card Module
There are several types of microSD card modules available, but in this tutorial, we’re using the one shown in the image below.

This specific microSD card module is a good choice because it operates at 3.3 volts. That’s the same voltage level used by both the microSD card itself and the ESP32 board, which means everything works together nicely without needing any extra components to shift voltage levels.
The module uses the SPI communication protocol to talk to the ESP32. On this module, the SPI data lines have 10K pull-up resistors connected to 3.3V, which helps ensure reliable data transfer.
It’s important to understand that the amount of current the microSD card uses can change depending on what it’s doing.
- When the card is not being used, it usually draws about 500 µA.
- When reading data from the card, it can use between 15 to 30 mA.
- Writing data takes more power—some microSD cards might use up to 100mA when writing.
Because of this, it’s important to make sure your power supply can provide enough current. The ESP32’s 3.3V output can supply up to 500mA, which is usually enough for both reading and writing to the card. However, if you run into problems when trying to read or write data, power issues are one of the first things you should check.
Pinout
The microSD card module has six pins, and here’s what each one does:

3V3 is the power input. Connect it to the ESP32’s 3.3V pin.
CS (Chip Select) is a control pin used to activate the module on the SPI bus, allowing it to communicate when needed.
MOSI (Master Out Slave In) is the SPI input pin to the microSD card module that receives data from the ESP32.
CLK (Serial Clock) receives timing pulses from the master device (your ESP32) to keep the data transmission in sync.
MISO (Master In Slave Out) is the SPI output pin from the microSD card module that sends data to the ESP32.
GND is a ground pin.
Wiring the microSD Card Module to the ESP32
Now let’s connect the microSD card module to your ESP32.
Begin by connecting the 3V3 pin on the microSD card module to the 3.3V power pin on the ESP32. Then, connect the GND pin on the module to one of the GND pins on the ESP32.
Next, we’ll set up the pins used for SPI communication. Since microSD cards need fast data transfer, they work best when connected to the hardware SPI pins of the ESP32. On ESP32 the default SPI pins are: GPIO 18 (CLK), GPIO 19 (MISO), GPIO 23 (MOSI), and GPIO 5 (CS).
Here’s a quick reference table for the pin connections:
| microSD Card Module | ESP32 | |
| 3V3 | 3.3V | |
| CS | 5 | |
| MOSI | 23 | |
| CLK | 18 | |
| MISO | 19 | |
| GND | GND |
The diagram below shows exactly how to connect everything:

Preparing the microSD card
Before using your microSD card in a project, it’s important to make sure it’s properly formatted with the correct file system—FAT16 or FAT32. This helps the ESP32 read and write files without any issues.
If you’re using a brand new SD card, it probably came already formatted with a FAT file system. However, this factory formatting might not be perfect, and you could run into problems. If you’re using an older card that’s been used before, it will definitely need to be reformatted. Either way, it’s a good idea to format the card before using it in your project.
There are two methods to format your microSD card:
Method 1
First, insert your microSD card into your computer. Then, find your SD card’s drive, and right-click on it. Choose Format from the menu. A window will pop up—select FAT32 as the file system, then click Start to begin formatting. Follow the instructions on the screen until it’s done.

Method 2
For better results and fewer errors, it’s highly recommended to use the official SD card formatter utility made by the SD Association. This tool is more reliable than the basic formatter that comes with your computer. You can download it from the SD Association’s website. Once installed, run the program, choose your SD card from the list of drives, and click the Format button. This special tool helps avoid common problems caused by bad or incomplete formatting, which can save you a lot of troubleshooting later on.

Setting Up the Arduino IDE
We will be using the Arduino IDE to program the ESP32, so please ensure you have the ESP32 add-on installed before you proceed:
Example Code
The Arduino IDE includes several built-in examples that come with the Arduino core for the ESP32, which show how to work with files on a microSD card using the ESP32.
To access these examples, open the Arduino IDE and go to File > Examples > SD. You will see two example sketches. You can choose any of them to load the sketch into your IDE. Let’s load the SD_Test.

This example sketch covers nearly all the basic tasks you might want to do with a microSD card. It shows how to list the contents of a folder (also called a directory), create a new directory, and remove one. It also shows how to read the contents of a file, write new data to a file, and append more content to an existing file. In addition, the example includes how to rename a file and delete a file from the card.
Beyond file handling, the sketch also shows how to initialize the microSD card, check what type of card is connected, and find out the size of the card.
Give this sketch a try before we get into all the details.
#include "FS.h"
#include "SD.h"
#include "SPI.h"
void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
Serial.printf("Listing directory: %s\n", dirname);
File root = fs.open(dirname);
if(!root){
Serial.println("Failed to open directory");
return;
}
if(!root.isDirectory()){
Serial.println("Not a directory");
return;
}
File file = root.openNextFile();
while(file){
if(file.isDirectory()){
Serial.print(" DIR : ");
Serial.println(file.name());
if(levels){
listDir(fs, file.name(), levels -1);
}
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print(" SIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
void createDir(fs::FS &fs, const char * path){
Serial.printf("Creating Dir: %s\n", path);
if(fs.mkdir(path)){
Serial.println("Dir created");
} else {
Serial.println("mkdir failed");
}
}
void removeDir(fs::FS &fs, const char * path){
Serial.printf("Removing Dir: %s\n", path);
if(fs.rmdir(path)){
Serial.println("Dir removed");
} else {
Serial.println("rmdir failed");
}
}
void readFile(fs::FS &fs, const char * path){
Serial.printf("Reading file: %s\n", path);
File file = fs.open(path);
if(!file){
Serial.println("Failed to open file for reading");
return;
}
Serial.print("Read from file: ");
while(file.available()){
Serial.write(file.read());
}
file.close();
}
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
void appendFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Appending to file: %s\n", path);
File file = fs.open(path, FILE_APPEND);
if(!file){
Serial.println("Failed to open file for appending");
return;
}
if(file.print(message)){
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}
void renameFile(fs::FS &fs, const char * path1, const char * path2){
Serial.printf("Renaming file %s to %s\n", path1, path2);
if (fs.rename(path1, path2)) {
Serial.println("File renamed");
} else {
Serial.println("Rename failed");
}
}
void deleteFile(fs::FS &fs, const char * path){
Serial.printf("Deleting file: %s\n", path);
if(fs.remove(path)){
Serial.println("File deleted");
} else {
Serial.println("Delete failed");
}
}
void testFileIO(fs::FS &fs, const char * path){
File file = fs.open(path);
static uint8_t buf[512];
size_t len = 0;
uint32_t start = millis();
uint32_t end = start;
if(file){
len = file.size();
size_t flen = len;
start = millis();
while(len){
size_t toRead = len;
if(toRead > 512){
toRead = 512;
}
file.read(buf, toRead);
len -= toRead;
}
end = millis() - start;
Serial.printf("%u bytes read for %u ms\n", flen, end);
file.close();
} else {
Serial.println("Failed to open file for reading");
}
file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
size_t i;
start = millis();
for(i=0; i<2048; i++){
file.write(buf, 512);
}
end = millis() - start;
Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end);
file.close();
}
void setup(){
Serial.begin(115200);
if(!SD.begin(5)){
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
Serial.print("SD Card Type: ");
if(cardType == CARD_MMC){
Serial.println("MMC");
} else if(cardType == CARD_SD){
Serial.println("SDSC");
} else if(cardType == CARD_SDHC){
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB\n", cardSize);
listDir(SD, "/", 0);
createDir(SD, "/mydir");
listDir(SD, "/", 0);
removeDir(SD, "/mydir");
listDir(SD, "/", 2);
writeFile(SD, "/hello.txt", "Hello ");
appendFile(SD, "/hello.txt", "World!\n");
readFile(SD, "/hello.txt");
deleteFile(SD, "/foo.txt");
renameFile(SD, "/hello.txt", "/foo.txt");
readFile(SD, "/foo.txt");
testFileIO(SD, "/test.txt");
Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));
}
void loop(){
}After uploading the sketch, open the Serial Monitor and make sure the baud rate is set to 115200. Then press the EN (reset) button on your ESP32. If everything is working correctly, you should see similar messages on the Serial Monitor.

Code Explanation
The code starts by including the three main libraries needed: FS.h, SD.h, and SPI.h. These libraries allow the ESP32 to use the SPI protocol to talk to the microSD card and to perform file system operations like reading, writing, and managing files and folders.
#include "FS.h"
#include "SD.h"
#include "SPI.h"There is also a section of code (currently commented out) that allows you to reassign the SPI pins. If you don’t want to use the ESP32’s default SPI pins, you can uncomment this section and set your own custom pin numbers for the SPI connection.
/*
Uncomment and set up if you want to use custom pins for the SPI communication
#define REASSIGN_PINS
int sck = -1;
int miso = -1;
int mosi = -1;
int cs = -1;
*/The setup() function
The setup() function is where all the action happens. First, we initialize the serial monitor so we can see the output of our program.
Serial.begin(115200);Next, we attempt to initialize the SD card. The code first checks if you have defined REASSIGN_PINS to use custom pins; otherwise, it defaults to the standard ESP32 SPI pins for the SD card. If the card fails to initialize, it will print an error message and stop.
#ifdef REASSIGN_PINS
SPI.begin(sck, miso, mosi, cs);
if (!SD.begin(cs)) {
#else
if (!SD.begin()) {
#endif
Serial.println("Card Mount Failed");
return;
}Once the card is successfully mounted, we check its type (MMC, SDSC, or SDHC) and print this information, along with the total size of the card in megabytes. This initial setup gives us a good overview of the card we are working with.
uint8_t cardType = SD.cardType();
if (cardType == CARD_NONE) {
Serial.println("No SD card attached");
return;
}
Serial.print("SD Card Type: ");
if (cardType == CARD_MMC) {
Serial.println("MMC");
} else if (cardType == CARD_SD) {
Serial.println("SDSC");
} else if (cardType == CARD_SDHC) {
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB\n", cardSize);After the initial checks, the setup() function proceeds to demonstrate various file system operations on a microSD card. We start by listing the contents of the root directory (“/“).
listDir(SD, "/", 0);Then, we create a new directory named mydir and list the root directory again to confirm it was created.
createDir(SD, "/mydir");
listDir(SD, "/", 0);Right after that, we delete that same directory and list the contents one more time. This shows us how to create and delete directories.
removeDir(SD, "/mydir");
listDir(SD, "/", 2);The code then moves on to file operations. We create a file named hello.txt and write the text Hello to it.
writeFile(SD, "/hello.txt", "Hello ");Next, we append the text World!\n to the same file without overwriting the original content. We then read the complete file and print its contents to the serial monitor.
appendFile(SD, "/hello.txt", "World!\n");
readFile(SD, "/hello.txt");To show how to handle files, the code attempts to delete a file named foo.txt which does not exist, demonstrating a failure case.
deleteFile(SD, "/foo.txt");Then, it renames our hello.txt file to foo.txt and reads the newly renamed file to confirm the change.
renameFile(SD, "/hello.txt", "/foo.txt");
readFile(SD, "/foo.txt");The program then performs a file I/O test using testFileIO(). This function reads and writes a large amount of data to a file named test.txt, and it reports how long each operation takes. This helps you understand the read and write speed of your SD card.
testFileIO(SD, "/test.txt");Finally, the program prints out how much total and used space is on the SD card.
Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));Now, let’s go over each of the custom functions used in the sketch:
Listing a directory
The listDir() function is used to list all files and folders in a specific directory on the SD card. You give it the SD file system, the path to the folder you want to look at, and a number that tells how deep it should look into subfolders. The function opens the folder, checks if it’s valid, and then goes through each file or folder inside it, printing their names and sizes. If it finds another folder and the level is greater than 0, it goes deeper into that folder as well.
void listDir(fs::FS &fs, const char *dirname, uint8_t levels) {
Serial.printf("Listing directory: %s\n", dirname);
File root = fs.open(dirname);
if (!root) {
Serial.println("Failed to open directory");
return;
}
if (!root.isDirectory()) {
Serial.println("Not a directory");
return;
}
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
Serial.print(" DIR : ");
Serial.println(file.name());
if (levels) {
listDir(fs, file.path(), levels - 1);
}
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print(" SIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}For example, to list the contents of the root directory, you would call it like this:
listDir(SD, "/", 0);Creating a Directory
The createDir() function creates a new folder on the SD card. You give it the SD file system and the folder name (or path). It tries to create the folder and tells you whether it worked or not.
void createDir(fs::FS &fs, const char * path){
Serial.printf("Creating Dir: %s\n", path);
if(fs.mkdir(path)){
Serial.println("Dir created");
} else {
Serial.println("mkdir failed");
}
}For example, to create a folder called mydir, you would call:
createDir(SD, "/mydir");Removing a Directory
The removeDir() function deletes a folder from the SD card. Just like the createDir() function, you give it the SD file system and the folder path. It will try to remove it and print a message to let you know if it was successful.
void removeDir(fs::FS &fs, const char * path){
Serial.printf("Removing Dir: %s\n", path);
if(fs.rmdir(path)){
Serial.println("Dir removed");
} else {
Serial.println("rmdir failed");
}
}For example, to remove mydir, you would call:
removeDir(SD, "/mydir");Reading a File
The readFile() function opens a file, reads it character by character, and prints it to the Serial Monitor. As with previous functions, you give it the SD file system and the file path.
void readFile(fs::FS &fs, const char * path){
Serial.printf("Reading file: %s\n", path);
File file = fs.open(path);
if(!file){
Serial.println("Failed to open file for reading");
return;
}
Serial.print("Read from file: ");
while(file.available()){
Serial.write(file.read());
}
file.close();
}For example, the following line reads the content of the hello.txt file.
readFile(SD, "/hello.txt")Writing to a File
The writeFile() function opens a file in write mode and writes a provided message, overwriting any existing content. You need to give it the SD file system, the file path, and the message you want to write.
void writeFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}The following line writes Hello in the hello.txt file.
writeFile(SD, "/hello.txt", "Hello ");Appending Content to a File
The appendFile() function also writes a message to a file, but instead of overwriting the content, it adds the message to the end. This is called appending. It’s useful when you want to keep adding new data without deleting what was already there.
void appendFile(fs::FS &fs, const char * path, const char * message){
Serial.printf("Appending to file: %s\n", path);
File file = fs.open(path, FILE_APPEND);
if(!file){
Serial.println("Failed to open file for appending");
return;
}
if(file.print(message)){
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}The following line appends the message World!\n in the hello.txt file. The \n means that the next time you write something to the file, it will go on a new line.
appendFile(SD, "/hello.txt", "World!\n");Renaming a File
The renameFile() function changes the name of an existing file. You give it the SD file system, the current name of the file, and the new name you want. If the rename is successful, it will print a message saying so.
void renameFile(fs::FS &fs, const char * path1, const char * path2){
Serial.printf("Renaming file %s to %s\n", path1, path2);
if (fs.rename(path1, path2)) {
Serial.println("File renamed");
} else {
Serial.println("Rename failed");
}
}The following line renames the hello.txt file to foo.txt.
renameFile(SD, "/hello.txt", "/foo.txt");Deleting a File
The deleteFile() function removes a file from the SD card. You provide the SD file system and the path to the file you want to delete.
void deleteFile(fs::FS &fs, const char * path){
Serial.printf("Deleting file: %s\n", path);
if(fs.remove(path)){
Serial.println("File deleted");
} else {
Serial.println("Delete failed");
}
}The following line deletes the foo.txt file from the microSD card.
deleteFile(SD, "/foo.txt");Measuring the read write speed
The testFileIO() function is a more advanced example. It tests how fast your microSD card can read and write data. First, it opens a file, reads a large number of bytes in chunks to measure the read speed, then closes it. Then it opens the same file for writing and writes a large amount of data in chunks to measure the write speed. This function is helpful if you want to understand your SD card’s performance for large data operations.
void testFileIO(fs::FS &fs, const char * path){
File file = fs.open(path);
static uint8_t buf[512];
size_t len = 0;
uint32_t start = millis();
uint32_t end = start;
if(file){
len = file.size();
size_t flen = len;
start = millis();
while(len){
size_t toRead = len;
if(toRead > 512){
toRead = 512;
}
file.read(buf, toRead);
len -= toRead;
}
end = millis() - start;
Serial.printf("%u bytes read for %u ms\n", flen, end);
file.close();
}
else {
Serial.println("Failed to open file for reading");
}
file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
size_t i;
start = millis();
for(i=0; i<2048; i++){
file.write(buf, 512);
}
end = millis() - start;
Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end);
file.close();
}

