mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 22:42:25 +00:00
fix(nrf52): align external QSPI DMA buffers
This commit is contained in:
1 parent
c48e679284
commit
d58bedd8ca
8 files changed
+1568
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2019 hathach for Adafruit Industries
|
||||
* Copyright (c) 2025 oltaco <taco@sly.nu>
|
||||
*
|
||||
* 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_ */
|
||||
@@ -0,0 +1,917 @@
|
||||
/*
|
||||
* CustomLFS_QSPIFlash.cpp - QSPI Flash support for CustomLFS
|
||||
*
|
||||
* Copyright (c) 2025, 2026 oltaco <taco@sly.nu>
|
||||
*
|
||||
* 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
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* CustomLFS_QSPIFlash.h - QSPI Flash support for CustomLFS
|
||||
*
|
||||
* Copyright (c) 2025 oltaco <taco@sly.nu>
|
||||
*
|
||||
* 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_ */
|
||||
@@ -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 <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#define HEX 16
|
||||
struct SerialStub {
|
||||
template<class T> void print(T) {}
|
||||
template<class T> void println(T) {}
|
||||
template<class T> 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<uintptr_t>(destination) & 3u) != 0 ||
|
||||
(reinterpret_cast<uintptr_t>(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<uintptr_t>(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<uint8_t>(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 <cstring>', '#include <cstring>\n#include <initializer_list>')
|
||||
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 <stdint.h>
|
||||
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()
|
||||
Reference in new issue
Block a user