#include "sd_manager.h" #include "storage_config.h" #include // 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 #include 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(); } StorageCardType SDManager::cardType() const { if (mounted && backend != nullptr) { return backend->cardType(); } return StorageCardType::None; } const char* SDManager::cardTypeName() const { return cardTypeName(cardType()); } uint64_t SDManager::totalBytes() const { if (mounted && backend != nullptr) { return backend->totalBytes(); } return 0; } uint64_t SDManager::usedBytes() const { if (mounted && backend != nullptr) { return backend->usedBytes(); } return 0; } uint64_t SDManager::freeBytes() const { uint64_t total = totalBytes(); uint64_t used = usedBytes(); return (total > used) ? (total - used) : 0; } uint32_t SDManager::measureWriteSpeed() { if (!mounted || backend == nullptr) { return 0; } fs::FS& files = backend->fs(); const char* scratchPath = STORAGE_SPEED_MEASURE_PATH; const size_t bufferSize = 16 * 1024; // Remove any leftover from a previous crashed run. files.remove(scratchPath); uint8_t* buffer = (uint8_t*)malloc(bufferSize); if (buffer == nullptr) { return 0; } memset(buffer, 0xA5, bufferSize); File file = files.open(scratchPath, FILE_WRITE); if (!file) { free(buffer); return 0; } uint32_t remaining = STORAGE_SPEED_MEASURE_BYTES; uint32_t startUs = micros(); while (remaining > 0) { size_t toWrite = (remaining < bufferSize) ? remaining : bufferSize; size_t written = file.write(buffer, toWrite); if (written == 0) { break; } remaining -= written; } file.close(); uint32_t elapsedUs = micros() - startUs; Serial.print("Elapsed microseconds: "); Serial.println(elapsedUs); files.remove(scratchPath); free(buffer); if (remaining != 0 || elapsedUs == 0) { return 0; } uint64_t writtenBytes = (uint64_t)STORAGE_SPEED_MEASURE_BYTES - remaining; return (uint32_t)((writtenBytes * 1000000ULL) / elapsedUs); } 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(); }