From 8a44887b6f1c4ea054dbfcc2c721cca50d0f1d58 Mon Sep 17 00:00:00 2001 From: bionickatana Date: Sun, 9 Aug 2026 15:44:04 -0600 Subject: [PATCH] Basic SD working --- src/core/diagnostics_state.cpp | 12 +- src/main.cpp | 7 + src/services/web_service.cpp | 1 - src/storage/sd_manager.cpp | 334 +++++++++++++++++++++++++++++++++ src/storage/sd_manager.h | 61 ++++++ src/storage/storage_config.h | 99 ++++++++++ src/tasks/storage_task.cpp | 72 +++++++ src/tasks/storage_task.h | 22 +++ 8 files changed, 596 insertions(+), 12 deletions(-) create mode 100644 src/storage/sd_manager.cpp create mode 100644 src/storage/sd_manager.h create mode 100644 src/storage/storage_config.h create mode 100644 src/tasks/storage_task.cpp create mode 100644 src/tasks/storage_task.h diff --git a/src/core/diagnostics_state.cpp b/src/core/diagnostics_state.cpp index bed3b76..2717bf1 100644 --- a/src/core/diagnostics_state.cpp +++ b/src/core/diagnostics_state.cpp @@ -11,13 +11,7 @@ void DiagnosticsState::update() ESP.getMinFreeHeap(); current.cpuFrequency = - ESP.getCpuFreqMHz(); - - Serial.print("#update: "); - Serial.println(current.freeHeap); - Serial.println("Calling current from update"); - getCurrent(); - + ESP.getCpuFreqMHz(); } @@ -29,9 +23,5 @@ DiagnosticSample DiagnosticsState::getCurrent() sample.freeHeap = current.freeHeap; sample.minimumFreeHeap = current.minimumFreeHeap; sample.cpuFrequency = current.cpuFrequency; - - Serial.print("#getCurrent: "); - Serial.println(sample.freeHeap); - Serial.println(sample.timestamp); return sample; } diff --git a/src/main.cpp b/src/main.cpp index 111dee2..7bbd11d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,8 +11,11 @@ #include "services/ota_service.h" #include "services/web_service.h" +#include "storage/sd_manager.h" + #include "tasks/system_task.h" #include "tasks/diagnostics_task.h" +#include "tasks/storage_task.h" DashboardState dashboardState; @@ -31,6 +34,9 @@ SystemTask systemTask(services); DiagnosticsTask diagnosticsTask(diagnosticsState); +SDManager sdManager; +StorageTask storageTask(sdManager); + void setup() { Serial.begin(115200); @@ -42,6 +48,7 @@ void setup() systemTask.start(); diagnosticsTask.start(); + storageTask.start(); } diff --git a/src/services/web_service.cpp b/src/services/web_service.cpp index defc279..c108139 100644 --- a/src/services/web_service.cpp +++ b/src/services/web_service.cpp @@ -128,7 +128,6 @@ Firmware Version:
Loading...
void WebService::broadcastState() { DiagnosticSample diagnostics = diagnosticsState.getCurrent(); - Serial.println(diagnostics.freeHeap); String json = "{"; // Opening system tag: diff --git a/src/storage/sd_manager.cpp b/src/storage/sd_manager.cpp new file mode 100644 index 0000000..f8579fb --- /dev/null +++ b/src/storage/sd_manager.cpp @@ -0,0 +1,334 @@ +#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(); +} + + +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(); +} diff --git a/src/storage/sd_manager.h b/src/storage/sd_manager.h new file mode 100644 index 0000000..5a9c5b8 --- /dev/null +++ b/src/storage/sd_manager.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +// 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); +}; diff --git a/src/storage/storage_config.h b/src/storage/storage_config.h new file mode 100644 index 0000000..aabb929 --- /dev/null +++ b/src/storage/storage_config.h @@ -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 diff --git a/src/tasks/storage_task.cpp b/src/tasks/storage_task.cpp new file mode 100644 index 0000000..2946d60 --- /dev/null +++ b/src/tasks/storage_task.cpp @@ -0,0 +1,72 @@ +#include "storage_task.h" + + +StorageTask::StorageTask(SDManager& manager) +: +storage(manager), +taskHandle(nullptr) +{ +} + + +void StorageTask::start() +{ + // Dedicated task on core 0 so SD card work never blocks the services + // running on core 1 (SystemTask / DiagnosticsTask). The 8 KB stack is + // sized for the recursive boot-time file listing; the eventual + // producer/consumer logging loop will also live here. + xTaskCreatePinnedToCore( + taskEntry, + "StorageTask", + 8192, + this, + 3, + &taskHandle, + 0 + ); +} + + +void StorageTask::taskEntry(void* parameter) +{ + StorageTask* task = + static_cast(parameter); + + task->run(); +} + + +void StorageTask::run() +{ + Serial.println("[Storage] Initializing SD card..."); + + // Retry the mount until it succeeds. This keeps the card dead-pin-capable: + // reseating the card, fixing a wire, or powering the module will bring it + // up without rebooting the Hub. (The SD init handshake runs at 400 kHz, so + // a failure here is wiring/power/seating/pull-up related, not speed.) + while (!storage.begin()) + { + Serial.println("[Storage] SD card mount FAILED."); + Serial.println("[Storage] Check: 3.3V power + common ground, CS/SCK/MOSI/MISO wiring,"); + Serial.println("[Storage] card fully seated (click), and module pull-ups."); + Serial.println("[Storage] Retrying in 5 s..."); + vTaskDelay(pdMS_TO_TICKS(5000)); + } + + storage.printCardInfo(); + storage.listFiles(); + + TickType_t lastWake = + xTaskGetTickCount(); + + while (true) + { + // Future integration point: a producer/consumer queue will feed + // audio data here to be flushed to the card. For now the task + // simply idles while the system runs. + vTaskDelayUntil( + &lastWake, + pdMS_TO_TICKS(1000) + ); + } +} diff --git a/src/tasks/storage_task.h b/src/tasks/storage_task.h new file mode 100644 index 0000000..4b65e2f --- /dev/null +++ b/src/tasks/storage_task.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "../storage/sd_manager.h" + + +class StorageTask +{ +public: + StorageTask(SDManager& manager); + + void start(); + +private: + static void taskEntry(void* parameter); + void run(); + + SDManager& storage; + + TaskHandle_t taskHandle = nullptr; +};