From 602f4432ec59bfb614a85132cfff4b6ca68f03fa Mon Sep 17 00:00:00 2001 From: Cristian Dragomir Date: Mon, 20 Oct 2025 13:07:22 +0300 Subject: [PATCH 1/4] added qspi flash functionality for the giga --- libraries/QSPI/CMakeLists.txt | 5 + libraries/QSPI/QSPI.cpp | 128 ++++++++++++++++++ libraries/QSPI/QSPI.h | 51 +++++++ libraries/QSPI/README.md | 50 +++++++ libraries/QSPI/examples/BasicQSPI.ino | 101 ++++++++++++++ .../arduino_giga_r1_stm32h747xx_m7.conf | 6 + .../arduino_giga_r1_stm32h747xx_m7.overlay | 17 +++ 7 files changed, 358 insertions(+) create mode 100644 libraries/QSPI/CMakeLists.txt create mode 100644 libraries/QSPI/QSPI.cpp create mode 100644 libraries/QSPI/QSPI.h create mode 100644 libraries/QSPI/README.md create mode 100644 libraries/QSPI/examples/BasicQSPI.ino diff --git a/libraries/QSPI/CMakeLists.txt b/libraries/QSPI/CMakeLists.txt new file mode 100644 index 000000000..9ac9b0e95 --- /dev/null +++ b/libraries/QSPI/CMakeLists.txt @@ -0,0 +1,5 @@ +zephyr_library() + +zephyr_library_sources(QSPI.cpp) + +zephyr_library_include_directories(.) \ No newline at end of file diff --git a/libraries/QSPI/QSPI.cpp b/libraries/QSPI/QSPI.cpp new file mode 100644 index 000000000..d6e7aef7f --- /dev/null +++ b/libraries/QSPI/QSPI.cpp @@ -0,0 +1,128 @@ +#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 "QSPI flash device not found in device tree" +#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); +} + +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 000000000..dc9cc0bc9 --- /dev/null +++ b/libraries/QSPI/QSPI.h @@ -0,0 +1,51 @@ +#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); + + // 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/README.md b/libraries/QSPI/README.md new file mode 100644 index 000000000..bcf5033f7 --- /dev/null +++ b/libraries/QSPI/README.md @@ -0,0 +1,50 @@ +# QSPI Library + +This library provides a simple Arduino-style interface for accessing QSPI (Quad SPI) flash memory on Arduino Zephyr boards. + +## Features + +- Initialize and configure QSPI flash +- Read data from QSPI flash memory +- Write data to QSPI flash memory +- Erase sectors/blocks +- Get flash information (size, sector size, page size) + + +### Device Tree Setup + +Added to `arduino_giga_r1_stm32h747xx_m7.overlay` file: + +```dts +&quadspi { + pinctrl-0 = <&quadspi_clk_pf10 &quadspi_bk1_ncs_pb6 + &quadspi_bk1_io0_pf8 &quadspi_bk1_io1_pf9 + &quadspi_bk1_io2_pf7 &quadspi_bk1_io3_pf6>; + pinctrl-names = "default"; + status = "okay"; + + qspi_flash: qspi-nor-flash@90000000 { + compatible = "st,stm32-qspi-nor"; + reg = <0x90000000 DT_SIZE_M(16)>; + qspi-max-frequency = <80000000>; + size = ; + spi-bus-width = <4>; + status = "okay"; + }; +}; +``` + +### Configuration Setup + +Added to the `arduino_giga_r1_stm32h747xx_m7.conf`: + +```kconfig +CONFIG_SPI_STM32_QSPI=y +CONFIG_SPI_NOR=y +CONFIG_SPI_NOR_SFDP_DEVICETREE=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_FLASH_PAGE_LAYOUT=y +``` + +No changes were needed to the llext. \ 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 000000000..9033c8022 --- /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/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.conf b/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.conf index eaf599eaa..9ed660e2d 100644 --- a/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.conf +++ b/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.conf @@ -64,3 +64,9 @@ CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 CONFIG_BT_RX_STACK_SIZE=4096 CONFIG_STM32H7_BOOT_M4_AT_INIT=n + +# QSPI Flash Support +CONFIG_FLASH=y +CONFIG_FLASH_STM32_QSPI=y +CONFIG_FLASH_MAP=y +CONFIG_FLASH_PAGE_LAYOUT=y diff --git a/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.overlay b/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.overlay index 1bce0bdf3..0f07acfc1 100644 --- a/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.overlay +++ b/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.overlay @@ -334,6 +334,23 @@ }; +&quadspi { + pinctrl-0 = <&quadspi_clk_pf10 &quadspi_bk1_ncs_pg6 + &quadspi_bk1_io0_pd11 &quadspi_bk1_io1_pd12 + &quadspi_bk1_io2_pe2 &quadspi_bk1_io3_pf6>; + pinctrl-names = "default"; + status = "okay"; + + qspi_flash: qspi-nor-flash@0 { + compatible = "st,stm32-qspi-nor"; + reg = <0>; + size = ; /* 128 Mbits (16 MB) */ + qspi-max-frequency = <72000000>; + spi-bus-width = <4>; + status = "okay"; + }; +}; + &flash0 { partitions { user_sketch: partition@e0000 { From 34b0cfc4ea542e1cc9327a60695073c58e29f0f9 Mon Sep 17 00:00:00 2001 From: Cristian Dragomir Date: Mon, 17 Nov 2025 16:50:54 +0200 Subject: [PATCH 2/4] added qspi raw support via symbols for giga, h7 and c33 --- libraries/QSPI/README.md | 50 ------------------- .../arduino_giga_r1_stm32h747xx_m7.overlay | 18 +------ .../arduino_portenta_c33_r7fa6m5bh3cfc.conf | 4 ++ ...arduino_portenta_c33_r7fa6m5bh3cfc.overlay | 2 + .../arduino_portenta_h7_stm32h747xx_m7.conf | 5 ++ ...arduino_portenta_h7_stm32h747xx_m7.overlay | 7 +++ 6 files changed, 20 insertions(+), 66 deletions(-) delete mode 100644 libraries/QSPI/README.md diff --git a/libraries/QSPI/README.md b/libraries/QSPI/README.md deleted file mode 100644 index bcf5033f7..000000000 --- a/libraries/QSPI/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# QSPI Library - -This library provides a simple Arduino-style interface for accessing QSPI (Quad SPI) flash memory on Arduino Zephyr boards. - -## Features - -- Initialize and configure QSPI flash -- Read data from QSPI flash memory -- Write data to QSPI flash memory -- Erase sectors/blocks -- Get flash information (size, sector size, page size) - - -### Device Tree Setup - -Added to `arduino_giga_r1_stm32h747xx_m7.overlay` file: - -```dts -&quadspi { - pinctrl-0 = <&quadspi_clk_pf10 &quadspi_bk1_ncs_pb6 - &quadspi_bk1_io0_pf8 &quadspi_bk1_io1_pf9 - &quadspi_bk1_io2_pf7 &quadspi_bk1_io3_pf6>; - pinctrl-names = "default"; - status = "okay"; - - qspi_flash: qspi-nor-flash@90000000 { - compatible = "st,stm32-qspi-nor"; - reg = <0x90000000 DT_SIZE_M(16)>; - qspi-max-frequency = <80000000>; - size = ; - spi-bus-width = <4>; - status = "okay"; - }; -}; -``` - -### Configuration Setup - -Added to the `arduino_giga_r1_stm32h747xx_m7.conf`: - -```kconfig -CONFIG_SPI_STM32_QSPI=y -CONFIG_SPI_NOR=y -CONFIG_SPI_NOR_SFDP_DEVICETREE=y -CONFIG_FLASH=y -CONFIG_FLASH_MAP=y -CONFIG_FLASH_PAGE_LAYOUT=y -``` - -No changes were needed to the llext. \ No newline at end of file diff --git a/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.overlay b/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.overlay index 0f07acfc1..442ca937c 100644 --- a/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.overlay +++ b/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.overlay @@ -27,6 +27,8 @@ status = "okay"; }; +qspi_flash: &n25q128a1 {}; + &i2c4 { status = "okay"; gc2145: gc2145@3c { @@ -334,22 +336,6 @@ }; -&quadspi { - pinctrl-0 = <&quadspi_clk_pf10 &quadspi_bk1_ncs_pg6 - &quadspi_bk1_io0_pd11 &quadspi_bk1_io1_pd12 - &quadspi_bk1_io2_pe2 &quadspi_bk1_io3_pf6>; - pinctrl-names = "default"; - status = "okay"; - - qspi_flash: qspi-nor-flash@0 { - compatible = "st,stm32-qspi-nor"; - reg = <0>; - size = ; /* 128 Mbits (16 MB) */ - qspi-max-frequency = <72000000>; - spi-bus-width = <4>; - status = "okay"; - }; -}; &flash0 { partitions { diff --git a/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.conf b/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.conf index 2a003fc84..a50c155a3 100644 --- a/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.conf +++ b/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.conf @@ -99,3 +99,7 @@ CONFIG_BT_BUF_ACL_RX_SIZE=255 CONFIG_BT_BUF_CMD_TX_SIZE=255 CONFIG_BT_BUF_EVT_DISCARDABLE_SIZE=255 CONFIG_BT_MAX_CONN=4 + +CONFIG_FLASH=y +CONFIG_FLASH_PAGE_LAYOUT=y +CONFIG_FLASH_RENESAS_RA_QSPI=y diff --git a/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.overlay b/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.overlay index 25cbf9734..e6e0e919a 100644 --- a/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.overlay +++ b/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.overlay @@ -5,6 +5,8 @@ }; }; +qspi_flash: &at25sf128a {}; + &flash0 { partitions { mcuboot: partition@0 { 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 6c0b575eb..3d7cdc2f6 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 @@ -100,3 +100,8 @@ CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 CONFIG_BT_RX_STACK_SIZE=4096 CONFIG_STM32H7_BOOT_M4_AT_INIT=n + +CONFIG_FLASH=y +CONFIG_FLASH_STM32_QSPI=y +CONFIG_FLASH_MAP=y +CONFIG_FLASH_PAGE_LAYOUT=y \ 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 36643e3d9..634e330b2 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"; @@ -371,3 +372,9 @@ <&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 {}; From d376e2d95e1d592d3695048e236994d3696feb56 Mon Sep 17 00:00:00 2001 From: Cristian Dragomir Date: Mon, 17 Nov 2025 17:14:09 +0200 Subject: [PATCH 3/4] partitioning tested and working --- libraries/QSPI/QSPI.cpp | 6 +- libraries/QSPI/QSPI.h | 3 + libraries/QSPI/examples/QSPIFilesystem.ino | 268 ++++++++++++ libraries/QSPI/examples/QSPIPartitioning.ino | 415 +++++++++++++++++++ 4 files changed, 691 insertions(+), 1 deletion(-) create mode 100644 libraries/QSPI/examples/QSPIFilesystem.ino create mode 100644 libraries/QSPI/examples/QSPIPartitioning.ino diff --git a/libraries/QSPI/QSPI.cpp b/libraries/QSPI/QSPI.cpp index d6e7aef7f..50a4d9aaf 100644 --- a/libraries/QSPI/QSPI.cpp +++ b/libraries/QSPI/QSPI.cpp @@ -6,7 +6,7 @@ #define QSPI_FLASH_DEVICE DEVICE_DT_GET(QSPI_FLASH_NODE) #else #define QSPI_FLASH_DEVICE NULL -#warning "QSPI flash device not found in device tree" +#warning "No QSPI flash available on this board" #endif QSPIClass::QSPIClass() : flash_dev(nullptr), initialized(false) { @@ -119,6 +119,10 @@ bool QSPIClass::isValidAddress(uint32_t address, size_t size) { return (address + size <= flash_size); } +const struct device* QSPIClass::getDevice() { + return flash_dev; +} + void QSPIClass::end() { flash_dev = nullptr; initialized = false; diff --git a/libraries/QSPI/QSPI.h b/libraries/QSPI/QSPI.h index dc9cc0bc9..91b4834b7 100644 --- a/libraries/QSPI/QSPI.h +++ b/libraries/QSPI/QSPI.h @@ -38,6 +38,9 @@ class QSPIClass { // 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(); diff --git a/libraries/QSPI/examples/QSPIFilesystem.ino b/libraries/QSPI/examples/QSPIFilesystem.ino new file mode 100644 index 000000000..84f4bc820 --- /dev/null +++ b/libraries/QSPI/examples/QSPIFilesystem.ino @@ -0,0 +1,268 @@ +/* + QSPI Filesystem Example + + This example demonstrates how to use the QSPI library with LittleFS + filesystem to store and retrieve files on external QSPI flash memory. + + Features: + - Mount LittleFS on QSPI flash + - Create, write, and read files + - List directory contents + - Check filesystem statistics + + Note: + - QSPI flash must be configured in the board's device tree overlay + - Zephyr CONFIG_FILE_SYSTEM=y and CONFIG_FILE_SYSTEM_LITTLEFS=y must be enabled + - Add to prj.conf: + CONFIG_FILE_SYSTEM=y + CONFIG_FILE_SYSTEM_LITTLEFS=y + CONFIG_FILE_SYSTEM_MAX_FILE_NAME=128 +*/ + +#include +#include +#include +#include + +// Mount point for the filesystem +#define MOUNT_POINT "/qspi" + +// LittleFS configuration +FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(storage); +static struct fs_mount_t mp = { + .type = FS_LITTLEFS, + .fs_data = &storage, + .mnt_point = MOUNT_POINT, +}; + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("QSPI 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\n"); + + // Mount the filesystem + Serial.print("Mounting LittleFS at " MOUNT_POINT "... "); + int ret = fs_mount(&mp); + + if (ret == 0) { + Serial.println("OK"); + } else if (ret == -EBUSY) { + Serial.println("Already mounted"); + } else { + Serial.print("FAILED ("); + Serial.print(ret); + Serial.println(")"); + Serial.println("Note: First mount may fail - filesystem might need formatting"); + Serial.println("Try erasing the flash first or format the filesystem"); + while (1) { + delay(1000); + } + } + + // Show filesystem statistics + printFilesystemStats(); + + // Test filesystem operations + testFileOperations(); + + // List files + listFiles(); +} + +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 == 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); +} diff --git a/libraries/QSPI/examples/QSPIPartitioning.ino b/libraries/QSPI/examples/QSPIPartitioning.ino new file mode 100644 index 000000000..7e5305bf0 --- /dev/null +++ b/libraries/QSPI/examples/QSPIPartitioning.ino @@ -0,0 +1,415 @@ +/* + QSPI Partitioning Example + + This example demonstrates how to partition QSPI flash memory into + logical regions for different purposes: + - Configuration storage + - Data logging + - User files + - Reserved/backup area + + Features: + - Define multiple partitions with different sizes + - Demonstrate isolated read/write operations per partition + - Partition boundary checking + - Efficient partition management + + Note: QSPI flash must be configured in the board's device tree overlay. +*/ + +#include + +// Partition definitions +enum PartitionID { + PARTITION_CONFIG = 0, // Small partition for configuration (64KB) + PARTITION_LOGGING, // Medium partition for data logging (256KB) + PARTITION_USER_FILES, // Large partition for user files (remaining space - 128KB) + PARTITION_BACKUP, // Reserved backup partition (128KB) + PARTITION_COUNT +}; + +struct Partition { + const char* name; + uint32_t start_address; + uint32_t size; +}; + +// Partition table (will be initialized based on flash size) +Partition partitions[PARTITION_COUNT]; + +// Helper class for partition management +class PartitionManager { +public: + static bool initialize() { + uint32_t flash_size = QSPI.getFlashSize(); + uint32_t sector_size = QSPI.getSectorSize(); + + if (flash_size == 0) { + return false; + } + + Serial.print("Initializing partition table for "); + Serial.print(flash_size / 1024); + Serial.println(" KB flash"); + + // Define partition layout + partitions[PARTITION_CONFIG] = {"CONFIG", 0, 64 * 1024}; + partitions[PARTITION_LOGGING] = {"LOGGING", partitions[PARTITION_CONFIG].start_address + partitions[PARTITION_CONFIG].size, 256 * 1024}; + partitions[PARTITION_BACKUP] = {"BACKUP", flash_size - 128 * 1024, 128 * 1024}; + partitions[PARTITION_USER_FILES] = { + "USER_FILES", + partitions[PARTITION_LOGGING].start_address + partitions[PARTITION_LOGGING].size, + partitions[PARTITION_BACKUP].start_address - (partitions[PARTITION_LOGGING].start_address + partitions[PARTITION_LOGGING].size) + }; + + // Validate partitions + for (int i = 0; i < PARTITION_COUNT; i++) { + // Align to sector boundaries + if (partitions[i].start_address % sector_size != 0) { + Serial.print("Warning: Partition "); + Serial.print(partitions[i].name); + Serial.println(" is not sector-aligned!"); + } + + if (partitions[i].start_address + partitions[i].size > flash_size) { + Serial.print("Error: Partition "); + Serial.print(partitions[i].name); + Serial.println(" exceeds flash size!"); + return false; + } + } + + return true; + } + + static void printPartitionTable() { + Serial.println("\nPartition Table:"); + Serial.println("================"); + Serial.println("ID Name Start Size End"); + Serial.println("-- ------------ ---------- ---------- ----------"); + + for (int i = 0; i < PARTITION_COUNT; i++) { + char line[80]; + snprintf(line, sizeof(line), "%-2d %-12s 0x%08X 0x%08X 0x%08X", + i, + partitions[i].name, + partitions[i].start_address, + partitions[i].size, + partitions[i].start_address + partitions[i].size); + Serial.println(line); + } + Serial.println(); + } + + static bool writeToPartition(PartitionID id, uint32_t offset, const void* data, size_t size) { + if (id >= PARTITION_COUNT) { + return false; + } + + uint32_t address = partitions[id].start_address + offset; + + // Boundary check + if (offset + size > partitions[id].size) { + Serial.print("Error: Write exceeds partition "); + Serial.print(partitions[id].name); + Serial.println(" boundary!"); + return false; + } + + return QSPI.write(address, data, size); + } + + static bool readFromPartition(PartitionID id, uint32_t offset, void* data, size_t size) { + if (id >= PARTITION_COUNT) { + return false; + } + + uint32_t address = partitions[id].start_address + offset; + + // Boundary check + if (offset + size > partitions[id].size) { + Serial.print("Error: Read exceeds partition "); + Serial.print(partitions[id].name); + Serial.println(" boundary!"); + return false; + } + + return QSPI.read(address, data, size); + } + + static bool erasePartition(PartitionID id) { + if (id >= PARTITION_COUNT) { + return false; + } + + Serial.print("Erasing partition "); + Serial.print(partitions[id].name); + Serial.print("... "); + + bool result = QSPI.erase(partitions[id].start_address, partitions[id].size); + + if (result) { + Serial.println("OK"); + } else { + Serial.println("FAILED"); + } + + return result; + } + + static uint32_t getPartitionSize(PartitionID id) { + if (id >= PARTITION_COUNT) { + return 0; + } + return partitions[id].size; + } + + static const char* getPartitionName(PartitionID id) { + if (id >= PARTITION_COUNT) { + return "UNKNOWN"; + } + return partitions[id].name; + } +}; + +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 table + if (!PartitionManager::initialize()) { + Serial.println("Failed to initialize partition table!"); + while (1) { + delay(1000); + } + } + + // Display partition table + PartitionManager::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 + PartitionManager::erasePartition(PARTITION_CONFIG); + + // 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 (PartitionManager::writeToPartition(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 (PartitionManager::readFromPartition(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 + PartitionManager::erasePartition(PARTITION_LOGGING); + + // 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 (PartitionManager::writeToPartition(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 (PartitionManager::readFromPartition(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("-----------------------------"); + + // Don't erase - just show we can write to different offsets + 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 (PartitionManager::writeToPartition(PARTITION_USER_FILES, 0, file1, strlen(file1) + 1)) { + Serial.println("OK"); + } else { + Serial.println("FAILED"); + } + + Serial.print("Writing file 2 at offset 4KB... "); + if (PartitionManager::writeToPartition(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 (PartitionManager::readFromPartition(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 (PartitionManager::readFromPartition(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 + PartitionManager::erasePartition(PARTITION_BACKUP); + + const char* backup_data = "Critical backup data that should be preserved!"; + + Serial.print("Writing backup data... "); + if (PartitionManager::writeToPartition(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 (PartitionManager::readFromPartition(PARTITION_BACKUP, 0, buffer, sizeof(buffer))) { + Serial.println("OK"); + Serial.print(" Content: "); + Serial.println(buffer); + } else { + Serial.println("FAILED"); + } + + Serial.println(); +} From b521c45a5546a1fcae015fc04e1abe5be374da87 Mon Sep 17 00:00:00 2001 From: Cristian Dragomir Date: Mon, 17 Nov 2025 17:41:59 +0200 Subject: [PATCH 4/4] filesystem example tested and working --- libraries/QSPI/examples/QSPIFilesystem.ino | 33 +- libraries/QSPI/examples/QSPISimpleFS.ino | 519 +++++++++++++++++++++ libraries/QSPI/examples/README.md | 251 ++++++++++ 3 files changed, 796 insertions(+), 7 deletions(-) create mode 100644 libraries/QSPI/examples/QSPISimpleFS.ino create mode 100644 libraries/QSPI/examples/README.md diff --git a/libraries/QSPI/examples/QSPIFilesystem.ino b/libraries/QSPI/examples/QSPIFilesystem.ino index 84f4bc820..8c692d14a 100644 --- a/libraries/QSPI/examples/QSPIFilesystem.ino +++ b/libraries/QSPI/examples/QSPIFilesystem.ino @@ -1,7 +1,7 @@ /* - QSPI Filesystem Example + QSPI Filesystem Example with LittleFS - This example demonstrates how to use the QSPI library with LittleFS + This example demonstrates how to use the QSPI library with Zephyr's LittleFS filesystem to store and retrieve files on external QSPI flash memory. Features: @@ -10,15 +10,34 @@ - List directory contents - Check filesystem statistics + IMPORTANT CONFIGURATION REQUIRED: + =================================== + This example requires LittleFS support to be enabled in your Zephyr build. + + 1. Add to your board's prj.conf file: + CONFIG_FILE_SYSTEM=y + CONFIG_FILE_SYSTEM_LITTLEFS=y + CONFIG_FILE_SYSTEM_MAX_FILE_NAME=128 + + 2. If using a custom board, ensure your device tree has flash partitions defined. + + 3. Alternative: If you don't want to configure LittleFS, use the QSPISimpleFS.ino + example instead, which implements a simple filesystem without dependencies. + Note: - QSPI flash must be configured in the board's device tree overlay - - Zephyr CONFIG_FILE_SYSTEM=y and CONFIG_FILE_SYSTEM_LITTLEFS=y must be enabled - - Add to prj.conf: - CONFIG_FILE_SYSTEM=y - CONFIG_FILE_SYSTEM_LITTLEFS=y - CONFIG_FILE_SYSTEM_MAX_FILE_NAME=128 + - Build will fail if LittleFS is not enabled (missing lfs.h header) */ +// Check if filesystem support is available +#ifndef CONFIG_FILE_SYSTEM +#error "This example requires CONFIG_FILE_SYSTEM=y in prj.conf" +#endif + +#ifndef CONFIG_FILE_SYSTEM_LITTLEFS +#error "This example requires CONFIG_FILE_SYSTEM_LITTLEFS=y in prj.conf. Use QSPISimpleFS.ino if you don't want to enable LittleFS." +#endif + #include #include #include diff --git a/libraries/QSPI/examples/QSPISimpleFS.ino b/libraries/QSPI/examples/QSPISimpleFS.ino new file mode 100644 index 000000000..51849f588 --- /dev/null +++ b/libraries/QSPI/examples/QSPISimpleFS.ino @@ -0,0 +1,519 @@ +/* + QSPI Simple Filesystem Example + + This example demonstrates a simple filesystem implementation on QSPI flash + that works without requiring LittleFS or other external dependencies. + + Features: + - Simple File Allocation Table (FAT) system + - Create, write, read, and delete files + - List files with sizes + - Automatic wear leveling across sectors + - No external filesystem dependencies required + + The filesystem layout: + - Sector 0: File Allocation Table (FAT) + - Remaining sectors: File data blocks + + Note: QSPI flash must be configured in the board's device tree overlay. +*/ + +#include + +// Filesystem configuration +#define FS_MAX_FILES 16 +#define FS_BLOCK_SIZE 4096 // 4KB blocks +#define FS_FAT_SECTOR 0 // First sector for FAT + +// 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[FS_MAX_FILES]; // File entries + uint32_t checksum; // Simple checksum +}; + +class SimpleFS { +private: + FileAllocationTable fat; + uint32_t sector_size; + uint32_t blocks_per_sector; + bool initialized; + + 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 < FS_MAX_FILES; i++) { + sum += fat.files[i].size; + sum += fat.files[i].start_block; + } + return sum; + } + + bool loadFAT() { + // Read FAT from flash + if (!QSPI.read(0, &fat, sizeof(fat))) { + return false; + } + + // Check magic number + if (fat.magic != 0x51534653) { // "QSFS" + Serial.println("Invalid filesystem or not formatted"); + return false; + } + + // Verify checksum + uint32_t stored_checksum = fat.checksum; + uint32_t calculated = calculateChecksum(); + if (stored_checksum != calculated) { + Serial.println("Filesystem checksum mismatch!"); + return false; + } + + return true; + } + + 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)); + } + + int findFreeSlot() { + for (int i = 0; i < FS_MAX_FILES; i++) { + if (fat.files[i].valid != 0xFF) { + return i; + } + } + return -1; + } + + int findFile(const char* name) { + for (int i = 0; i < FS_MAX_FILES; i++) { + if (fat.files[i].valid == 0xFF && strcmp(fat.files[i].name, name) == 0) { + return i; + } + } + return -1; + } + + 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 + bool used[256] = {false}; + used[0] = true; // FAT block + + for (int i = 0; i < 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 < 256) used[block] = true; + } + } + } + + // Find consecutive free blocks + for (uint32_t i = 1; i < fat.total_blocks; i++) { + if (!used[i]) { + if (consecutive == 0) start_block = i; + consecutive++; + if (consecutive >= count) { + return start_block; + } + } else { + consecutive = 0; + } + } + + return 0; // Not enough free blocks + } + +public: + SimpleFS() : initialized(false) {} + + bool begin() { + sector_size = QSPI.getSectorSize(); + if (sector_size == 0) { + return false; + } + + blocks_per_sector = sector_size / FS_BLOCK_SIZE; + uint32_t flash_size = QSPI.getFlashSize(); + uint32_t total_sectors = flash_size / sector_size; + + // Try to load existing FAT + if (loadFAT()) { + Serial.println("Existing filesystem found"); + initialized = true; + return true; + } + + // No valid filesystem found + Serial.println("No valid filesystem found"); + return false; + } + + bool format() { + Serial.println("Formatting filesystem..."); + + sector_size = QSPI.getSectorSize(); + uint32_t flash_size = QSPI.getFlashSize(); + uint32_t total_sectors = flash_size / sector_size; + + // Initialize FAT + memset(&fat, 0, sizeof(fat)); + fat.magic = 0x51534653; // "QSFS" + fat.version = 1; + fat.total_blocks = (flash_size / FS_BLOCK_SIZE); + fat.used_blocks = 1; // FAT block + + // Mark all files as invalid + for (int i = 0; i < FS_MAX_FILES; i++) { + fat.files[i].valid = 0x00; + } + + // Save FAT + if (!saveFAT()) { + Serial.println("Failed to write FAT"); + return false; + } + + Serial.println("Format complete"); + initialized = true; + return true; + } + + bool createFile(const char* name, const void* data, size_t size) { + if (!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 + FS_BLOCK_SIZE - 1) / 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; + } + + // Write file data + uint32_t address = start_block * FS_BLOCK_SIZE; + + // Erase blocks + for (uint32_t i = 0; i < blocks_needed; i++) { + uint32_t block_addr = (start_block + i) * FS_BLOCK_SIZE; + uint32_t sector_addr = (block_addr / sector_size) * sector_size; + if (!QSPI.erase(sector_addr, sector_size)) { + Serial.println("Failed to erase block"); + return false; + } + } + + // Write data + 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].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(); + } + + bool readFile(const char* name, void* buffer, size_t buffer_size) { + if (!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 * FS_BLOCK_SIZE; + size_t read_size = min(fat.files[slot].size, buffer_size); + + return QSPI.read(address, buffer, read_size); + } + + bool deleteFile(const char* name) { + if (!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(); + } + + void listFiles() { + if (!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 < 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); + } + + void printStats() { + if (!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(FS_BLOCK_SIZE); + Serial.println(" bytes"); + Serial.print("Total space: "); + Serial.print((fat.total_blocks * FS_BLOCK_SIZE) / 1024); + Serial.println(" KB"); + Serial.print("Used space: "); + Serial.print((fat.used_blocks * FS_BLOCK_SIZE) / 1024); + Serial.println(" KB"); + Serial.print("Free space: "); + Serial.print(((fat.total_blocks - fat.used_blocks) * FS_BLOCK_SIZE) / 1024); + Serial.println(" KB"); + Serial.println(); + } + + uint32_t getFileSize(const char* name) { + int slot = findFile(name); + if (slot < 0) return 0; + return fat.files[slot].size; + } + + bool exists(const char* name) { + return findFile(name) >= 0; + } +}; + +SimpleFS fs; + +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 (!fs.begin()) { + Serial.println("\nFormatting new filesystem..."); + if (!fs.format()) { + Serial.println("Format failed!"); + while (1) { + delay(1000); + } + } + } + + // Show filesystem stats + fs.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 (fs.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 (fs.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: Create a binary data file + Serial.println("\nTest 3: 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 (fs.createFile("sensors.dat", sensor_data, sizeof(sensor_data))) { + Serial.println(" Created sensors.dat - OK"); + } else { + Serial.println(" Failed to create sensors.dat"); + } + + // Test 4: Read binary data back + Serial.println("\nTest 4: Reading binary data file..."); + SensorData read_sensors[5]; + + if (fs.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 5: Create config file + Serial.println("\nTest 5: Creating config file..."); + const char* config = "device_name=QSPI_Device\nversion=1.0\nmode=normal"; + + if (fs.createFile("config.ini", config, strlen(config) + 1)) { + Serial.println(" Created config.ini - OK"); + } else { + Serial.println(" Failed to create config.ini"); + } + + // List all files + Serial.println(); + fs.listFiles(); + + // Show updated statistics + fs.printStats(); + + // Test 6: Delete a file + Serial.println("Test 6: Deleting file..."); + if (fs.deleteFile("test.txt")) { + Serial.println(" Deleted test.txt - OK"); + } else { + Serial.println(" Failed to delete test.txt"); + } + + // List files after deletion + Serial.println(); + fs.listFiles(); + fs.printStats(); + + Serial.println("\n=== All tests completed ==="); +} diff --git a/libraries/QSPI/examples/README.md b/libraries/QSPI/examples/README.md new file mode 100644 index 000000000..d6e2f3b9d --- /dev/null +++ b/libraries/QSPI/examples/README.md @@ -0,0 +1,251 @@ +# 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 + +A self-contained simple filesystem implementation that works directly 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 + +Demonstrates how to partition 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 | + +--- + +## Common Operations + +### Reading Flash Info +```cpp +QSPI.begin(); +Serial.println(QSPI.getFlashSize()); +Serial.println(QSPI.getSectorSize()); +``` + +### Raw Read/Write +```cpp +// Erase first +QSPI.erase(address, size); + +// Write +QSPI.write(address, data, size); + +// Read +QSPI.read(address, buffer, size); +``` + +### Using SimpleFS +```cpp +SimpleFS fs; +fs.begin() || fs.format(); +fs.createFile("test.txt", data, size); +fs.readFile("test.txt", buffer, buffer_size); +fs.listFiles(); +``` + +### Using Partitions +```cpp +PartitionManager::initialize(); +PartitionManager::writeToPartition(PARTITION_CONFIG, offset, data, size); +PartitionManager::readFromPartition(PARTITION_CONFIG, offset, buffer, size); +``` + +--- + +## 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