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 eSDMMC_FREQ_HIGHSPEED(40 MHz) sono le stesse scelte del BSP.slot.width = 4attiva il bus a 4 bit.allocation_unit_sizeconta 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 daesp_timer_get_time()e due valori simulati: al loro posto metterai le letture dei tuoi sensori. - fflush e fsync.
fprintfscrive in un buffer della libc.fflushlo passa a FatFs, ma solofsyncscrive sulla scheda anche la tabella FAT e la voce di directory con la nuova dimensione del file. Se l’alimentazione salta tra unfsynce l’altro perdi al massimo l’ultima riga, non il file intero. In alternativa esiste l’opzioneCONFIG_FATFS_IMMEDIATE_FSYNC, che fa lo stesso dopo ogni scrittura ma rallenta tutti i file. - Lettura.
fgetslegge riga per riga,sscanffa il parsing dei tre campi e il programma calcola la media della temperatura. - Elenco dei file.
opendir/readdirscorrono la directory estatrestituisce 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_TIMEOUTo0x107durante 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 abbassahost.max_freq_khza 10000 o anche 400.- Il nome lungo viene troncato o
fopenfallisce: LFN non è attivo. Se hai creato il progetto prima di aggiungeresdkconfig.defaults, cancellasdkconfige ricompila, oppure attivalo da menuconfig. - File a zero byte dopo un reset: manca
fsyncofclose. 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_MEMcon molti file aperti: alzamax_filesnella 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 conesp_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.
