ESP32-P4 e microSD: FAT32 con SDMMC 4 bit e log CSV in ESP-IDF

ESP32-P4 e microSD: FAT32 con SDMMC 4 bit e log CSV in ESP-IDF

Salvare dati su una microSD è il modo più semplice per dare memoria a un progetto: un datalogger che registra temperature per mesi, un file di configurazione modificabile dal PC, una cartella di campioni audio o di immagini. In questo tutorial usi l’host SDMMC nativo a 4 bit dell’ESP32-P4, lo stesso che pilota lo slot microSD della ESP32-P4-Function-EV-Board, e monti la scheda come filesystem FAT con esp_vfs_fat_sdmmc_mount. Poi scrivi un log CSV, lo rileggi, elenchi i file, stampi le informazioni della scheda e la smonti in sicurezza. Per chi lavora con l’ESP32-C6, che non ha un host SDMMC, lo stesso main.c compila anche nella variante SPI (sdspi) con un modulo microSD esterno.

Cosa serve

Componente Esempio Note
Scheda ESP32-P4 ESP32-P4-Function-EV-Board Ha lo slot microSD cablato sull’host SDMMC a 4 bit
microSD SDHC da 8-32 GB, classe 10 Formattata FAT32. Le SDXC da 64 GB in su escono dalla fabbrica in exFAT
In alternativa: ESP32-C6 ESP32-C6-DevKitC-1 Variante SPI, più lenta ma con qualsiasi GPIO
Modulo microSD SPI (solo C6) Modulo a 3,3 V con resistenze di pull-up Evita i moduli “5 V” con regolatore e traslatore se alimenti a 3,3 V
Software ESP-IDF 5.4 Componenti fatfs, sdmmc, esp_driver_sdmmc, esp_driver_sdspi

Come funziona

Una scheda SD parla due protocolli. Il modo SD nativo usa una linea di clock (CLK), una linea comandi bidirezionale (CMD) e 1 o 4 linee dati (D0-D3): con 4 bit a 40 MHz arrivi a circa 20 MB/s teorici. Il modo SPI usa solo MOSI, MISO, SCLK e CS: è supportato da tutte le schede, funziona su qualsiasi microcontrollore ma trasferisce un bit per clock ed è limitato a 20-25 MHz.

Sopra il protocollo c’è il filesystem. ESP-IDF integra FatFs di ChaN e lo registra nel VFS: dopo il mount puoi usare le normali funzioni C (fopen, fprintf, fgets, stat, opendir) sul percorso /sdcard, come su un PC.

FAT32 o exFAT? In ESP-IDF 5.4 il supporto exFAT è disattivato a livello di sorgente (FF_FS_EXFAT 0 in ffconf.h) e non compare in menuconfig. Una scheda exFAT fallisce il mount con ESP_FAIL. Hai due strade: riformattarla in FAT32 dal PC (Windows limita la formattazione FAT32 a 32 GB dall’interfaccia grafica, usa uno strumento come l’SD Card Formatter dell’SD Association o mkfs.vfat su Linux), oppure impostare format_if_mount_failed = true e lasciare che sia l’ESP32 a formattarla, perdendo tutto il contenuto. Nel codice qui sotto questa opzione è volutamente false: una formattazione automatica su un datalogger in campo è un rischio.

Nomi di file lunghi. Per default FatFs accetta solo nomi 8.3 (LOG0001.CSV). Per usare nomi come registro_temperature.csv devi abilitare il supporto LFN: idf.py menuconfig → Component config → FAT Filesystem support → Long filename support → Long filename buffer in heap. Nel progetto lo trovi già impostato in sdkconfig.defaults, insieme alla codifica UTF-8 dei nomi:

CONFIG_FATFS_LFN_HEAP=y
CONFIG_FATFS_MAX_LFN=255
CONFIG_FATFS_API_ENCODING_UTF_8=y
CONFIG_FATFS_VFS_FSTAT_BLKSIZE=4096

Collegamenti

ESP32-P4-Function-EV-Board: non devi collegare nulla, lo slot è già cablato. I pin sono quelli del BSP ufficiale della scheda e dell’esempio storage/sd_card/sdmmc di ESP-IDF, e coincidono con i pin IO MUX dello slot 0 dell’host SDMMC.

