Add shared SPI bus mutex for SD card, display, and LoRa coexistence

The T-Deck Plus shares HSPI across the display (CS=12), LoRa (CS=9),
and SD card (CS=39). Previously SD logging was disabled because
SD.begin() reconfigured the SPI bus and blanked the display.

This introduces a FreeRTOS mutex created in main.cpp and injected into
Display, SX1262Interface, and a new SDAccess class so all three
peripherals serialize their SPI transactions safely.

- Add SDAccess class wrapping SD.begin() and file ops with mutex
- Add set_spi_mutex() to Display and SX1262Interface
- Wrap Display flush, fill, draw, and power ops in mutex
- Refactor SDLogger to use SDAccess mutex instead of owning SD.begin()
- Wire up mutex creation and injection order in setup()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
torlando-tech
2026-03-04 00:19:10 -05:00
co-authored by Claude Opus 4.6
parent d9411fb4bb
commit d03f0b308f
9 changed files with 313 additions and 25 deletions
+11 -3
View File
@@ -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
+6
View File
@@ -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().
+26
View File
@@ -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();
+10
View File
@@ -10,6 +10,8 @@
#include <Arduino.h>
#include <SPI.h>
#include <lvgl.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
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;
+120
View File
@@ -0,0 +1,120 @@
// Copyright (c) 2024 microReticulum contributors
// SPDX-License-Identifier: MIT
#include "SDAccess.h"
#ifdef ARDUINO
#include <Log.h>
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
+79
View File
@@ -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 <Arduino.h>
#include <SD.h>
#include <SPI.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
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
+26 -15
View File
@@ -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
+4 -3
View File
@@ -5,6 +5,7 @@
#define HARDWARE_TDECK_SDLOGGER_H
#include "Config.h"
#include "SDAccess.h"
#include <Log.h>
#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();
+31 -4
View File
@@ -63,7 +63,8 @@
// Logging
#include <Log.h>
// SD Card logging for crash debugging
// SD Card access and logging
#include <Hardware/TDeck/SDAccess.h>
#include <Hardware/TDeck/SDLogger.h>
// 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();