Basic SD working

This commit is contained in:
2026-08-09 15:44:04 -06:00
parent 243419b401
commit 8a44887b6f
8 changed files with 596 additions and 12 deletions
+334
View File
@@ -0,0 +1,334 @@
#include "sd_manager.h"
#include "storage_config.h"
#include <driver/gpio.h>
// Both backends are compiled into the image so that the SDIO path is
// compile-checked even while the system still runs SPI for bring-up.
#include <SD.h>
#include <SD_MMC.h>
namespace
{
class SpiBackend : public StorageBackend
{
public:
bool begin() override
{
// Explicit SPI instance so the pin wiring is driven from
// storage_config.h rather than the board defaults. The second
// spi.begin() call made inside SDFS::begin() is a no-op because the
// bus is already started with these pins.
static SPIClass spi;
spi.begin(
STORAGE_SPI_SCK,
STORAGE_SPI_MISO,
STORAGE_SPI_MOSI,
STORAGE_SPI_CS
);
// Cheap breakout modules often omit the pull-up resistors the SD
// spec expects on the idle-high lines. The ESP32 SPI HAL clears the
// internal pull-ups when it attaches a pin (esp32-hal-spi.c), so a
// floating MISO/CS means the card never answers CMD0 during init
// ("Card Failed! cmd: 0x00"). Re-enable the pull-ups directly so we
// do not disturb the pin's peripheral function.
gpio_pullup_en((gpio_num_t)STORAGE_SPI_MISO);
gpio_pullup_en((gpio_num_t)STORAGE_SPI_CS);
// Give the card a moment to stabilize after power-on before the init
// handshake starts (the framework sends 74+ dummy clocks, but some
// cards need a little more settling time on a fresh mount attempt).
delay(20);
return SD.begin(
STORAGE_SPI_CS,
spi,
STORAGE_SPI_FREQ,
STORAGE_MOUNT_POINT,
STORAGE_MAX_OPEN_FILES,
false // format_if_empty: never auto-format
);
}
void end() override
{
SD.end();
}
fs::FS& fs() override
{
return SD;
}
StorageCardType cardType() override
{
return mapType(SD.cardType());
}
uint64_t totalBytes() override
{
return SD.totalBytes();
}
uint64_t usedBytes() override
{
return SD.usedBytes();
}
private:
static StorageCardType mapType(sdcard_type_t type)
{
switch (type)
{
case CARD_MMC: return StorageCardType::MMC;
case CARD_SD: return StorageCardType::SD;
case CARD_SDHC: return StorageCardType::SDHC;
case CARD_NONE: return StorageCardType::None;
default: return StorageCardType::Unknown;
}
}
};
class SdmmcBackend : public StorageBackend
{
public:
bool begin() override
{
// The plain esp32dev variant does not pre-wire the SDMMC pins, so
// they must always be set explicitly. The classic ESP32 routes the
// SDMMC peripheral through the GPIO matrix, so the pins defined in
// storage_config.h are fully re-routable.
if (!SD_MMC.setPins(
STORAGE_SDMMC_CLK,
STORAGE_SDMMC_CMD,
STORAGE_SDMMC_D0,
STORAGE_SDMMC_D1,
STORAGE_SDMMC_D2,
STORAGE_SDMMC_D3))
{
return false;
}
return SD_MMC.begin(
STORAGE_MOUNT_POINT,
STORAGE_SDMMC_MODE_1BIT, // false == 4-bit bus
STORAGE_SDMMC_FORMAT_IF_FAILED, // never auto-format
STORAGE_SDMMC_FREQ_HZ, // 40 MHz == SDMMC_FREQ_HIGHSPEED
STORAGE_MAX_OPEN_FILES
);
}
void end() override
{
SD_MMC.end();
}
fs::FS& fs() override
{
return SD_MMC;
}
StorageCardType cardType() override
{
return mapType(SD_MMC.cardType());
}
uint64_t totalBytes() override
{
return SD_MMC.totalBytes();
}
uint64_t usedBytes() override
{
return SD_MMC.usedBytes();
}
private:
static StorageCardType mapType(sdcard_type_t type)
{
switch (type)
{
case CARD_MMC: return StorageCardType::MMC;
case CARD_SD: return StorageCardType::SD;
case CARD_SDHC: return StorageCardType::SDHC;
case CARD_NONE: return StorageCardType::None;
default: return StorageCardType::Unknown;
}
}
};
} // namespace
SDManager::SDManager()
:
backend(nullptr),
mounted(false)
{
}
bool SDManager::begin()
{
if (mounted)
{
return true;
}
#if STORAGE_IFACE == STORAGE_IFACE_SDMMC
backend = new SdmmcBackend();
#else
backend = new SpiBackend();
#endif
if (backend == nullptr)
{
return false;
}
mounted = backend->begin();
if (!mounted)
{
Serial.println("[Storage] SD card mount FAILED");
delete backend;
backend = nullptr;
}
return mounted;
}
void SDManager::end()
{
if (backend != nullptr)
{
backend->end();
delete backend;
backend = nullptr;
}
mounted = false;
}
bool SDManager::isMounted() const
{
return mounted;
}
fs::FS& SDManager::fs()
{
return backend->fs();
}
const char* SDManager::cardTypeName(StorageCardType type)
{
switch (type)
{
case StorageCardType::MMC: return "MMC";
case StorageCardType::SD: return "SD";
case StorageCardType::SDHC: return "SDHC";
case StorageCardType::Unknown: return "Unknown";
case StorageCardType::None:
default: return "None";
}
}
void SDManager::printCardInfo()
{
if (!mounted)
{
Serial.println("[Storage] Card info unavailable (not mounted)");
return;
}
uint64_t total = backend->totalBytes();
uint64_t used = backend->usedBytes();
uint64_t free = (total > used) ? (total - used) : 0;
Serial.println("----------------------------------------");
Serial.println("SD card information");
Serial.println("----------------------------------------");
Serial.printf(" Type : %s\n", cardTypeName(backend->cardType()));
Serial.printf(" Total : %llu bytes\n", (unsigned long long)total);
Serial.printf(" Used : %llu bytes\n", (unsigned long long)used);
Serial.printf(" Free : %llu bytes\n", (unsigned long long)free);
Serial.println("----------------------------------------");
}
void SDManager::listFiles()
{
if (!mounted)
{
Serial.println("[Storage] Cannot list files (not mounted)");
return;
}
Serial.println("Files on SD card:");
Serial.println("----------------------------------------");
char path[256];
snprintf(path, sizeof(path), "/");
listFilesRecursive(backend->fs(), path, sizeof(path), 0);
Serial.println("----------------------------------------");
Serial.println("End of listing");
}
void SDManager::listFilesRecursive(fs::FS& files, char* path, size_t pathSize, uint8_t depth)
{
if (depth > STORAGE_LIST_MAX_DEPTH)
{
Serial.printf(" ... (max depth %u reached)\n", STORAGE_LIST_MAX_DEPTH);
return;
}
File dir = files.open(path);
if (!dir)
{
Serial.printf(" [error] cannot open: %s\n", path);
return;
}
if (!dir.isDirectory())
{
Serial.printf(" %s (%llu bytes)\n", path, (unsigned long long)dir.size());
dir.close();
return;
}
File entry;
while ((entry = dir.openNextFile()))
{
size_t base = strlen(path);
if (entry.isDirectory())
{
snprintf(path + base, pathSize - base, "/%s", entry.name());
Serial.printf(" %s/\n", path);
listFilesRecursive(files, path, pathSize, depth + 1);
path[base] = '\0';
}
else
{
snprintf(path + base, pathSize - base, "/%s", entry.name());
Serial.printf(" %s (%llu bytes)\n", path, (unsigned long long)entry.size());
}
entry.close();
}
dir.close();
}
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include <Arduino.h>
#include <FS.h>
// StorageCardType decouples the rest of the system from the SD/SD_MMC
// library enum so nothing outside sd_manager.cpp needs to know which
// hardware interface is in use.
enum class StorageCardType
{
None,
MMC,
SD,
SDHC,
Unknown
};
// Hardware abstraction for the SD card transport. The concrete backend
// (SPI today, SDIO later) is selected at build time in storage_config.h.
// Everything downstream (StorageTask, future DataLogger) talks only to
// the `fs::FS` reference, so swapping SPI for SDIO touches no other code.
class StorageBackend
{
public:
virtual ~StorageBackend() {}
virtual bool begin() = 0;
virtual void end() = 0;
virtual fs::FS& fs() = 0;
virtual StorageCardType cardType() = 0;
virtual uint64_t totalBytes() = 0;
virtual uint64_t usedBytes() = 0;
};
// Owns the selected backend, mounts the card, and provides the boot-time
// info/listing report. The mount and any file access are blocking and are
// expected to be driven from the dedicated StorageTask on core 0.
class SDManager
{
public:
SDManager();
bool begin();
void end();
bool isMounted() const;
fs::FS& fs();
void printCardInfo();
void listFiles();
private:
StorageBackend* backend;
bool mounted;
void listFilesRecursive(fs::FS& files, char* path, size_t pathSize, uint8_t depth);
static const char* cardTypeName(StorageCardType type);
};
+99
View File
@@ -0,0 +1,99 @@
#pragma once
// ============================================================================
// Storage configuration
// ============================================================================
//
// This header is the single place to configure how the Hub talks to the
// SD card. Two hardware interfaces are supported by the Arduino-ESP32
// framework, and both are implemented behind the StorageBackend interface
// in sd_manager.cpp:
//
// STORAGE_IFACE_SPI - Uses the `SD` library (SPI protocol). This is the
// bring-up path for the current breakout module,
// which only breaks out the 4 SPI lines.
// Max practical throughput: ~1-2 MB/s. NOT enough
// for the 6-8 MB/s audio logging target.
//
// STORAGE_IFACE_SDMMC - Uses the `SD_MMC` library (SDIO protocol, the
// SDMMC peripheral). This is the production path.
// 4-bit mode @ 40 MHz (SDMMC_FREQ_HIGHSPEED) gives
// roughly 8-12 MB/s, which comfortably meets the
// target. REQUIRED for the final design.
//
// TO MIGRATE TO SDIO (the new SDIO-capable board that is being shipped):
//
// 1. Change STORAGE_IFACE below from STORAGE_IFACE_SPI to
// STORAGE_IFACE_SDMMC.
//
// 2. Wire the SD card to the SDIO lines listed in the STORAGE_SDMMC_*
// defines below. On the classic ESP32 the SDMMC peripheral is routed
// through the GPIO matrix, so these pins can be changed to any free
// GPIO simply by editing the defines.
//
// 3. Keep STORAGE_SDMMC_MODE_1BIT as `false` (4-bit bus is required for
// the write throughput) and keep STORAGE_SDMMC_FORMAT_IF_FAILED as
// `false` (a foreign/unformatted card must never be auto-formatted).
//
// 4. Everything downstream (StorageTask, SDManager, and the future
// DataLogger) talks only through the `fs::FS` interface, so no other
// code changes are needed.
// ============================================================================
// --- Backend selection ------------------------------------------------------
#define STORAGE_IFACE_SPI 1
#define STORAGE_IFACE_SDMMC 2
// SPI until the SDIO-capable board arrives.
#ifndef STORAGE_IFACE
#define STORAGE_IFACE STORAGE_IFACE_SPI
#endif
// --- Common -----------------------------------------------------------------
// Single canonical mount point so file paths never change when the backend
// is switched (the SD lib defaults to "/sd", SD_MMC to "/sdcard").
#define STORAGE_MOUNT_POINT "/sdcard"
#define STORAGE_MAX_OPEN_FILES 5
// --- SPI (bring-up only) ----------------------------------------------------
// Default VSPI pins on the classic ESP32 DevKitC (variant/pins_arduino.h).
#define STORAGE_SPI_CS 5
#define STORAGE_SPI_SCK 18
#define STORAGE_SPI_MOSI 23
#define STORAGE_SPI_MISO 19
// 10 MHz is a reliable default for breadboard/jumper-wire bring-up. The SD
// init handshake always runs at 400 kHz regardless (see sd_diskio.cpp), so a
// mount failure is NOT a frequency problem - check power, wiring, pull-ups
// and card seating first. Raise to 20 MHz once the wiring is proven.
#define STORAGE_SPI_FREQ 10000000UL
// --- SDIO / SD_MMC (production, 4-bit) --------------------------------------
// Default ESP32 SDMMC slot-1 pins (GPIO matrix, freely re-routable).
#define STORAGE_SDMMC_CLK 6
#define STORAGE_SDMMC_CMD 11
#define STORAGE_SDMMC_D0 7
#define STORAGE_SDMMC_D1 8
#define STORAGE_SDMMC_D2 9
#define STORAGE_SDMMC_D3 10
// false = 4-bit wide bus (required for >8 MB/s). Do NOT enable 1-bit mode.
#define STORAGE_SDMMC_MODE_1BIT false
// NEVER auto-format: an unformatted/foreign card must never be destroyed.
#define STORAGE_SDMMC_FORMAT_IF_FAILED false
// 40 MHz == SDMMC_FREQ_HIGHSPEED. Written as a literal to keep this header
// free of driver includes.
#define STORAGE_SDMMC_FREQ_HZ 40000000
// --- Boot-time file listing -------------------------------------------------
// Maximum directory depth printed during the recursive boot listing. Guards
// the StorageTask stack against pathological directory nesting.
#define STORAGE_LIST_MAX_DEPTH 10