Segnale SD GPIO ESP32-P4 Note
CLK GPIO43 Pull-up 10 kΩ sulla scheda
CMD GPIO44 Pull-up 10 kΩ
D0 GPIO39 Pull-up 10 kΩ
D1 GPIO40 Pull-up 10 kΩ
D2 GPIO41 Pull-up 10 kΩ
D3 GPIO42 Pull-up obbligatorio anche in modo 1 bit
Alimentazione IO LDO interno canale 4 (VO4) Da abilitare via software, vedi sotto

ESP32-C6-DevKitC-1 + modulo microSD SPI:

Modulo SD GPIO ESP32-C6 Note
MISO (DO) GPIO2 Serve un pull-up (di solito è sul modulo)
MOSI (DI) GPIO7
SCK GPIO6
CS GPIO18
VCC 3V3 La scheda assorbe picchi di 100-200 mA in scrittura
GND GND

La periferica dell’ESP32-P4

L’ESP32-P4 ha un controller SDMMC host con due slot. Lo slot 0 ha pin dedicati sull’IO MUX (GPIO39-44, più GPIO45-48 per l’eMMC a 8 bit) e supporta anche le modalità UHS-I a 1,8 V; lo slot 1 passa per la GPIO matrix. Il driver sceglie da solo l’IO MUX se i pin che gli passi coincidono con quelli dedicati dello slot 0, come in questo caso.

C’è un dettaglio che su altri chip non esiste: i pin 39-48 appartengono a un dominio di alimentazione IO separato, collegato sulla scheda all’LDO interno VO4 del P4, così da poter passare da 3,3 V a 1,8 V per le schede UHS-I. Se non accendi quell’LDO, lo slot resta senza tensione sui pin e la scheda non risponde. Per questo il codice crea un driver di controllo alimentazione con sd_pwr_ctrl_new_on_chip_ldo() (canale 4, come nell’esempio ufficiale e nel BSP della Function-EV-Board) e lo aggancia a host.pwr_ctrl_handle. Se usi una scheda P4 diversa, controlla sullo schematico da dove arriva l’alimentazione dei pin SD: se è un regolatore esterno, togli questa parte.

Sull’ESP32-C6 non esiste un host SDMMC (è previsto solo come slave SDIO), quindi usi il driver sdspi sopra il bus SPI2. L’API di alto livello è la stessa: cambia solo la funzione di mount (esp_vfs_fat_sdspi_mount) e la configurazione dello slot. Il codice sceglie la variante con #if CONFIG_IDF_TARGET_ESP32P4.

Il codice

Il progetto completo è in progetti/esp-c6p4/07-sd-fat32. Questo è il main/main.c, compilato con ESP-IDF 5.4 sia per esp32p4 sia per esp32c6:

/*
 * Lettura e scrittura di una microSD con FAT32 - ESP-IDF 5.4
 * ESP32-P4: host SDMMC nativo a 4 bit (slot microSD della ESP32-P4-Function-EV-Board)
 * ESP32-C6: variante SPI (sdspi) con un modulo microSD esterno
 * Serie ESP32-C6 / ESP32-P4 di www.webappportal.com
 */
#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"

#if CONFIG_IDF_TARGET_ESP32P4
#include "driver/sdmmc_host.h"
#include "sd_pwr_ctrl_by_on_chip_ldo.h"
#else
#include "driver/sdspi_host.h"
#include "driver/spi_common.h"
#endif

static const char *TAG = "sd_fat32";

#define MOUNT_POINT  "/sdcard"
#define LOG_DIR      MOUNT_POINT "/misure"
#define LOG_FILE     LOG_DIR "/registro_temperature.csv"   /* nome lungo: serve LFN */
#define NUM_RIGHE    10

#if CONFIG_IDF_TARGET_ESP32P4
/* Slot microSD della ESP32-P4-Function-EV-Board: pin IO MUX dello slot 0 */
#define PIN_SD_CLK   GPIO_NUM_43
#define PIN_SD_CMD   GPIO_NUM_44
#define PIN_SD_D0    GPIO_NUM_39
#define PIN_SD_D1    GPIO_NUM_40
#define PIN_SD_D2    GPIO_NUM_41
#define PIN_SD_D3    GPIO_NUM_42
#define SD_LDO_CHAN  4          /* LDO_VO4 alimenta il dominio IO dello slot */
static sd_pwr_ctrl_handle_t s_pwr_ctrl = NULL;
#else
/* ESP32-C6-DevKitC-1 + modulo microSD SPI */
#define PIN_SD_MISO  GPIO_NUM_2
#define PIN_SD_MOSI  GPIO_NUM_7
#define PIN_SD_SCLK  GPIO_NUM_6
#define PIN_SD_CS    GPIO_NUM_18
#endif

