diff --git a/exercises/05_SD_Card_Watcher/src/main.cpp b/exercises/05_SD_Card_Watcher/src/main.cpp index dd778d7..f3f4373 100644 --- a/exercises/05_SD_Card_Watcher/src/main.cpp +++ b/exercises/05_SD_Card_Watcher/src/main.cpp @@ -1,4 +1,4 @@ -// 20260213 ChatGPT +// 20260214 ChatGPT // $Id$ // $HeadURL$ @@ -7,8 +7,19 @@ #include #include #include +#include "driver/gpio.h" // gpio_get_level() #include "tbeam_supreme_adapter.h" +// ------------------------- +// Configuration toggles +// ------------------------- +#define ENABLE_SD_RAIL_CYCLE 1 // Power-cycle AXP2101 BLDO1 (SD rail) at boot. +#define ENABLE_PIN_DUMPS 1 // Log SPI pin logic levels at key points (NON-INTRUSIVE). +#define STARTUP_SERIAL_DELAY_MS 5000 + +// ------------------------- +// Globals +// ------------------------- static SPIClass sdSpiH(HSPI); static SPIClass sdSpiF(FSPI); static SPIClass* g_sdSpi = nullptr; @@ -16,10 +27,10 @@ static const char* g_sdBusName = "none"; static uint32_t g_sdFreq = 0; static XPowersLibInterface* g_pmu = nullptr; -static const char* kRootTestFile = "/Exercise_05_test.txt"; -static const char* kNestedDir = "/test/testsub1/testsubsub1"; +static const char* kRootTestFile = "/Exercise_05_test.txt"; +static const char* kNestedDir = "/test/testsub1/testsubsub1"; static const char* kNestedTestFile = "/test/testsub1/testsubsub1/Exercise_05_test.txt"; -static const char* kPayload = "This is a test"; +static const char* kPayload = "This is a test"; enum class WatchState : uint8_t { UNKNOWN = 0, @@ -33,37 +44,127 @@ static uint8_t g_absentVotes = 0; static uint32_t g_lastPollMs = 0; static uint32_t g_lastFullScanMs = 0; static uint32_t g_lastPeriodicActionMs = 0; -static const uint32_t kPollIntervalAbsentMs = 1000; -static const uint32_t kPollIntervalMountedMs = 2000; -static const uint32_t kFullScanIntervalMs = 10000; -static const uint32_t kPeriodicActionMs = 15000; -static const uint8_t kVotesToPresent = 2; -//static const uint8_t kVotesToAbsent = 4; -static const uint8_t kVotesToAbsent = 5; // More votes needed to declare absent to prevent false removes on transient errors. -//static const uint32_t kStartupWarmupMs = 800; -static const uint32_t kStartupWarmupMs = 1500; // Longer warmup to allow PMU and card stabilization after power-on. +static const uint32_t kPollIntervalAbsentMs = 1000; +static const uint32_t kPollIntervalMountedMs = 2000; +static const uint32_t kFullScanIntervalMs = 10000; +static const uint32_t kPeriodicActionMs = 15000; + +static const uint8_t kVotesToPresent = 2; +static const uint8_t kVotesToAbsent = 5; // More votes needed to declare absent to prevent false removes. +static const uint32_t kStartupWarmupMs = 1500; // Allow PMU and SD rail to stabilize. + static uint32_t g_logSeq = 0; +// ------------------------- +// Logging helpers +// ------------------------- static void logf(const char* fmt, ...) { char msg[192]; va_list args; va_start(args, fmt); vsnprintf(msg, sizeof(msg), fmt, args); va_end(args); - Serial.printf("[%10lu][%06lu] %s\r\n", (unsigned long)millis(), (unsigned long)g_logSeq++, msg); + Serial.printf("[%10lu][%06lu] %s\r\n", + (unsigned long)millis(), + (unsigned long)g_logSeq++, + msg); +} + +static void dumpPmu(const char* tag, XPowersLibInterface* pmu) { + if (!pmu) { + logf("PMU(%s): pmu=null", tag); + return; + } + + bool bldo1 = pmu->isPowerChannelEnable(XPOWERS_BLDO1); + int vbus = pmu->getVbusVoltage(); + int batt = pmu->getBattVoltage(); + + logf("PMU(%s): BLDO1(SD)=%s VBUS=%dmV VBAT=%dmV", + tag, bldo1 ? "ON" : "OFF", vbus, batt); +} + +// IMPORTANT: this function MUST NOT modify pin modes (regression cause). +static void dumpSdPins(const char* tag) { +#if ENABLE_PIN_DUMPS + const gpio_num_t CS = (gpio_num_t)tbeam_supreme::sdCs(); + const gpio_num_t SCK = (gpio_num_t)tbeam_supreme::sdSck(); + const gpio_num_t MISO = (gpio_num_t)tbeam_supreme::sdMiso(); + const gpio_num_t MOSI = (gpio_num_t)tbeam_supreme::sdMosi(); + + int cs = gpio_get_level(CS); + int sck = gpio_get_level(SCK); + int miso = gpio_get_level(MISO); + int mosi = gpio_get_level(MOSI); + + logf("PINS(%s): CS=%d SCK=%d MISO=%d MOSI=%d", tag, cs, sck, miso, mosi); +#else + (void)tag; +#endif +} + +// ------------------------- +// Power + bus conditioning +// ------------------------- +static void forceSpiDeselected() { + pinMode(tbeam_supreme::sdCs(), OUTPUT); + digitalWrite(tbeam_supreme::sdCs(), HIGH); + + pinMode(tbeam_supreme::imuCs(), OUTPUT); + digitalWrite(tbeam_supreme::imuCs(), HIGH); } static bool initPmuForSdPower() { - return tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial); + bool ok = tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial); + if (!ok) { + logf("ERROR: PMU init failed"); + return false; + } + + logf("PMU adapter: AXP2101 ready, BLDO1(SD)=%s", + g_pmu && g_pmu->isPowerChannelEnable(XPOWERS_BLDO1) ? "ON" : "OFF"); + return true; } +static void cycleSdRail(XPowersLibInterface* pmu, + uint32_t off_ms = 250, + uint32_t on_settle_ms = 600) { +#if ENABLE_SD_RAIL_CYCLE + if (!pmu) { + logf("SD rail cycle skipped: pmu=null"); + return; + } + + dumpPmu("pre-sd-cycle", pmu); + + // Ensure the card is NOT selected while power is unstable. + forceSpiDeselected(); + dumpSdPins("pre-sd-cycle"); + + pmu->disablePowerOutput(XPOWERS_BLDO1); + delay(off_ms); + + pmu->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); + pmu->enablePowerOutput(XPOWERS_BLDO1); + delay(on_settle_ms); + + dumpPmu("post-sd-cycle", pmu); + dumpSdPins("post-sd-cycle"); +#else + (void)pmu; (void)off_ms; (void)on_settle_ms; +#endif +} + +// ------------------------- +// SD helpers +// ------------------------- static const char* cardTypeToString(uint8_t type) { switch (type) { - case CARD_MMC: return "MMC"; - case CARD_SD: return "SDSC"; + case CARD_MMC: return "MMC"; + case CARD_SD: return "SDSC"; case CARD_SDHC: return "SDHC/SDXC"; - default: return "UNKNOWN"; + default: return "UNKNOWN"; } } @@ -72,22 +173,26 @@ static bool tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz, boo bus.end(); delay(10); - // Keep inactive devices deselected on shared bus lines. - pinMode(tbeam_supreme::sdCs(), OUTPUT); - digitalWrite(tbeam_supreme::sdCs(), HIGH); - pinMode(tbeam_supreme::imuCs(), OUTPUT); - digitalWrite(tbeam_supreme::imuCs(), HIGH); + forceSpiDeselected(); bus.begin(tbeam_supreme::sdSck(), tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi(), tbeam_supreme::sdCs()); + + // SD SPI "idle clocks" ritual: CS HIGH + >= 74 clocks with MOSI high (0xFF). + digitalWrite(tbeam_supreme::sdCs(), HIGH); delay(2); + for (int i = 0; i < 10; i++) { + bus.transfer(0xFF); // 80 clocks total + } + delay(2); + + dumpSdPins("after-idle-clocks"); if (verbose) { logf("SD: trying bus=%s freq=%lu Hz", busName, (unsigned long)hz); } + if (!SD.begin(tbeam_supreme::sdCs(), bus, hz)) { - if (verbose) { - logf("SD: mount failed (possible non-FAT format, power, or bus issue)"); - } + if (verbose) logf("SD: mount failed (possible non-FAT format, power, or bus issue)"); return false; } @@ -107,7 +212,7 @@ static bool mountPreferred(bool verbose) { return tryMountWithBus(sdSpiH, "HSPI", 400000, verbose); } -static bool mountCard() { +static bool mountCardFullScan() { const uint32_t freqs[] = {400000, 1000000, 4000000, 10000000}; for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) { @@ -131,8 +236,8 @@ static bool mountCard() { static void printCardInfo() { uint8_t cardType = SD.cardType(); uint64_t cardSizeMB = SD.cardSize() / (1024ULL * 1024ULL); - uint64_t totalMB = SD.totalBytes() / (1024ULL * 1024ULL); - uint64_t usedMB = SD.usedBytes() / (1024ULL * 1024ULL); + uint64_t totalMB = SD.totalBytes() / (1024ULL * 1024ULL); + uint64_t usedMB = SD.usedBytes() / (1024ULL * 1024ULL); logf("SD type: %s", cardTypeToString(cardType)); logf("SD size: %llu MB", cardSizeMB); @@ -143,9 +248,7 @@ static void printCardInfo() { static bool ensureDirRecursive(const char* path) { String full(path); - if (!full.startsWith("/")) { - full = "/" + full; - } + if (!full.startsWith("/")) full = "/" + full; int start = 1; while (start > 0 && start < (int)full.length()) { @@ -160,12 +263,9 @@ static bool ensureDirRecursive(const char* path) { } } - if (slash < 0) { - break; - } + if (slash < 0) break; start = slash + 1; } - return true; } @@ -219,9 +319,7 @@ static void permissionsDemo(const char* path) { static bool verifyMountedCard() { File root = SD.open("/", FILE_READ); - if (!root) { - return false; - } + if (!root) return false; root.close(); return true; } @@ -245,6 +343,9 @@ static void runCardWorkflow() { permissionsDemo(kRootTestFile); } +// ------------------------- +// Watcher state transitions +// ------------------------- static void setStateMounted() { if (g_watchState != WatchState::MOUNTED) { logf("EVENT: card inserted/mounted"); @@ -264,13 +365,22 @@ static void setStateAbsent() { g_watchState = WatchState::ABSENT; } +// ------------------------- +// Arduino entry points +// ------------------------- void setup() { Serial.begin(115200); Serial.println("[WATCHER: startup]"); - Serial.println("Sleeping for 5 seconds to allow Serial Monitor connection..."); - delay(5000); // Time to open Serial Monitor after reset - Serial.println("\r\n=================================================="); + // De-select SPI devices immediately. + forceSpiDeselected(); + dumpSdPins("very-early"); + + logf("Sleeping for %lu ms to allow Serial Monitor connection...", (unsigned long)STARTUP_SERIAL_DELAY_MS); + delay(STARTUP_SERIAL_DELAY_MS); + + Serial.println(); + Serial.println("=================================================="); Serial.println("Exercise 05: SD Card Watcher"); Serial.println("=================================================="); Serial.printf("Pins: CS=%d SCK=%d MISO=%d MOSI=%d\r\n", @@ -278,13 +388,20 @@ void setup() { tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi()); Serial.printf("PMU I2C: SDA1=%d SCL1=%d\r\n", tbeam_supreme::i2cSda(), tbeam_supreme::i2cScl()); - Serial.println("Note: SD must be FAT16/FAT32 for Arduino SD library.\r\n"); + Serial.println("Note: SD must be FAT16/FAT32 for Arduino SD library."); + Serial.println(); initPmuForSdPower(); + dumpPmu("post-pmu-init", g_pmu); + + // Software equivalent of "remove/insert card". + cycleSdRail(g_pmu); + logf("Watcher: waiting %lu ms for SD rail/card stabilization", (unsigned long)kStartupWarmupMs); delay(kStartupWarmupMs); - // Warm-up attempts before first status decision. + dumpSdPins("pre-warmup-mount"); + bool warmMounted = false; for (uint8_t i = 0; i < 3; ++i) { if (mountPreferred(false)) { @@ -293,6 +410,7 @@ void setup() { } delay(200); } + if (warmMounted) { logf("Watcher: startup warmup mount succeeded"); setStateMounted(); @@ -304,9 +422,9 @@ void setup() { void loop() { const uint32_t now = millis(); - const uint32_t pollInterval = (g_watchState == WatchState::MOUNTED) - ? kPollIntervalMountedMs - : kPollIntervalAbsentMs; + const uint32_t pollInterval = + (g_watchState == WatchState::MOUNTED) ? kPollIntervalMountedMs : kPollIntervalAbsentMs; + if ((uint32_t)(now - g_lastPollMs) < pollInterval) { delay(10); return; @@ -321,14 +439,14 @@ void loop() { g_lastPeriodicActionMs = now; } g_presentVotes = 0; - g_absentVotes = 0; + g_absentVotes = 0; return; } - // One immediate remount attempt prevents false remove events on transient SPI errors. + // One immediate remount attempt prevents false removes on transient SPI errors. if (mountPreferred(false) && verifyMountedCard()) { g_presentVotes = 0; - g_absentVotes = 0; + g_absentVotes = 0; return; } @@ -341,11 +459,12 @@ void loop() { return; } + // ABSENT/UNKNOWN state bool mounted = mountPreferred(false); if (!mounted && (uint32_t)(now - g_lastFullScanMs) >= kFullScanIntervalMs) { g_lastFullScanMs = now; logf("Watcher: preferred probe failed, running full scan"); - mounted = mountCard(); + mounted = mountCardFullScan(); } if (mounted) {