Skip to Content

Modulo SD Card con ESP32

Modulo SD Card con ESP32

In questo tutorial imparerai come collegare un modulo Micro SD all’ESP32 ed eseguire operazioni di base sui file. Tratteremo l’elenco delle directory, la creazione e la cancellazione di file e cartelle, la lettura e la scrittura di dati e altro ancora. Alla fine, avrai una solida base per integrare lo storage su scheda SD nei tuoi progetti.

Componenti necessari

Di seguito trovi i componenti necessari per questo tutorial. Ho usato una vecchia scheda ESP32, ormai obsoleta ma ancora reperibile a basso costo. Comunque qualsiasi altro ESP32 va bene.

Inoltre, serve un modulo Micro SD. Qualsiasi modulo con interfaccia SPI e logica a 3.3V funzionerà. Se vuoi collegare il modulo SD a un Arduino UNO, ti serve un modulo con logica a 5V o un convertitore di livello. Ho scelto un modulo molto semplice e piccolo senza convertitore, dato che lo collegheremo a un ESP32 con logica a 3.3V.

Modulo Micro SD

Micro SD Card 8GB

ESP32 lite Lolin32

ESP32 lite

USB data cable

Cavo dati USB

Dupont wire set

Set di fili Dupont

Half_breadboard56a

Breadboard

Nozioni di base sui moduli lettore SD

I moduli MicroSD fungono da interfaccia tra un microcontrollore, come l’ESP32, e una scheda MicroSD. Permettono al microcontrollore di leggere e scrivere dati sulla scheda, espandendo efficacemente la capacità di archiviazione per log, media o file di configurazione.

Come funzionano i moduli MicroSD

Alla base, le schede MicroSD comunicano usando il protocollo SPI (Serial Peripheral Interface). Il modulo contiene un alloggiamento per la MicroSD e un circuito level shifter se necessario, per adattare i livelli di tensione tra la scheda e il microcontrollore. Collegato, l’ESP32 agisce da master SPI, inviando comandi e ricevendo dati dalla MicroSD, che agisce da slave SPI.

Il bus SPI usa linee separate per input dati (MOSI), output dati (MISO), clock (SCK) e chip select (CS). Questa comunicazione full-duplex permette trasferimenti dati veloci e affidabili, essenziali per le operazioni sul file system.

Perché SPI e non I2C?

SPI è preferito a I2C per la comunicazione con MicroSD per le sue velocità di trasferimento dati più elevate e protocollo più semplice. I2C è un bus a due fili progettato per comunicazioni a breve distanza e bassa velocità, tipicamente fino a 400 kHz o 1 MHz in modalità veloce. Invece, SPI può facilmente operare a diversi MHz, fondamentale per leggere e scrivere file grandi in modo efficiente.

Inoltre, SPI supporta comunicazione full-duplex, permettendo invio e ricezione simultanei di dati, mentre I2C è half-duplex. La specifica MicroSD supporta esplicitamente la modalità SPI, rendendo SPI la scelta naturale.

Pin SPI

La tabella seguente mostra i pin necessari per SPI:

Pin SPIFunzioneDescrizione
MOSIMaster Out Slave InTrasporta dati dall’ESP32 alla MicroSD
MISOMaster In Slave OutTrasporta dati dalla MicroSD all’ESP32
SCKSerial ClockSincronizza la trasmissione dati
CSChip Select (Slave Select)Seleziona la MicroSD per la comunicazione

L’ESP32 permette tipicamente l’assegnazione flessibile di questi pin tramite la sua matrice GPIO, ma i default comuni sono GPIO 23 per MOSI, GPIO 19 per MISO, GPIO 18 per SCK e GPIO 5 per CS.

Tipi di moduli MicroSD: 5V vs 3.3V

Esistono due tipi principali di moduli MicroSD basati sulla compatibilità di tensione. La maggior parte delle schede Arduino opera a livelli logici 5V, quindi i moduli progettati per Arduino includono level shifter e regolatori di tensione onboard per interfacciarsi in sicurezza con le MicroSD a 3.3V.

L’immagine seguente mostra il fronte e il retro di un modulo MicroSD a 5V. Si vedono il regolatore di tensione e il circuito level shifter:

5V MicroSD card module
Modulo MicroSD 5V

Tuttavia, se il tuo microcontrollore opera già a 3.3V (ESP32), non servono regolatori o level shifter. Puoi usare un modulo lettore SD più semplice e economico. L’immagine seguente mostra il fronte e il retro di un tipico modulo MicroSD a 3.3V:

3.3V MicroSD card module
Modulo MicroSD 3.3V

Se osservi bene la scheda a 3.3V, non ci sono IC per regolazione o conversione di livello. Ma dovrebbero esserci resistori di pull-up da 10K per l’interfaccia SPI:

Pull-up resistors on MicroSD card module
Resistori di pull-up sul modulo MicroSD

Evita schede senza questi resistori di pull-up e non collegare un modulo MicroSD a 3.3V a un Arduino a 5V!

Dimensioni e tipi di MicroSD

