diff --git a/.gitattributes b/.gitattributes index 094d6379f..6e9943930 100644 --- a/.gitattributes +++ b/.gitattributes @@ -17,3 +17,6 @@ tools/hil/sf5_250*.json -text whitespace=cr-at-eol # Preserve the pinned upstream FS/newlib fixtures byte-for-byte. test/fixtures/esp32_file_buffer/* -text whitespace=-blank-at-eol,-blank-at-eof + +# Preserve the pinned upstream QSPI fixtures and their source fingerprints. +test/fixtures/customlfs_qspi_dma/* -text whitespace=-blank-at-eol,-blank-at-eof diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index e1f7443b9..a69fe8ac8 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -468,6 +468,7 @@ jobs: python3 -B test/test_companion_channel_storage.py -v python3 -B test/test_nrf52_extrafs_contract.py -v python3 -B test/test_xiao_qspi_recovery.py -v + python3 -B test/test_nrf52_qspi_dma_fix.py -v python3 -B test/test_xiao_identity_recovery.py -v python3 -B test/test_companion_ota_config.py -v python3 -B test/test_ota_identity_policy.py -v diff --git a/platformio.ini b/platformio.ini index f1f0b70db..48ff7a46e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -170,6 +170,7 @@ extra_scripts = create-uf2.py pre:scripts/ota_device_deflate_role.py pre:scripts/nrf52_usb_power_fix.py pre:scripts/nrf52_internal_flash_fix.py + pre:scripts/nrf52_qspi_dma_fix.py pre:scripts/nrf52_ble_cccd_fix.py pre:scripts/nrf52_ble_dfu_fix.py pre:scripts/nrf52_loop_stack_fix.py diff --git a/scripts/nrf52_qspi_dma_fix.py b/scripts/nrf52_qspi_dma_fix.py new file mode 100644 index 000000000..8233f9687 --- /dev/null +++ b/scripts/nrf52_qspi_dma_fix.py @@ -0,0 +1,77 @@ +"""Align reviewed CustomLFS QSPI buffers in private build copies. + +The nRF52840 QSPI driver rejects non-word-aligned EasyDMA addresses. uint8_t +static arrays only guarantee byte alignment; unrelated code/layout changes +can silently break mounting. Retain all three original pools and their size, +allocation lifetime, callbacks and filesystem behavior. Never edit libdeps. +""" +import hashlib +from pathlib import Path + +SOURCE_SHA256 = 'b36b21e52480d1c4e56a235d0d9e3d72c4f2141a9d6c46e1781d64dc08180540' +PATCHED_SHA256 = '1d68cf61699e3988c59d9c7e87efb5479caf5391a847bab6c0e502b5593e35dd' +HEADERS = { + 'CustomLFS_QSPIFlash.h': '941596581f9f745b546e60e77e5738548b891d38f1a2830160a50646d6cf7486', + 'CustomLFS.h': '65a2efcf699d6069dd68fd4f19b856a9e0573bda61fa3fc20568951b8d31f52d', +} +ORIGINAL_BUFFERS = ''' static uint8_t read_buffer[256]; + static uint8_t prog_buffer[256]; + static uint8_t lookahead_buffer[32];''' +PATCHED_BUFFERS = ''' // QSPI EasyDMA and the LittleFS word bitmap require word alignment. + alignas(uint32_t) static uint8_t read_buffer[256]; + alignas(uint32_t) static uint8_t prog_buffer[256]; + alignas(uint32_t) static uint8_t lookahead_buffer[32];''' + + +def digest(source): + return hashlib.sha256(source.encode('ascii')).hexdigest() + + +def patched_source(source): + source = source.replace('\r\n', '\n') + fingerprint = digest(source) + if fingerprint == PATCHED_SHA256: + return source + if fingerprint != SOURCE_SHA256 or source.count(ORIGINAL_BUFFERS) != 1: + raise RuntimeError('nRF52 QSPI DMA fix: changed reviewed CustomLFS source; review dependency update') + result = source.replace(ORIGINAL_BUFFERS, PATCHED_BUFFERS, 1) + if digest(result) != PATCHED_SHA256: + raise RuntimeError('nRF52 QSPI DMA fix: unexpected transform result') + return result + + +def write_if_changed(path, content): + if not path.exists() or path.read_text(encoding='ascii') != content: + path.write_text(content, encoding='ascii') + + +def replace_qspi_source(build_env, node): + source = Path(node.srcnode().get_abspath()) + if source.name != 'CustomLFS_QSPIFlash.cpp' or source.parent.name != 'src': + return node + patched = patched_source(source.read_text(encoding='ascii')) + headers = {} + for name, expected in HEADERS.items(): + try: + header = (source.parent / name).read_text(encoding='ascii').replace('\r\n', '\n') + except (OSError, UnicodeError) as error: + raise RuntimeError('nRF52 QSPI DMA fix: missing reviewed header') from error + if digest(header) != expected: + raise RuntimeError('nRF52 QSPI DMA fix: changed reviewed header; review dependency update') + headers[name] = header + destination = Path(build_env.subst('$BUILD_DIR')) / 'patched-nrf52-qspi' / source.name + destination.parent.mkdir(parents=True, exist_ok=True) + # Quoted includes bind both headers to this reviewed dependency version. + for name, header in headers.items(): + write_if_changed(destination.parent / name, header) + write_if_changed(destination, patched) + return build_env.File(str(destination)) + + +def install(build_env): + build_env.AddBuildMiddleware(replace_qspi_source, '*CustomLFS_QSPIFlash.cpp') + + +if 'Import' in globals(): + Import('env') + install(env) diff --git a/test/fixtures/customlfs_qspi_dma/CustomLFS.h b/test/fixtures/customlfs_qspi_dma/CustomLFS.h new file mode 100644 index 000000000..a8b7d3e45 --- /dev/null +++ b/test/fixtures/customlfs_qspi_dma/CustomLFS.h @@ -0,0 +1,108 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 hathach for Adafruit Industries + * Copyright (c) 2025 oltaco + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// CustomLFS.h - Custom LittleFS wrapper to support arbitrary flash regions +#ifndef CUSTOMLFS_H_ +#define CUSTOMLFS_H_ + +#include "Adafruit_LittleFS.h" +#include "flash/flash_nrf5x.h" + +using namespace Adafruit_LittleFS_Namespace; + +// Default flash configuration (maintains backward compatibility with UserData region) +#ifdef NRF52840_XXAA + #define LFS_DEFAULT_FLASH_ADDR 0xED000 +#else + #define LFS_DEFAULT_FLASH_ADDR 0x6D000 +#endif + +#define LFS_DEFAULT_FLASH_TOTAL_SIZE (7*FLASH_NRF52_PAGE_SIZE) +#define LFS_DEFAULT_BLOCK_SIZE 128 + +class CustomLFS : public Adafruit_LittleFS +{ +protected: + // Flash region configuration + uint32_t _flash_addr; + uint32_t _flash_total_size; + uint32_t _block_size; + + // LFS configuration structure (one per instance) + struct lfs_config _lfs_config; + + // Helper function to convert block to address + uint32_t lba2addr(uint32_t block) const { + return _flash_addr + block * _block_size; + } + + // Configure the LFS config structure + virtual void _configure_lfs(); + +private: + // Static callback functions for LittleFS operations + static int _flash_read(const struct lfs_config *c, lfs_block_t block, + lfs_off_t off, void *buffer, lfs_size_t size); + static int _flash_prog(const struct lfs_config *c, lfs_block_t block, + lfs_off_t off, const void *buffer, lfs_size_t size); + static int _flash_erase(const struct lfs_config *c, lfs_block_t block); + static int _flash_sync(const struct lfs_config *c); + + bool _begun = false; + +public: + // Default constructor (backward compatibility) + CustomLFS(void); + + // Constructor with custom flash region + CustomLFS(uint32_t flash_addr, uint32_t flash_size, + uint32_t block_size = LFS_DEFAULT_BLOCK_SIZE); + + // Protected constructor for derived classes (doesn't auto-configure) + CustomLFS(bool auto_configure); + + // Set flash region (must be called before begin()) + bool setFlashRegion(uint32_t flash_addr, uint32_t flash_size, + uint32_t block_size = LFS_DEFAULT_BLOCK_SIZE); + + // Get current configuration + uint32_t getFlashAddr(void) const { return _flash_addr; } + uint32_t getFlashSize(void) const { return _flash_total_size; } + uint32_t getBlockSize(void) const { return _block_size; } + + // Begin to handle custom flash regions + virtual bool begin(void); + + // Format the specific flash region + bool formatRegion(void); + + // Validation helper + static bool validateFlashRegion(uint32_t flash_addr, uint32_t flash_size, uint32_t block_size); +}; + +// Global instance for backward compatibility +extern CustomLFS CustomFS; + +#endif /* CUSTOMLFS_H_ */ \ No newline at end of file diff --git a/test/fixtures/customlfs_qspi_dma/CustomLFS_QSPIFlash.cpp b/test/fixtures/customlfs_qspi_dma/CustomLFS_QSPIFlash.cpp new file mode 100644 index 000000000..bb3b8c5d7 --- /dev/null +++ b/test/fixtures/customlfs_qspi_dma/CustomLFS_QSPIFlash.cpp @@ -0,0 +1,917 @@ +/* + * CustomLFS_QSPIFlash.cpp - QSPI Flash support for CustomLFS + * +* Copyright (c) 2025, 2026 oltaco + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifdef NRF52840_XXAA + +#include "CustomLFS_QSPIFlash.h" +#include "Adafruit_TinyUSB.h" +#include "Arduino.h" + +// Check if QSPI is enabled +#if !defined(NRFX_QSPI_ENABLED) || (NRFX_QSPI_ENABLED != 1) +#error "QSPI must be enabled. Add -DNRFX_QSPI_ENABLED=1 to build flags" +#endif + +// Simple QSPI Flash chip database - minimal for compatibility +const QSPIFlashChip qspiFlashChips[] = { + // Winbond W25Q16JV - Very common + { + .jedec_id = {0xEF, 0x40, 0x15}, + .total_size = 2097152, // 2MB + .sector_size = 4096, + .page_size = 256, + .address_bits = 24, + .read_opcode = 0xEB, + .program_opcode = 0x32, + .erase_opcode = 0x20, + .status_opcode = 0x05, + .supports_quad_read = true, + .supports_quad_write = true, + .quad_enable_register = 2, + .quad_enable_bit = 1, + .quad_enable_volatile = false, + .max_clock_hz = 80000000, + .write_timeout_ms = 5, + .erase_timeout_ms = 400, + .startup_delay_us = 10000, + .name = "W25Q16JV" + }, + + // PUYA P25Q16H - Nordic boards + { + .jedec_id = {0x85, 0x60, 0x15}, + .total_size = 2097152, + .sector_size = 4096, + .page_size = 256, + .address_bits = 24, + .read_opcode = 0xEB, + .program_opcode = 0x32, + .erase_opcode = 0x20, + .status_opcode = 0x05, + .supports_quad_read = true, + .supports_quad_write = true, + .quad_enable_register = 2, + .quad_enable_bit = 1, + .quad_enable_volatile = false, + .max_clock_hz = 55000000, + .write_timeout_ms = 5, + .erase_timeout_ms = 300, + .startup_delay_us = 10000, + .name = "P25Q16H" + }, + // Macronix MX25R1635F - Thinknode M1 / Lilygo T-Echo + { + .jedec_id = {0xC2, 0x28, 0x15}, // Manufacturer ID (Macronix), Memory Type, Capacity + .total_size = 2097152, // 2MB + .sector_size = 4096, // 4KB sectors + .page_size = 256, // 256-byte pages + .address_bits = 24, // 3-byte addressing + .read_opcode = 0xEB, // Fast Read Quad I/O (4-4-4) + .program_opcode = 0x38, // Quad Page Program (1-1-4) + .erase_opcode = 0x20, // Sector Erase (4KB) + .status_opcode = 0x05, // Read Status Register + .supports_quad_read = true, + .supports_quad_write = true, + .quad_enable_register = 1, // QE bit is in Status Register-2 (C2h = Volatile SR, but we use SR2) + .quad_enable_bit = 6, // QE bit is bit 6 in Status Register 2 + .quad_enable_volatile = false, // QE is non-volatile + .max_clock_hz = 80000000, // Max QSPI clock rate + .write_timeout_ms = 5, + .erase_timeout_ms = 400, + .startup_delay_us = 10000, + .name = "MX25R1635F" +}, +// Zetta ZD25WQ16BUIGR - Ultra Low Power 16Mb Flash +{ + .jedec_id = {0xBA, 0x60, 0x15}, + .total_size = 2097152, // 2MB (16Mbit) + .sector_size = 4096, + .page_size = 256, + .address_bits = 24, + .read_opcode = 0xEB, // 4x I/O Read (QREAD) + .program_opcode = 0x32, // Quad Page Program (QPP) + .erase_opcode = 0x20, // Sector Erase (SE) + .status_opcode = 0x05, // Read Status Register (RDSR) + .supports_quad_read = true, + .supports_quad_write = true, + .quad_enable_register = 2, // Status Register S9 (QE bit) + .quad_enable_bit = 1, // QE is bit 1 in register 2 + .quad_enable_volatile = false, + .max_clock_hz = 104000000, // 104MHz max frequency + .write_timeout_ms = 3, // Page program time max + .erase_timeout_ms = 12, // Sector erase time max + .startup_delay_us = 10000, // Conservative startup delay + .name = "ZD25WQ16BUIGR" +}, +// Zetta ZD25WQ32CEIGR - Ultra Low Power 32Mb Flash +{ + .jedec_id = {0xBA, 0x60, 0x16}, + .total_size = 4194304, // 4MB (32Mbit) + .sector_size = 4096, + .page_size = 256, + .address_bits = 24, + .read_opcode = 0xEB, // 4x I/O Read (QREAD) + .program_opcode = 0x32, // Quad Page Program (QPP) + .erase_opcode = 0x20, // Sector Erase (SE) + .status_opcode = 0x05, // Read Status Register (RDSR) + .supports_quad_read = true, + .supports_quad_write = true, + .quad_enable_register = 2, // Status Register S9 (QE bit) + .quad_enable_bit = 1, // QE is bit 1 in register 2 + .quad_enable_volatile = false, + .max_clock_hz = 104000000, // 104MHz max frequency + .write_timeout_ms = 3, // Page program time max + .erase_timeout_ms = 20, // Sector erase time max + .startup_delay_us = 10000, // Conservative startup delay + .name = "ZD25WQ32CEIGR" +}, +// Gigadevices GD25Q64C +{ + .jedec_id = {0xC8, 0x40, 0x17}, + .total_size = 8388608, // 8MB (64Mbit) + .sector_size = 4096, + .page_size = 256, + .address_bits = 24, + .read_opcode = 0xEB, // 4x I/O Read (QREAD) + .program_opcode = 0x32, // Quad Page Program (QPP) + .erase_opcode = 0x20, // Sector Erase (SE) + .status_opcode = 0x05, // Read Status Register (RDSR) + .supports_quad_read = true, + .supports_quad_write = true, + .quad_enable_register = 2, // Status Register S9 (QE bit) + .quad_enable_bit = 1, // QE is bit 1 in register 2 + .quad_enable_volatile = false, + .max_clock_hz = 104000000, // 104MHz max frequency + .write_timeout_ms = 3, // Page program time max + .erase_timeout_ms = 20, // Sector erase time max + .startup_delay_us = 10000, // Conservative startup delay + .name = "GD25Q64C" +} +}; + +const uint32_t qspiFlashChipCount = sizeof(qspiFlashChips) / sizeof(qspiFlashChips[0]); + +// Global instance +CustomLFS_QSPIFlash QSPIFlash; + +//--------------------------------------------------------------------+ +// Event Handler - Simplified for compatibility +//--------------------------------------------------------------------+ + +// Modified event handler +void CustomLFS_QSPIFlash::qspi_event_handler(nrfx_qspi_evt_t event, void *p_context) +{ + CustomLFS_QSPIFlash* fs = (CustomLFS_QSPIFlash*)p_context; + + if (fs) { + fs->_operation_pending = false; + fs->_operation_complete = true; + fs->_operation_result = (event == NRFX_QSPI_EVENT_DONE) ? NRFX_SUCCESS : NRFX_ERROR_INTERNAL; + fs->_current_operation = QSPI_OP_NONE; + } +} + +//--------------------------------------------------------------------+ +// LittleFS Callbacks +//--------------------------------------------------------------------+ + +int CustomLFS_QSPIFlash::_qspi_read(const struct lfs_config *c, lfs_block_t block, + lfs_off_t off, void *buffer, lfs_size_t size) +{ + CustomLFS_QSPIFlash* fs = (CustomLFS_QSPIFlash*)c->context; + uint32_t addr = block * c->block_size + off; + + bool result = fs->qspiRead(addr, buffer, size); + if (!result) { + Serial.print("LFS READ FAILED: block="); + Serial.print(block); + Serial.print(", addr=0x"); + Serial.println(addr, HEX); + } + + return result ? 0 : -1; +} + +int CustomLFS_QSPIFlash::_qspi_erase(const struct lfs_config *c, lfs_block_t block) +{ + CustomLFS_QSPIFlash* fs = (CustomLFS_QSPIFlash*)c->context; + uint32_t addr = block * c->block_size; + + bool result = fs->qspiErase(addr); + if (!result) { + Serial.print("LFS ERASE FAILED: block="); + Serial.print(block); + Serial.print(", addr=0x"); + Serial.println(addr, HEX); + } + + return result ? 0 : -1; +} + +int CustomLFS_QSPIFlash::_qspi_prog(const struct lfs_config *c, lfs_block_t block, + lfs_off_t off, const void *buffer, lfs_size_t size) +{ + CustomLFS_QSPIFlash* fs = (CustomLFS_QSPIFlash*)c->context; + uint32_t addr = block * c->block_size + off; + + bool result = fs->qspiWrite(addr, buffer, size); + if (!result) { + Serial.print("LFS WRITE FAILED: block="); + Serial.print(block); + Serial.print(", addr=0x"); + Serial.println(addr, HEX); + } + + return result ? 0 : -1; +} + + +int CustomLFS_QSPIFlash::_qspi_sync(const struct lfs_config *c) +{ + CustomLFS_QSPIFlash* fs = (CustomLFS_QSPIFlash*)c->context; + return fs->qspiWaitReady(5000) ? 0 : -1; // 5 second timeout +} + +//--------------------------------------------------------------------+ +// CustomLFS_QSPIFlash Implementation +//--------------------------------------------------------------------+ + +CustomLFS_QSPIFlash::CustomLFS_QSPIFlash() + : CustomLFS(false) // Don't auto-configure + , _qspi_initialized(false) + , _quad_mode_enabled(false) + , _chip(nullptr) + , _total_size(1048576) // Default 1MB + , _sector_size(4096) // Default 4KB sectors + , _page_size(256) // Default 256B pages + , _is_4byte_addr(false) + , _use_quad_read(false) + , _use_quad_write(false) + , _clock_frequency(16000000) // Default 16MHz + , _operation_pending(false) + , _last_error(NRFX_SUCCESS) +{ + memset(&_qspi_config, 0, sizeof(_qspi_config)); +} + +CustomLFS_QSPIFlash::~CustomLFS_QSPIFlash() +{ + if (_qspi_initialized) { + nrfx_qspi_uninit(); + _qspi_initialized = false; + } +} + +bool CustomLFS_QSPIFlash::begin(uint8_t sck_pin, uint8_t csn_pin, uint8_t io0_pin, + uint8_t io1_pin, uint8_t io2_pin, uint8_t io3_pin) +{ + if (_qspi_initialized) { + return false; + } + Serial.println("Starting QSPI initialization..."); + + // Initialize operation state + _operation_pending = false; + _operation_complete = false; + _current_operation = QSPI_OP_NONE; + _operation_result = NRFX_SUCCESS; + _quad_mode_enabled = false; + _quad_read_enabled = false; + _quad_write_enabled = false; + + // Configure using conservative settings initially + memset(&_qspi_config, 0, sizeof(_qspi_config)); + + // Pin configuration + _qspi_config.pins.sck_pin = g_ADigitalPinMap[sck_pin]; + _qspi_config.pins.csn_pin = g_ADigitalPinMap[csn_pin]; + _qspi_config.pins.io0_pin = g_ADigitalPinMap[io0_pin]; + _qspi_config.pins.io1_pin = g_ADigitalPinMap[io1_pin]; + _qspi_config.pins.io2_pin = g_ADigitalPinMap[io2_pin]; + _qspi_config.pins.io3_pin = g_ADigitalPinMap[io3_pin]; + + // Protocol configuration (start conservative) + _qspi_config.prot_if.readoc = NRF_QSPI_READOC_FASTREAD; + _qspi_config.prot_if.writeoc = NRF_QSPI_WRITEOC_PP; + _qspi_config.prot_if.addrmode = NRF_QSPI_ADDRMODE_24BIT; + _qspi_config.prot_if.dpmconfig = false; + + // Physical interface (start with conservative speed) + _qspi_config.phy_if.sck_freq = NRF_QSPI_FREQ_32MDIV2; + _qspi_config.phy_if.spi_mode = NRF_QSPI_MODE_0; + _qspi_config.phy_if.dpmen = false; + + _clock_frequency = 16000000; + + + // Initialize QSPI with event handler + nrfx_err_t err = nrfx_qspi_init(&_qspi_config, qspi_event_handler, this); + if (err != NRFX_SUCCESS) { + Serial.print("QSPI init failed: 0x"); + Serial.println(err, HEX); + return false; + } + NVIC_SetPriority(QSPI_IRQn, 2); // Set QSPI IRQ priority lower to avoid conflict. + + + Serial.println("QSPI driver initialized"); + + // Activate QSPI tasks + Serial.println("Activating QSPI..."); + NRF_QSPI->TASKS_ACTIVATE = 1; + + // Wait for QSPI to be ready + Serial.println("Waiting for QSPI ready..."); + if (!waitForQSPIReady()) { + Serial.println("QSPI ready timeout!"); + return false; + } + + Serial.println("QSPI is ready"); + _qspi_initialized = true; + + // Detect flash chip + if (!detectChip()) { + Serial.println("Chip detection failed"); + return false; + } + + // // Run comprehensive tests + // testFlash(); + + _configure_lfs(); + // testLFSCallbacks(); + + // Mount filesystem + if (!Adafruit_LittleFS::begin(&_lfs_config)) { + Serial.println("Mount failed, formatting..."); + if (!format()) { + Serial.println("Format failed!"); + return false; + } + if (!Adafruit_LittleFS::begin(&_lfs_config)) { + Serial.println("Mount failed after format!"); + return false; + } + Serial.println("Formatted and mounted"); + } else { + Serial.println("Filesystem mounted"); + } + + return true; +} + +// handy for debugging but can probably be removed now. +bool CustomLFS_QSPIFlash::testLFSCallbacks() +{ + Serial.println("Testing LittleFS callbacks directly..."); + + resetOperationState(); + + uint8_t testBuffer[256]; + memset(testBuffer, 0xAA, sizeof(testBuffer)); + + Serial.println("Testing erase callback..."); + int result = _qspi_erase(&_lfs_config, 0); + if (result != 0) { + Serial.print("Erase callback failed: "); + Serial.println(result); + resetOperationState(); + return false; + } + + Serial.println("Testing write callback..."); + result = _qspi_prog(&_lfs_config, 0, 0, testBuffer, sizeof(testBuffer)); + if (result != 0) { + Serial.print("Write callback failed: "); + Serial.println(result); + resetOperationState(); + return false; + } + + Serial.println("Testing read callback..."); + uint8_t readBuffer[256]; + result = _qspi_read(&_lfs_config, 0, 0, readBuffer, sizeof(readBuffer)); + if (result != 0) { + Serial.print("Read callback failed: "); + Serial.println(result); + resetOperationState(); + return false; + } + + bool dataOk = true; + for (int i = 0; i < 256; i++) { + if (readBuffer[i] != 0xAA) { + dataOk = false; + break; + } + } + + Serial.print("Callback test: "); + Serial.println(dataOk ? "SUCCESS" : "FAILED"); + + resetOperationState(); + + return dataOk; +} + +// Callback-compatible qspiRead() +bool CustomLFS_QSPIFlash::qspiRead(uint32_t addr, void *buffer, uint32_t size) +{ + if (!_qspi_initialized || !buffer || size == 0) { + return false; + } + + if (_operation_pending) { + return false; + } + + _operation_pending = true; + _operation_complete = false; + _current_operation = QSPI_OP_READ; + _operation_result = NRFX_ERROR_BUSY; + + nrfx_err_t err = nrfx_qspi_read(buffer, size, addr); + if (err != NRFX_SUCCESS) { + resetOperationState(); + return false; + } + + bool success = waitForOperation(5000); + + if (_operation_pending) { + resetOperationState(); + } + + return success; +} + +// Callback-compatible qspiWrite() +bool CustomLFS_QSPIFlash::qspiWrite(uint32_t addr, const void *buffer, uint32_t size) +{ + if (!_qspi_initialized || !buffer || size == 0) { + return false; + } + + if (_operation_pending) { + return false; + } + + _operation_pending = true; + _operation_complete = false; + _current_operation = QSPI_OP_WRITE; + _operation_result = NRFX_ERROR_BUSY; + + nrfx_err_t err = nrfx_qspi_write(buffer, size, addr); + if (err != NRFX_SUCCESS) { + resetOperationState(); + return false; + } + + bool success = waitForOperation(10000); // Longer timeout for writes + + if (_operation_pending) { + resetOperationState(); + } + + return success; +} + +// Callback-compatible qspiErase() +bool CustomLFS_QSPIFlash::qspiErase(uint32_t addr, uint32_t size) +{ + if (!_qspi_initialized) { + return false; + } + + if (_operation_pending) { + return false; + } + + _operation_pending = true; + _operation_complete = false; + _current_operation = QSPI_OP_ERASE; + _operation_result = NRFX_ERROR_BUSY; + + nrfx_err_t err = nrfx_qspi_erase(NRF_QSPI_ERASE_LEN_4KB, addr); + if (err != NRFX_SUCCESS) { + resetOperationState(); + return false; + } + + bool success = waitForOperation(30000); // Longest timeout for erase + + if (_operation_pending) { + resetOperationState(); + } + + return success; +} + +void CustomLFS_QSPIFlash::resetOperationState() +{ + _operation_pending = false; + _operation_complete = false; + _current_operation = QSPI_OP_NONE; + _operation_result = NRFX_SUCCESS; +} + +// Helper function - waitForOperation() +bool CustomLFS_QSPIFlash::waitForOperation(uint32_t timeout_ms) +{ + uint32_t start = millis(); + + while (_operation_pending && (millis() - start) < timeout_ms) { + delay(1); + + if (_operation_complete) { + _operation_pending = false; + return (_operation_result == NRFX_SUCCESS); + } + } + + if (_operation_complete) { + _operation_pending = false; + return (_operation_result == NRFX_SUCCESS); + } + + if (_operation_pending) { + resetOperationState(); + return false; + } + + return (_operation_result == NRFX_SUCCESS); +} + + +uint8_t CustomLFS_QSPIFlash::readStatus(uint8_t register_num) +{ + if (!_qspi_initialized) { + return 0xFF; + } + + uint8_t opcode; + switch (register_num) { + case 1: + opcode = 0x05; // Read Status Register 1 + break; + case 2: + opcode = 0x35; // Read Status Register 2 + break; + case 3: + opcode = 0x15; // Read Status Register 3 + break; + default: + return 0xFF; + } + + uint8_t status = 0xFF; + nrf_qspi_cinstr_conf_t cfg = { + .opcode = opcode, + .length = NRF_QSPI_CINSTR_LEN_2B, + .io2_level = true, + .io3_level = true, + .wipwait = false, + .wren = false + }; + + if (nrfx_qspi_cinstr_xfer(&cfg, NULL, &status) == NRFX_SUCCESS) { + return status; + } + + return 0xFF; +} + +bool CustomLFS_QSPIFlash::writeStatus(uint8_t value, uint8_t register_num) +{ + if (!_qspi_initialized) { + return false; + } + + // Write Enable first + nrf_qspi_cinstr_conf_t cfg = { + .opcode = 0x06, // WREN + .length = NRF_QSPI_CINSTR_LEN_1B, + .io2_level = true, + .io3_level = true, + .wipwait = false, + .wren = false + }; + + if (nrfx_qspi_cinstr_xfer(&cfg, NULL, NULL) != NRFX_SUCCESS) { + return false; + } + + // Write status register + uint8_t opcode; + switch (register_num) { + case 1: + opcode = 0x01; // Write Status Register 1 + break; + case 2: + opcode = 0x31; // Write Status Register 2 (some chips) + break; + case 3: + opcode = 0x11; // Write Status Register 3 (some chips) + break; + default: + return false; + } + + cfg.opcode = opcode; + cfg.length = NRF_QSPI_CINSTR_LEN_2B; + cfg.wipwait = true; // Wait for operation to complete + + return (nrfx_qspi_cinstr_xfer(&cfg, &value, NULL) == NRFX_SUCCESS); +} + + + +bool CustomLFS_QSPIFlash::waitForQSPIReady() +{ + uint32_t timeout = millis() + 5000; + + while (millis() < timeout) { + // Use the same check as the working example + uint32_t status = NRF_QSPI->STATUS; + if ((status & 0x08) == 0x08 && (status & 0x01000000) == 0) { + return true; + } + delay(1); + } + + return false; +} + +bool CustomLFS_QSPIFlash::detectChip() +{ + uint8_t jedec_id[3] = {0, 0, 0}; + + // Try to read JEDEC ID + nrf_qspi_cinstr_conf_t cinstr_cfg = { + .opcode = 0x9F, + .length = NRF_QSPI_CINSTR_LEN_4B, + .io2_level = true, + .io3_level = true, + .wipwait = false, + .wren = false + }; + + nrfx_err_t err = nrfx_qspi_cinstr_xfer(&cinstr_cfg, NULL, jedec_id); + if (err != NRFX_SUCCESS) { + Serial.print("JEDEC read failed: 0x"); + Serial.println(err, HEX); + return false; + } + + Serial.print("JEDEC ID: "); + for (int i = 0; i < 3; i++) { + if (jedec_id[i] < 0x10) Serial.print("0"); + Serial.print(jedec_id[i], HEX); + if (i < 2) Serial.print(" "); + } + Serial.println(); + + // Check for valid ID + if ((jedec_id[0] == 0x00 && jedec_id[1] == 0x00 && jedec_id[2] == 0x00) || + (jedec_id[0] == 0xFF && jedec_id[1] == 0xFF && jedec_id[2] == 0xFF)) { + Serial.println("Invalid JEDEC ID - check connections"); + return false; + } + + // Look for known chip + for (uint32_t i = 0; i < qspiFlashChipCount; i++) { + if (jedec_id[0] == qspiFlashChips[i].jedec_id[0] && + jedec_id[1] == qspiFlashChips[i].jedec_id[1] && + jedec_id[2] == qspiFlashChips[i].jedec_id[2]) { + + _chip = &qspiFlashChips[i]; + _total_size = _chip->total_size; + _sector_size = _chip->sector_size; + _page_size = _chip->page_size; + + Serial.print("Known chip: "); + Serial.println(_chip->name); + return true; + } + } + + Serial.println("Unknown chip - using defaults"); + return true; // Continue with defaults +} + + + +bool CustomLFS_QSPIFlash::qspiWaitReady(uint32_t timeout_ms) +{ + uint32_t start = millis(); + + while ((millis() - start) < timeout_ms) { + // Simple status check using custom instruction + uint8_t status = 0xFF; + nrf_qspi_cinstr_conf_t cfg = { + .opcode = 0x05, // Read status + .length = NRF_QSPI_CINSTR_LEN_2B, + .io2_level = true, + .io3_level = true, + .wipwait = false, + .wren = false + }; + + if (nrfx_qspi_cinstr_xfer(&cfg, NULL, &status) == NRFX_SUCCESS) { + if ((status & 0x01) == 0) { // WIP bit clear + return true; + } + } + + delay(1); + } + + return false; +} + +void CustomLFS_QSPIFlash::_configure_lfs() +{ + memset(&_lfs_config, 0, sizeof(_lfs_config)); + + _lfs_config.context = this; + // DEBUG: Verify context pointer immediately + Serial.print("_configure_lfs: Setting context to: 0x"); + Serial.println((uintptr_t)this, HEX); + Serial.print("_configure_lfs: Context verification - _operation_pending = "); + Serial.println(_operation_pending); + Serial.print("_configure_lfs: Context verification - _qspi_initialized = "); + Serial.println(_qspi_initialized); + + // Test the context pointer immediately + CustomLFS_QSPIFlash* test_ctx = (CustomLFS_QSPIFlash*)_lfs_config.context; + Serial.print("_configure_lfs: Retrieved context: 0x"); + Serial.println((uintptr_t)test_ctx, HEX); + Serial.print("_configure_lfs: Retrieved context _operation_pending = "); + Serial.println(test_ctx->_operation_pending); + + + // Callbacks + _lfs_config.read = _qspi_read; + _lfs_config.prog = _qspi_prog; + _lfs_config.erase = _qspi_erase; + _lfs_config.sync = _qspi_sync; + + // CRITICAL: Adjust these parameters for QSPI flash + _lfs_config.read_size = 256; // Match page size + _lfs_config.prog_size = 256; // Match page size + _lfs_config.block_size = _sector_size; // 4096 bytes + _lfs_config.block_count = _total_size / _sector_size; + + // Reduce lookahead for smaller flash + _lfs_config.lookahead = 32; // Smaller lookahead for 2MB flash + + // Allocate static buffers instead of dynamic (more reliable) + static uint8_t read_buffer[256]; + static uint8_t prog_buffer[256]; + static uint8_t lookahead_buffer[32]; + + _lfs_config.read_buffer = read_buffer; + _lfs_config.prog_buffer = prog_buffer; + _lfs_config.lookahead_buffer = lookahead_buffer; + _lfs_config.file_buffer = NULL; // Keep this dynamic + + // Update parent class members + _flash_addr = 0; + _flash_total_size = _total_size; + _block_size = _sector_size; + + // // Debug the configuration + // Serial.println("=== LittleFS Configuration ==="); + // Serial.print("Block size: "); Serial.println(_lfs_config.block_size); + // Serial.print("Block count: "); Serial.println(_lfs_config.block_count); + // Serial.print("Read size: "); Serial.println(_lfs_config.read_size); + // Serial.print("Prog size: "); Serial.println(_lfs_config.prog_size); + // Serial.print("Lookahead: "); Serial.println(_lfs_config.lookahead); + // Serial.print("Total size: "); + // Serial.print(_lfs_config.block_count * _lfs_config.block_size / 1024); + // Serial.println(" KB"); +} + +bool CustomLFS_QSPIFlash::testFlash() +{ + if (!_qspi_initialized) { + return false; + } + + Serial.println("Testing QSPI flash..."); + + uint8_t testData[256]; + uint8_t readData[256]; + + // Create test pattern + for (int i = 0; i < 256; i++) { + testData[i] = i ^ 0xAA; + } + + // Use last sector + uint32_t testAddr = _total_size - _sector_size; + + // Erase + if (!qspiErase(testAddr)) { + Serial.println("Erase failed"); + return false; + } + + // Write + if (!qspiWrite(testAddr, testData, sizeof(testData))) { + Serial.println("Write failed"); + return false; + } + + // Read + if (!qspiRead(testAddr, readData, sizeof(readData))) { + Serial.println("Read failed"); + return false; + } + + // Compare + for (int i = 0; i < 256; i++) { + if (testData[i] != readData[i]) { + Serial.print("Verify failed at "); + Serial.println(i); + return false; + } + } + + Serial.println("Flash test passed"); + return true; +} + +bool CustomLFS_QSPIFlash::lowLevelFormat() +{ + Serial.println("WARNING: This will erase all data!"); + Serial.println("Formatting flash..."); + + uint32_t sectors = _total_size / _sector_size; + for (uint32_t i = 0; i < sectors; i++) { + if (!qspiErase(i * _sector_size)) { + Serial.print("Format failed at sector "); + Serial.println(i); + return false; + } + + if ((i % 32) == 0) { + Serial.print("Progress: "); + Serial.print((i * 100) / sectors); + Serial.println("%"); + } + } + + Serial.println("Format complete"); + return true; +} + +bool CustomLFS_QSPIFlash::isQSPIReady() +{ + if (!_qspi_initialized) { + return false; + } + + // Check if QSPI is enabled and ready + bool enabled = (NRF_QSPI->ENABLE == 1); + bool ready = (NRF_QSPI->STATUS & QSPI_STATUS_READY_Msk) != 0; + + Serial.print("QSPI enabled: "); + Serial.print(enabled); + Serial.print(", ready: "); + Serial.println(ready); + + return enabled && ready; +} + + +// // placeholders for now +// bool CustomLFS_QSPIFlash::setClockFrequency(uint32_t freq) { return false; } +// bool CustomLFS_QSPIFlash::enableMemoryMapping() { return _qspi_initialized; } +// bool CustomLFS_QSPIFlash::disableMemoryMapping() { return true; } + +#endif // NRF52840_XXAA \ No newline at end of file diff --git a/test/fixtures/customlfs_qspi_dma/CustomLFS_QSPIFlash.h b/test/fixtures/customlfs_qspi_dma/CustomLFS_QSPIFlash.h new file mode 100644 index 000000000..34647209d --- /dev/null +++ b/test/fixtures/customlfs_qspi_dma/CustomLFS_QSPIFlash.h @@ -0,0 +1,235 @@ +/* + * CustomLFS_QSPIFlash.h - QSPI Flash support for CustomLFS + * + * Copyright (c) 2025 oltaco + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef CUSTOMLFS_QSPI_H_ +#define CUSTOMLFS_QSPI_H_ + +// Only compile for nRF52840 which has QSPI hardware +#ifdef NRF52840_XXAA + +#include "CustomLFS.h" +#include "nrfx_qspi.h" +#include "nrf_qspi.h" +#include "variant.h" + +#include "Adafruit_LittleFS.h" +using namespace Adafruit_LittleFS_Namespace; + +// if the pins aren't defined we can't compile on boards without QSPI. +#ifndef PIN_QSPI_SCK +#define PIN_QSPI_SCK -1 +#endif + +#ifndef PIN_QSPI_CS +#define PIN_QSPI_CS -1 +#endif + +#ifndef PIN_QSPI_IO0 +#define PIN_QSPI_IO0 -1 +#endif + +#ifndef PIN_QSPI_IO1 +#define PIN_QSPI_IO1 -1 +#endif + +#ifndef PIN_QSPI_IO2 +#define PIN_QSPI_IO2 -1 +#endif + +#ifndef PIN_QSPI_IO3 +#define PIN_QSPI_IO3 -1 +#endif + + +// QSPI Flash chip configuration structure +struct QSPIFlashChip { + uint8_t jedec_id[3]; // JEDEC ID bytes [manufacturer, memory_type, capacity] + uint32_t total_size; // Total flash size in bytes + uint32_t sector_size; // Sector erase size (typically 4KB) + uint32_t page_size; // Page program size (typically 256B) + uint8_t address_bits; // 24 or 32 bit addressing + + // QSPI-specific configuration + uint8_t read_opcode; // Fast read opcode (0x0B, 0xEB for quad) + uint8_t program_opcode; // Page program opcode (0x02, 0x32 for quad) + uint8_t erase_opcode; // Sector erase opcode (0x20, 0xD8) + uint8_t status_opcode; // Read status register opcode (0x05) + + // Quad mode configuration + bool supports_quad_read; // Supports quad read operations + bool supports_quad_write; // Supports quad write operations + uint8_t quad_enable_register; // Status register for quad enable (1 or 2) + uint8_t quad_enable_bit; // Bit position for quad enable + bool quad_enable_volatile; // True if quad enable is volatile + + // Timing specifications + uint32_t max_clock_hz; // Maximum QSPI clock frequency + uint16_t write_timeout_ms; // Write operation timeout + uint16_t erase_timeout_ms; // Erase operation timeout + uint16_t startup_delay_us; // Power-up delay + + const char *name; // Chip name +}; + +class CustomLFS_QSPIFlash : public CustomLFS +{ +private: + // QSPI hardware configuration + nrfx_qspi_config_t _qspi_config; + + bool _quad_mode_enabled; + bool _quad_read_enabled; + bool _quad_write_enabled; + + // Flash chip information + const QSPIFlashChip *_chip; + uint32_t _total_size; + uint32_t _sector_size; + uint32_t _page_size; + bool _is_4byte_addr; + + // Performance optimization + bool _use_quad_read; + bool _use_quad_write; + uint32_t _clock_frequency; + + // Internal state + volatile bool _operation_pending; + nrfx_err_t _last_error; + + // Operation state tracking + enum QSPIOperation { + QSPI_OP_NONE, + QSPI_OP_READ, + QSPI_OP_WRITE, + QSPI_OP_ERASE + }; + + volatile QSPIOperation _current_operation; + volatile bool _operation_complete; + volatile nrfx_err_t _operation_result; + + bool waitForOperation(uint32_t timeout_ms); + void resetOperationState(); + + // Flash detection and configuration + bool detectChip(); + + // Low-level flash operations + bool qspiRead(uint32_t addr, void *buffer, uint32_t size); + bool qspiWrite(uint32_t addr, const void *buffer, uint32_t size); + bool qspiErase(uint32_t addr, uint32_t size = 0); // 0 = sector erase + bool qspiWaitReady(uint32_t timeout_ms = 10000); + + // handy for debugging but maybe remove? + bool testLFSCallbacks(); + + // Status and control operations + uint8_t readStatus(uint8_t register_num = 1); + bool writeStatus(uint8_t value, uint8_t register_num = 1); + + // Static callback functions for LittleFS operations + static int _qspi_read(const struct lfs_config *c, lfs_block_t block, + lfs_off_t off, void *buffer, lfs_size_t size); + static int _qspi_prog(const struct lfs_config *c, lfs_block_t block, + lfs_off_t off, const void *buffer, lfs_size_t size); + static int _qspi_erase(const struct lfs_config *c, lfs_block_t block); + static int _qspi_sync(const struct lfs_config *c); + + // QSPI event handler + static void qspi_event_handler(nrfx_qspi_evt_t event, void *p_context); + + // Configure LFS for QSPI flash + virtual void _configure_lfs() override; + + +public: + // Constructor + CustomLFS_QSPIFlash(); + + bool _qspi_initialized; + // Initialization with pin configuration + bool begin(uint8_t sck_pin = PIN_QSPI_SCK, uint8_t csn_pin = PIN_QSPI_CS, + uint8_t io0_pin = PIN_QSPI_IO0, uint8_t io1_pin = PIN_QSPI_IO1, + uint8_t io2_pin = PIN_QSPI_IO2, uint8_t io3_pin = PIN_QSPI_IO3); + + // Configuration methods + // bool setClockFrequency(uint32_t frequency_hz); + + // Information methods + uint32_t getFlashSize() const { return _total_size; } + uint32_t getSectorSize() const { return _sector_size; } + uint32_t getPageSize() const { return _page_size; } + uint32_t getClockFrequency() const { return _clock_frequency; } + const char* getChipName() const { return _chip ? _chip->name : "Unknown"; } + + // remove? + bool isReady() const { return _qspi_initialized && !_operation_pending; } + bool isQuadModeEnabled() const { return _quad_mode_enabled; } + bool supportsQuadRead() const { return _chip ? _chip->supports_quad_read : false; } + bool supportsQuadWrite() const { return _chip ? _chip->supports_quad_write : false; } + + // Advanced operations + bool lowLevelFormat(); + bool testFlash(); + + + bool isQSPIReady(); + bool waitForQSPIReady(); + + // Direct flash access for advanced users + bool rawRead(uint32_t addr, void *buffer, uint32_t size) { return qspiRead(addr, buffer, size); } + bool rawWrite(uint32_t addr, const void *buffer, uint32_t size) { return qspiWrite(addr, buffer, size); } + bool rawErase(uint32_t addr, uint32_t size = 0) { return qspiErase(addr, size); } + + // // Memory mapping support (XIP - Execute in Place) + // void* getMemoryMappedAddress() const { return (void*)0x12000000; } // nRF52840 QSPI XIP base + // bool enableMemoryMapping(); + // bool disableMemoryMapping(); + + // Override parent class methods that don't apply to QSPI + bool setFlashRegion(uint32_t flash_addr, uint32_t flash_size, + uint32_t block_size = 0) = delete; // Not applicable for external QSPI flash + + // Destructor + ~CustomLFS_QSPIFlash(); +}; + +// Known QSPI flash chips database +extern const QSPIFlashChip qspiFlashChips[]; +extern const uint32_t qspiFlashChipCount; + +// Global instance for convenience +extern CustomLFS_QSPIFlash QSPIFlash; + +// Type alias for easier usage +typedef CustomLFS_QSPIFlash LittleFS_QSPI; + +#else +#error "QSPI is only supported on nRF52840" +#endif // NRF52840_XXAA + +#endif /* CUSTOMLFS_QSPI_H_ */ \ No newline at end of file diff --git a/test/test_nrf52_qspi_dma_fix.py b/test/test_nrf52_qspi_dma_fix.py new file mode 100644 index 000000000..72da41903 --- /dev/null +++ b/test/test_nrf52_qspi_dma_fix.py @@ -0,0 +1,226 @@ +"""Exercise actual CustomLFS buffer declarations, DMA admission and build isolation.""" +import importlib.util +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location('nrf52_qspi_dma_fix', ROOT / 'scripts/nrf52_qspi_dma_fix.py') +fix = importlib.util.module_from_spec(spec) +spec.loader.exec_module(fix) +FIXTURE = ROOT / 'test/fixtures/customlfs_qspi_dma' +RAW = (FIXTURE / 'CustomLFS_QSPIFlash.cpp').read_text(encoding='ascii') + + +def configure_method(source): + marker = 'void CustomLFS_QSPIFlash::_configure_lfs()' + start = source.index(marker) + opening = source.index('{', start) + depth = 1 + for end in range(opening + 1, len(source)): + if source[end] == '{': depth += 1 + elif source[end] == '}': depth -= 1 + if depth == 0: return source[start:end + 1] + raise AssertionError('Unclosed reviewed configure method') + + +HARNESS = r''' +#include +#include +#include +#define HEX 16 +struct SerialStub { + template void print(T) {} + template void println(T) {} + template void println(T, int) {} +} Serial; +struct Config { + void* context; + void (*read)(); void (*prog)(); void (*erase)(); void (*sync)(); + uint32_t read_size, prog_size, block_size, block_count, lookahead; + void *read_buffer, *prog_buffer, *lookahead_buffer, *file_buffer; +}; +struct CustomLFS_QSPIFlash { + Config _lfs_config{}; + bool _operation_pending = false, _qspi_initialized = true; + uint32_t _sector_size = 4096, _total_size = 2097152; + uint32_t _flash_addr = 0, _flash_total_size = 0, _block_size = 0; + static void _qspi_read() {} static void _qspi_prog() {} + static void _qspi_erase() {} static void _qspi_sync() {} + void _configure_lfs(); +}; +@METHOD@ +// Exact nrfx contract under test: RAM buffer must be word aligned. This +// adapter records admission and copies only after the alignment gate. +static unsigned admissions; +static bool dma(void* destination, const void* source, size_t bytes) { + if ((reinterpret_cast(destination) & 3u) != 0 || + (reinterpret_cast(source) & 3u) != 0) return false; + ++admissions; std::memcpy(destination, source, bytes); return true; +} +int main() { + CustomLFS_QSPIFlash fs; fs._configure_lfs(); + auto saved = fs._lfs_config; + assert(saved.context == &fs && saved.block_size == 4096 && saved.block_count == 512); + assert(saved.read_size == 256 && saved.prog_size == 256 && saved.lookahead == 32); + assert(saved.file_buffer == nullptr); + for (void* buffer : {saved.read_buffer, saved.prog_buffer, saved.lookahead_buffer}) + assert((reinterpret_cast(buffer) & 3u) == 0); + alignas(uint32_t) uint8_t pattern[256], media[256], bad[260]; + for (unsigned i=0; i<256; ++i) pattern[i] = static_cast(i ^ 0xa5); + std::memset(media, 0, sizeof(media)); std::memset(bad, 0, sizeof(bad)); + assert(!dma(bad + 1, pattern, 256) && !dma(media, bad + 1, 256)); + assert(admissions == 0 && bad[1] == 0 && media[0] == 0); + assert(dma(saved.prog_buffer, pattern, 256)); + assert(dma(media, saved.prog_buffer, 256)); + assert(dma(saved.read_buffer, media, 256)); + assert(std::memcmp(saved.read_buffer, pattern, 256) == 0 && admissions == 3); + fs._configure_lfs(); + assert(fs._lfs_config.read_buffer == saved.read_buffer); + assert(fs._lfs_config.prog_buffer == saved.prog_buffer); + assert(fs._lfs_config.lookahead_buffer == saved.lookahead_buffer); + assert(std::memcmp(saved.read_buffer, pattern, 256) == 0); +} +''' + + +class QspiDmaFixTest(unittest.TestCase): + def test_exact_source_transform_idempotence_and_unknown_refusal(self): + fixed = fix.patched_source(RAW) + self.assertEqual(fix.digest(RAW), fix.SOURCE_SHA256) + self.assertEqual(fix.digest(fixed), fix.PATCHED_SHA256) + self.assertEqual(fix.patched_source(fixed), fixed) + self.assertEqual(fix.patched_source(RAW.replace('\n', '\r\n')), fixed) + self.assertEqual(fixed.replace(fix.PATCHED_BUFFERS, fix.ORIGINAL_BUFFERS), RAW) + for changed in (RAW + '// future dependency\n', RAW.replace('read_buffer[256]', 'read_buffer[128]'), + RAW.replace('static uint8_t read_buffer', 'alignas(uint32_t) static uint8_t read_buffer')): + with self.subTest(changed=changed[-30:]), self.assertRaises(RuntimeError): + fix.patched_source(changed) + + def test_compiled_actual_configure_method_retains_pools_and_dma_admission(self): + compiler = shutil.which('c++') or shutil.which('g++') + if not compiler: self.skipTest('Host C++ compiler required') + code = HARNESS.replace('#include ', '#include \n#include ') + code = code.replace('@METHOD@', configure_method(fix.patched_source(RAW))) + with tempfile.TemporaryDirectory(prefix='qspi-dma-native-') as temp: + cpp = Path(temp) / 'check.cpp'; binary = Path(temp) / 'check' + cpp.write_text(code, encoding='ascii') + result = subprocess.run([compiler, '-std=c++17', '-O2', '-Wall', '-Wextra', '-Werror', + '-fsanitize=address,undefined', '-fno-pie', '-no-pie', str(cpp), '-o', str(binary)], + capture_output=True, text=True, timeout=60) + self.assertEqual(result.returncode, 0, result.stderr) + result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_arm_declared_alignment_survives_odd_preceding_section(self): + core = Path(os.environ.get('PLATFORMIO_CORE_DIR', Path.home() / '.platformio')) + choices = [os.environ.get('MESHCORE_ARM_GXX', ''), + str(core / 'packages/toolchain-gccarmnoneeabi/bin/arm-none-eabi-g++'), + shutil.which('arm-none-eabi-g++') or ''] + compiler = next((p for p in choices if p and Path(p).is_file()), None) + if not compiler: self.skipTest('ARM C++ compiler required for linked ABI check') + # Keep the exact dependency declarations. The linker places a retained + # one-byte object first; only declared alignment may insert padding. + script = '''SECTIONS { .text 0x10000 : { *(.text*) } + .bss 0x20000000 (NOLOAD) : { *(.bss.prefix) *(.bss.*read_buffer*) + *(.bss.*prog_buffer*) *(.bss.*lookahead_buffer*) *(.bss*) } }''' + for fixed in (False, True): + with self.subTest(fixed=fixed), tempfile.TemporaryDirectory(prefix='qspi-dma-arm-') as temp: + work = Path(temp); cpp = work / 'check.cpp'; elf = work / 'check.elf' + declarations = fix.PATCHED_BUFFERS if fixed else fix.ORIGINAL_BUFFERS + # Function-local static storage has the original lifetime and + # symbol form, with no attributes added to the declarations. + cpp.write_text('''#include +extern "C" { __attribute__((used,section(".bss.prefix"))) unsigned char prefix[1]; } +extern "C" void* selected(unsigned index) { +''' + declarations + ''' + return index == 0 ? read_buffer : index == 1 ? prog_buffer : lookahead_buffer; +} +''', encoding='ascii') + (work / 'layout.ld').write_text(script, encoding='ascii') + result = subprocess.run([compiler, '-std=c++17', '-mcpu=cortex-m4', '-mthumb', '-Oz', + '-fdata-sections', '-ffunction-sections', '-nostdlib', '-Wl,--build-id=none', + '-Wl,-T,' + str(work / 'layout.ld'), str(cpp), '-o', str(elf)], + capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stderr) + sys.path.insert(0, str(ROOT / 'scripts')) + from firmware_elf import FirmwareElf + image = FirmwareElf(elf) + buffers = {name: symbol[0] for name, symbol in image.symbols.items() + if any(buffer in name for buffer in ('read_buffer', 'prog_buffer', 'lookahead_buffer'))} + self.assertEqual(len(buffers), 3) + expected = 0 if fixed else 1 + self.assertTrue(all(address % 4 == expected for address in buffers.values()), buffers) + + def test_middleware_uses_reviewed_private_copies_and_leaves_dependency_untouched(self): + class Node: + def __init__(self, path): self.path = path + def srcnode(self): return self + def get_abspath(self): return str(self.path) + class Env: + def __init__(self, build): self.build = build; self.middleware = [] + def subst(self, value): + self_test.assertEqual(value, '$BUILD_DIR'); return str(self.build) + def File(self, path): return Path(path) + def AddBuildMiddleware(self, callback, pattern): self.middleware.append((callback, pattern)) + self_test = self + with tempfile.TemporaryDirectory(prefix='qspi-dma-build-') as temp: + root = Path(temp); source = root / 'libdeps/CustomLFS/src'; source.mkdir(parents=True) + for path in FIXTURE.iterdir(): shutil.copyfile(path, source / path.name) + originals = {p.name: p.read_bytes() for p in source.iterdir()} + env = Env(root / 'build'); fix.install(env) + self.assertEqual(env.middleware, [(fix.replace_qspi_source, '*CustomLFS_QSPIFlash.cpp')]) + node = Node(source / 'CustomLFS_QSPIFlash.cpp') + target = fix.replace_qspi_source(env, node) + self.assertEqual(target, env.build / 'patched-nrf52-qspi/CustomLFS_QSPIFlash.cpp') + self.assertEqual(target.read_text(), fix.patched_source(RAW)) + self.assertEqual({p.name: p.read_bytes() for p in source.iterdir()}, originals) + before = {p.name: p.stat().st_mtime_ns for p in target.parent.iterdir()} + self.assertEqual(fix.replace_qspi_source(env, node), target) + self.assertEqual({p.name: p.stat().st_mtime_ns for p in target.parent.iterdir()}, before) + unrelated = Node(source / 'Other.cpp') + self.assertIs(fix.replace_qspi_source(env, unrelated), unrelated) + for name in fix.HEADERS: + self.assertEqual((target.parent / name).read_bytes(), originals[name]) + (source / name).write_text('changed header', encoding='ascii') + with self.assertRaises(RuntimeError): fix.replace_qspi_source(env, node) + (source / name).write_bytes(originals[name]) + (source / 'CustomLFS.h').unlink() + with self.assertRaises(RuntimeError): fix.replace_qspi_source(env, node) + + def test_resolved_qspi_recipes_retain_one_hook(self): + from platformio.project.config import ProjectConfig + config = ProjectConfig(str(ROOT / 'platformio.ini'), parse_extra=False) + for path in sorted((ROOT / 'variants').glob('*/platformio.ini')): + config.read(str(path), parse_extra=False) + hook = 'pre:scripts/nrf52_qspi_dma_fix.py' + selected = [] + for section in config.sections(): + if not section.startswith('env:'): continue + dependencies = config.get(section, 'lib_deps', []) + if not any('oltaco/CustomLFS' in item for item in dependencies): continue + with self.subTest(target=section): + self.assertEqual(config.get(section, 'extra_scripts', []).count(hook), 1) + selected.append(section) + self.assertGreater(len(selected), 100) + for target in ('Xiao_nrf52_companion_radio_ble', 'Xiao_nrf52_companion_radio_usb', + 'Xiao_nrf52_repeater', 'Mesh_pocket_companion_radio_usb'): + self.assertIn('env:' + target, selected) + + def test_nrf52_base_and_ci_load_fix_once(self): + config = (ROOT / 'platformio.ini').read_text(encoding='ascii') + hook = 'pre:scripts/nrf52_qspi_dma_fix.py' + nrf = config.split('[nrf52_base]', 1)[1].split('\n[', 1)[0] + self.assertEqual(config.count(hook), 1) + self.assertEqual(nrf.count(hook), 1) + workflow = (ROOT / '.github/workflows/run-unit-tests.yml').read_text(encoding='ascii') + self.assertIn('python3 -B test/test_nrf52_qspi_dma_fix.py -v', workflow) + self.assertIn('bash build.sh build-firmware Xiao_nrf52_companion_radio_full', workflow) + + +if __name__ == '__main__': unittest.main()