static sdmmc_card_t *s_card = NULL;

static esp_err_t sd_mount(void)
{
    esp_vfs_fat_sdmmc_mount_config_t mount_cfg = {
        .format_if_mount_failed = false,   /* mai formattare in automatico */
        .max_files = 4,
        .allocation_unit_size = 16 * 1024,
    };
    esp_err_t ret;

#if CONFIG_IDF_TARGET_ESP32P4
    sdmmc_host_t host = SDMMC_HOST_DEFAULT();
    host.slot = SDMMC_HOST_SLOT_0;
    host.max_freq_khz = SDMMC_FREQ_HIGHSPEED;          /* 40 MHz */

    /* I pin 39-44 appartengono al dominio IO alimentato dall'LDO interno */
    sd_pwr_ctrl_ldo_config_t ldo_cfg = { .ldo_chan_id = SD_LDO_CHAN };
    ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_cfg, &s_pwr_ctrl);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "LDO dello slot SD non inizializzato: %s", esp_err_to_name(ret));
        return ret;
    }
    host.pwr_ctrl_handle = s_pwr_ctrl;

    sdmmc_slot_config_t slot = SDMMC_SLOT_CONFIG_DEFAULT();
    slot.width = 4;
    slot.clk = PIN_SD_CLK;
    slot.cmd = PIN_SD_CMD;
    slot.d0 = PIN_SD_D0;
    slot.d1 = PIN_SD_D1;
    slot.d2 = PIN_SD_D2;
    slot.d3 = PIN_SD_D3;
    slot.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;

    ESP_LOGI(TAG, "Mount SDMMC 4 bit...");
    ret = esp_vfs_fat_sdmmc_mount(MOUNT_POINT, &host, &slot, &mount_cfg, &s_card);
#else
    sdmmc_host_t host = SDSPI_HOST_DEFAULT();          /* SPI2_HOST, 20 MHz */
    spi_bus_config_t bus = {
        .mosi_io_num = PIN_SD_MOSI,
        .miso_io_num = PIN_SD_MISO,
        .sclk_io_num = PIN_SD_SCLK,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
        .max_transfer_sz = 4000,
    };
    ret = spi_bus_initialize(host.slot, &bus, SDSPI_DEFAULT_DMA);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Bus SPI non inizializzato: %s", esp_err_to_name(ret));
        return ret;
    }
    sdspi_device_config_t slot = SDSPI_DEVICE_CONFIG_DEFAULT();
    slot.gpio_cs = PIN_SD_CS;
    slot.host_id = host.slot;

    ESP_LOGI(TAG, "Mount SD via SPI...");
    ret = esp_vfs_fat_sdspi_mount(MOUNT_POINT, &host, &slot, &mount_cfg, &s_card);
#endif

    if (ret == ESP_FAIL) {
        ESP_LOGE(TAG, "Nessun filesystem FAT valido (scheda exFAT o non formattata?)");
    } else if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Scheda non risponde: %s", esp_err_to_name(ret));
    }
    return ret;
}

static void sd_unmount(void)
{
    esp_vfs_fat_sdcard_unmount(MOUNT_POINT, s_card);
    s_card = NULL;
#if CONFIG_IDF_TARGET_ESP32P4
    sd_pwr_ctrl_del_on_chip_ldo(s_pwr_ctrl);
    s_pwr_ctrl = NULL;
#else
    spi_bus_free(SPI2_HOST);
#endif
    ESP_LOGI(TAG, "Scheda smontata: ora puoi toglierla o spegnere");
}

static void print_card_info(void)
{
    sdmmc_card_print_info(stdout, s_card);
    uint64_t total = 0, libero = 0;
    if (esp_vfs_fat_info(MOUNT_POINT, &total, &libero) == ESP_OK) {
        ESP_LOGI(TAG, "Volume FAT: %llu MB totali, %llu MB liberi",
                 total / (1024 * 1024), libero / (1024 * 1024));
    }
}

