diff --git a/lib/sx1262_interface/SX1262Interface.cpp b/lib/sx1262_interface/SX1262Interface.cpp index 56f529aa..d830517c 100644 --- a/lib/sx1262_interface/SX1262Interface.cpp +++ b/lib/sx1262_interface/SX1262Interface.cpp @@ -12,9 +12,17 @@ using namespace RNS; #ifdef ARDUINO -// Static members for SPI mutex (shared with display) +// Static members for SPI mutex (shared with display and SD card) SemaphoreHandle_t SX1262Interface::_spi_mutex = nullptr; bool SX1262Interface::_mutex_initialized = false; + +void SX1262Interface::set_spi_mutex(SemaphoreHandle_t mutex) { + _spi_mutex = mutex; + _mutex_initialized = (mutex != nullptr); + if (_mutex_initialized) { + DEBUG("SX1262Interface: Using external SPI mutex"); + } +} #endif SX1262Interface::SX1262Interface(const char* name) : InterfaceImpl(name) { @@ -58,15 +66,15 @@ bool SX1262Interface::start() { INFO(" CR: 4/" + std::to_string(_config.coding_rate)); INFO(" TX Power: " + std::to_string(_config.tx_power) + " dBm"); - // Initialize SPI mutex if not already done + // Use external mutex if provided, otherwise create our own (fallback) if (!_mutex_initialized) { + WARNING("SX1262Interface: No external SPI mutex set, creating own"); _spi_mutex = xSemaphoreCreateMutex(); if (_spi_mutex == nullptr) { ERROR("SX1262Interface: Failed to create SPI mutex"); return false; } _mutex_initialized = true; - DEBUG("SX1262Interface: SPI mutex created"); } // Acquire SPI mutex diff --git a/lib/sx1262_interface/SX1262Interface.h b/lib/sx1262_interface/SX1262Interface.h index 471c5b59..4420e315 100644 --- a/lib/sx1262_interface/SX1262Interface.h +++ b/lib/sx1262_interface/SX1262Interface.h @@ -50,6 +50,12 @@ public: SX1262Interface(const char* name = "LoRa"); virtual ~SX1262Interface(); + /** + * Set the shared SPI bus mutex. Call before start(). + * If set, the interface uses this mutex instead of creating its own. + */ + static void set_spi_mutex(SemaphoreHandle_t mutex); + /** * Set configuration before calling start(). * Changes take effect on next start(). diff --git a/lib/tdeck_ui/Hardware/TDeck/Display.cpp b/lib/tdeck_ui/Hardware/TDeck/Display.cpp index a2f058f1..c338b5e5 100644 --- a/lib/tdeck_ui/Hardware/TDeck/Display.cpp +++ b/lib/tdeck_ui/Hardware/TDeck/Display.cpp @@ -14,12 +14,17 @@ namespace Hardware { namespace TDeck { SPIClass* Display::_spi = nullptr; +SemaphoreHandle_t Display::_spi_mutex = nullptr; uint8_t Display::_brightness = Disp::BACKLIGHT_DEFAULT; bool Display::_initialized = false; volatile uint32_t Display::_flush_count = 0; volatile uint32_t Display::_last_flush_ms = 0; uint32_t Display::_last_health_log_ms = 0; +void Display::set_spi_mutex(SemaphoreHandle_t mutex) { + _spi_mutex = mutex; +} + bool Display::init() { if (_initialized) { return true; @@ -157,6 +162,9 @@ uint8_t Display::get_brightness() { } void Display::set_power(bool on) { + if (_spi_mutex && xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { + return; + } if (on) { write_command(Command::DISPON); set_brightness(_brightness); @@ -164,9 +172,13 @@ void Display::set_power(bool on) { set_brightness(0); write_command(Command::DISPOFF); } + if (_spi_mutex) xSemaphoreGive(_spi_mutex); } void Display::fill_screen(uint16_t color) { + if (_spi_mutex && xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { + return; + } set_addr_window(0, 0, Disp::WIDTH - 1, Disp::HEIGHT - 1); begin_write(); @@ -181,6 +193,7 @@ void Display::fill_screen(uint16_t color) { write_data(color_bytes, 2); } end_write(); + if (_spi_mutex) xSemaphoreGive(_spi_mutex); } void Display::draw_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) { @@ -188,6 +201,10 @@ void Display::draw_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t col return; } + if (_spi_mutex && xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { + return; + } + set_addr_window(x, y, x + w - 1, y + h - 1); begin_write(); @@ -201,9 +218,16 @@ void Display::draw_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t col write_data(color_bytes, 2); } end_write(); + if (_spi_mutex) xSemaphoreGive(_spi_mutex); } void Display::lvgl_flush_cb(lv_disp_drv_t* drv, const lv_area_t* area, lv_color_t* color_p) { + if (_spi_mutex && xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(100)) != pdTRUE) { + // Skip this frame — LVGL will retry next tick + lv_disp_flush_ready(drv); + return; + } + uint32_t start = millis(); int32_t w = area->x2 - area->x1 + 1; @@ -221,6 +245,8 @@ void Display::lvgl_flush_cb(lv_disp_drv_t* drv, const lv_area_t* area, lv_color_ end_write(); + if (_spi_mutex) xSemaphoreGive(_spi_mutex); + _flush_count++; _last_flush_ms = millis(); diff --git a/lib/tdeck_ui/Hardware/TDeck/Display.h b/lib/tdeck_ui/Hardware/TDeck/Display.h index 0f866b53..8aeafa45 100644 --- a/lib/tdeck_ui/Hardware/TDeck/Display.h +++ b/lib/tdeck_ui/Hardware/TDeck/Display.h @@ -10,6 +10,8 @@ #include #include #include +#include +#include namespace Hardware { namespace TDeck { @@ -27,6 +29,13 @@ namespace TDeck { */ class Display { public: + /** + * Set the shared SPI bus mutex. Call before init(). + * If not set, Display operates without mutex protection + * (safe during early boot before other SPI users exist). + */ + static void set_spi_mutex(SemaphoreHandle_t mutex); + /** * Initialize display and LVGL * @return true if initialization successful @@ -131,6 +140,7 @@ private: }; static SPIClass* _spi; + static SemaphoreHandle_t _spi_mutex; static uint8_t _brightness; static bool _initialized; diff --git a/lib/tdeck_ui/Hardware/TDeck/SDAccess.cpp b/lib/tdeck_ui/Hardware/TDeck/SDAccess.cpp new file mode 100644 index 00000000..bc00c93c --- /dev/null +++ b/lib/tdeck_ui/Hardware/TDeck/SDAccess.cpp @@ -0,0 +1,120 @@ +// Copyright (c) 2024 microReticulum contributors +// SPDX-License-Identifier: MIT + +#include "SDAccess.h" + +#ifdef ARDUINO + +#include + +using namespace RNS; + +namespace Hardware { +namespace TDeck { + +SemaphoreHandle_t SDAccess::_spi_mutex = nullptr; +SPIClass* SDAccess::_sd_spi = nullptr; +bool SDAccess::_ready = false; + +bool SDAccess::init(SemaphoreHandle_t mutex) { + if (_ready) return true; + if (mutex == nullptr) { + Serial.println("[SDAccess] ERROR: null mutex"); + return false; + } + + _spi_mutex = mutex; + + // Drive SD CS high before init to avoid bus contention + pinMode(SDCard::CS, OUTPUT); + digitalWrite(SDCard::CS, HIGH); + + // Acquire mutex for SD.begin() since it reconfigures SPI + if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(2000)) != pdTRUE) { + Serial.println("[SDAccess] Failed to acquire SPI mutex for init"); + return false; + } + + // Create SPIClass on HSPI — attaches to same hardware peripheral + // as display and LoRa, but SD.begin() needs its own instance + _sd_spi = new SPIClass(HSPI); + _sd_spi->begin(Pin::DISPLAY_SCK, Radio::SPI_MISO, Pin::DISPLAY_MOSI, SDCard::CS); + + bool ok = SD.begin(SDCard::CS, *_sd_spi, SD_SPI_FREQ); + + xSemaphoreGive(_spi_mutex); + + if (!ok) { + Serial.println("[SDAccess] SD card mount failed (no card?)"); + return false; + } + + uint8_t cardType = SD.cardType(); + if (cardType == CARD_NONE) { + Serial.println("[SDAccess] No SD card detected"); + return false; + } + + _ready = true; + + const char* type_str = "UNKNOWN"; + switch (cardType) { + case CARD_MMC: type_str = "MMC"; break; + case CARD_SD: type_str = "SD"; break; + case CARD_SDHC: type_str = "SDHC"; break; + default: break; + } + Serial.printf("[SDAccess] Card type: %s, size: %lluMB\n", + type_str, SD.cardSize() / (1024 * 1024)); + + return true; +} + +int SDAccess::read_file(const char* path, uint8_t* buf, size_t max_len) { + if (!_ready || !_spi_mutex) return -1; + + if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { + return -1; + } + + File f = SD.open(path, FILE_READ); + if (!f) { + xSemaphoreGive(_spi_mutex); + return -1; + } + + int bytes_read = f.read(buf, max_len); + f.close(); + + xSemaphoreGive(_spi_mutex); + return bytes_read; +} + +bool SDAccess::file_exists(const char* path) { + if (!_ready || !_spi_mutex) return false; + + if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { + return false; + } + + bool exists = SD.exists(path); + + xSemaphoreGive(_spi_mutex); + return exists; +} + +bool SDAccess::acquire_bus(uint32_t timeout_ms) { + if (!_spi_mutex) return false; + return xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(timeout_ms)) == pdTRUE; +} + +void SDAccess::release_bus() { + if (_spi_mutex) { + xSemaphoreGive(_spi_mutex); + } +} + +} // namespace TDeck +} // namespace Hardware + +#endif // ARDUINO diff --git a/lib/tdeck_ui/Hardware/TDeck/SDAccess.h b/lib/tdeck_ui/Hardware/TDeck/SDAccess.h new file mode 100644 index 00000000..1852c1bf --- /dev/null +++ b/lib/tdeck_ui/Hardware/TDeck/SDAccess.h @@ -0,0 +1,79 @@ +// Copyright (c) 2024 microReticulum contributors +// SPDX-License-Identifier: MIT + +#ifndef HARDWARE_TDECK_SDACCESS_H +#define HARDWARE_TDECK_SDACCESS_H + +#include "Config.h" + +#ifdef ARDUINO +#include +#include +#include +#include +#include + +namespace Hardware { +namespace TDeck { + +/** + * Mutex-protected SD card access for shared SPI bus. + * + * The T-Deck Plus shares HSPI (SCK=40, MOSI=41, MISO=38) across + * display (CS=12), LoRa (CS=9), and SD card (CS=39). All SPI + * operations must be serialized via a shared FreeRTOS mutex. + * + * SDAccess wraps SD.begin() and all file operations in the mutex + * so callers don't need to manage bus arbitration themselves. + */ +class SDAccess { +public: + /** + * Initialize SD card on the shared HSPI bus. + * Creates its own SPIClass(HSPI) attached to the same hardware + * peripheral, then calls SD.begin() with that instance. + * + * @param mutex Shared SPI bus mutex (created in main.cpp) + * @return true if SD card mounted successfully + */ + static bool init(SemaphoreHandle_t mutex); + + /** Check if SD card is mounted and ready */ + static bool is_ready() { return _ready; } + + /** Get the shared SPI mutex (for SDLogger to wrap its own operations) */ + static SemaphoreHandle_t get_mutex() { return _spi_mutex; } + + /** + * Read an entire file into a buffer with mutex protection. + * @return bytes read, or -1 on error + */ + static int read_file(const char* path, uint8_t* buf, size_t max_len); + + /** Check if a file exists (with mutex) */ + static bool file_exists(const char* path); + + /** + * Acquire the SPI bus mutex for streaming operations. + * Caller MUST call release_bus() when done. + * @param timeout_ms Max wait time + * @return true if mutex acquired + */ + static bool acquire_bus(uint32_t timeout_ms = 500); + + /** Release the SPI bus mutex after streaming operations */ + static void release_bus(); + +private: + static SemaphoreHandle_t _spi_mutex; + static SPIClass* _sd_spi; + static bool _ready; + + static constexpr uint32_t SD_SPI_FREQ = 20000000; // 20MHz for SD card +}; + +} // namespace TDeck +} // namespace Hardware + +#endif // ARDUINO +#endif // HARDWARE_TDECK_SDACCESS_H diff --git a/lib/tdeck_ui/Hardware/TDeck/SDLogger.cpp b/lib/tdeck_ui/Hardware/TDeck/SDLogger.cpp index 6fa4bd2e..336a5cf3 100644 --- a/lib/tdeck_ui/Hardware/TDeck/SDLogger.cpp +++ b/lib/tdeck_ui/Hardware/TDeck/SDLogger.cpp @@ -25,24 +25,21 @@ bool SDLogger::init() { } _initialized = true; - // Initialize SD card on shared SPI bus - // Note: SPI should already be initialized by display - if (!SD.begin(SDCard::CS)) { - Serial.println("[SDLogger] SD card mount failed"); + // SDAccess must already be initialized (handles SD.begin with mutex) + if (!SDAccess::is_ready()) { + Serial.println("[SDLogger] SDAccess not ready, skipping SD logging"); return false; } - // Check card type - uint8_t cardType = SD.cardType(); - if (cardType == CARD_NONE) { - Serial.println("[SDLogger] No SD card detected"); + // Open log file with mutex protection + if (!SDAccess::acquire_bus(1000)) { + Serial.println("[SDLogger] Failed to acquire SPI mutex for log file open"); return false; } - // Open or create log file - // Use append mode to preserve history across reboots _logFile = SD.open(CURRENT_LOG, FILE_APPEND); if (!_logFile) { + SDAccess::release_bus(); Serial.println("[SDLogger] Failed to open log file"); return false; } @@ -55,6 +52,8 @@ bool SDLogger::init() { _logFile.println("========================================\n"); _logFile.flush(); + SDAccess::release_bus(); + _bytes_written = 0; _last_flush = millis(); _line_count = 0; @@ -64,7 +63,6 @@ bool SDLogger::init() { RNS::setLogCallback(logCallback); Serial.println("[SDLogger] SD card logging active"); - Serial.printf("[SDLogger] Card size: %lluMB\n", SD.cardSize() / (1024 * 1024)); return true; } @@ -85,6 +83,10 @@ void SDLogger::logCallback(const char* msg, RNS::LogLevel level) { } void SDLogger::writeToFile(const char* msg, RNS::LogLevel level) { + if (!SDAccess::acquire_bus(50)) { + return; // Skip this log line rather than block the caller + } + // Format: timestamp [LEVEL] message int written = _logFile.printf("%s [%s] %s\n", RNS::getTimeString(), @@ -118,28 +120,34 @@ void SDLogger::writeToFile(const char* msg, RNS::LogLevel level) { _line_count = 0; } - // Check if we need to rotate + // Check if we need to rotate (still holding mutex) if (_bytes_written >= MAX_LOG_SIZE) { rotateIfNeeded(); } + + SDAccess::release_bus(); } void SDLogger::flush() { if (_active && _logFile) { + if (!SDAccess::acquire_bus(100)) return; _logFile.flush(); _last_flush = millis(); _line_count = 0; + SDAccess::release_bus(); } } void SDLogger::marker(const char* msg) { if (_active && _logFile) { + if (!SDAccess::acquire_bus(200)) return; _logFile.println("----------------------------------------"); _logFile.printf(">>> MARKER: %s <<<\n", msg); _logFile.printf(">>> Heap: %lu / Min: %lu <<<\n", ESP.getFreeHeap(), ESP.getMinFreeHeap()); _logFile.println("----------------------------------------"); _logFile.flush(); + SDAccess::release_bus(); } } @@ -175,9 +183,12 @@ void SDLogger::rotateIfNeeded() { void SDLogger::close() { if (_active && _logFile) { - _logFile.println("\n=== LOG CLOSED CLEANLY ==="); - _logFile.flush(); - _logFile.close(); + if (SDAccess::acquire_bus(500)) { + _logFile.println("\n=== LOG CLOSED CLEANLY ==="); + _logFile.flush(); + _logFile.close(); + SDAccess::release_bus(); + } _active = false; } // Restore default logging diff --git a/lib/tdeck_ui/Hardware/TDeck/SDLogger.h b/lib/tdeck_ui/Hardware/TDeck/SDLogger.h index decd5310..9b76d38d 100644 --- a/lib/tdeck_ui/Hardware/TDeck/SDLogger.h +++ b/lib/tdeck_ui/Hardware/TDeck/SDLogger.h @@ -5,6 +5,7 @@ #define HARDWARE_TDECK_SDLOGGER_H #include "Config.h" +#include "SDAccess.h" #include #ifdef ARDUINO @@ -29,10 +30,10 @@ namespace TDeck { class SDLogger { public: /** - * Initialize SD card and set up logging callback. - * Must be called after SPI bus is initialized (after display init). + * Initialize SD logging. Must be called after SDAccess::init(). + * Uses the shared SPI mutex from SDAccess for all file operations. * - * @return true if SD card mounted and logging active + * @return true if SD card accessible and logging active */ static bool init(); diff --git a/src/main.cpp b/src/main.cpp index a8e108e1..65120523 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -63,7 +63,8 @@ // Logging #include -// SD Card logging for crash debugging +// SD Card access and logging +#include #include // OTA flashing @@ -1276,19 +1277,45 @@ void setup() { } } + // Create shared SPI bus mutex (display, LoRa, SD card all share HSPI) + SemaphoreHandle_t spi_mutex = xSemaphoreCreateMutex(); + if (!spi_mutex) { + ERROR("Failed to create SPI bus mutex!"); + } + + // Set SPI mutex on Display before init (null-safe if mutex creation failed) + Hardware::TDeck::Display::set_spi_mutex(spi_mutex); + // Initialize LVGL and hardware drivers BOOT_PROFILE_START("lvgl"); setup_lvgl_and_ui(); BOOT_PROFILE_END("lvgl"); - // NOTE: SD card logging disabled - shares SPI with display and causes blank screen - // TODO: Need to reinitialize display SPI after SD.begin() or use separate bus + // Set SPI mutex on LoRa interface (before setup_reticulum creates it) + if (spi_mutex) { + SX1262Interface::set_spi_mutex(spi_mutex); + } - // Initialize Reticulum + // Initialize Reticulum (includes LoRa on shared SPI bus) BOOT_PROFILE_START("reticulum"); setup_reticulum(); BOOT_PROFILE_END("reticulum"); + // Initialize SD card on shared SPI bus (after display + LoRa) + BOOT_PROFILE_START("sd_card"); + if (spi_mutex) { + if (Hardware::TDeck::SDAccess::init(spi_mutex)) { + INFO("SD card initialized on shared SPI bus"); + // Initialize SD logging now that SDAccess is ready + if (Hardware::TDeck::SDLogger::init()) { + INFO("SD card logging active"); + } + } else { + INFO("SD card not available (no card inserted?)"); + } + } + BOOT_PROFILE_END("sd_card"); + // Initialize LXMF BOOT_PROFILE_START("lxmf"); setup_lxmf();