diff --git a/libraries/ArduinoStorage/ArduinoStorage.h b/libraries/ArduinoStorage/ArduinoStorage.h new file mode 100644 index 00000000..2dbd2778 --- /dev/null +++ b/libraries/ArduinoStorage/ArduinoStorage.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ARDUINO_STORAGE_H +#define ARDUINO_STORAGE_H + +/** + * @brief Arduino Storage Library + * + * This library provides a unified interface for all Arduino storage implementations. + * It defines the base classes and error handling that all storage types (QSPI, SD, Flash, etc.) + * conform to. + * + * Include this header to get access to: + * - StorageError: Error handling class + * - File: Abstract base class for file operations + * - Folder: Abstract base class for folder operations + * - FileMode: Enum for file opening modes + * - FilesystemType: Enum for filesystem types + * + * For specific storage implementations, include their respective headers: + * - QSPIStorage.h: QSPI flash storage + * - SDStorage.h: SD card storage + * - FlashStorage.h: Internal flash storage + */ + +#include "StorageCommon.h" +#include "StorageError.h" +#include "StorageFile.h" +#include "StorageFolder.h" + +#endif // ARDUINO_STORAGE_H diff --git a/libraries/ArduinoStorage/CMakeLists.txt b/libraries/ArduinoStorage/CMakeLists.txt new file mode 100644 index 00000000..a7dccfab --- /dev/null +++ b/libraries/ArduinoStorage/CMakeLists.txt @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 +zephyr_include_directories(.) + +if(NOT DEFINED ARDUINO_BUILD_PATH) + zephyr_sources_ifdef(CONFIG_FILE_SYSTEM + StorageError.cpp + StorageFile.cpp + StorageFolder.cpp + ) +endif() diff --git a/libraries/ArduinoStorage/StorageCommon.h b/libraries/ArduinoStorage/StorageCommon.h new file mode 100644 index 00000000..0c448e8d --- /dev/null +++ b/libraries/ArduinoStorage/StorageCommon.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ARDUINO_STORAGE_COMMON_H +#define ARDUINO_STORAGE_COMMON_H + +#include + +// File opening modes +enum class FileMode { + READ, // Open for reading, file must exist + WRITE, // Open for writing, creates if doesn't exist, truncates if exists + APPEND, // Open for writing at end, creates if doesn't exist + READ_WRITE, // Open for reading and writing, file must exist + READ_WRITE_CREATE // Open for reading and writing, creates if doesn't exist +}; + +// Supported filesystem types +enum class FilesystemType { + LITTLEFS, // LittleFS - recommended for flash storage + FAT, // FAT32 - better compatibility, larger overhead + EXT2, // Extended 2 - Linux-style filesystem + AUTO // Auto-detect or use default +}; + +// Storage information structure +struct StorageInfo { + char mountPoint[64]; + FilesystemType fsType; + size_t totalBytes; + size_t usedBytes; + size_t availableBytes; + size_t blockSize; + size_t totalBlocks; + size_t usedBlocks; + bool readOnly; + bool mounted; +}; + +// Storage health structure +struct StorageHealth { + bool healthy; // Overall health status + uint32_t errorCount; // Number of errors encountered + uint32_t badBlocks; // Number of bad blocks (flash) + uint32_t writeCount; // Total write operations + uint32_t eraseCount; // Total erase operations + float fragmentationPercent; // File system fragmentation + char statusMessage[128]; // Human-readable status +}; + +// Partition information +struct PartitionInfo { + const char* label; // Partition name/label + size_t offset; // Start offset in bytes + size_t size; // Size in bytes + FilesystemType fsType; // File system type for this partition +}; + +// Maximum path length +constexpr size_t STORAGE_MAX_PATH_LENGTH = 256; + +#endif // ARDUINO_STORAGE_COMMON_H diff --git a/libraries/ArduinoStorage/StorageError.cpp b/libraries/ArduinoStorage/StorageError.cpp new file mode 100644 index 00000000..15ee09f9 --- /dev/null +++ b/libraries/ArduinoStorage/StorageError.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "StorageError.h" +#include + +StorageError::StorageError() : code_(StorageErrorCode::NONE) { + message_[0] = '\0'; +} + +StorageErrorCode StorageError::getCode() const { + return code_; +} + +const char* StorageError::getMessage() const { + if (message_[0] != '\0') { + return message_; + } + + // Return default message based on error code + switch (code_) { + case StorageErrorCode::NONE: + return "No error"; + case StorageErrorCode::FILE_NOT_FOUND: + return "File not found"; + case StorageErrorCode::FOLDER_NOT_FOUND: + return "Folder not found"; + case StorageErrorCode::ALREADY_EXISTS: + return "Already exists"; + case StorageErrorCode::INVALID_PATH: + return "Invalid path"; + case StorageErrorCode::PERMISSION_DENIED: + return "Permission denied"; + case StorageErrorCode::READ_ERROR: + return "Read error"; + case StorageErrorCode::WRITE_ERROR: + return "Write error"; + case StorageErrorCode::SEEK_ERROR: + return "Seek error"; + case StorageErrorCode::OPEN_ERROR: + return "Open error"; + case StorageErrorCode::CLOSE_ERROR: + return "Close error"; + case StorageErrorCode::STORAGE_FULL: + return "Storage full"; + case StorageErrorCode::STORAGE_NOT_MOUNTED: + return "Storage not mounted"; + case StorageErrorCode::STORAGE_CORRUPTED: + return "Storage corrupted"; + case StorageErrorCode::STORAGE_NOT_FORMATTED: + return "Storage not formatted"; + case StorageErrorCode::INVALID_OPERATION: + return "Invalid operation"; + case StorageErrorCode::INVALID_MODE: + return "Invalid mode"; + case StorageErrorCode::BUFFER_OVERFLOW: + return "Buffer overflow"; + case StorageErrorCode::OUT_OF_MEMORY: + return "Out of memory"; + case StorageErrorCode::TIMEOUT: + return "Timeout"; + case StorageErrorCode::HARDWARE_ERROR: + return "Hardware error"; + case StorageErrorCode::NOT_INITIALIZED: + return "Not initialized"; + case StorageErrorCode::UNKNOWN_ERROR: + default: + return "Unknown error"; + } +} + +bool StorageError::hasError() const { + return code_ != StorageErrorCode::NONE; +} + +void StorageError::setError(StorageErrorCode code, const char* message) { + code_ = code; + if (message != nullptr) { + strncpy(message_, message, sizeof(message_) - 1); + message_[sizeof(message_) - 1] = '\0'; + } else { + message_[0] = '\0'; + } +} + +void StorageError::clear() { + code_ = StorageErrorCode::NONE; + message_[0] = '\0'; +} + +StorageError::operator bool() const { + return hasError(); +} diff --git a/libraries/ArduinoStorage/StorageError.h b/libraries/ArduinoStorage/StorageError.h new file mode 100644 index 00000000..510cb4f8 --- /dev/null +++ b/libraries/ArduinoStorage/StorageError.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ARDUINO_STORAGE_ERROR_H +#define ARDUINO_STORAGE_ERROR_H + +#include + +enum class StorageErrorCode { + NONE = 0, + + // File/Folder errors + FILE_NOT_FOUND, + FOLDER_NOT_FOUND, + ALREADY_EXISTS, + INVALID_PATH, + PERMISSION_DENIED, + + // I/O errors + READ_ERROR, + WRITE_ERROR, + SEEK_ERROR, + OPEN_ERROR, + CLOSE_ERROR, + + // Storage errors + STORAGE_FULL, + STORAGE_NOT_MOUNTED, + STORAGE_CORRUPTED, + STORAGE_NOT_FORMATTED, + + // Operation errors + INVALID_OPERATION, + INVALID_MODE, + BUFFER_OVERFLOW, + OUT_OF_MEMORY, + TIMEOUT, + + // Hardware errors + HARDWARE_ERROR, + NOT_INITIALIZED, + + // Generic + UNKNOWN_ERROR +}; + +class StorageError { +public: + StorageError(); + + // Error state + StorageErrorCode getCode() const; + const char* getMessage() const; + bool hasError() const; + + // Error setting (for implementations) + void setError(StorageErrorCode code, const char* message = nullptr); + void clear(); + + // Convenience operators + operator bool() const; // Returns true if error exists + +private: + StorageErrorCode code_; + char message_[128]; +}; + +#endif // ARDUINO_STORAGE_ERROR_H diff --git a/libraries/ArduinoStorage/StorageFile.cpp b/libraries/ArduinoStorage/StorageFile.cpp new file mode 100644 index 00000000..e2aac2dc --- /dev/null +++ b/libraries/ArduinoStorage/StorageFile.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "StorageFile.h" +#include + +File::File() { + path_[0] = '\0'; +} + +File::File(const char* path) { + if (path != nullptr) { + strncpy(path_, path, sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; + } else { + path_[0] = '\0'; + } +} + +File::File(const String& path) { + strncpy(path_, path.c_str(), sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; +} + +File::~File() { +} + +const char* File::getPath() const { + return path_; +} + +String File::getPathAsString() const { + return String(path_); +} + +String File::getFilename() const { + const char* lastSep = strrchr(path_, '/'); + if (lastSep != nullptr) { + return String(lastSep + 1); + } + return String(path_); +} + +void File::setErrorIfNotNull(StorageError* error, StorageErrorCode code, const char* message) { + if (error != nullptr) { + error->setError(code, message); + } +} diff --git a/libraries/ArduinoStorage/StorageFile.h b/libraries/ArduinoStorage/StorageFile.h new file mode 100644 index 00000000..800cadba --- /dev/null +++ b/libraries/ArduinoStorage/StorageFile.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ARDUINO_STORAGE_FILE_H +#define ARDUINO_STORAGE_FILE_H + +#include +#include "StorageCommon.h" +#include "StorageError.h" + +// Forward declaration +class Folder; + +/** + * @brief Abstract base class for file operations in Arduino storage implementations. + * + * This class defines the interface for all file operations. Concrete implementations + * (QSPIFile, SDFile, etc.) should inherit from this class and implement all + * pure virtual methods. + */ +class File { +public: + File(); + File(const char* path); + File(const String& path); + virtual ~File(); + + // Opening and Closing + virtual bool open(const char* filename, FileMode mode, StorageError* error = nullptr) = 0; + virtual bool open(const String& filename, FileMode mode, StorageError* error = nullptr) = 0; + virtual bool open(FileMode mode = FileMode::READ, StorageError* error = nullptr) = 0; + virtual bool close(StorageError* error = nullptr) = 0; + virtual bool changeMode(FileMode mode, StorageError* error = nullptr) = 0; + virtual bool isOpen() const = 0; + + // Reading Operations + virtual size_t read(uint8_t* buffer, size_t size, StorageError* error = nullptr) = 0; + virtual int read(StorageError* error = nullptr) = 0; + virtual String readAsString(StorageError* error = nullptr) = 0; + virtual uint32_t available(StorageError* error = nullptr) = 0; + virtual bool seek(size_t offset, StorageError* error = nullptr) = 0; + virtual size_t position(StorageError* error = nullptr) = 0; + virtual size_t size(StorageError* error = nullptr) = 0; + + // Writing Operations + virtual size_t write(const uint8_t* buffer, size_t size, StorageError* error = nullptr) = 0; + virtual size_t write(const String& data, StorageError* error = nullptr) = 0; + virtual size_t write(uint8_t value, StorageError* error = nullptr) = 0; + virtual bool flush(StorageError* error = nullptr) = 0; + + // File Management + virtual bool exists(StorageError* error = nullptr) const = 0; + virtual bool remove(StorageError* error = nullptr) = 0; + virtual bool rename(const char* newFilename, StorageError* error = nullptr) = 0; + virtual bool rename(const String& newFilename, StorageError* error = nullptr) = 0; + + // Path Information + virtual const char* getPath() const; + virtual String getPathAsString() const; + virtual String getFilename() const; + +protected: + char path_[STORAGE_MAX_PATH_LENGTH]; + + // Helper to set error if pointer is not null + static void setErrorIfNotNull(StorageError* error, StorageErrorCode code, const char* message = nullptr); +}; + +#endif // ARDUINO_STORAGE_FILE_H diff --git a/libraries/ArduinoStorage/StorageFolder.cpp b/libraries/ArduinoStorage/StorageFolder.cpp new file mode 100644 index 00000000..3373323e --- /dev/null +++ b/libraries/ArduinoStorage/StorageFolder.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "StorageFolder.h" +#include + +Folder::Folder() { + path_[0] = '\0'; +} + +Folder::Folder(const char* path) { + if (path != nullptr) { + strncpy(path_, path, sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; + } else { + path_[0] = '\0'; + } +} + +Folder::Folder(const String& path) { + strncpy(path_, path.c_str(), sizeof(path_) - 1); + path_[sizeof(path_) - 1] = '\0'; +} + +Folder::~Folder() { +} + +const char* Folder::getPath() const { + return path_; +} + +String Folder::getPathAsString() const { + return String(path_); +} + +String Folder::getFolderName() const { + const char* lastSep = strrchr(path_, '/'); + if (lastSep != nullptr && *(lastSep + 1) != '\0') { + return String(lastSep + 1); + } + // Handle root path or trailing slash + if (path_[0] == '/' && path_[1] == '\0') { + return String("/"); + } + return String(path_); +} + +void Folder::setErrorIfNotNull(StorageError* error, StorageErrorCode code, const char* message) { + if (error != nullptr) { + error->setError(code, message); + } +} diff --git a/libraries/ArduinoStorage/StorageFolder.h b/libraries/ArduinoStorage/StorageFolder.h new file mode 100644 index 00000000..6396c7ba --- /dev/null +++ b/libraries/ArduinoStorage/StorageFolder.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ARDUINO_STORAGE_FOLDER_H +#define ARDUINO_STORAGE_FOLDER_H + +#include +#include +#include "StorageCommon.h" +#include "StorageError.h" +#include "StorageFile.h" + +/** + * @brief Abstract base class for folder/directory operations in Arduino storage implementations. + * + * This class defines the interface for all folder operations. Concrete implementations + * (QSPIFolder, SDFolder, etc.) should inherit from this class and implement all + * pure virtual methods. + */ +class Folder { +public: + Folder(); + Folder(const char* path); + Folder(const String& path); + virtual ~Folder(); + + // Directory Management + virtual bool exists(StorageError* error = nullptr) const = 0; + virtual bool create(StorageError* error = nullptr) = 0; + virtual bool remove(bool recursive = false, StorageError* error = nullptr) = 0; + virtual bool rename(const char* newName, StorageError* error = nullptr) = 0; + virtual bool rename(const String& newName, StorageError* error = nullptr) = 0; + + // Content Enumeration + virtual size_t getFileCount(StorageError* error = nullptr) = 0; + virtual size_t getFolderCount(StorageError* error = nullptr) = 0; + + // Path Information + virtual const char* getPath() const; + virtual String getPathAsString() const; + virtual String getFolderName() const; + +protected: + char path_[STORAGE_MAX_PATH_LENGTH]; + + // Helper to set error if pointer is not null + static void setErrorIfNotNull(StorageError* error, StorageErrorCode code, const char* message = nullptr); +}; + +#endif // ARDUINO_STORAGE_FOLDER_H diff --git a/libraries/ArduinoStorage/library.properties b/libraries/ArduinoStorage/library.properties new file mode 100644 index 00000000..cd73b48b --- /dev/null +++ b/libraries/ArduinoStorage/library.properties @@ -0,0 +1,9 @@ +name=ArduinoStorage +version=1.0.0 +author=Arduino +maintainer=Arduino +sentence=Base storage API for Arduino storage implementations. +paragraph=Provides abstract File, Folder, and StorageError classes that storage implementations (QSPI, SD, etc.) inherit from. +category=Data Storage +url=https://github.com/arduino/ArduinoCore-zephyr +architectures=* diff --git a/libraries/QSPI/QSPI.cpp b/libraries/QSPI/QSPI.cpp new file mode 100644 index 00000000..50a4d9aa --- /dev/null +++ b/libraries/QSPI/QSPI.cpp @@ -0,0 +1,132 @@ +#include "QSPI.h" + +// Define the QSPI flash device - will be available when overlay is active +#if DT_NODE_EXISTS(DT_NODELABEL(qspi_flash)) +#define QSPI_FLASH_NODE DT_NODELABEL(qspi_flash) +#define QSPI_FLASH_DEVICE DEVICE_DT_GET(QSPI_FLASH_NODE) +#else +#define QSPI_FLASH_DEVICE NULL +#warning "No QSPI flash available on this board" +#endif + +QSPIClass::QSPIClass() : flash_dev(nullptr), initialized(false) { +} + +bool QSPIClass::begin() { + if (QSPI_FLASH_DEVICE == NULL) { + return false; + } + + flash_dev = QSPI_FLASH_DEVICE; + + if (!device_is_ready(flash_dev)) { + flash_dev = nullptr; + return false; + } + + initialized = true; + return true; +} + +bool QSPIClass::read(uint32_t address, void* data, size_t size) { + if (!initialized || !flash_dev) { + return false; + } + + int ret = flash_read(flash_dev, address, data, size); + return (ret == 0); +} + +bool QSPIClass::write(uint32_t address, const void* data, size_t size) { + if (!initialized || !flash_dev) { + return false; + } + + int ret = flash_write(flash_dev, address, data, size); + return (ret == 0); +} + +bool QSPIClass::erase(uint32_t address, size_t size) { + if (!initialized || !flash_dev) { + return false; + } + + int ret = flash_erase(flash_dev, address, size); + return (ret == 0); +} + +size_t QSPIClass::getFlashSize() { + if (!initialized || !flash_dev) { + return 0; + } + + uint64_t size = 0; + int ret = flash_get_size(flash_dev, &size); + if (ret != 0) { + return 0; + } + + return (size_t)size; +} + +size_t QSPIClass::getSectorSize() { + if (!initialized || !flash_dev) { + return 0; + } + + struct flash_pages_info page_info; + int ret = flash_get_page_info_by_offs(flash_dev, 0, &page_info); + if (ret != 0) { + return 0; + } + + return page_info.size; +} + +size_t QSPIClass::getPageSize() { + if (!initialized || !flash_dev) { + return 0; + } + + const struct flash_parameters *flash_params = flash_get_parameters(flash_dev); + if (!flash_params) { + return 0; + } + + return flash_params->write_block_size; +} + +bool QSPIClass::isReady() { + if (!flash_dev) { + return false; + } + + return device_is_ready(flash_dev); +} + +uint32_t QSPIClass::getFlashID() { + // This would require implementing JEDEC ID reading + // For now, return 0 as placeholder + return 0; +} + +bool QSPIClass::isValidAddress(uint32_t address, size_t size) { + if (!initialized || !flash_dev) { + return false; + } + + size_t flash_size = getFlashSize(); + return (address + size <= flash_size); +} + +const struct device* QSPIClass::getDevice() { + return flash_dev; +} + +void QSPIClass::end() { + flash_dev = nullptr; + initialized = false; +} + +// Create global instance +QSPIClass QSPI; \ No newline at end of file diff --git a/libraries/QSPI/QSPI.h b/libraries/QSPI/QSPI.h new file mode 100644 index 00000000..91b4834b --- /dev/null +++ b/libraries/QSPI/QSPI.h @@ -0,0 +1,54 @@ +#ifndef QSPI_H +#define QSPI_H + +#include +#include +#include +#include +#include + +class QSPIClass { + +public: + QSPIClass(); + + // Initialize QSPI flash + bool begin(); + + // Read data from QSPI flash + bool read(uint32_t address, void* data, size_t size); + + // Write data to QSPI flash + bool write(uint32_t address, const void* data, size_t size); + + // Erase sector/block + bool erase(uint32_t address, size_t size); + + // Get flash information + size_t getFlashSize(); + size_t getSectorSize(); + size_t getPageSize(); + + // Check if flash is ready + bool isReady(); + + // Get flash ID + uint32_t getFlashID(); + + // Utility functions + bool isValidAddress(uint32_t address, size_t size = 1); + + // Get the underlying Zephyr device (for filesystem/advanced usage) + const struct device* getDevice(); + + // End/deinitialize + void end(); + +private: + const struct device *flash_dev; + bool initialized; +}; + +extern QSPIClass QSPI; + +#endif \ No newline at end of file diff --git a/libraries/QSPI/examples/BasicQSPI.ino b/libraries/QSPI/examples/BasicQSPI.ino new file mode 100644 index 00000000..9033c802 --- /dev/null +++ b/libraries/QSPI/examples/BasicQSPI.ino @@ -0,0 +1,101 @@ +/* + Basic QSPI Flash Example + + This example demonstrates how to use the QSPI library to read and write + data to external QSPI flash memory on Arduino boards with QSPI support. + + Note: QSPI flash must be configured in the board's device tree overlay. +*/ + +#include + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("QSPI Flash Test"); + + // Initialize QSPI flash + if (!QSPI.begin()) { + Serial.println("Failed to initialize QSPI flash!"); + while (1) { + delay(1000); + } + } + + Serial.println("QSPI flash initialized successfully"); + + // Get flash information + Serial.print("Flash size: "); + Serial.print(QSPI.getFlashSize()); + Serial.println(" bytes"); + + Serial.print("Sector size: "); + Serial.print(QSPI.getSectorSize()); + Serial.println(" bytes"); + + Serial.print("Page size: "); + Serial.print(QSPI.getPageSize()); + Serial.println(" bytes"); + + // Test write and read + testWriteRead(); +} + +void loop() { + // Nothing to do in loop + delay(5000); + + Serial.println("Running periodic test..."); + testWriteRead(); +} + +void testWriteRead() { + const uint32_t test_address = 0x1000; // Test address (4KB offset) + const char test_data[] = "Hello QSPI Flash!"; + char read_buffer[32]; + + Serial.println("\n--- Testing Write/Read ---"); + + // Erase sector first + Serial.print("Erasing sector at 0x"); + Serial.print(test_address, HEX); + Serial.print("... "); + + if (QSPI.erase(test_address, QSPI.getSectorSize())) { + Serial.println("OK"); + } else { + Serial.println("FAILED"); + return; + } + + // Write test data + Serial.print("Writing data... "); + if (QSPI.write(test_address, test_data, strlen(test_data) + 1)) { + Serial.println("OK"); + } else { + Serial.println("FAILED"); + return; + } + + // Read back data + Serial.print("Reading data... "); + memset(read_buffer, 0, sizeof(read_buffer)); + if (QSPI.read(test_address, read_buffer, sizeof(read_buffer))) { + Serial.println("OK"); + + Serial.print("Read data: "); + Serial.println(read_buffer); + + // Verify data + if (strcmp(test_data, read_buffer) == 0) { + Serial.println("Data verification: PASSED"); + } else { + Serial.println("Data verification: FAILED"); + } + } else { + Serial.println("FAILED"); + } +} \ No newline at end of file diff --git a/libraries/QSPI/examples/QSPIFilesystem.ino b/libraries/QSPI/examples/QSPIFilesystem.ino new file mode 100644 index 00000000..59fbcbfe --- /dev/null +++ b/libraries/QSPI/examples/QSPIFilesystem.ino @@ -0,0 +1,283 @@ +/* + QSPI Filesystem Example with LittleFS + + This example demonstrates how to use Zephyr's LittleFS filesystem + to store and retrieve files on external QSPI flash memory. + + Features: + - Create, write, and read files on the auto-mounted /storage partition + - List directory contents + - Check filesystem statistics + + The filesystem is automatically mounted at boot via device tree FSTAB. + No manual mounting is required - just use files at /storage/filename. + + Note: + - QSPI flash partitions are defined in the board's device tree overlay + - The /storage partition uses LittleFS (7MB user data) + - The /wlan: and /ota: partitions use FAT +*/ + +#include +#include + +// Mount point for the LittleFS user data partition (auto-mounted via FSTAB) +#define MOUNT_POINT "/storage" + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("QSPI Filesystem Example"); + Serial.println("========================\n"); + + // Check if filesystem is mounted (should be auto-mounted via FSTAB) + struct fs_statvfs stats; + int ret = fs_statvfs(MOUNT_POINT, &stats); + + if (ret == 0) { + Serial.println("Filesystem is mounted at " MOUNT_POINT); + } else { + Serial.print("Filesystem not available (error: "); + Serial.print(ret); + Serial.println(")"); + Serial.println("Note: The filesystem should be auto-mounted via FSTAB."); + Serial.println("You may need to format the partition first using the"); + Serial.println("FlashFormat example from the Storage library."); + while (1) { + delay(1000); + } + } + + // Show filesystem statistics + printFilesystemStats(); + + // Test filesystem operations + testFileOperations(); + + // List files + listFiles(); + + // List all mounted filesystems + listMounts(); +} + +void loop() { + // Nothing to do in loop + delay(1000); +} + +void printFilesystemStats() { + struct fs_statvfs stats; + int ret = fs_statvfs(MOUNT_POINT, &stats); + + if (ret == 0) { + Serial.println("\nFilesystem Statistics:"); + Serial.print(" Block size: "); + Serial.print(stats.f_bsize); + Serial.println(" bytes"); + + Serial.print(" Total blocks: "); + Serial.println(stats.f_blocks); + + Serial.print(" Free blocks: "); + Serial.println(stats.f_bfree); + + uint32_t total_kb = (stats.f_blocks * stats.f_bsize) / 1024; + uint32_t free_kb = (stats.f_bfree * stats.f_bsize) / 1024; + uint32_t used_kb = total_kb - free_kb; + + Serial.print(" Total space: "); + Serial.print(total_kb); + Serial.println(" KB"); + + Serial.print(" Used space: "); + Serial.print(used_kb); + Serial.println(" KB"); + + Serial.print(" Free space: "); + Serial.print(free_kb); + Serial.println(" KB\n"); + } else { + Serial.print("Failed to get filesystem stats: "); + Serial.println(ret); + } +} + +void testFileOperations() { + Serial.println("Testing File Operations:"); + Serial.println("------------------------"); + + // Create and write to a file + const char *filepath = MOUNT_POINT "/test.txt"; + const char *data = "Hello from QSPI filesystem!\nThis is a test file.\n"; + + Serial.print("Writing file: "); + Serial.print(filepath); + Serial.print("... "); + + struct fs_file_t file; + fs_file_t_init(&file); + + int ret = fs_open(&file, filepath, FS_O_CREATE | FS_O_WRITE); + if (ret < 0) { + Serial.print("FAILED to open ("); + Serial.print(ret); + Serial.println(")"); + return; + } + + ssize_t written = fs_write(&file, data, strlen(data)); + fs_close(&file); + + if (written == (ssize_t)strlen(data)) { + Serial.print("OK ("); + Serial.print(written); + Serial.println(" bytes)"); + } else { + Serial.println("FAILED"); + return; + } + + // Read the file back + Serial.print("Reading file... "); + char read_buffer[128]; + memset(read_buffer, 0, sizeof(read_buffer)); + + fs_file_t_init(&file); + ret = fs_open(&file, filepath, FS_O_READ); + if (ret < 0) { + Serial.print("FAILED to open ("); + Serial.print(ret); + Serial.println(")"); + return; + } + + ssize_t bytes_read = fs_read(&file, read_buffer, sizeof(read_buffer) - 1); + fs_close(&file); + + if (bytes_read > 0) { + Serial.print("OK ("); + Serial.print(bytes_read); + Serial.println(" bytes)"); + Serial.println("\nFile contents:"); + Serial.println("---"); + Serial.print(read_buffer); + Serial.println("---\n"); + } else { + Serial.println("FAILED"); + } + + // Write a second file with sensor data simulation + const char *datafile = MOUNT_POINT "/sensor_data.txt"; + Serial.print("Creating sensor data file... "); + + fs_file_t_init(&file); + ret = fs_open(&file, datafile, FS_O_CREATE | FS_O_WRITE); + if (ret == 0) { + // Simulate writing sensor readings + for (int i = 0; i < 10; i++) { + char line[64]; + snprintf(line, sizeof(line), "Reading %d: Temperature=%.1f C, Humidity=%.1f%%\n", + i, 20.0 + i * 0.5, 45.0 + i * 1.2); + fs_write(&file, line, strlen(line)); + } + fs_close(&file); + Serial.println("OK"); + } else { + Serial.println("FAILED"); + } +} + +void listFiles() { + Serial.println("\nDirectory Listing:"); + Serial.println("------------------"); + + struct fs_dir_t dir; + fs_dir_t_init(&dir); + + int ret = fs_opendir(&dir, MOUNT_POINT); + if (ret < 0) { + Serial.print("Failed to open directory ("); + Serial.print(ret); + Serial.println(")"); + return; + } + + struct fs_dirent entry; + int count = 0; + + while (true) { + ret = fs_readdir(&dir, &entry); + if (ret < 0) { + Serial.println("Error reading directory"); + break; + } + + if (entry.name[0] == 0) { + // End of directory + break; + } + + Serial.print(" "); + if (entry.type == FS_DIR_ENTRY_DIR) { + Serial.print("[DIR] "); + } else { + Serial.print("[FILE] "); + } + Serial.print(entry.name); + Serial.print(" ("); + Serial.print(entry.size); + Serial.println(" bytes)"); + + count++; + } + + fs_closedir(&dir); + + if (count == 0) { + Serial.println(" (empty)"); + } + + Serial.print("\nTotal items: "); + Serial.println(count); +} + +void listMounts() { + Serial.println("\n=== Mounted Filesystems ==="); + + const char *mnt_point; + int idx = 0; + int res; + bool found = false; + + while (true) { + res = fs_readmount(&idx, &mnt_point); + if (res < 0) { + break; + } + + Serial.print("Mount point "); + Serial.print(idx - 1); + Serial.print(": "); + Serial.print(mnt_point); + + // Detect filesystem type by mount point naming convention + // FAT mount points typically end with ':', LittleFS mount points don't + size_t len = strlen(mnt_point); + if (len > 0 && mnt_point[len - 1] == ':') { + Serial.print(" (FAT)"); + } else { + Serial.print(" (LittleFS)"); + } + Serial.println(); + + found = true; + } + + if (!found) { + Serial.println("No mounted filesystems found!"); + } +} diff --git a/libraries/QSPI/examples/QSPIPartitioning.ino b/libraries/QSPI/examples/QSPIPartitioning.ino new file mode 100644 index 00000000..40fac3c1 --- /dev/null +++ b/libraries/QSPI/examples/QSPIPartitioning.ino @@ -0,0 +1,526 @@ +/* + QSPI Partitioning Example + + This example demonstrates how to partition QSPI flash memory into + logical regions for different purposes. + + Features: + - Define multiple partitions with different sizes + - Demonstrate isolated read/write operations per partition + - Partition boundary checking + - Efficient partition management + + Partitions created: + - CONFIG: Small partition for configuration (64KB) + - LOGGING: Medium partition for data logging (256KB) + - USER_FILES: Large partition for user files (remaining space - 128KB) + - BACKUP: Reserved backup partition (128KB) + + Note: QSPI flash must be configured in the board's device tree overlay. +*/ + +#include + +// Maximum number of partitions supported +#define MAX_PARTITIONS 8 + +// Partition definition +struct PartitionInfo { + const char* name; + uint32_t start_address; + uint32_t size; +}; + +// Global partition state +PartitionInfo partitions[MAX_PARTITIONS]; +uint8_t partition_count = 0; +bool partition_initialized = false; +uint32_t flash_size = 0; +uint32_t sector_size = 0; + +// Forward declarations +bool validatePartitions(); +bool isValidPartition(uint8_t partition_id); +bool checkPartitionBoundaries(uint8_t partition_id, uint32_t offset, size_t size); + +// Initialize partition system +bool initPartitions() { + if (!QSPI.isReady()) { + return false; + } + + flash_size = QSPI.getFlashSize(); + sector_size = QSPI.getSectorSize(); + + if (flash_size == 0 || sector_size == 0) { + return false; + } + + partition_initialized = true; + return true; +} + +// Define a new partition +bool definePartition(uint8_t id, const char* name, uint32_t start_address, uint32_t size) { + if (!partition_initialized || id >= MAX_PARTITIONS) { + return false; + } + + // Check if partition fits in flash + if (start_address + size > flash_size) { + return false; + } + + // Warn if not sector-aligned + if (start_address % sector_size != 0 || size % sector_size != 0) { + Serial.print("Warning: Partition '"); + Serial.print(name); + Serial.println("' is not sector-aligned!"); + } + + partitions[id].name = name; + partitions[id].start_address = start_address; + partitions[id].size = size; + + if (id >= partition_count) { + partition_count = id + 1; + } + + return validatePartitions(); +} + +// Write data to partition +bool writePartition(uint8_t partition_id, uint32_t offset, const void* data, size_t size) { + if (!checkPartitionBoundaries(partition_id, offset, size)) { + return false; + } + + uint32_t address = partitions[partition_id].start_address + offset; + return QSPI.write(address, data, size); +} + +// Read data from partition +bool readPartition(uint8_t partition_id, uint32_t offset, void* data, size_t size) { + if (!checkPartitionBoundaries(partition_id, offset, size)) { + return false; + } + + uint32_t address = partitions[partition_id].start_address + offset; + return QSPI.read(address, data, size); +} + +// Erase entire partition +bool erasePartition(uint8_t partition_id) { + if (!isValidPartition(partition_id)) { + return false; + } + + return QSPI.erase(partitions[partition_id].start_address, partitions[partition_id].size); +} + +// Erase region within partition +bool erasePartitionRegion(uint8_t partition_id, uint32_t offset, size_t size) { + if (!checkPartitionBoundaries(partition_id, offset, size)) { + return false; + } + + uint32_t address = partitions[partition_id].start_address + offset; + + // Align to sector boundary + uint32_t aligned_address = (address / sector_size) * sector_size; + uint32_t aligned_size = ((size + sector_size - 1) / sector_size) * sector_size; + + return QSPI.erase(aligned_address, aligned_size); +} + +// Get partition size +uint32_t getPartitionSize(uint8_t partition_id) { + if (!isValidPartition(partition_id)) { + return 0; + } + return partitions[partition_id].size; +} + +// Get partition start address +uint32_t getPartitionStart(uint8_t partition_id) { + if (!isValidPartition(partition_id)) { + return 0; + } + return partitions[partition_id].start_address; +} + +// Get partition name +const char* getPartitionName(uint8_t partition_id) { + if (!isValidPartition(partition_id)) { + return "INVALID"; + } + return partitions[partition_id].name; +} + +// Check if partition is valid +bool isValidPartition(uint8_t partition_id) { + return partition_initialized && partition_id < partition_count && partitions[partition_id].size > 0; +} + +// Check partition boundaries +bool checkPartitionBoundaries(uint8_t partition_id, uint32_t offset, size_t size) { + if (!isValidPartition(partition_id)) { + return false; + } + + if (offset + size > partitions[partition_id].size) { + Serial.print("Error: Access exceeds partition '"); + Serial.print(partitions[partition_id].name); + Serial.println("' boundary!"); + return false; + } + + return true; +} + +// Display partition table +void printPartitionTable() { + if (!partition_initialized) { + Serial.println("Partition table not initialized"); + return; + } + + Serial.println("\nPartition Table:"); + Serial.println("================"); + Serial.print("Flash Size: "); + Serial.print(flash_size / 1024); + Serial.println(" KB"); + Serial.print("Sector Size: "); + Serial.print(sector_size); + Serial.println(" bytes\n"); + + Serial.println("ID Name Start Size End Size(KB)"); + Serial.println("-- ------------ ---------- ---------- ---------- --------"); + + for (uint8_t i = 0; i < partition_count; i++) { + if (partitions[i].size > 0) { + char line[100]; + snprintf(line, sizeof(line), "%-2d %-12s 0x%08X 0x%08X 0x%08X %8u", + i, + partitions[i].name, + partitions[i].start_address, + partitions[i].size, + partitions[i].start_address + partitions[i].size, + partitions[i].size / 1024); + Serial.println(line); + } + } + Serial.println(); +} + +// Validate partitions don't overlap +bool validatePartitions() { + for (uint8_t i = 0; i < partition_count; i++) { + if (partitions[i].size == 0) continue; + + uint32_t i_end = partitions[i].start_address + partitions[i].size; + + for (uint8_t j = i + 1; j < partition_count; j++) { + if (partitions[j].size == 0) continue; + + uint32_t j_end = partitions[j].start_address + partitions[j].size; + + // Check for overlap + if (!(i_end <= partitions[j].start_address || j_end <= partitions[i].start_address)) { + Serial.print("Error: Partitions '"); + Serial.print(partitions[i].name); + Serial.print("' and '"); + Serial.print(partitions[j].name); + Serial.println("' overlap!"); + return false; + } + } + } + + return true; +} + +// Partition IDs +enum PartitionID { + PARTITION_CONFIG = 0, + PARTITION_LOGGING, + PARTITION_USER_FILES, + PARTITION_BACKUP, + PARTITION_COUNT +}; + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("QSPI Partitioning Example"); + Serial.println("=========================\n"); + + // Initialize QSPI flash + if (!QSPI.begin()) { + Serial.println("Failed to initialize QSPI flash!"); + while (1) { + delay(1000); + } + } + + Serial.println("QSPI flash initialized successfully"); + Serial.print("Flash size: "); + Serial.print(QSPI.getFlashSize() / 1024); + Serial.println(" KB"); + Serial.print("Sector size: "); + Serial.print(QSPI.getSectorSize()); + Serial.println(" bytes\n"); + + // Initialize partition manager + if (!initPartitions()) { + Serial.println("Failed to initialize partition manager!"); + while (1) { + delay(1000); + } + } + + // Define partition layout + uint32_t total_flash_size = QSPI.getFlashSize(); + + // CONFIG partition at the start (64KB) + definePartition(PARTITION_CONFIG, "CONFIG", 0, 64 * 1024); + + // LOGGING partition after CONFIG (256KB) + definePartition(PARTITION_LOGGING, "LOGGING", 64 * 1024, 256 * 1024); + + // BACKUP partition at the end (128KB) + definePartition(PARTITION_BACKUP, "BACKUP", total_flash_size - 128 * 1024, 128 * 1024); + + // USER_FILES partition in the middle (remaining space) + uint32_t user_start = 64 * 1024 + 256 * 1024; + uint32_t user_size = (total_flash_size - 128 * 1024) - user_start; + definePartition(PARTITION_USER_FILES, "USER_FILES", user_start, user_size); + + // Display partition table + printPartitionTable(); + + // Test each partition + testConfigPartition(); + testLoggingPartition(); + testUserFilesPartition(); + testBackupPartition(); + + Serial.println("\n=== All partition tests completed ==="); +} + +void loop() { + // Nothing to do in loop + delay(1000); +} + +void testConfigPartition() { + Serial.println("Testing CONFIG Partition:"); + Serial.println("-------------------------"); + + // Erase partition first + Serial.print("Erasing CONFIG partition... "); + if (erasePartition(PARTITION_CONFIG)) { + Serial.println("OK"); + } else { + Serial.println("FAILED"); + return; + } + + // Simulate storing configuration data + struct Config { + uint32_t magic; + uint8_t version; + char device_name[32]; + uint32_t flags; + uint32_t checksum; + } config; + + config.magic = 0xC0FF1234; + config.version = 1; + strncpy(config.device_name, "QSPI-Device-001", sizeof(config.device_name)); + config.flags = 0x0000ABCD; + config.checksum = 0xDEADBEEF; + + Serial.print("Writing config... "); + if (writePartition(PARTITION_CONFIG, 0, &config, sizeof(config))) { + Serial.println("OK"); + } else { + Serial.println("FAILED"); + return; + } + + // Read back and verify + Config read_config; + Serial.print("Reading config... "); + if (readPartition(PARTITION_CONFIG, 0, &read_config, sizeof(read_config))) { + Serial.println("OK"); + + Serial.print(" Magic: 0x"); + Serial.println(read_config.magic, HEX); + Serial.print(" Version: "); + Serial.println(read_config.version); + Serial.print(" Device Name: "); + Serial.println(read_config.device_name); + Serial.print(" Flags: 0x"); + Serial.println(read_config.flags, HEX); + + if (memcmp(&config, &read_config, sizeof(config)) == 0) { + Serial.println(" Verification: PASSED"); + } else { + Serial.println(" Verification: FAILED"); + } + } else { + Serial.println("FAILED"); + } + + Serial.println(); +} + +void testLoggingPartition() { + Serial.println("Testing LOGGING Partition:"); + Serial.println("--------------------------"); + + // Erase partition first + Serial.print("Erasing LOGGING partition... "); + if (erasePartition(PARTITION_LOGGING)) { + Serial.println("OK"); + } else { + Serial.println("FAILED"); + return; + } + + // Simulate logging sensor data + struct LogEntry { + uint32_t timestamp; + float temperature; + float humidity; + uint16_t pressure; + }; + + const int num_entries = 5; + LogEntry logs[num_entries]; + + // Generate sample log entries + for (int i = 0; i < num_entries; i++) { + logs[i].timestamp = millis() + i * 1000; + logs[i].temperature = 20.0 + i * 0.5; + logs[i].humidity = 45.0 + i * 2.0; + logs[i].pressure = 1013 + i; + } + + Serial.print("Writing "); + Serial.print(num_entries); + Serial.print(" log entries... "); + if (writePartition(PARTITION_LOGGING, 0, logs, sizeof(logs))) { + Serial.println("OK"); + } else { + Serial.println("FAILED"); + return; + } + + // Read back + LogEntry read_logs[num_entries]; + Serial.print("Reading log entries... "); + if (readPartition(PARTITION_LOGGING, 0, read_logs, sizeof(read_logs))) { + Serial.println("OK"); + + Serial.println(" Log entries:"); + for (int i = 0; i < num_entries; i++) { + Serial.print(" Entry "); + Serial.print(i); + Serial.print(": T="); + Serial.print(read_logs[i].temperature); + Serial.print("C, H="); + Serial.print(read_logs[i].humidity); + Serial.print("%, P="); + Serial.println(read_logs[i].pressure); + } + } else { + Serial.println("FAILED"); + } + + Serial.println(); +} + +void testUserFilesPartition() { + Serial.println("Testing USER_FILES Partition:"); + Serial.println("-----------------------------"); + + const char* file1 = "This is user file 1 data"; + const char* file2 = "User file 2 contains different content"; + + Serial.print("Writing file 1 at offset 0... "); + if (writePartition(PARTITION_USER_FILES, 0, file1, strlen(file1) + 1)) { + Serial.println("OK"); + } else { + Serial.println("FAILED"); + } + + Serial.print("Writing file 2 at offset 4KB... "); + if (writePartition(PARTITION_USER_FILES, 4096, file2, strlen(file2) + 1)) { + Serial.println("OK"); + } else { + Serial.println("FAILED"); + } + + // Read back + char buffer[64]; + Serial.print("Reading file 1... "); + if (readPartition(PARTITION_USER_FILES, 0, buffer, sizeof(buffer))) { + Serial.println("OK"); + Serial.print(" Content: "); + Serial.println(buffer); + } else { + Serial.println("FAILED"); + } + + Serial.print("Reading file 2... "); + if (readPartition(PARTITION_USER_FILES, 4096, buffer, sizeof(buffer))) { + Serial.println("OK"); + Serial.print(" Content: "); + Serial.println(buffer); + } else { + Serial.println("FAILED"); + } + + Serial.println(); +} + +void testBackupPartition() { + Serial.println("Testing BACKUP Partition:"); + Serial.println("-------------------------"); + + // Erase partition first + Serial.print("Erasing BACKUP partition... "); + if (erasePartition(PARTITION_BACKUP)) { + Serial.println("OK"); + } else { + Serial.println("FAILED"); + return; + } + + const char* backup_data = "Critical backup data that should be preserved!"; + + Serial.print("Writing backup data... "); + if (writePartition(PARTITION_BACKUP, 0, backup_data, strlen(backup_data) + 1)) { + Serial.println("OK"); + } else { + Serial.println("FAILED"); + return; + } + + char buffer[64]; + Serial.print("Reading backup data... "); + if (readPartition(PARTITION_BACKUP, 0, buffer, sizeof(buffer))) { + Serial.println("OK"); + Serial.print(" Content: "); + Serial.println(buffer); + } else { + Serial.println("FAILED"); + } + + Serial.println(); +} diff --git a/libraries/QSPI/examples/QSPISimpleFS.ino b/libraries/QSPI/examples/QSPISimpleFS.ino new file mode 100644 index 00000000..82d9be08 --- /dev/null +++ b/libraries/QSPI/examples/QSPISimpleFS.ino @@ -0,0 +1,616 @@ +/* + QSPI Simple Filesystem Example + + This example demonstrates a simple filesystem implementation on QSPI flash + without requiring LittleFS or other external dependencies. + + Features: + - Simple File Allocation Table (FAT) system + - Create, write, read, and delete files + - List files with sizes + - Filesystem statistics + - No external filesystem dependencies required + + The filesystem layout: + - Block 0: File Allocation Table (FAT) + - Remaining blocks: File data storage + + Note: QSPI flash must be configured in the board's device tree overlay. +*/ + +#include + +// Filesystem configuration +#define QSPI_FS_MAX_FILES 16 +#define QSPI_FS_BLOCK_SIZE 4096 // 4KB blocks +#define QSPI_FS_MAGIC 0x51534653 // "QSFS" + +// File entry in the FAT +struct FileEntry { + char name[32]; // Filename + uint32_t size; // File size in bytes + uint32_t start_block; // Starting block number + uint32_t block_count; // Number of blocks used + uint8_t valid; // 0xFF = valid, 0x00 = deleted + uint8_t reserved[3]; // Padding +}; + +// File Allocation Table +struct FileAllocationTable { + uint32_t magic; // Magic number for validation + uint32_t version; // Filesystem version + uint32_t total_blocks; // Total blocks available + uint32_t used_blocks; // Blocks currently in use + FileEntry files[QSPI_FS_MAX_FILES]; // File entries + uint32_t checksum; // Simple checksum +}; + +// Global filesystem state +FileAllocationTable fat; +uint32_t sector_size = 0; +uint32_t blocks_per_sector = 0; +bool fs_initialized = false; + +// Forward declarations +uint32_t calculateChecksum(); +bool loadFAT(); +bool saveFAT(); +int findFreeSlot(); +int findFile(const char* name); +uint32_t findFreeBlocks(uint32_t count); + +// Calculate FAT checksum +uint32_t calculateChecksum() { + uint32_t sum = 0; + sum += fat.magic; + sum += fat.version; + sum += fat.total_blocks; + sum += fat.used_blocks; + for (int i = 0; i < QSPI_FS_MAX_FILES; i++) { + sum += fat.files[i].size; + sum += fat.files[i].start_block; + } + return sum; +} + +// Load FAT from flash +bool loadFAT() { + // Read FAT from flash + if (!QSPI.read(0, &fat, sizeof(fat))) { + return false; + } + + // Check magic number + if (fat.magic != QSPI_FS_MAGIC) { + return false; + } + + // Verify checksum + uint32_t stored_checksum = fat.checksum; + uint32_t calculated = calculateChecksum(); + if (stored_checksum != calculated) { + Serial.println("Warning: Filesystem checksum mismatch!"); + return false; + } + + return true; +} + +// Save FAT to flash +bool saveFAT() { + fat.checksum = calculateChecksum(); + + // Erase FAT sector + if (!QSPI.erase(0, sector_size)) { + return false; + } + + // Write FAT + return QSPI.write(0, &fat, sizeof(fat)); +} + +// Find free file slot +int findFreeSlot() { + for (int i = 0; i < QSPI_FS_MAX_FILES; i++) { + if (fat.files[i].valid != 0xFF) { + return i; + } + } + return -1; +} + +// Find file by name +int findFile(const char* name) { + for (int i = 0; i < QSPI_FS_MAX_FILES; i++) { + if (fat.files[i].valid == 0xFF && strcmp(fat.files[i].name, name) == 0) { + return i; + } + } + return -1; +} + +// Find consecutive free blocks +uint32_t findFreeBlocks(uint32_t count) { + // Simple sequential allocation + uint32_t consecutive = 0; + uint32_t start_block = 1; // Block 0 is FAT + + // Build used blocks bitmap (limit to reasonable size) + const uint32_t max_trackable = 512; + bool* used = new bool[max_trackable]; + if (!used) return 0; + + memset(used, 0, max_trackable * sizeof(bool)); + used[0] = true; // FAT block + + // Mark used blocks + for (int i = 0; i < QSPI_FS_MAX_FILES; i++) { + if (fat.files[i].valid == 0xFF) { + for (uint32_t b = 0; b < fat.files[i].block_count; b++) { + uint32_t block = fat.files[i].start_block + b; + if (block < max_trackable) { + used[block] = true; + } + } + } + } + + // Find consecutive free blocks + uint32_t result = 0; + for (uint32_t i = 1; i < min(fat.total_blocks, max_trackable); i++) { + if (!used[i]) { + if (consecutive == 0) start_block = i; + consecutive++; + if (consecutive >= count) { + result = start_block; + break; + } + } else { + consecutive = 0; + } + } + + delete[] used; + return result; +} + +// Initialize filesystem (mount) +bool beginFS() { + if (!QSPI.isReady()) { + return false; + } + + sector_size = QSPI.getSectorSize(); + if (sector_size == 0) { + return false; + } + + blocks_per_sector = sector_size / QSPI_FS_BLOCK_SIZE; + uint32_t flash_size = QSPI.getFlashSize(); + + // Try to load existing FAT + if (loadFAT()) { + fs_initialized = true; + return true; + } + + // No valid filesystem found + return false; +} + +// Format the filesystem +bool formatFS() { + if (!QSPI.isReady()) { + return false; + } + + sector_size = QSPI.getSectorSize(); + uint32_t flash_size = QSPI.getFlashSize(); + + // Initialize FAT + memset(&fat, 0, sizeof(fat)); + fat.magic = QSPI_FS_MAGIC; + fat.version = 1; + fat.total_blocks = (flash_size / QSPI_FS_BLOCK_SIZE); + fat.used_blocks = 1; // FAT block + + // Mark all files as invalid + for (int i = 0; i < QSPI_FS_MAX_FILES; i++) { + fat.files[i].valid = 0x00; + } + + // Save FAT + if (!saveFAT()) { + return false; + } + + fs_initialized = true; + return true; +} + +// Create file +bool createFile(const char* name, const void* data, size_t size) { + if (!fs_initialized) { + Serial.println("Filesystem not initialized"); + return false; + } + + // Check if file already exists + if (findFile(name) >= 0) { + Serial.println("File already exists"); + return false; + } + + // Find free slot + int slot = findFreeSlot(); + if (slot < 0) { + Serial.println("No free file slots"); + return false; + } + + // Calculate blocks needed + uint32_t blocks_needed = (size + QSPI_FS_BLOCK_SIZE - 1) / QSPI_FS_BLOCK_SIZE; + + // Find free blocks + uint32_t start_block = findFreeBlocks(blocks_needed); + if (start_block == 0) { + Serial.println("Not enough free space"); + return false; + } + + // Erase and write blocks + for (uint32_t i = 0; i < blocks_needed; i++) { + uint32_t block_addr = (start_block + i) * QSPI_FS_BLOCK_SIZE; + uint32_t sector_addr = (block_addr / sector_size) * sector_size; + + // Only erase once per sector + static uint32_t last_erased_sector = 0xFFFFFFFF; + if (sector_addr != last_erased_sector) { + if (!QSPI.erase(sector_addr, sector_size)) { + Serial.println("Failed to erase block"); + return false; + } + last_erased_sector = sector_addr; + } + } + + // Write data + uint32_t address = start_block * QSPI_FS_BLOCK_SIZE; + if (!QSPI.write(address, data, size)) { + Serial.println("Failed to write file data"); + return false; + } + + // Update FAT + strncpy(fat.files[slot].name, name, sizeof(fat.files[slot].name) - 1); + fat.files[slot].name[sizeof(fat.files[slot].name) - 1] = '\0'; + fat.files[slot].size = size; + fat.files[slot].start_block = start_block; + fat.files[slot].block_count = blocks_needed; + fat.files[slot].valid = 0xFF; + fat.used_blocks += blocks_needed; + + return saveFAT(); +} + +// Read file +bool readFile(const char* name, void* buffer, size_t buffer_size) { + if (!fs_initialized) { + Serial.println("Filesystem not initialized"); + return false; + } + + int slot = findFile(name); + if (slot < 0) { + Serial.println("File not found"); + return false; + } + + uint32_t address = fat.files[slot].start_block * QSPI_FS_BLOCK_SIZE; + size_t read_size = min(fat.files[slot].size, buffer_size); + + return QSPI.read(address, buffer, read_size); +} + +// Delete file +bool deleteFile(const char* name) { + if (!fs_initialized) { + return false; + } + + int slot = findFile(name); + if (slot < 0) { + return false; + } + + fat.used_blocks -= fat.files[slot].block_count; + fat.files[slot].valid = 0x00; + + return saveFAT(); +} + +// Check if file exists +bool fileExists(const char* name) { + return findFile(name) >= 0; +} + +// Get file size +uint32_t getFileSize(const char* name) { + int slot = findFile(name); + if (slot < 0) return 0; + return fat.files[slot].size; +} + +// Get file count +int getFileCount() { + if (!fs_initialized) return 0; + + int count = 0; + for (int i = 0; i < QSPI_FS_MAX_FILES; i++) { + if (fat.files[i].valid == 0xFF) { + count++; + } + } + return count; +} + +// List all files +void listFiles() { + if (!fs_initialized) { + Serial.println("Filesystem not initialized"); + return; + } + + Serial.println("\nFile Listing:"); + Serial.println("-------------------------------"); + Serial.println("Name Size"); + Serial.println("-------------------------------"); + + int count = 0; + for (int i = 0; i < QSPI_FS_MAX_FILES; i++) { + if (fat.files[i].valid == 0xFF) { + char line[50]; + snprintf(line, sizeof(line), "%-24s %6u bytes", + fat.files[i].name, fat.files[i].size); + Serial.println(line); + count++; + } + } + + if (count == 0) { + Serial.println("(no files)"); + } + Serial.println("-------------------------------"); + Serial.print("Total files: "); + Serial.println(count); +} + +// Get total space +uint32_t getTotalSpace() { + return fat.total_blocks * QSPI_FS_BLOCK_SIZE; +} + +// Get used space +uint32_t getUsedSpace() { + return fat.used_blocks * QSPI_FS_BLOCK_SIZE; +} + +// Get free space +uint32_t getFreeSpace() { + return getTotalSpace() - getUsedSpace(); +} + +// Print filesystem statistics +void printStats() { + if (!fs_initialized) { + Serial.println("Filesystem not initialized"); + return; + } + + Serial.println("\nFilesystem Statistics:"); + Serial.println("----------------------"); + Serial.print("Total blocks: "); + Serial.println(fat.total_blocks); + Serial.print("Used blocks: "); + Serial.println(fat.used_blocks); + Serial.print("Free blocks: "); + Serial.println(fat.total_blocks - fat.used_blocks); + Serial.print("Block size: "); + Serial.print(QSPI_FS_BLOCK_SIZE); + Serial.println(" bytes"); + Serial.print("Total space: "); + Serial.print(getTotalSpace() / 1024); + Serial.println(" KB"); + Serial.print("Used space: "); + Serial.print(getUsedSpace() / 1024); + Serial.println(" KB"); + Serial.print("Free space: "); + Serial.print(getFreeSpace() / 1024); + Serial.println(" KB"); + Serial.print("File slots: "); + Serial.print(getFileCount()); + Serial.print(" / "); + Serial.println(QSPI_FS_MAX_FILES); + Serial.println(); +} + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("QSPI Simple Filesystem Example"); + Serial.println("===============================\n"); + + // Initialize QSPI flash + if (!QSPI.begin()) { + Serial.println("Failed to initialize QSPI flash!"); + while (1) { + delay(1000); + } + } + + Serial.println("QSPI flash initialized successfully"); + Serial.print("Flash size: "); + Serial.print(QSPI.getFlashSize() / 1024); + Serial.println(" KB"); + Serial.print("Sector size: "); + Serial.print(QSPI.getSectorSize()); + Serial.println(" bytes\n"); + + // Try to mount existing filesystem + if (!beginFS()) { + Serial.println("No valid filesystem found"); + Serial.println("Formatting new filesystem..."); + if (!formatFS()) { + Serial.println("Format failed!"); + while (1) { + delay(1000); + } + } + Serial.println("Format complete\n"); + } else { + Serial.println("Existing filesystem mounted\n"); + } + + // Show filesystem stats + printStats(); + + // Run filesystem tests + testFileSystem(); +} + +void loop() { + // Nothing to do in loop + delay(1000); +} + +void testFileSystem() { + Serial.println("Testing Filesystem Operations:"); + Serial.println("==============================\n"); + + // Test 1: Create a text file + Serial.println("Test 1: Creating text file..."); + const char* text_data = "Hello from QSPI Simple Filesystem!\nThis is a test file.\nLine 3 of test data."; + + if (createFile("test.txt", text_data, strlen(text_data) + 1)) { + Serial.println(" Created test.txt - OK"); + } else { + Serial.println(" Failed to create test.txt"); + } + + // Test 2: Read the file back + Serial.println("\nTest 2: Reading text file..."); + char read_buffer[128]; + memset(read_buffer, 0, sizeof(read_buffer)); + + if (readFile("test.txt", read_buffer, sizeof(read_buffer))) { + Serial.println(" Read test.txt - OK"); + Serial.println(" Content:"); + Serial.println(" ---"); + Serial.print(" "); + Serial.println(read_buffer); + Serial.println(" ---"); + } else { + Serial.println(" Failed to read test.txt"); + } + + // Test 3: Check file existence + Serial.println("\nTest 3: Checking file existence..."); + if (fileExists("test.txt")) { + Serial.println(" test.txt exists - OK"); + Serial.print(" File size: "); + Serial.print(getFileSize("test.txt")); + Serial.println(" bytes"); + } else { + Serial.println(" test.txt not found"); + } + + // Test 4: Create a binary data file + Serial.println("\nTest 4: Creating binary data file..."); + struct SensorData { + uint32_t timestamp; + float temperature; + float humidity; + uint16_t pressure; + } sensor_data[5]; + + for (int i = 0; i < 5; i++) { + sensor_data[i].timestamp = millis() + i * 1000; + sensor_data[i].temperature = 20.0 + i * 0.5; + sensor_data[i].humidity = 45.0 + i * 2.0; + sensor_data[i].pressure = 1013 + i; + } + + if (createFile("sensors.dat", sensor_data, sizeof(sensor_data))) { + Serial.println(" Created sensors.dat - OK"); + } else { + Serial.println(" Failed to create sensors.dat"); + } + + // Test 5: Read binary data back + Serial.println("\nTest 5: Reading binary data file..."); + SensorData read_sensors[5]; + + if (readFile("sensors.dat", read_sensors, sizeof(read_sensors))) { + Serial.println(" Read sensors.dat - OK"); + Serial.println(" Sensor readings:"); + for (int i = 0; i < 5; i++) { + Serial.print(" ["); + Serial.print(i); + Serial.print("] T="); + Serial.print(read_sensors[i].temperature, 1); + Serial.print("C, H="); + Serial.print(read_sensors[i].humidity, 1); + Serial.print("%, P="); + Serial.println(read_sensors[i].pressure); + } + } else { + Serial.println(" Failed to read sensors.dat"); + } + + // Test 6: Create config file + Serial.println("\nTest 6: Creating config file..."); + const char* config = "device_name=QSPI_Device\nversion=1.0\nmode=normal"; + + if (createFile("config.ini", config, strlen(config) + 1)) { + Serial.println(" Created config.ini - OK"); + } else { + Serial.println(" Failed to create config.ini"); + } + + // Test 7: List all files + Serial.println("\nTest 7: Listing all files..."); + listFiles(); + + // Show updated statistics + printStats(); + + // Test 8: Delete a file + Serial.println("Test 8: Deleting file..."); + if (deleteFile("test.txt")) { + Serial.println(" Deleted test.txt - OK"); + } else { + Serial.println(" Failed to delete test.txt"); + } + + // Test 9: Verify deletion + Serial.println("\nTest 9: Verifying deletion..."); + if (!fileExists("test.txt")) { + Serial.println(" test.txt no longer exists - OK"); + } else { + Serial.println(" test.txt still exists - FAILED"); + } + + // List files after deletion + Serial.println(); + listFiles(); + printStats(); + + Serial.println("\n=== All tests completed ==="); + + // Show file count + Serial.print("Total files in filesystem: "); + Serial.println(getFileCount()); +} diff --git a/libraries/QSPI/examples/README.md b/libraries/QSPI/examples/README.md new file mode 100644 index 00000000..e679c180 --- /dev/null +++ b/libraries/QSPI/examples/README.md @@ -0,0 +1,245 @@ +# QSPI Library Examples + +This directory contains examples demonstrating various uses of the QSPI library for external flash memory. + +## Examples Overview + +### 1. BasicQSPI.ino +**Difficulty:** Beginner +**Dependencies:** None + +Basic example showing fundamental QSPI operations: +- Initialize QSPI flash +- Get flash information (size, sector size, page size) +- Erase sectors +- Write and read data +- Verify data integrity + +**Best for:** Learning QSPI basics and testing your flash hardware. + +--- + +### 2. QSPISimpleFS.ino +**Difficulty:** Intermediate +**Dependencies:** None (class implementation included in sketch) + +Demonstrates a simple filesystem for file storage on QSPI flash without requiring external filesystem libraries. + +**Features:** +- File Allocation Table (FAT) system +- Create, read, and delete files +- File listing and statistics +- Works out-of-the-box without additional configuration +- Supports up to 16 files +- Automatic space management + +**Best for:** Projects needing simple file storage without LittleFS configuration. + +**Limitations:** +- Maximum 16 files +- No directory support +- Sequential block allocation +- Fixed 4KB block size + +--- + +### 3. QSPIPartitioning.ino +**Difficulty:** Intermediate +**Dependencies:** None (class implementation included in sketch) + +Demonstrates partitioning QSPI flash into logical regions for different purposes. + +**Features:** +- Multiple partition support (Config, Logging, User Files, Backup) +- Partition boundary protection +- Per-partition read/write operations +- Partition table visualization +- Safe partition management class + +**Best for:** Complex applications needing organized flash storage regions. + +**Use cases:** +- Separating configuration from data +- Dedicated logging areas +- Protected backup regions +- Multi-purpose flash organization + +--- + +### 4. QSPIFilesystem.ino +**Difficulty:** Advanced +**Dependencies:** Zephyr LittleFS + +Full-featured filesystem example using Zephyr's LittleFS implementation. + +**Features:** +- Complete filesystem with LittleFS +- Standard file operations (create, read, write, delete) +- Directory listing +- Filesystem statistics +- Wear leveling (provided by LittleFS) +- Power-loss resilient + +**Configuration Required:** + +Add to your board's `prj.conf`: +``` +CONFIG_FILE_SYSTEM=y +CONFIG_FILE_SYSTEM_LITTLEFS=y +CONFIG_FILE_SYSTEM_MAX_FILE_NAME=128 +``` + +**Best for:** Production applications needing robust filesystem support. + +**Why use this over QSPISimpleFS?** +- No file count limit +- Better wear leveling +- Power-loss protection +- Directory support +- Standard POSIX-like API + +--- + +## Quick Start Guide + +### Hardware Requirements +- Arduino board with QSPI flash support (e.g., GIGA R1, Portenta H7) +- QSPI flash configured in device tree overlay + +### Choosing the Right Example + +``` +Need basic flash operations? +→ Use BasicQSPI.ino + +Need simple file storage without configuration? +→ Use QSPISimpleFS.ino + +Need organized flash regions? +→ Use QSPIPartitioning.ino + +Need production-grade filesystem? +→ Use QSPIFilesystem.ino (requires LittleFS setup) +``` + +--- + +## Configuring LittleFS (for QSPIFilesystem.ino) + +### Step 1: Create/Edit prj.conf + +Create a `prj.conf` file in your sketch directory with: + +```conf +CONFIG_FILE_SYSTEM=y +CONFIG_FILE_SYSTEM_LITTLEFS=y +CONFIG_FILE_SYSTEM_MAX_FILE_NAME=128 +``` + +### Step 2: Verify Device Tree + +Your board should have QSPI flash defined in its overlay. Example: + +```dts +&qspi { + status = "okay"; + + qspi_flash: qspi-nor-flash@0 { + compatible = "nordic,qspi-nor"; + reg = <0>; + /* ... other properties ... */ + }; +}; +``` + +### Step 3: Build and Upload + +The Arduino-Zephyr build system should automatically pick up your `prj.conf`. + +### Troubleshooting LittleFS + +**Error: `lfs.h: No such file or directory`** +- LittleFS is not enabled in your build +- Make sure `CONFIG_FILE_SYSTEM_LITTLEFS=y` is in `prj.conf` +- Use `QSPISimpleFS.ino` instead if you don't need LittleFS + +**Error: Filesystem mount fails** +- Flash might not be formatted +- Try erasing the flash first using `BasicQSPI.ino` +- Check that QSPI flash is properly configured in device tree + +--- + +## Example Comparison + +| Feature | BasicQSPI | QSPISimpleFS | QSPIPartitioning | QSPIFilesystem | +|---------|-----------|--------------|------------------|----------------| +| Difficulty | ⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐ | +| Configuration | None | None | None | LittleFS required | +| File Support | No | Yes (16 max) | No | Unlimited | +| Partitions | No | No | Yes | No | +| Wear Leveling | No | Basic | No | Yes (LittleFS) | +| Power-Loss Safe | No | No | No | Yes | +| Production Ready | No | Good | Good | Excellent | + +--- + +## API Reference + +### QSPI - Low-level flash operations +```cpp +QSPI.begin(); +QSPI.getFlashSize(); +QSPI.erase(address, size); +QSPI.write(address, data, size); +QSPI.read(address, buffer, size); +``` + +### QSPISimpleFS - Simple filesystem (implemented in QSPISimpleFS.ino) +```cpp +QSPISimpleFS.begin() || QSPISimpleFS.format(); +QSPISimpleFS.createFile("test.txt", data, size); +QSPISimpleFS.readFile("test.txt", buffer, buffer_size); +QSPISimpleFS.deleteFile("test.txt"); +QSPISimpleFS.listFiles(); +QSPISimpleFS.printStats(); +``` + +### QSPIPartition - Partition management (implemented in QSPIPartitioning.ino) +```cpp +QSPIPartition.begin(); +QSPIPartition.definePartition(0, "CONFIG", start, size); +QSPIPartition.write(partition_id, offset, data, size); +QSPIPartition.read(partition_id, offset, buffer, size); +QSPIPartition.erase(partition_id); +QSPIPartition.printPartitionTable(); +``` + +--- + +## Tips and Best Practices + +1. **Always erase before writing**: Flash memory requires erasure before writing new data +2. **Sector alignment**: Erase operations work on sector boundaries (typically 4KB) +3. **Wear leveling**: Distribute writes across flash to extend lifetime +4. **Check return values**: Always verify that operations succeeded +5. **Checksums**: Use checksums for critical data to detect corruption +6. **Power-loss**: Consider what happens if power is lost during write operations + +--- + +## Further Reading + +- [Zephyr Flash API Documentation](https://docs.zephyrproject.org/latest/hardware/peripherals/flash.html) +- [LittleFS Documentation](https://github.com/littlefs-project/littlefs) +- [QSPI Library Reference](../QSPI.h) + +--- + +## Support + +For issues or questions: +- Check the example code comments +- Review error messages carefully +- Start with `BasicQSPI.ino` to verify hardware +- Use `QSPISimpleFS.ino` if LittleFS configuration is problematic diff --git a/libraries/QSPIStorage/QSPIFile.h b/libraries/QSPIStorage/QSPIFile.h new file mode 100644 index 00000000..785eb1e2 --- /dev/null +++ b/libraries/QSPIStorage/QSPIFile.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef QSPI_FILE_H +#define QSPI_FILE_H + +#include +#include + +// Forward declarations - avoid including zephyr headers in public header +// to prevent static initialization issues +struct fs_file_t; +class QSPIFolder; + +/** + * @brief QSPI File implementation + * + * Implements the File interface for QSPI storage using Zephyr's + * file system API. + */ +class QSPIFile : public File { +public: + QSPIFile(); + QSPIFile(const char* path); + QSPIFile(const String& path); + ~QSPIFile() override; + + // Opening and Closing + bool open(const char* filename, FileMode mode, StorageError* error = nullptr) override; + bool open(const String& filename, FileMode mode, StorageError* error = nullptr) override; + bool open(FileMode mode = FileMode::READ, StorageError* error = nullptr) override; + bool close(StorageError* error = nullptr) override; + bool changeMode(FileMode mode, StorageError* error = nullptr) override; + bool isOpen() const override; + + // Reading Operations + size_t read(uint8_t* buffer, size_t size, StorageError* error = nullptr) override; + int read(StorageError* error = nullptr) override; + String readAsString(StorageError* error = nullptr) override; + uint32_t available(StorageError* error = nullptr) override; + bool seek(size_t offset, StorageError* error = nullptr) override; + size_t position(StorageError* error = nullptr) override; + size_t size(StorageError* error = nullptr) override; + + // Writing Operations + size_t write(const uint8_t* buffer, size_t size, StorageError* error = nullptr) override; + size_t write(const String& data, StorageError* error = nullptr) override; + size_t write(uint8_t value, StorageError* error = nullptr) override; + bool flush(StorageError* error = nullptr) override; + + // File Management + bool exists(StorageError* error = nullptr) const override; + bool remove(StorageError* error = nullptr) override; + bool rename(const char* newFilename, StorageError* error = nullptr) override; + bool rename(const String& newFilename, StorageError* error = nullptr) override; + + // Path Information + QSPIFolder getParentFolder(StorageError* error = nullptr) const; + +private: + struct fs_file_t* file_; // Pointer to avoid including zephyr headers + bool is_open_; + FileMode mode_; + + // Internal helpers + bool resolvePath(const char* path, char* resolved, StorageError* error); + int fileModeToFlags(FileMode mode); + bool ensureFileHandle(); // Allocate file handle if needed + void freeFileHandle(); // Free file handle + + // Map Zephyr error codes to StorageErrorCode + static StorageErrorCode mapZephyrError(int err); +}; + +#endif // QSPI_FILE_H diff --git a/libraries/QSPIStorage/QSPIFolder.h b/libraries/QSPIStorage/QSPIFolder.h new file mode 100644 index 00000000..3cc525fe --- /dev/null +++ b/libraries/QSPIStorage/QSPIFolder.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef QSPI_FOLDER_H +#define QSPI_FOLDER_H + +#include +#include +#include + +// Note: zephyr/fs/fs.h is only included in the .cpp file +// to avoid static initialization issues + +#include "QSPIFile.h" + +/** + * @brief QSPI Folder implementation + * + * Implements the Folder interface for QSPI storage using Zephyr's + * file system API. + */ +class QSPIFolder : public Folder { +public: + QSPIFolder(); + QSPIFolder(const char* path); + QSPIFolder(const String& path); + ~QSPIFolder() override; + + // File Operations + QSPIFile createFile(const char* filename, FileMode mode = FileMode::WRITE, StorageError* error = nullptr); + QSPIFile createFile(const String& filename, FileMode mode = FileMode::WRITE, StorageError* error = nullptr); + QSPIFile getFile(const char* filename, StorageError* error = nullptr); + QSPIFile getFile(const String& filename, StorageError* error = nullptr); + + // Directory Management + bool exists(StorageError* error = nullptr) const override; + bool create(StorageError* error = nullptr) override; + bool remove(bool recursive = false, StorageError* error = nullptr) override; + bool rename(const char* newName, StorageError* error = nullptr) override; + bool rename(const String& newName, StorageError* error = nullptr) override; + + // Subfolder Operations + QSPIFolder createSubfolder(const char* name, bool overwrite = false, StorageError* error = nullptr); + QSPIFolder createSubfolder(const String& name, bool overwrite = false, StorageError* error = nullptr); + QSPIFolder getSubfolder(const char* name, StorageError* error = nullptr); + QSPIFolder getSubfolder(const String& name, StorageError* error = nullptr); + + // Content Enumeration + std::vector getFiles(StorageError* error = nullptr); + std::vector getFolders(StorageError* error = nullptr); + size_t getFileCount(StorageError* error = nullptr) override; + size_t getFolderCount(StorageError* error = nullptr) override; + + // Path Information + QSPIFolder getParentFolder(StorageError* error = nullptr) const; + +private: + // Internal helpers + bool resolvePath(const char* path, char* resolved, StorageError* error); + bool removeRecursive(const char* path, StorageError* error); + + // Map Zephyr error codes to StorageErrorCode + static StorageErrorCode mapZephyrError(int err); +}; + +#endif // QSPI_FOLDER_H diff --git a/libraries/QSPIStorage/QSPIStorage.cpp b/libraries/QSPIStorage/QSPIStorage.cpp new file mode 100644 index 00000000..778e78a6 --- /dev/null +++ b/libraries/QSPIStorage/QSPIStorage.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "QSPIStorage.h" + +#include +#include + +bool QSPIStorage::begin(StorageError* error) { + if (mounted_) { + return true; + } + + // Check if the filesystem is already mounted (via devicetree FSTAB auto-mount) + struct fs_statvfs stat; + int ret = fs_statvfs("/qspi", &stat); + + if (ret == 0) { + mounted_ = true; + return true; + } + + // Filesystem not mounted - provide helpful error message + if (error) { + if (ret == -ENOENT) { + error->setError(StorageErrorCode::STORAGE_NOT_MOUNTED, + "Filesystem not mounted. Ensure LittleFS FSTAB is configured in devicetree."); + } else { + error->setError(StorageErrorCode::UNKNOWN_ERROR, "Failed to access filesystem"); + } + } + return false; +} + +bool QSPIStorage::getStorageInfo(size_t& total, size_t& used, size_t& available, StorageError* error) { + if (!mounted_) { + if (error) { + error->setError(StorageErrorCode::STORAGE_NOT_MOUNTED, "Storage not mounted"); + } + return false; + } + + struct fs_statvfs stat; + int ret = fs_statvfs("/qspi", &stat); + if (ret != 0) { + if (error) { + error->setError(StorageErrorCode::READ_ERROR, "Failed to get storage info"); + } + return false; + } + + total = stat.f_frsize * stat.f_blocks; + available = stat.f_frsize * stat.f_bfree; + used = total - available; + + return true; +} diff --git a/libraries/QSPIStorage/QSPIStorage.h b/libraries/QSPIStorage/QSPIStorage.h new file mode 100644 index 00000000..aa0c0303 --- /dev/null +++ b/libraries/QSPIStorage/QSPIStorage.h @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef QSPI_STORAGE_H +#define QSPI_STORAGE_H + +#include +#include + +/** + * @brief QSPI Storage manager class + * + * Provides high-level access to QSPI flash storage with LittleFS filesystem. + * + * IMPORTANT: This library requires LittleFS to be auto-mounted via devicetree FSTAB. + * Your board's devicetree must include an FSTAB entry that mounts LittleFS at "/qspi". + */ +class QSPIStorage { +public: + QSPIStorage() : mounted_(false) {} + ~QSPIStorage() {} + + /** + * Initialize and verify the QSPI file system is mounted + * @param error Optional error output parameter + * @return true if successful, false otherwise + */ + bool begin(StorageError* error = nullptr); + + /** + * Mark storage as not in use + * @param error Optional error output parameter + */ + void end(StorageError* error = nullptr) { mounted_ = false; } + + /** + * Check if storage is mounted and ready + * @return true if mounted + */ + bool isMounted() const { return mounted_; } + + /** + * Get the mount point path + * @return Mount point string "/qspi" + */ + const char* getMountPoint() const { return "/qspi"; } + + /** + * Get storage statistics + * @param total Output total bytes + * @param used Output used bytes + * @param available Output available bytes + * @param error Optional error output parameter + * @return true if successful + */ + bool getStorageInfo(size_t& total, size_t& used, size_t& available, StorageError* error = nullptr); + + /** + * Format is not supported with FSTAB mounting + */ + bool format(FilesystemType fsType = FilesystemType::LITTLEFS, StorageError* error = nullptr) { + if (error) { + error->setError(StorageErrorCode::INVALID_OPERATION, "Format not supported with FSTAB mounting."); + } + return false; + } + +private: + bool mounted_; +}; + +#endif // QSPI_STORAGE_H diff --git a/libraries/QSPIStorage/examples/AdvancedStorageUsage/AdvancedStorageUsage.ino b/libraries/QSPIStorage/examples/AdvancedStorageUsage/AdvancedStorageUsage.ino new file mode 100644 index 00000000..b3a6181a --- /dev/null +++ b/libraries/QSPIStorage/examples/AdvancedStorageUsage/AdvancedStorageUsage.ino @@ -0,0 +1,511 @@ +/* + QSPIStorage - Advanced Storage Usage Example + + This example demonstrates advanced storage operations including: + - Binary data storage with structs + - Data logging with rotation + - Recursive folder operations + - Configuration file management + - Error recovery patterns + - Storage statistics and health monitoring + + Note: QSPI flash must be configured in the board's device tree overlay. +*/ + +#include + +QSPIStorage storage; + +// Sensor data structure for binary storage +struct SensorReading { + uint32_t timestamp; + float temperature; + float humidity; + float pressure; + uint16_t lightLevel; + uint8_t batteryPercent; + uint8_t flags; +}; + +// Configuration structure +struct DeviceConfig { + char deviceName[32]; + uint32_t sampleInterval; + uint32_t logRotationSize; + bool debugEnabled; + uint8_t logLevel; + uint8_t reserved[26]; // Padding to 64 bytes +}; + +// Constants +const char* CONFIG_FILE = "/qspi/config.bin"; +const char* DATA_DIR = "/qspi/data"; +const char* LOG_DIR = "/qspi/logs"; +const uint32_t MAX_LOG_SIZE = 4096; // Rotate logs at 4KB + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("QSPIStorage - Advanced Usage Example"); + Serial.println("=====================================\n"); + + // Initialize storage with error recovery + if (!initializeWithRecovery()) { + Serial.println("Fatal: Could not initialize storage"); + while (1) delay(1000); + } + + // Setup directory structure + setupDirectoryStructure(); + + // Demo advanced features + demoConfigManagement(); + demoBinaryDataStorage(); + demoDataLogging(); + demoFolderOperations(); + demoStorageStatistics(); + + Serial.println("\n=== All advanced demos completed ==="); +} + +void loop() { + // Simulate periodic data logging + static unsigned long lastLog = 0; + if (millis() - lastLog > 5000) { + lastLog = millis(); + logSensorReading(); + } +} + +// Initialize storage with automatic recovery +bool initializeWithRecovery() { + StorageError error; + + Serial.println("Initializing storage with recovery..."); + + // First attempt: normal mount + if (storage.begin(&error)) { + Serial.println("Storage mounted successfully"); + return true; + } + + Serial.print("Initial mount failed: "); + Serial.println(error.getMessage()); + + // Recovery attempt based on error type + switch (error.getCode()) { + case StorageErrorCode::STORAGE_NOT_FORMATTED: + case StorageErrorCode::STORAGE_CORRUPTED: + Serial.println("Attempting format recovery..."); + error.clear(); + + if (storage.format(FilesystemType::LITTLEFS, &error)) { + Serial.println("Format successful, retrying mount..."); + error.clear(); + return storage.begin(&error); + } + Serial.print("Format failed: "); + Serial.println(error.getMessage()); + break; + + case StorageErrorCode::HARDWARE_ERROR: + Serial.println("Hardware error - check QSPI connections"); + break; + + default: + Serial.println("Unknown error - cannot recover"); + break; + } + + return false; +} + +// Create the directory structure for the application +void setupDirectoryStructure() { + Serial.println("\nSetting up directory structure..."); + + StorageError error; + QSPIFolder root = storage.getRootFolder(&error); + + // Create data directory + QSPIFolder dataDir = root.createSubfolder("data", false, &error); + if (!error || error.getCode() == StorageErrorCode::ALREADY_EXISTS) { + Serial.println(" /data - OK"); + } + error.clear(); + + // Create logs directory + QSPIFolder logsDir = root.createSubfolder("logs", false, &error); + if (!error || error.getCode() == StorageErrorCode::ALREADY_EXISTS) { + Serial.println(" /logs - OK"); + } + error.clear(); + + // Create backup directory + QSPIFolder backupDir = root.createSubfolder("backup", false, &error); + if (!error || error.getCode() == StorageErrorCode::ALREADY_EXISTS) { + Serial.println(" /backup - OK"); + } + + Serial.println(); +} + +// Demo: Configuration file management +void demoConfigManagement() { + Serial.println("--- Demo: Configuration Management ---"); + + StorageError error; + DeviceConfig config; + + // Try to load existing config + if (loadConfig(config, &error)) { + Serial.println("Loaded existing configuration:"); + } else { + Serial.println("No config found, creating default..."); + + // Initialize default config + strncpy(config.deviceName, "QSPI_Device_001", sizeof(config.deviceName)); + config.sampleInterval = 1000; + config.logRotationSize = MAX_LOG_SIZE; + config.debugEnabled = true; + config.logLevel = 2; + + if (!saveConfig(config, &error)) { + Serial.print("Failed to save config: "); + Serial.println(error.getMessage()); + return; + } + Serial.println("Default configuration saved."); + } + + // Display config + Serial.print(" Device Name: "); + Serial.println(config.deviceName); + Serial.print(" Sample Interval: "); + Serial.print(config.sampleInterval); + Serial.println(" ms"); + Serial.print(" Log Rotation Size: "); + Serial.print(config.logRotationSize); + Serial.println(" bytes"); + Serial.print(" Debug Enabled: "); + Serial.println(config.debugEnabled ? "Yes" : "No"); + Serial.print(" Log Level: "); + Serial.println(config.logLevel); + + // Modify and save config + config.sampleInterval = 2000; + if (saveConfig(config, &error)) { + Serial.println("Configuration updated successfully"); + } + + Serial.println(); +} + +// Load configuration from binary file +bool loadConfig(DeviceConfig& config, StorageError* error) { + QSPIFile file(CONFIG_FILE); + + if (!file.exists(error)) { + return false; + } + + if (!file.open(FileMode::READ, error)) { + return false; + } + + size_t bytesRead = file.read(reinterpret_cast(&config), sizeof(config), error); + file.close(error); + + return (bytesRead == sizeof(config)); +} + +// Save configuration to binary file +bool saveConfig(const DeviceConfig& config, StorageError* error) { + QSPIFile file(CONFIG_FILE); + + if (!file.open(FileMode::WRITE, error)) { + return false; + } + + size_t bytesWritten = file.write(reinterpret_cast(&config), sizeof(config), error); + file.flush(error); + file.close(error); + + return (bytesWritten == sizeof(config)); +} + +// Demo: Binary data storage +void demoBinaryDataStorage() { + Serial.println("--- Demo: Binary Data Storage ---"); + + StorageError error; + + // Create sample sensor readings + const int NUM_READINGS = 10; + SensorReading readings[NUM_READINGS]; + + Serial.println("Generating sensor readings..."); + for (int i = 0; i < NUM_READINGS; i++) { + readings[i].timestamp = millis() + (i * 100); + readings[i].temperature = 20.0 + (random(100) / 10.0); + readings[i].humidity = 40.0 + (random(200) / 10.0); + readings[i].pressure = 1000.0 + (random(50) / 10.0); + readings[i].lightLevel = random(1024); + readings[i].batteryPercent = 80 + random(20); + readings[i].flags = 0x01; + } + + // Write binary data + QSPIFile dataFile("/qspi/data/readings.bin"); + + if (dataFile.open(FileMode::WRITE, &error)) { + // Write header (number of readings) + uint32_t count = NUM_READINGS; + dataFile.write(reinterpret_cast(&count), sizeof(count), &error); + + // Write all readings + size_t dataSize = sizeof(SensorReading) * NUM_READINGS; + size_t written = dataFile.write(reinterpret_cast(readings), dataSize, &error); + dataFile.close(&error); + + Serial.print("Wrote "); + Serial.print(written + sizeof(count)); + Serial.println(" bytes of binary data"); + } + + // Read binary data back + if (dataFile.open(FileMode::READ, &error)) { + uint32_t readCount; + dataFile.read(reinterpret_cast(&readCount), sizeof(readCount), &error); + + Serial.print("Reading "); + Serial.print(readCount); + Serial.println(" sensor readings:"); + + SensorReading reading; + for (uint32_t i = 0; i < min(readCount, (uint32_t)3); i++) { // Show first 3 + dataFile.read(reinterpret_cast(&reading), sizeof(reading), &error); + Serial.print(" ["); + Serial.print(i); + Serial.print("] T="); + Serial.print(reading.temperature, 1); + Serial.print("C, H="); + Serial.print(reading.humidity, 1); + Serial.print("%, P="); + Serial.print(reading.pressure, 1); + Serial.println("hPa"); + } + if (readCount > 3) { + Serial.println(" ..."); + } + + dataFile.close(&error); + } + + Serial.println(); +} + +// Demo: Data logging with rotation +void demoDataLogging() { + Serial.println("--- Demo: Data Logging with Rotation ---"); + + StorageError error; + + // Create some log entries + for (int i = 0; i < 5; i++) { + String logEntry = "[" + String(millis()) + "] INFO: Sample log entry #" + String(i + 1); + appendToLog("app.log", logEntry, &error); + delay(50); + } + + // Show log content + QSPIFile logFile("/qspi/logs/app.log"); + if (logFile.open(FileMode::READ, &error)) { + Serial.println("Current log content:"); + Serial.println("---"); + Serial.println(logFile.readAsString(&error)); + Serial.println("---"); + Serial.print("Log size: "); + Serial.print(logFile.size(&error)); + Serial.println(" bytes"); + logFile.close(&error); + } + + Serial.println(); +} + +// Append to log file with automatic rotation +void appendToLog(const char* logName, const String& message, StorageError* error) { + char logPath[64]; + snprintf(logPath, sizeof(logPath), "%s/%s", LOG_DIR, logName); + + QSPIFile logFile(logPath); + + // Check if rotation is needed + if (logFile.exists(error) && logFile.size(error) > MAX_LOG_SIZE) { + rotateLog(logName, error); + } + + // Append new entry + if (logFile.open(FileMode::APPEND, error)) { + logFile.write(message + "\n", error); + logFile.close(error); + } +} + +// Rotate log file +void rotateLog(const char* logName, StorageError* error) { + char currentPath[64]; + char backupPath[64]; + + snprintf(currentPath, sizeof(currentPath), "%s/%s", LOG_DIR, logName); + snprintf(backupPath, sizeof(backupPath), "%s/%s.old", LOG_DIR, logName); + + // Delete old backup if exists + QSPIFile oldBackup(backupPath); + if (oldBackup.exists(error)) { + oldBackup.remove(error); + } + + // Rename current to backup + QSPIFile current(currentPath); + current.rename(backupPath, error); + + Serial.println("Log rotated"); +} + +// Log a single sensor reading (called from loop) +void logSensorReading() { + StorageError error; + + // Generate fake reading + SensorReading reading; + reading.timestamp = millis(); + reading.temperature = 22.0 + (random(50) / 10.0); + reading.humidity = 45.0; + reading.pressure = 1013.25; + reading.lightLevel = random(1024); + reading.batteryPercent = 95; + reading.flags = 0; + + String logEntry = "[" + String(reading.timestamp) + "] SENSOR: T=" + + String(reading.temperature, 1) + "C, L=" + + String(reading.lightLevel); + appendToLog("sensor.log", logEntry, &error); +} + +// Demo: Folder operations +void demoFolderOperations() { + Serial.println("--- Demo: Folder Operations ---"); + + StorageError error; + QSPIFolder root = storage.getRootFolder(&error); + + // Create nested folder structure + Serial.println("Creating nested folders..."); + QSPIFolder dataFolder = root.getSubfolder("data", &error); + + QSPIFolder year2024 = dataFolder.createSubfolder("2024", false, &error); + QSPIFolder jan = year2024.createSubfolder("01", false, &error); + QSPIFolder feb = year2024.createSubfolder("02", false, &error); + + // Create files in nested folders + QSPIFile janData = jan.createFile("data.csv", FileMode::WRITE, &error); + if (janData.isOpen()) { + janData.write("timestamp,value\n1704067200,42\n", &error); + janData.close(&error); + } + + // Count items + Serial.print("Files in /data/2024: "); + Serial.println(year2024.getFileCount(&error)); + Serial.print("Folders in /data/2024: "); + Serial.println(year2024.getFolderCount(&error)); + + // List all folders + Serial.println("\nFolders in /data/2024:"); + std::vector subfolders = year2024.getFolders(&error); + for (auto& folder : subfolders) { + Serial.print(" [DIR] "); + Serial.println(folder.getFolderName()); + + // List files in each subfolder + std::vector files = folder.getFiles(&error); + for (auto& file : files) { + Serial.print(" - "); + Serial.println(file.getFilename()); + } + } + + // Get parent folder + QSPIFolder parent = jan.getParentFolder(&error); + Serial.print("\nParent of /data/2024/01: "); + Serial.println(parent.getPath()); + + Serial.println(); +} + +// Demo: Storage statistics +void demoStorageStatistics() { + Serial.println("--- Demo: Storage Statistics ---"); + + StorageError error; + size_t total, used, available; + + if (storage.getStorageInfo(total, used, available, &error)) { + Serial.println("Storage Statistics:"); + Serial.println("-------------------"); + Serial.print("Total Space: "); + printSize(total); + Serial.print("Used Space: "); + printSize(used); + Serial.print("Available Space: "); + printSize(available); + Serial.print("Usage: "); + Serial.print((used * 100) / total); + Serial.println("%"); + } + + // Count all files recursively + QSPIFolder root = storage.getRootFolder(&error); + uint32_t fileCount = 0; + uint32_t folderCount = 0; + countRecursive(root, fileCount, folderCount, &error); + + Serial.print("\nTotal Files: "); + Serial.println(fileCount); + Serial.print("Total Folders: "); + Serial.println(folderCount); + + Serial.println(); +} + +// Helper: Print size in human-readable format +void printSize(size_t bytes) { + if (bytes >= 1024 * 1024) { + Serial.print(bytes / (1024 * 1024)); + Serial.println(" MB"); + } else if (bytes >= 1024) { + Serial.print(bytes / 1024); + Serial.println(" KB"); + } else { + Serial.print(bytes); + Serial.println(" bytes"); + } +} + +// Helper: Count files and folders recursively +void countRecursive(QSPIFolder& folder, uint32_t& fileCount, uint32_t& folderCount, StorageError* error) { + std::vector files = folder.getFiles(error); + fileCount += files.size(); + + std::vector subfolders = folder.getFolders(error); + folderCount += subfolders.size(); + + for (auto& subfolder : subfolders) { + countRecursive(subfolder, fileCount, folderCount, error); + } +} diff --git a/libraries/QSPIStorage/examples/BasicFileOperations/BasicFileOperations.ino b/libraries/QSPIStorage/examples/BasicFileOperations/BasicFileOperations.ino new file mode 100644 index 00000000..dc929274 --- /dev/null +++ b/libraries/QSPIStorage/examples/BasicFileOperations/BasicFileOperations.ino @@ -0,0 +1,216 @@ +/* + QSPIStorage - Basic File Operations Example + + This example demonstrates basic file and folder operations using the + QSPIStorage library. + + IMPORTANT REQUIREMENTS: + ======================= + This library requires LittleFS to be auto-mounted via devicetree FSTAB. + Your board's devicetree must include: + + 1. A storage partition on the QSPI flash + 2. An FSTAB entry that mounts LittleFS at "/qspi" + + If you see "Filesystem not mounted" error, the board's devicetree + needs to be configured for auto-mounting. + + For boards without FSTAB configuration, use the low-level QSPI library + directly (see QSPI/examples/QSPIFilesystem.ino). +*/ + +#include + +QSPIStorage storage; + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("QSPIStorage - Basic File Operations"); + Serial.println("====================================\n"); + + // Initialize storage + StorageError error; + + Serial.println("Initializing QSPI storage..."); + if (!storage.begin(&error)) { + Serial.print("Storage initialization failed: "); + Serial.println(error.getMessage()); + Serial.println("\nNote: This library requires LittleFS auto-mount via devicetree FSTAB."); + Serial.println("Check your board's devicetree configuration."); + while (1) delay(1000); + } + + Serial.println("Storage mounted successfully!\n"); + + // Show storage info + showStorageInfo(); + + // Run file operation demos + demoWriteFile(); + demoReadFile(); + demoListFiles(); + demoDeleteFile(); + + Serial.println("\n=== All demos completed ==="); +} + +void loop() { + delay(1000); +} + +void showStorageInfo() { + StorageError error; + size_t total, used, available; + + if (storage.getStorageInfo(total, used, available, &error)) { + Serial.println("Storage Information:"); + Serial.print(" Total: "); + Serial.print(total / 1024); + Serial.println(" KB"); + Serial.print(" Used: "); + Serial.print(used / 1024); + Serial.println(" KB"); + Serial.print(" Available: "); + Serial.print(available / 1024); + Serial.println(" KB"); + Serial.println(); + } +} + +void demoWriteFile() { + Serial.println("--- Demo: Writing a File ---"); + + StorageError error; + QSPIFile file("/qspi/hello.txt"); + + if (file.open(FileMode::WRITE, &error)) { + String content = "Hello from QSPIStorage!\n"; + content += "Timestamp: "; + content += String(millis()); + content += " ms"; + + size_t written = file.write(content, &error); + file.close(&error); + + if (!error) { + Serial.print("Wrote "); + Serial.print(written); + Serial.println(" bytes to hello.txt"); + } else { + Serial.print("Write error: "); + Serial.println(error.getMessage()); + } + } else { + Serial.print("Failed to open file: "); + Serial.println(error.getMessage()); + } + + Serial.println(); +} + +void demoReadFile() { + Serial.println("--- Demo: Reading a File ---"); + + StorageError error; + QSPIFile file("/qspi/hello.txt"); + + if (file.open(FileMode::READ, &error)) { + String content = file.readAsString(&error); + file.close(&error); + + if (!error) { + Serial.println("Content of hello.txt:"); + Serial.println("---"); + Serial.println(content); + Serial.println("---"); + } else { + Serial.print("Read error: "); + Serial.println(error.getMessage()); + } + } else { + Serial.print("Failed to open file: "); + Serial.println(error.getMessage()); + } + + Serial.println(); +} + +void demoListFiles() { + Serial.println("--- Demo: Listing Files ---"); + + StorageError error; + QSPIFolder root = storage.getRootFolder(&error); + + if (error) { + Serial.print("Error getting root folder: "); + Serial.println(error.getMessage()); + return; + } + + // List files in root + Serial.println("Files in /qspi:"); + std::vector files = root.getFiles(&error); + + if (files.empty()) { + Serial.println(" (no files)"); + } else { + for (auto& f : files) { + Serial.print(" "); + Serial.print(f.getFilename()); + Serial.print(" ("); + Serial.print(f.size(&error)); + Serial.println(" bytes)"); + } + } + + // List folders + Serial.println("\nFolders in /qspi:"); + std::vector folders = root.getFolders(&error); + + if (folders.empty()) { + Serial.println(" (no folders)"); + } else { + for (auto& folder : folders) { + Serial.print(" [DIR] "); + Serial.println(folder.getFolderName()); + } + } + + Serial.println(); +} + +void demoDeleteFile() { + Serial.println("--- Demo: Deleting a File ---"); + + StorageError error; + + // Create a temp file + QSPIFile tempFile("/qspi/temp.txt"); + if (tempFile.open(FileMode::WRITE, &error)) { + tempFile.write("Temporary file", &error); + tempFile.close(&error); + Serial.println("Created temp.txt"); + } + + // Check existence + Serial.print("temp.txt exists: "); + Serial.println(tempFile.exists(&error) ? "Yes" : "No"); + + // Delete it + if (tempFile.remove(&error)) { + Serial.println("Deleted temp.txt"); + } else { + Serial.print("Delete failed: "); + Serial.println(error.getMessage()); + } + + // Verify deletion + Serial.print("temp.txt exists: "); + Serial.println(tempFile.exists(&error) ? "Yes" : "No"); + + Serial.println(); +} diff --git a/libraries/QSPIStorage/examples/MinimalTest/MinimalTest.ino b/libraries/QSPIStorage/examples/MinimalTest/MinimalTest.ino new file mode 100644 index 00000000..4d7ce8bd --- /dev/null +++ b/libraries/QSPIStorage/examples/MinimalTest/MinimalTest.ino @@ -0,0 +1,45 @@ +/* + QSPIStorage Minimal Test +*/ + +#include + +QSPIStorage storage; + +void setup() { + Serial.begin(115200); + while (!Serial) delay(10); + + Serial.println("QSPIStorage Test"); + Serial.println("================"); + Serial.print("Mount point: "); + Serial.println(storage.getMountPoint()); + + Serial.println("Calling begin()..."); + StorageError error; + if (storage.begin(&error)) { + Serial.println("Storage mounted!"); + + size_t total, used, available; + if (storage.getStorageInfo(total, used, available)) { + Serial.print("Total: "); + Serial.print(total / 1024); + Serial.println(" KB"); + Serial.print("Used: "); + Serial.print(used / 1024); + Serial.println(" KB"); + Serial.print("Available: "); + Serial.print(available / 1024); + Serial.println(" KB"); + } + } else { + Serial.print("Mount failed: "); + Serial.println(error.getMessage()); + } + + Serial.println("Done!"); +} + +void loop() { + delay(1000); +} diff --git a/libraries/QSPIStorage/library.properties b/libraries/QSPIStorage/library.properties new file mode 100644 index 00000000..36e216a5 --- /dev/null +++ b/libraries/QSPIStorage/library.properties @@ -0,0 +1,10 @@ +name=QSPIStorage +version=1.0.0 +author=Arduino +maintainer=Arduino +sentence=High-level QSPI flash storage library with file and folder abstractions. +paragraph=Provides QSPIStorage, QSPIFile, and QSPIFolder classes for easy file operations on QSPI flash. Requires LittleFS auto-mount via devicetree FSTAB. +category=Data Storage +url=https://github.com/arduino/ArduinoCore-zephyr +architectures=* +depends=ArduinoStorage diff --git a/libraries/proposed/BaseStorageAPI.md b/libraries/proposed/BaseStorageAPI.md new file mode 100644 index 00000000..be0bb265 --- /dev/null +++ b/libraries/proposed/BaseStorageAPI.md @@ -0,0 +1,311 @@ +# Base Storage API Reference + +## Overview + +This document defines the base API for all Arduino storage libraries. It provides a unified interface inspired by Arduino_UnifiedStorage, with comprehensive error handling through an error object system. This API serves as a reference specification and should not be implemented directly - instead, storage implementations (QSPI, SD, Flash, etc.) should conform to these interfaces. + +## Core Design Principles + +1. **Unified Interface**: All storage types expose the same API surface +2. **Error Object Pattern**: Every method accepts an optional `StorageError*` parameter +3. **Path-based Operations**: Files and folders are referenced by paths +4. **Minimal Dependencies**: Standard Arduino types (String, uint8_t, etc.) +5. **Resource Safety**: Explicit open/close semantics with automatic cleanup + +--- + +## Error Handling + +### StorageError Class + +The `StorageError` class provides detailed error information across all storage operations. + +```cpp +enum class StorageErrorCode { + NONE = 0, + // File/Folder errors + FILE_NOT_FOUND, + FOLDER_NOT_FOUND, + ALREADY_EXISTS, + INVALID_PATH, + PERMISSION_DENIED, + + // I/O errors + READ_ERROR, + WRITE_ERROR, + SEEK_ERROR, + OPEN_ERROR, + CLOSE_ERROR, + + // Storage errors + STORAGE_FULL, + STORAGE_NOT_MOUNTED, + STORAGE_CORRUPTED, + STORAGE_NOT_FORMATTED, + + // Operation errors + INVALID_OPERATION, + INVALID_MODE, + BUFFER_OVERFLOW, + OUT_OF_MEMORY, + TIMEOUT, + + // Hardware errors + HARDWARE_ERROR, + NOT_INITIALIZED, + + // Generic + UNKNOWN_ERROR +}; + +class StorageError { +public: + StorageError(); + + // Error state + StorageErrorCode getCode() const; + const char* getMessage() const; + bool hasError() const; + + // Error setting (for implementations) + void setError(StorageErrorCode code, const char* message = nullptr); + void clear(); + + // Convenience operators + operator bool() const; // Returns true if error exists + +private: + StorageErrorCode code_; + char message_[128]; +}; +``` + +### Error Usage Pattern + +```cpp +// Example 1: Check error after operation +StorageError error; +file.open("data.txt", FileMode::READ, &error); +if (error) { + Serial.print("Error: "); + Serial.println(error.getMessage()); +} + +// Example 2: Ignore errors (backwards compatible) +file.open("data.txt", FileMode::READ); + +// Example 3: Error accumulation +StorageError error; +file.open("data.txt", FileMode::WRITE, &error); +file.write(buffer, size, &error); +file.close(&error); +if (error) { + Serial.println("Operation failed"); +} +``` + +--- + +## File Modes + +```cpp +enum class FileMode { + READ, // Open for reading, file must exist + WRITE, // Open for writing, creates if doesn't exist, truncates if exists + APPEND, // Open for writing at end, creates if doesn't exist + READ_WRITE, // Open for reading and writing, file must exist + READ_WRITE_CREATE // Open for reading and writing, creates if doesn't exist +}; +``` + +--- + +## File Class + +Represents a file in the storage system. Files can be read, written, and manipulated. + +### Constructors + +```cpp +File(); +File(const char* path); +File(const String& path); +``` + +### Opening and Closing + +```cpp +// Open file with specific mode +bool open(const char* filename, FileMode mode, StorageError* error = nullptr); +bool open(const String& filename, FileMode mode, StorageError* error = nullptr); +bool open(FileMode mode = FileMode::READ, StorageError* error = nullptr); // Uses constructor path + +// Close file and release resources +bool close(StorageError* error = nullptr); + +// Change mode without closing/reopening +bool changeMode(FileMode mode, StorageError* error = nullptr); + +// Check if file is currently open +bool isOpen() const; +``` + +### Reading Operations + +```cpp +// Read data into buffer, returns bytes actually read +size_t read(uint8_t* buffer, size_t size, StorageError* error = nullptr); + +// Read single byte, returns -1 on error or EOF +int read(StorageError* error = nullptr); + +// Read entire file as string +String readAsString(StorageError* error = nullptr); + +// Check bytes available for reading +uint32_t available(StorageError* error = nullptr); + +// Position file pointer +bool seek(size_t offset, StorageError* error = nullptr); + +// Get current position +size_t position(StorageError* error = nullptr); + +// Get file size +size_t size(StorageError* error = nullptr); +``` + +### Writing Operations + +```cpp +// Write buffer to file, returns bytes actually written +size_t write(const uint8_t* buffer, size_t size, StorageError* error = nullptr); + +// Write string to file +size_t write(const String& data, StorageError* error = nullptr); + +// Write single byte +size_t write(uint8_t value, StorageError* error = nullptr); + +// Flush write buffer to storage +bool flush(StorageError* error = nullptr); +``` + +### File Management + +```cpp +// Check if file exists +bool exists(StorageError* error = nullptr) const; + +// Delete file +bool remove(StorageError* error = nullptr); + +// Rename file +bool rename(const char* newFilename, StorageError* error = nullptr); +bool rename(const String& newFilename, StorageError* error = nullptr); +``` + +### Path Information + +```cpp +// Get file path as C-string +const char* getPath() const; + +// Get file path as Arduino String +String getPathAsString() const; + +// Get parent folder +Folder getParentFolder(StorageError* error = nullptr) const; + +// Get filename without path +String getFilename() const; +``` + +--- + +## Folder Class + +Represents a folder/directory in the storage system. + +### Constructors + +```cpp +Folder(); +Folder(const char* path); +Folder(const String& path); +``` + +### File Operationsf + +```cpp +// Create file in this folder +File createFile(const char* filename, FileMode mode = FileMode::WRITE, StorageError* error = nullptr); +File createFile(const String& filename, FileMode mode = FileMode::WRITE, StorageError* error = nullptr); + +// Get file from this folder (doesn't create) +File getFile(const char* filename, StorageError* error = nullptr); +File getFile(const String& filename, StorageError* error = nullptr); +``` + +### Directory Management + +```cpp +// Check if folder exists +bool exists(StorageError* error = nullptr) const; + +// Create this folder if it doesn't exist +bool create(StorageError* error = nullptr); + +// Delete folder and all contents +bool remove(bool recursive = false, StorageError* error = nullptr); + +// Rename folder +bool rename(const char* newName, StorageError* error = nullptr); +bool rename(const String& newName, StorageError* error = nullptr); +``` + +### Subfolder Operations + +```cpp +// Create subfolder +Folder createSubfolder(const char* name, bool overwrite = false, StorageError* error = nullptr); +Folder createSubfolder(const String& name, bool overwrite = false, StorageError* error = nullptr); + +// Get existing subfolder +Folder getSubfolder(const char* name, StorageError* error = nullptr); +Folder getSubfolder(const String& name, StorageError* error = nullptr); +``` + +### Content Enumeration + +```cpp +// Get all files in this folder (non-recursive) +std::vector getFiles(StorageError* error = nullptr); + +// Get all subfolders (non-recursive) +std::vector getFolders(StorageError* error = nullptr); + +// Get number of files +size_t getFileCount(StorageError* error = nullptr); + +// Get number of subfolders +size_t getFolderCount(StorageError* error = nullptr); +``` + +### Path Information + +```cpp +// Get folder path as C-string +const char* getPath() const; + +// Get folder path as Arduino String +String getPathAsString() const; + +// Get parent folder +Folder getParentFolder(StorageError* error = nullptr) const; + +// Get folder name without path +String getFolderName() const; +``` + +--- diff --git a/libraries/proposed/QSPIStorage.md b/libraries/proposed/QSPIStorage.md new file mode 100644 index 00000000..c28cde16 --- /dev/null +++ b/libraries/proposed/QSPIStorage.md @@ -0,0 +1,514 @@ +# QSPI Storage Implementation + +## Overview + +The QSPI Storage library provides a file system implementation for QSPI flash memory, conforming to the Base Storage API. It builds upon the low-level `QSPIClass` interface to provide file and folder abstractions with comprehensive error handling. + +This library implements the Base Storage API on top of a file system (LittleFS recommended) mounted on QSPI flash. + +--- + +## Architecture + +``` +┌─────────────────────────────────────┐ +│ QSPIFile / QSPIFolder Classes │ ← Base Storage API Implementation +├─────────────────────────────────────┤ +│ File System Layer (LittleFS) │ ← Zephyr FS Subsystem +├─────────────────────────────────────┤ +│ QSPIClass Interface │ ← Low-level flash operations +├─────────────────────────────────────┤ +│ Zephyr Flash Driver (QSPI) │ ← Hardware abstraction +└─────────────────────────────────────┘ +``` + +--- + +## Dependencies + +- **Base Storage API**: Conforms to `BaseStorageAPI.md` specification +- **QSPIClass**: Low-level flash interface (`QSPI.h`) +- **Zephyr FS**: File system subsystem (LittleFS or FAT) +- **StorageError**: Shared error handling from Base API + +--- + +## QSPIStorage Class + +Main storage manager that handles mounting, formatting, and partition management. + +### Initialization + +```cpp +class QSPIStorage { +public: + QSPIStorage(); + + // Initialize and mount file system + bool begin(StorageError* error = nullptr); + + // Unmount and deinitialize + void end(StorageError* error = nullptr); + + // Check if storage is mounted and ready + bool isMounted() const; + + // Get mount point path (e.g., "/qspi") + const char* getMountPoint() const; + + // Get storage statistics + bool getStorageInfo(size_t& total, size_t& used, size_t& available, StorageError* error = nullptr); + +private: + const char* mount_point_; + bool mounted_; + struct fs_mount_t mount_config_; +}; +``` + +### Usage Example + +```cpp +#include + +QSPIStorage storage; +StorageError error; + +void setup() { + Serial.begin(115200); + + // Initialize QSPI storage + if (!storage.begin(&error)) { + Serial.print("Storage init failed: "); + Serial.println(error.getMessage()); + return; + } + + Serial.println("QSPI Storage ready!"); + + // Get storage info + size_t total, used, available; + storage.getStorageInfo(total, used, available, &error); + Serial.print("Total: "); Serial.print(total / 1024); Serial.println(" KB"); + Serial.print("Used: "); Serial.print(used / 1024); Serial.println(" KB"); + Serial.print("Available: "); Serial.print(available / 1024); Serial.println(" KB"); +} +``` + +--- + +## QSPIFile Class + +Implements the Base Storage API `StorageFile` interface for QSPI storage. + +### Header Definition + +```cpp +class QSPIFile { +public: + // Constructors + QSPIFile(); + QSPIFile(const char* path); + QSPIFile(const String& path); + + // Opening and Closing + bool open(const char* filename, FileMode mode, StorageError* error = nullptr); + bool open(const String& filename, FileMode mode, StorageError* error = nullptr); + bool open(FileMode mode = FileMode::READ, StorageError* error = nullptr); + bool close(StorageError* error = nullptr); + bool changeMode(FileMode mode, StorageError* error = nullptr); + bool isOpen() const; + + // Reading Operations + size_t read(uint8_t* buffer, size_t size, StorageError* error = nullptr); + int read(StorageError* error = nullptr); + String readAsString(StorageError* error = nullptr); + uint32_t available(StorageError* error = nullptr); + bool seek(size_t offset, StorageError* error = nullptr); + size_t position(StorageError* error = nullptr); + size_t size(StorageError* error = nullptr); + + // Writing Operations + size_t write(const uint8_t* buffer, size_t size, StorageError* error = nullptr); + size_t write(const String& data, StorageError* error = nullptr); + size_t write(uint8_t value, StorageError* error = nullptr); + bool flush(StorageError* error = nullptr); + + // File Management + bool exists(StorageError* error = nullptr) const; + bool remove(StorageError* error = nullptr); + bool rename(const char* newFilename, StorageError* error = nullptr); + bool rename(const String& newFilename, StorageError* error = nullptr); + + // Path Information + const char* getPath() const; + String getPathAsString() const; + QSPIFolder getParentFolder(StorageError* error = nullptr) const; + String getFilename() const; + +private: + char path_[256]; + struct fs_file_t file_; + bool is_open_; + FileMode mode_; + + // Internal helpers + bool resolvePath(const char* path, char* resolved, StorageError* error); + int fileModeToFlags(FileMode mode); +}; +``` + +### Implementation Notes + +1. **Path Resolution**: All paths are resolved relative to mount point (e.g., "/qspi/data.txt") +2. **File Handles**: Uses Zephyr `fs_file_t` structure +3. **Buffering**: Write operations are buffered; use `flush()` to ensure data is written +4. **Error Mapping**: Zephyr error codes are mapped to `StorageErrorCode` + +### Usage Example + +```cpp +QSPIFile file("/qspi/config.txt"); +StorageError error; + +// Write configuration +if (file.open(FileMode::WRITE, &error)) { + String config = "wifi_ssid=MyNetwork\n"; + config += "wifi_pass=MyPassword\n"; + + file.write(config, &error); + file.flush(&error); + file.close(&error); +} + +if (error) { + Serial.print("Write error: "); + Serial.println(error.getMessage()); +} + +// Read configuration +if (file.open(FileMode::READ, &error)) { + String content = file.readAsString(&error); + Serial.println(content); + file.close(&error); +} +``` + +--- + +## QSPIFolder Class + +Implements the Base Storage API `StorageFolder` interface for QSPI storage. + +### Header Definition + +```cpp +class QSPIFolder { +public: + // Constructors + QSPIFolder(); + QSPIFolder(const char* path); + QSPIFolder(const String& path); + + // File Operations + QSPIFile createFile(const char* filename, FileMode mode = FileMode::WRITE, StorageError* error = nullptr); + QSPIFile createFile(const String& filename, FileMode mode = FileMode::WRITE, StorageError* error = nullptr); + QSPIFile getFile(const char* filename, StorageError* error = nullptr); + QSPIFile getFile(const String& filename, StorageError* error = nullptr); + + // Directory Management + bool exists(StorageError* error = nullptr) const; + bool create(StorageError* error = nullptr); + bool remove(bool recursive = false, StorageError* error = nullptr); + bool rename(const char* newName, StorageError* error = nullptr); + bool rename(const String& newName, StorageError* error = nullptr); + + // Subfolder Operations + QSPIFolder createSubfolder(const char* name, bool overwrite = false, StorageError* error = nullptr); + QSPIFolder createSubfolder(const String& name, bool overwrite = false, StorageError* error = nullptr); + QSPIFolder getSubfolder(const char* name, StorageError* error = nullptr); + QSPIFolder getSubfolder(const String& name, StorageError* error = nullptr); + + // Content Enumeration + std::vector getFiles(StorageError* error = nullptr); + std::vector getFolders(StorageError* error = nullptr); + size_t getFileCount(StorageError* error = nullptr); + size_t getFolderCount(StorageError* error = nullptr); + + // Path Information + const char* getPath() const; + String getPathAsString() const; + QSPIFolder getParentFolder(StorageError* error = nullptr) const; + String getFolderName() const; + +private: + char path_[256]; + + // Internal helpers + bool resolvePath(const char* path, char* resolved, StorageError* error); + bool removeRecursive(const char* path, StorageError* error); +}; +``` + +### Implementation Notes + +1. **Directory Operations**: Uses Zephyr `fs_opendir()` / `fs_readdir()` / `fs_closedir()` +2. **Recursive Operations**: `remove(true)` handles nested structures +3. **Path Building**: Automatically handles path separators and mount points +4. **Enumeration**: Returns vectors of file/folder objects for easy iteration + +### Usage Example + +```cpp +QSPIFolder dataFolder("/qspi/data"); +StorageError error; + +// Create folder structure +if (!dataFolder.exists(&error)) { + dataFolder.create(&error); +} + +// Create subfolders +QSPIFolder logsFolder = dataFolder.createSubfolder("logs", false, &error); +QSPIFolder configFolder = dataFolder.createSubfolder("config", false, &error); + +// Create files in subfolder +QSPIFile logFile = logsFolder.createFile("app.log", FileMode::WRITE, &error); +if (logFile.isOpen()) { + logFile.write("Application started\n", &error); + logFile.close(&error); +} + +// List all files +std::vector files = dataFolder.getFiles(&error); +Serial.print("Found "); +Serial.print(files.size()); +Serial.println(" files:"); + +for (auto& file : files) { + Serial.print(" - "); + Serial.println(file.getFilename()); +} +``` + +--- + +## Error Code Mapping + +Zephyr file system errors are mapped to `StorageErrorCode`: + +| Zephyr Error | StorageErrorCode | +|--------------|------------------| +| `-ENOENT` | `FILE_NOT_FOUND` / `FOLDER_NOT_FOUND` | +| `-EEXIST` | `ALREADY_EXISTS` | +| `-EINVAL` | `INVALID_PATH` | +| `-EACCES` | `PERMISSION_DENIED` | +| `-EIO` | `READ_ERROR` / `WRITE_ERROR` | +| `-ENOSPC` | `STORAGE_FULL` | +| `-EROFS` | `PERMISSION_DENIED` | +| `-ENODEV` | `STORAGE_NOT_MOUNTED` | +| Other | `UNKNOWN_ERROR` | + +--- + +## Performance Considerations + +### Write Optimization + +1. **Buffering**: Enable write buffering in LittleFS configuration +2. **Block Alignment**: Align writes to flash page size when possible +3. **Batch Operations**: Group multiple writes before calling `flush()` + +### Read Optimization + +1. **Read-Ahead**: Configure LittleFS cache size appropriately +2. **Sequential Access**: Sequential reads are faster than random access +3. **File Size**: Check file size before reading to allocate buffers efficiently + +### Memory Usage + +- **Stack**: Path buffers use 256 bytes per object +- **Heap**: File system cache configurable (default 512 bytes per cache) +- **Static**: Mount structures and device handles + +--- + +## Configuration + +### Zephyr Configuration (prj.conf) + +```ini +# File system support +CONFIG_FILE_SYSTEM=y +CONFIG_FILE_SYSTEM_LITTLEFS=y + +# QSPI Flash +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y + +# LittleFS settings +CONFIG_FS_LITTLEFS_CACHE_SIZE=512 +CONFIG_FS_LITTLEFS_LOOKAHEAD_SIZE=32 +CONFIG_FS_LITTLEFS_BLOCK_CYCLES=512 +``` + +### Device Tree Partition (board.overlay) + +```dts +&mx25r64 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + /* Storage partition: 7.5MB */ + storage_partition: partition@80000 { + label = "storage"; + reg = <0x00080000 0x00780000>; + }; + }; +}; +``` + +--- + +## Complete Example + +```cpp +#include +#include + +QSPIStorage storage; + +void setup() { + Serial.begin(115200); + while (!Serial) delay(10); + + // Initialize storage + StorageError error; + if (!storage.begin(&error)) { + Serial.print("Failed to initialize storage: "); + Serial.println(error.getMessage()); + return; + } + + // Create folder structure + QSPIFolder root("/qspi"); + QSPIFolder dataFolder = root.createSubfolder("data", false, &error); + + // Create and write to file + QSPIFile dataFile = dataFolder.createFile("sensor.csv", FileMode::WRITE, &error); + if (dataFile.isOpen()) { + dataFile.write("timestamp,temperature,humidity\n", &error); + dataFile.write("1234567890,23.5,45.2\n", &error); + dataFile.flush(&error); + dataFile.close(&error); + } + + // Read back file + dataFile.open(FileMode::READ, &error); + if (dataFile.isOpen()) { + String content = dataFile.readAsString(&error); + Serial.println("File content:"); + Serial.println(content); + dataFile.close(&error); + } + + // List all files in folder + std::vector files = dataFolder.getFiles(&error); + Serial.print("Files in /qspi/data: "); + Serial.println(files.size()); + + for (auto& file : files) { + Serial.print(" - "); + Serial.print(file.getFilename()); + Serial.print(" ("); + Serial.print(file.size(&error)); + Serial.println(" bytes)"); + } + + if (error) { + Serial.print("Error occurred: "); + Serial.println(error.getMessage()); + } +} + +void loop() { + // Nothing to do +} +``` + +--- + +## Testing Guidelines + +### Unit Tests + +1. **Initialization**: Test mount/unmount cycles +2. **File Operations**: Test create, read, write, delete, rename +3. **Folder Operations**: Test create, enumerate, remove (recursive) +4. **Error Handling**: Test error propagation and recovery +5. **Edge Cases**: Test full storage, long paths, special characters + +### Integration Tests + +1. **Power Loss**: Verify file system integrity after simulated power loss +2. **Stress Test**: Continuous read/write cycles +3. **Fragmentation**: Test performance with many small files +4. **Wear Leveling**: Monitor flash wear distribution + +--- + +## Limitations + +1. **Path Length**: Maximum path length is 255 characters +2. **Filename**: Maximum filename length depends on FS (typically 255 chars for LittleFS) +3. **Open Files**: Limited by `CONFIG_FS_LITTLEFS_CACHE_SIZE` and available memory +4. **Concurrent Access**: No file locking; avoid concurrent writes to same file +5. **Flash Wear**: QSPI flash has limited write/erase cycles (~100K typical) + +--- + +## Migration from Raw QSPI + +### Before (Raw QSPI) + +```cpp +#include + +QSPI.begin(); +uint8_t data[256]; +QSPI.read(0x1000, data, 256); +QSPI.write(0x2000, data, 256); +``` + +### After (QSPI Storage) + +```cpp +#include + +QSPIStorage storage; +storage.begin(); + +QSPIFile file("/qspi/data.bin"); +file.open(FileMode::READ_WRITE_CREATE); +file.write(data, 256); +file.seek(0); +file.read(buffer, 256); +file.close(); +``` + +**Benefits**: +- File system structure and organization +- Automatic wear leveling +- Power-loss recovery +- Standard file operations + +--- + +## Version + +**Library Version**: 1.0.0 +**Base API Version**: 1.0.0 +**Status**: Draft Proposal +**Last Updated**: 2025-12-04 diff --git a/libraries/proposed/StaticStorage.md b/libraries/proposed/StaticStorage.md new file mode 100644 index 00000000..6a7cde50 --- /dev/null +++ b/libraries/proposed/StaticStorage.md @@ -0,0 +1,843 @@ +# Static Storage Utilities Library + +## Overview + +The Static Storage library provides utility functions for managing storage devices across all storage implementations (QSPI, SD, Flash, etc.). It handles cross-storage operations like formatting, partitioning, and advanced copy/move operations that may span multiple storage backends. + +This library provides **static methods** that work with any storage implementation conforming to the Base Storage API, enabling operations that are not tied to a specific storage instance. + +--- + +## Design Principles + +1. **Storage-Agnostic**: Works with any Base Storage API implementation +2. **Static Interface**: All methods are static - no instantiation required +3. **Cross-Storage Operations**: Support operations between different storage types +4. **Comprehensive Error Handling**: All methods use `StorageError*` parameter +5. **Utility Functions**: High-level operations built on Base Storage API + +--- + +## Architecture + +``` +┌────────────────────────────────────────────┐ +│ StaticStorage Utility Layer │ ← Static helper methods +├────────────────────────────────────────────┤ +│ Base Storage API (File/Folder/Error) │ ← Common interface +├─────────────┬──────────────┬───────────────┤ +│ QSPI Storage│ SD Storage │ Flash Storage │ ← Specific implementations +└─────────────┴──────────────┴───────────────┘ +``` + +--- + +## Dependencies + +- **Base Storage API**: Uses `StorageFile`, `StorageFolder`, `StorageError` +- **Storage Implementations**: Works with any conforming implementation +- **Zephyr FS API**: For low-level formatting and partition operations + +--- + +## StaticStorage Class + +All methods are static. No instantiation required. + +```cpp +#include + +// Direct usage without creating an object +StaticStorage::format("/qspi", FilesystemType::LITTLEFS); +``` + +--- + +## File System Types + +```cpp +enum class FilesystemType { + LITTLEFS, // LittleFS - recommended for flash storage + FAT, // FAT32 - better compatibility, larger overhead + EXT2, // Extended 2 - Linux-style filesystem + AUTO // Auto-detect or use default +}; +``` + +--- + +## Formatting Operations + +### Format Storage + +Format a storage device with a specific file system. + +```cpp +class StaticStorage { +public: + /** + * Format a storage device + * @param mountPoint Mount point path (e.g., "/qspi", "/sd") + * @param fsType File system type to format with + * @param error Optional error output parameter + * @return true if successful, false otherwise + */ + static bool format( + const char* mountPoint, + FilesystemType fsType = FilesystemType::AUTO, + StorageError* error = nullptr + ); + + static bool format( + const String& mountPoint, + FilesystemType fsType = FilesystemType::AUTO, + StorageError* error = nullptr + ); + + /** + * Quick format (faster but less thorough) + * @param mountPoint Mount point path + * @param fsType File system type + * @param error Optional error output parameter + * @return true if successful + */ + static bool quickFormat( + const char* mountPoint, + FilesystemType fsType = FilesystemType::AUTO, + StorageError* error = nullptr + ); + + /** + * Check if a storage device needs formatting + * @param mountPoint Mount point path + * @param error Optional error output parameter + * @return true if formatting is needed + */ + static bool needsFormatting( + const char* mountPoint, + StorageError* error = nullptr + ); +}; +``` + +### Usage Example + +```cpp +#include + +void setup() { + Serial.begin(115200); + + StorageError error; + + // Check if QSPI needs formatting + if (StaticStorage::needsFormatting("/qspi", &error)) { + Serial.println("QSPI needs formatting..."); + + // Format with LittleFS + if (StaticStorage::format("/qspi", FilesystemType::LITTLEFS, &error)) { + Serial.println("Format successful!"); + } else { + Serial.print("Format failed: "); + Serial.println(error.getMessage()); + } + } +} +``` + +--- + +## Partitioning Operations + +### Partition Management + +Create and manage partitions on storage devices. + +```cpp +struct PartitionInfo { + const char* label; // Partition name/label + size_t offset; // Start offset in bytes + size_t size; // Size in bytes + FilesystemType fsType; // File system type for this partition +}; + +class StaticStorage { +public: + /** + * Create partitions on a storage device + * @param mountPoint Base mount point (e.g., "/qspi") + * @param partitions Array of partition definitions + * @param count Number of partitions + * @param error Optional error output parameter + * @return true if successful + */ + static bool createPartitions( + const char* mountPoint, + const PartitionInfo* partitions, + size_t count, + StorageError* error = nullptr + ); + + /** + * List existing partitions + * @param mountPoint Mount point to query + * @param partitions Output vector of partition info + * @param error Optional error output parameter + * @return true if successful + */ + static bool listPartitions( + const char* mountPoint, + std::vector& partitions, + StorageError* error = nullptr + ); + + /** + * Remove all partitions (restore to single partition) + * @param mountPoint Mount point + * @param error Optional error output parameter + * @return true if successful + */ + static bool removePartitions( + const char* mountPoint, + StorageError* error = nullptr + ); + + /** + * Get partition by label + * @param mountPoint Base mount point + * @param label Partition label + * @param info Output partition information + * @param error Optional error output parameter + * @return true if found + */ + static bool getPartition( + const char* mountPoint, + const char* label, + PartitionInfo& info, + StorageError* error = nullptr + ); +}; +``` + +### Usage Example + +```cpp +#include + +void setup() { + Serial.begin(115200); + StorageError error; + + // Define partitions for QSPI flash + // Assuming 8MB QSPI flash + PartitionInfo partitions[] = { + {"config", 0x000000, 512 * 1024, FilesystemType::LITTLEFS}, // 512KB for config + {"data", 0x080000, 2 * 1024 * 1024, FilesystemType::LITTLEFS}, // 2MB for data + {"logs", 0x280000, 1 * 1024 * 1024, FilesystemType::LITTLEFS}, // 1MB for logs + {"storage", 0x380000, 4 * 1024 * 1024, FilesystemType::FAT} // 4MB for storage + }; + + // Create partitions + if (StaticStorage::createPartitions("/qspi", partitions, 4, &error)) { + Serial.println("Partitions created successfully!"); + + // List partitions + std::vector found; + StaticStorage::listPartitions("/qspi", found, &error); + + Serial.print("Found "); + Serial.print(found.size()); + Serial.println(" partitions:"); + + for (auto& part : found) { + Serial.print(" - "); + Serial.print(part.label); + Serial.print(": "); + Serial.print(part.size / 1024); + Serial.println(" KB"); + } + } else { + Serial.print("Partition creation failed: "); + Serial.println(error.getMessage()); + } +} +``` + +--- + +## Cross-Storage Copy/Move Operations + +Advanced copy and move operations that work across different storage backends. + +```cpp +class StaticStorage { +public: + /** + * Copy file between different storage devices + * @param srcFile Source file object (any storage type) + * @param destPath Destination path (may be different storage) + * @param overwrite Overwrite if exists + * @param progress Optional progress callback (bytes_copied, total_bytes) + * @param error Optional error output parameter + * @return true if successful + */ + static bool copyFile( + const StorageFile& srcFile, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + static bool copyFile( + const char* srcPath, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + /** + * Move file between different storage devices + * @param srcFile Source file object + * @param destPath Destination path + * @param overwrite Overwrite if exists + * @param progress Optional progress callback + * @param error Optional error output parameter + * @return true if successful + */ + static bool moveFile( + const StorageFile& srcFile, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + static bool moveFile( + const char* srcPath, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + /** + * Copy folder between different storage devices (recursive) + * @param srcFolder Source folder object + * @param destPath Destination path + * @param overwrite Overwrite if exists + * @param progress Optional progress callback + * @param error Optional error output parameter + * @return true if successful + */ + static bool copyFolder( + const StorageFolder& srcFolder, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + static bool copyFolder( + const char* srcPath, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + /** + * Move folder between different storage devices (recursive) + * @param srcFolder Source folder object + * @param destPath Destination path + * @param overwrite Overwrite if exists + * @param progress Optional progress callback + * @param error Optional error output parameter + * @return true if successful + */ + static bool moveFolder( + const StorageFolder& srcFolder, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + static bool moveFolder( + const char* srcPath, + const char* destPath, + bool overwrite = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + /** + * Synchronize folders (copy only changed files) + * @param srcPath Source folder path + * @param destPath Destination folder path + * @param bidirectional If true, sync both ways + * @param progress Optional progress callback + * @param error Optional error output parameter + * @return true if successful + */ + static bool syncFolders( + const char* srcPath, + const char* destPath, + bool bidirectional = false, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); +}; +``` + +### Usage Example + +```cpp +#include +#include +#include + +void progressCallback(size_t copied, size_t total) { + int percent = (copied * 100) / total; + Serial.print("\rProgress: "); + Serial.print(percent); + Serial.print("%"); +} + +void setup() { + Serial.begin(115200); + + // Initialize both storage devices + QSPIStorage qspi; + SDStorage sd; + + StorageError error; + + qspi.begin(&error); + sd.begin(&error); + + // Copy file from QSPI to SD with progress + Serial.println("Copying large file from QSPI to SD..."); + if (StaticStorage::copyFile( + "/qspi/data/large_file.bin", + "/sd/backup/large_file.bin", + true, // overwrite + progressCallback, + &error + )) { + Serial.println("\nCopy successful!"); + } else { + Serial.print("\nCopy failed: "); + Serial.println(error.getMessage()); + } + + // Backup entire QSPI config folder to SD + Serial.println("Backing up config folder..."); + if (StaticStorage::copyFolder( + "/qspi/config", + "/sd/backup/config", + true, + progressCallback, + &error + )) { + Serial.println("\nBackup successful!"); + } + + // Synchronize data folders between QSPI and SD + Serial.println("Syncing data folders..."); + if (StaticStorage::syncFolders( + "/qspi/data", + "/sd/data", + true, // bidirectional + progressCallback, + &error + )) { + Serial.println("\nSync successful!"); + } +} +``` + +--- + +## Storage Information and Analysis + +```cpp +class StaticStorage { +public: + /** + * Get detailed storage information + * @param mountPoint Mount point to query + * @param info Output storage information structure + * @param error Optional error output parameter + * @return true if successful + */ + static bool getStorageInfo( + const char* mountPoint, + StorageInfo& info, + StorageError* error = nullptr + ); + + /** + * Check storage health + * @param mountPoint Mount point to check + * @param health Output health information + * @param error Optional error output parameter + * @return true if successful + */ + static bool checkHealth( + const char* mountPoint, + StorageHealth& health, + StorageError* error = nullptr + ); + + /** + * Estimate available write cycles (for flash storage) + * @param mountPoint Mount point + * @param error Optional error output parameter + * @return Estimated remaining write cycles (0 = unknown) + */ + static uint32_t estimateRemainingCycles( + const char* mountPoint, + StorageError* error = nullptr + ); + + /** + * Optimize storage (defragment, garbage collect, etc.) + * @param mountPoint Mount point + * @param error Optional error output parameter + * @return true if successful + */ + static bool optimize( + const char* mountPoint, + StorageError* error = nullptr + ); +}; + +struct StorageInfo { + char mountPoint[64]; + FilesystemType fsType; + size_t totalBytes; + size_t usedBytes; + size_t availableBytes; + size_t blockSize; + size_t totalBlocks; + size_t usedBlocks; + bool readOnly; + bool mounted; +}; + +struct StorageHealth { + bool healthy; // Overall health status + uint32_t errorCount; // Number of errors encountered + uint32_t badBlocks; // Number of bad blocks (flash) + uint32_t writeCount; // Total write operations + uint32_t eraseCount; // Total erase operations + float fragmentationPercent; // File system fragmentation + char statusMessage[128]; // Human-readable status +}; +``` + +### Usage Example + +```cpp +#include + +void printStorageInfo(const char* mountPoint) { + StorageInfo info; + StorageHealth health; + StorageError error; + + // Get storage info + if (StaticStorage::getStorageInfo(mountPoint, info, &error)) { + Serial.println("=== Storage Information ==="); + Serial.print("Mount Point: "); Serial.println(info.mountPoint); + Serial.print("Total: "); Serial.print(info.totalBytes / 1024); Serial.println(" KB"); + Serial.print("Used: "); Serial.print(info.usedBytes / 1024); Serial.println(" KB"); + Serial.print("Available: "); Serial.print(info.availableBytes / 1024); Serial.println(" KB"); + Serial.print("Usage: "); + Serial.print((info.usedBytes * 100) / info.totalBytes); + Serial.println("%"); + } + + // Check health + if (StaticStorage::checkHealth(mountPoint, health, &error)) { + Serial.println("\n=== Storage Health ==="); + Serial.print("Status: "); + Serial.println(health.healthy ? "HEALTHY" : "WARNING"); + Serial.print("Errors: "); Serial.println(health.errorCount); + Serial.print("Bad Blocks: "); Serial.println(health.badBlocks); + Serial.print("Fragmentation: "); + Serial.print(health.fragmentationPercent); + Serial.println("%"); + Serial.print("Message: "); Serial.println(health.statusMessage); + + // Optimize if fragmented + if (health.fragmentationPercent > 50.0) { + Serial.println("High fragmentation detected. Optimizing..."); + if (StaticStorage::optimize(mountPoint, &error)) { + Serial.println("Optimization complete!"); + } + } + } + + // Estimate remaining cycles (for flash) + uint32_t cycles = StaticStorage::estimateRemainingCycles(mountPoint, &error); + if (cycles > 0) { + Serial.print("Estimated remaining write cycles: "); + Serial.println(cycles); + } +} + +void setup() { + Serial.begin(115200); + + QSPIStorage qspi; + qspi.begin(); + + printStorageInfo("/qspi"); +} +``` + +--- + +## Utility Functions + +```cpp +class StaticStorage { +public: + /** + * Compare two files (byte-by-byte) + * @param file1Path First file path + * @param file2Path Second file path + * @param error Optional error output parameter + * @return true if files are identical + */ + static bool compareFiles( + const char* file1Path, + const char* file2Path, + StorageError* error = nullptr + ); + + /** + * Calculate file checksum (CRC32) + * @param filePath File path + * @param checksum Output checksum value + * @param error Optional error output parameter + * @return true if successful + */ + static bool calculateChecksum( + const char* filePath, + uint32_t& checksum, + StorageError* error = nullptr + ); + + /** + * Verify file integrity using checksum + * @param filePath File path + * @param expectedChecksum Expected checksum value + * @param error Optional error output parameter + * @return true if checksum matches + */ + static bool verifyChecksum( + const char* filePath, + uint32_t expectedChecksum, + StorageError* error = nullptr + ); + + /** + * Create backup of file/folder + * @param srcPath Source path + * @param backupSuffix Backup suffix (e.g., ".bak", ".backup") + * @param error Optional error output parameter + * @return true if successful + */ + static bool createBackup( + const char* srcPath, + const char* backupSuffix = ".bak", + StorageError* error = nullptr + ); + + /** + * Restore from backup + * @param backupPath Backup file path + * @param restorePath Where to restore (nullptr = original location) + * @param error Optional error output parameter + * @return true if successful + */ + static bool restoreBackup( + const char* backupPath, + const char* restorePath = nullptr, + StorageError* error = nullptr + ); + + /** + * Wipe storage securely (overwrite with zeros/random) + * @param mountPoint Mount point to wipe + * @param passes Number of overwrite passes (1-3 recommended) + * @param progress Optional progress callback + * @param error Optional error output parameter + * @return true if successful + */ + static bool secureWipe( + const char* mountPoint, + uint8_t passes = 1, + void (*progress)(size_t, size_t) = nullptr, + StorageError* error = nullptr + ); + + /** + * Mount storage device + * @param mountPoint Mount point path + * @param devicePath Device path (implementation-specific) + * @param fsType File system type + * @param readOnly Mount as read-only + * @param error Optional error output parameter + * @return true if successful + */ + static bool mount( + const char* mountPoint, + const char* devicePath, + FilesystemType fsType = FilesystemType::AUTO, + bool readOnly = false, + StorageError* error = nullptr + ); + + /** + * Unmount storage device + * @param mountPoint Mount point to unmount + * @param error Optional error output parameter + * @return true if successful + */ + static bool unmount( + const char* mountPoint, + StorageError* error = nullptr + ); +}; +``` + +### Usage Example + +```cpp +#include + +void setup() { + Serial.begin(115200); + StorageError error; + + // Calculate and verify file checksum + uint32_t checksum; + if (StaticStorage::calculateChecksum("/qspi/config.txt", checksum, &error)) { + Serial.print("File checksum: 0x"); + Serial.println(checksum, HEX); + + // Later, verify integrity + if (StaticStorage::verifyChecksum("/qspi/config.txt", checksum, &error)) { + Serial.println("File integrity verified!"); + } else { + Serial.println("File may be corrupted!"); + } + } + + // Create backup before modifying + if (StaticStorage::createBackup("/qspi/important.dat", ".bak", &error)) { + Serial.println("Backup created successfully"); + + // Modify file... + // If something goes wrong, restore: + StaticStorage::restoreBackup("/qspi/important.dat.bak", nullptr, &error); + } + + // Compare files + if (StaticStorage::compareFiles( + "/qspi/file1.txt", + "/sd/file1.txt", + &error + )) { + Serial.println("Files are identical"); + } else { + Serial.println("Files differ"); + } +} +``` + +--- + +## Complete Example: Storage Manager + +```cpp +#include +#include +#include + +QSPIStorage qspi; + +void setup() { + Serial.begin(115200); + while (!Serial) delay(10); + + StorageError error; + + // Initialize QSPI + if (!qspi.begin(&error)) { + Serial.println("QSPI init failed"); + return; + } + + // Check if formatting needed + if (StaticStorage::needsFormatting("/qspi", &error)) { + Serial.println("Formatting QSPI with LittleFS..."); + StaticStorage::format("/qspi", FilesystemType::LITTLEFS, &error); + } + + // Create partitions + PartitionInfo partitions[] = { + {"system", 0x000000, 1024 * 1024, FilesystemType::LITTLEFS}, + {"data", 0x100000, 6 * 1024 * 1024, FilesystemType::LITTLEFS} + }; + + StaticStorage::createPartitions("/qspi", partitions, 2, &error); + + // Get storage info + StorageInfo info; + if (StaticStorage::getStorageInfo("/qspi", info, &error)) { + Serial.print("Storage: "); + Serial.print(info.usedBytes / 1024); + Serial.print(" / "); + Serial.print(info.totalBytes / 1024); + Serial.println(" KB used"); + } + + // Check health + StorageHealth health; + if (StaticStorage::checkHealth("/qspi", health, &error)) { + Serial.print("Health: "); + Serial.println(health.statusMessage); + + if (health.fragmentationPercent > 30.0) { + Serial.println("Optimizing storage..."); + StaticStorage::optimize("/qspi", &error); + } + } + + Serial.println("Storage manager ready!"); +} + +void loop() { + // Monitor storage health periodically + static unsigned long lastCheck = 0; + if (millis() - lastCheck > 60000) { // Check every minute + lastCheck = millis(); + + StorageHealth health; + StorageError error; + + if (StaticStorage::checkHealth("/qspi", health, &error)) { + if (!health.healthy) { + Serial.print("WARNING: Storage health issue: "); + Serial.println(health.statusMessage); + } + } + } +} +``` \ No newline at end of file diff --git a/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf b/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf index a5cc5a0a..27998ea1 100644 --- a/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf +++ b/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf @@ -110,3 +110,33 @@ CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 CONFIG_BT_RX_STACK_SIZE=4096 CONFIG_STM32H7_BOOT_M4_AT_INIT=n + +# QSPI Flash Support +CONFIG_FLASH_STM32_QSPI=y +CONFIG_FLASH_MAP=y +CONFIG_FLASH_PAGE_LAYOUT=y + +# Filesystem Support +CONFIG_FILE_SYSTEM=y +CONFIG_FILE_SYSTEM_MKFS=y +CONFIG_FILE_SYSTEM_MAX_FILE_NAME=128 +CONFIG_DISK_ACCESS=y +CONFIG_DISK_DRIVER_FLASH=y + +# LittleFS Configuration +CONFIG_FILE_SYSTEM_LITTLEFS=y +CONFIG_FS_LITTLEFS_PROG_SIZE=4096 +CONFIG_FS_LITTLEFS_CACHE_SIZE=4096 + +# FAT Filesystem Configuration +CONFIG_FAT_FILESYSTEM_ELM=y +CONFIG_FS_FATFS_EXFAT=n +CONFIG_FS_FATFS_MKFS=y +CONFIG_FS_FATFS_LFN=y +CONFIG_FS_FATFS_LFN_MODE_HEAP=y +CONFIG_FS_FATFS_CODEPAGE=437 +CONFIG_FS_FATFS_MIN_SS=4096 +CONFIG_FS_FATFS_MAX_SS=4096 +CONFIG_FS_FATFS_MAX_LFN=255 +CONFIG_FS_FATFS_FSTAB_AUTOMOUNT=y +CONFIG_FS_FATFS_CUSTOM_MOUNT_POINTS="wlan,ota" \ No newline at end of file diff --git a/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.overlay b/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.overlay index f3f377b3..aeda0f2a 100644 --- a/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.overlay +++ b/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.overlay @@ -14,6 +14,7 @@ status = "okay"; }; + &i2c3 { status = "okay"; @@ -389,3 +390,12 @@ qspi_flash: &mx25l12833f {}; <&adc1 13>; /* Hack for D20 */ }; }; + + +/* QSPI flash (MX25L12833F) is already configured in arduino_portenta_h7-common.dtsi + * with the correct pins: IO2=PF7, IO3=PD13 (different from Giga R1!) + */ +qspi_flash: &mx25l12833f {}; + +/* Include common flash filesystem configuration */ +#include "../common/arduino_flash_fs.dtsi"