/* Scrive NUM_RIGHE righe CSV in append, con fsync dopo ogni riga */
static esp_err_t write_csv_log(void)
{
    struct stat st;
    if (stat(LOG_DIR, &st) != 0 && mkdir(LOG_DIR, 0775) != 0) {
        ESP_LOGE(TAG, "Impossibile creare %s", LOG_DIR);
        return ESP_FAIL;
    }
    bool nuovo = (stat(LOG_FILE, &st) != 0);

    FILE *f = fopen(LOG_FILE, "a");
    if (f == NULL) {
        ESP_LOGE(TAG, "Impossibile aprire %s", LOG_FILE);
        return ESP_FAIL;
    }
    if (nuovo) {
        fprintf(f, "tempo_ms,temperatura_c,umidita_pct\n");
    }
    for (int i = 0; i < NUM_RIGHE; i++) {
        int64_t t_ms = esp_timer_get_time() / 1000;
        float temp = 21.5f + (float)(i % 5) * 0.3f;     /* valori simulati */
        float umid = 48.0f + (float)(i % 3) * 1.5f;
        fprintf(f, "%lld,%.2f,%.1f\n", t_ms, temp, umid);
        fflush(f);             /* dal buffer della libc al driver FAT */
        fsync(fileno(f));      /* dal driver FAT alla scheda (FAT e directory aggiornate) */
        vTaskDelay(pdMS_TO_TICKS(100));
    }
    fclose(f);
    ESP_LOGI(TAG, "Scritte %d righe in %s", NUM_RIGHE, LOG_FILE);
    return ESP_OK;
}

/* Rilegge il CSV, conta le righe e calcola la media della temperatura */
static esp_err_t read_csv_log(void)
{
    FILE *f = fopen(LOG_FILE, "r");
    if (f == NULL) {
        ESP_LOGE(TAG, "File %s non trovato", LOG_FILE);
        return ESP_FAIL;
    }
    char riga[80];
    int n = 0;
    float somma = 0.0f;
    fgets(riga, sizeof(riga), f);                       /* salta l'intestazione */
    while (fgets(riga, sizeof(riga), f) != NULL) {
        long long t_ms;
        float temp, umid;
        if (sscanf(riga, "%lld,%f,%f", &t_ms, &temp, &umid) == 3) {
            somma += temp;
            n++;
        }
    }
    fclose(f);
    if (n > 0) {
        ESP_LOGI(TAG, "Lette %d righe, temperatura media %.2f C", n, somma / n);
    }
    return ESP_OK;
}

static void list_dir(const char *path)
{
    DIR *dir = opendir(path);
    if (dir == NULL) {
        ESP_LOGE(TAG, "opendir(%s) fallita", path);
        return;
    }
    ESP_LOGI(TAG, "Contenuto di %s:", path);
    struct dirent *e;
    char full[300];
    while ((e = readdir(dir)) != NULL) {
        struct stat st;
        snprintf(full, sizeof(full), "%s/%s", path, e->d_name);
        if (stat(full, &st) == 0) {
            ESP_LOGI(TAG, "  %s %-32s %8ld byte", S_ISDIR(st.st_mode) ? "[D]" : "   ",
                     e->d_name, (long)st.st_size);
        }
    }
    closedir(dir);
}

void app_main(void)
{
    if (sd_mount() != ESP_OK) {
        return;
    }
    print_card_info();

    if (write_csv_log() == ESP_OK) {
        read_csv_log();
    }
    list_dir(MOUNT_POINT);
    list_dir(LOG_DIR);

    sd_unmount();
}

Il main/CMakeLists.txt dichiara i componenti usati:

idf_component_register(SRCS "main.c"
                       INCLUDE_DIRS "."
                       PRIV_REQUIRES fatfs sdmmc esp_driver_sdmmc esp_driver_sdspi esp_timer)