Le schede MicroSD sono disponibili in varie capacità e classi di velocità. Le dimensioni più comuni vanno da 2GB fino a 1TB, con le seguenti classificazioni:

TipoNomeDimensione
SDSC Capacità StandardFino a 2GB
SDHC Alta Capacità4GB a 32GB
SDXC Capacità Estesa64GB a 2TB

La libreria SD dell’ESP32 supporta schede formattate con file system FAT32.

Classi di velocità delle MicroSD

Le classi di velocità definiscono la velocità minima di scrittura sostenuta della scheda, fondamentale per applicazioni come registrazione video o logging dati.

Classe di velocitàVelocità minima di scritturaDescrizione
Classe 22 MB/sAdatta per video a definizione standard
Classe 44 MB/sAdatta per video ad alta definizione
Classe 66 MB/sAdatta per registrazioni video HD
Classe 1010 MB/sAdatta per video Full HD
UHS-I U110 MB/sUltra High Speed, adatta per video HD
UHS-I U330 MB/sUltra High Speed, adatta per video 4K

Per la maggior parte dei progetti ESP32, una scheda Classe 10 o UHS-I U1 offre un buon equilibrio tra velocità e costo. Schede più veloci potrebbero non garantire prestazioni migliori a causa dei limiti del microcontrollore e del bus SPI.

Collegare il modulo SD all’ESP32

Collegare il modulo SD all’ESP32 via SPI è semplice. L’immagine seguente mostra il cablaggio tra un ESP32 Lite e un modulo MicroSD a 3.3V:

Connecting MicroSD Card Module to ESP32 Lite
Collegamento modulo MicroSD a ESP32 Lite

Per comodità, ecco la mappatura dei pin tra il modulo SD e l’ESP32:

Pin modulo SDPin ESP32
VCC3.3V
GNDGND
MISOGPIO 19
MOSIGPIO 23
SCK/CLKGPIO 18
CS (Chip Select)GPIO 5

Assicurati di usare il pin 3.3V dell’ESP32 per alimentare il modulo SD. La scheda SD funziona a 3.3V, applicare 5V può danneggiarla.

Formattare la scheda SD

Prima di usare la scheda SD con l’ESP32, è importante assicurarsi che sia formattata correttamente. La maggior parte delle schede SD arriva preformattata con file system FAT32, compatibile con la libreria SD dell’ESP32.

Tuttavia, se la tua scheda è nuova o è stata usata con altri dispositivi, potrebbe essere necessario formattarla. Puoi formattare la scheda SD usando il computer:

Windows

  1. Inserisci la scheda SD nel PC.
  2. Apri Esplora File e clicca col tasto destro sull’unità della scheda SD.
  3. Seleziona Formatta.
  4. Scegli FAT32 come file system.
  5. Clicca Avvia per formattare.

macOS

  1. Inserisci la scheda SD.
  2. Apri Utility Disco.
  3. Seleziona la scheda SD dalla barra laterale.
  4. Clicca su Cancella.
  5. Scegli MS-DOS (FAT) come formato.
  6. Clicca su Cancella per formattare.

Linux

Usa il comando mkfs nel terminale. Sostituisci /dev/sdX1 con l’identificatore del dispositivo della tua scheda SD:

  sudo mkfs.vfat -F 32 /dev/sdX1

Consigli importanti

  • Fai il backup di dati importanti prima di formattare.
  • Non usare exFAT o NTFS, non supportati dalla libreria SD dell’ESP32.
  • Dopo la formattazione, espelli la scheda in modo sicuro prima di inserirla nel modulo SD.

Dimensione massima della scheda SD

Su Windows la dimensione massima formattabile tramite Esplora File è 32GB. È possibile formattare schede di capacità superiore in FAT32 e usarle con ESP32, ma serve software diverso. Ad esempio, ci sono FAT32Format o guiformat-x64.

Classe SD

La maggior parte delle funzioni relative alle schede SD si trova nella libreria SD e specificamente nella SDClass. Il codice seguente mostra la SDClass con le sue dichiarazioni di funzione. Dagli un’occhiata veloce e nelle sezioni successive imparerai a usare le diverse funzioni della 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;
};

Inizializzare il modulo SD

Prima di lavorare con i file sulla scheda SD, devi inizializzare il modulo SD con l’ESP32. L’inizializzazione stabilisce la comunicazione tra ESP32 e scheda SD, assicurando che la scheda sia pronta per le operazioni sui file. Il codice seguente mostra come fare:

#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() { }

La funzione SD.begin() restituisce true se la scheda è inizializzata con successo. Se restituisce false, la scheda potrebbe non essere collegata correttamente o formattata in modo errato. Controlla anche il cablaggio del modulo SD con l’ESP32.

Nota che SD.begin() ha parametri che puoi impostare se il default non funziona:

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);

Soprattutto, puoi cambiare i pin per l’interfaccia SPI qui. La libreria SD usa di default i pin VSPI (23, 19, 18, 5).

Per cambiare i pin, ridefinisci le costanti dei pin, crei un’istanza di SPIClass e la passi al metodo SD.begin(). Ecco un esempio:

#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() {}

Una volta inizializzata, la scheda SD è pronta per operazioni come lettura, scrittura e gestione delle directory.

Lettura del tipo di scheda SD

Dopo aver inizializzato il modulo SD, puoi identificare programmaticamente il tipo di scheda collegata. La funzione sd.cardType() restituisce un intero che rappresenta il tipo di scheda. Ecco un esempio di codice che stampa il tipo di scheda sul Monitor Seriale:

#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() { }

Iniziamo inizializzando la scheda SD con SD.begin(). Poi chiamiamo SD.cardType() per ottenere il tipo di scheda e infine stampiamo il tipo sul Monitor Seriale.

Ottenere informazioni sulla scheda SD

La libreria SD ha anche funzioni per recuperare la dimensione della scheda, lo spazio totale disponibile e lo spazio usato. Vedi il codice seguente:

#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() {}

Elencare la directory

Purtroppo la libreria SD non ha una funzione per elencare ricorsivamente tutti i file e le cartelle su una scheda SD. Devi implementare questa funzione da solo. Ecco il codice per una funzione printDir() che stampa la struttura di cartelle e file sul Monitor Seriale:

#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() {}

Se ti servono solo i file in una cartella specifica, puoi usare questo codice:

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();
  }
}

Creare o cancellare directory

Per creare o rimuovere directory la libreria SD offre le funzioni mkdir() e rmdir(). Ecco un esempio che prima crea una directory e poi la cancella:

#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() {}

Nota che devi creare e cancellare cartelle annidate singolarmente e che le cartelle devono essere vuote prima di poterle cancellare. Ecco un esempio per creare e cancellare una struttura di cartelle annidate:

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");
}

Funzioni sui file

Tutte le funzioni relative ai file si trovano nel file File.cpp della libreria SD. Qui una panoramica rapida delle funzioni più importanti:

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);

Nelle sezioni successive discuteremo queste funzioni in dettaglio.

Scrivere dati su file

Quando scrivi dati su un file, devi prima aprirlo, scrivere i dati e poi chiuderlo. Ecco un esempio senza controllo errori:

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

Ho notato che il nome del file deve iniziare con una slash (‘/’) o il percorso dalla root, es. “/text/readme.txt”. Altrimenti non riuscivo a creare un file.

Nota che esiste anche un metodo printf() che permette di scrivere testo formattato su un file, per esempio:

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

Per semplificare, di solito conviene racchiudere il codice per scrivere testo in una funzione:

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();
}

Nota anche che puoi scrivere dati binari invece di testo su un 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();

Un caso d’uso comune è la scrittura di dati da sensori. Puoi creare una struct con i dati del sensore e poi scrivere o aggiungere questa struct come blocco dati a un file binario:

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();

Una volta scritto, puoi leggere i dati binari del sensore così:

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);

Se hai aggiunto più blocchi di dati sensore a un file e vuoi leggere un blocco specifico, puoi usare la funzione seek():

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();

Se ti serve conoscere la posizione corrente del puntatore del file mentre scrivi, usa la funzione position().

Aggiungere dati a un file

Se vuoi aggiungere dati a un file senza sovrascriverne il contenuto, usa il flag FILE_APPEND:

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

Ecco di nuovo una funzione di comodità con controllo errori per aggiungere testo a un 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();
}

Leggere il contenuto di un file

Puoi leggere il contenuto di un file usando il metodo read():

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

Il metodo read() restituisce un intero e devi chiamarlo ripetutamente per leggere tutto il file. Tipicamente lo fai in un ciclo while e usi la funzione available() per sapere quando fermarti. Ma potresti anche usare un ciclo for per leggere i primi n caratteri di un file, per esempio.

Qui sotto una funzione di comodità che stampa il contenuto di un file sul Monitor Seriale:

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 del file

Se tieni un file aperto a lungo, per esempio per aggiungere periodicamente dati di sensori a un log, assicurati di svuotare frequentemente il buffer del file. In caso di interruzione di corrente riduce il rischio di perdita o corruzione dati.

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();

Cancellare un file

Per cancellare un file, chiama la funzione remove() con il percorso del file. Puoi anche verificare prima se il file esiste:

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

Rinominare un file

Rinominare un file è semplice. Basta chiamare la funzione rename():

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

Ecco fatto. Ora conosci tutte le funzioni necessarie per leggere e scrivere dati su una scheda SD.

Conclusione

In questa guida hai imparato come collegare un modulo Micro SD all’ESP32, formattare la scheda e inizializzare la comunicazione. Hai anche visto come elencare directory, creare e cancellare cartelle, e gestire file leggendo, scrivendo, aggiungendo, rinominando e cancellando. Per altre applicazioni, vedi anche la cartella examples della libreria SD.

Ricorda di chiudere e svuotare i file per evitare corruzione dati. Questo è particolarmente importante per i data logger, che possono tenere un file aperto a lungo e sono soggetti a interruzioni di corrente.

Se hai domande, sentiti libero di lasciarle nei commenti.

Buon divertimento con il tinkering!