Basic SD working
This commit is contained in:
@@ -12,12 +12,6 @@ void DiagnosticsState::update()
|
||||
|
||||
current.cpuFrequency =
|
||||
ESP.getCpuFreqMHz();
|
||||
|
||||
Serial.print("#update: ");
|
||||
Serial.println(current.freeHeap);
|
||||
Serial.println("Calling current from update");
|
||||
getCurrent();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -128,7 +128,6 @@ Firmware Version: <div id="firmware_version">Loading...</div>
|
||||
void WebService::broadcastState()
|
||||
{
|
||||
DiagnosticSample diagnostics = diagnosticsState.getCurrent();
|
||||
Serial.println(diagnostics.freeHeap);
|
||||
String json = "{";
|
||||
|
||||
// Opening system tag:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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
|
||||
@@ -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<StorageTask*>(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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#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;
|
||||
};
|
||||
Reference in New Issue
Block a user