Cosa fa, pezzo per pezzo

  • Mount. SDMMC_HOST_DEFAULT() prepara l’host; lo slot 0 e SDMMC_FREQ_HIGHSPEED (40 MHz) sono le stesse scelte del BSP. slot.width = 4 attiva il bus a 4 bit. allocation_unit_size conta solo se la scheda viene formattata dal codice: cluster da 16 kB velocizzano le scritture sequenziali.
  • Informazioni della scheda. sdmmc_card_print_info() stampa nome, tipo (SDSC o SDHC/SDXC), velocità e capacità letti dai registri CID e CSD. esp_vfs_fat_info() calcola spazio totale e libero del volume FAT.
  • Log CSV. Il file viene aperto in modalità "a" (append) e l’intestazione viene scritta solo se il file non esisteva. Ogni riga contiene il tempo in millisecondi da esp_timer_get_time() e due valori simulati: al loro posto metterai le letture dei tuoi sensori.
  • fflush e fsync. fprintf scrive in un buffer della libc. fflush lo passa a FatFs, ma solo fsync scrive sulla scheda anche la tabella FAT e la voce di directory con la nuova dimensione del file. Se l’alimentazione salta tra un fsync e l’altro perdi al massimo l’ultima riga, non il file intero. In alternativa esiste l’opzione CONFIG_FATFS_IMMEDIATE_FSYNC, che fa lo stesso dopo ogni scrittura ma rallenta tutti i file.
  • Lettura. fgets legge riga per riga, sscanf fa il parsing dei tre campi e il programma calcola la media della temperatura.
  • Elenco dei file. opendir/readdir scorrono la directory e stat restituisce dimensione e tipo di ogni voce.
  • Smontaggio. esp_vfs_fat_sdcard_unmount() chiude il volume, svuota le cache di FatFs e rilascia il bus. Solo dopo puoi togliere la scheda o spegnere senza rischi. Sul P4 viene poi rilasciato anche l’LDO, sul C6 il bus SPI.

Prova e risultati attesi

idf.py set-target esp32p4
idf.py -p COMx flash monitor

Con una SDHC da 16 GB il log appare più o meno così (i valori dipendono dalla scheda):

I (1203) sd_fat32: Mount SDMMC 4 bit...
Name: SD16G
Type: SDHC
Speed: 40.00 MHz (limit: 40.00 MHz)
Size: 15193MB
I (1297) sd_fat32: Volume FAT: 15177 MB totali, 15102 MB liberi
I (2431) sd_fat32: Scritte 10 righe in /sdcard/misure/registro_temperature.csv
I (2447) sd_fat32: Lette 10 righe, temperatura media 22.10 C
I (2449) sd_fat32: Contenuto di /sdcard:
I (2452) sd_fat32:   [D] misure                                  0 byte
I (2455) sd_fat32: Contenuto di /sdcard/misure:
I (2459) sd_fat32:       registro_temperature.csv              195 byte
I (2466) sd_fat32: Scheda smontata: ora puoi toglierla o spegnere

A ogni riavvio il file cresce di 10 righe. Toglilo dalla scheda e aprilo con un foglio di calcolo: è un normale CSV con separatore virgola.

Problemi comuni

  • Nessun filesystem FAT valido (ESP_FAIL): la scheda è exFAT, NTFS o non formattata. Riformattala in FAT32 dal PC.
  • ESP_ERR_TIMEOUT o 0x107 durante l’inizializzazione: sul P4 controlla che l’LDO del canale 4 sia creato prima del mount. Sul C6 controlla i fili e i pull-up: con collegamenti volanti lunghi abbassa host.max_freq_khz a 10000 o anche 400.
  • Il nome lungo viene troncato o fopen fallisce: LFN non è attivo. Se hai creato il progetto prima di aggiungere sdkconfig.defaults, cancella sdkconfig e ricompila, oppure attivalo da menuconfig.
  • File a zero byte dopo un reset: manca fsync o fclose. I dati erano ancora nella cache.
  • Reset in scrittura: il picco di corrente della scheda fa calare la 3,3 V. Aggiungi 10-100 µF vicino al portaschede.
  • ESP_ERR_NO_MEM con molti file aperti: alza max_files nella configurazione di mount.

Va oltre

  • Unisci questo codice al sensore del progetto stazione meteo per avere uno storico locale anche senza Wi-Fi.
  • Fai ruotare i file ogni giorno (2026-09-25.csv) e cancella i più vecchi quando lo spazio libero scende sotto una soglia letta con esp_vfs_fat_info().
  • Sul P4 prova la modalità UHS-I SDR50 dell’esempio ufficiale: richiede una scheda UHS-I e il passaggio a 1,8 V tramite lo stesso LDO.
  • Altri tutorial sulla famiglia ESP32 li trovi nella sezione ESP32.

Imprenditore, maker, MBSE enthusiast e co-autore di Web App Portal. Collegati su LinkedIn.