fix(nrf52): align external QSPI DMA buffers

This commit is contained in:
mikecarper committed 2026-10-20 10:35:07 -07:00
1 parent c48e679284
commit d58bedd8ca
8 files changed
+1568

No files matched your search

+3
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+77
View File
@@ -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
View File
@@ -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_ */
+917
View File
@@ -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
+235
View File
@@ -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_ */
+226
View File
@@ -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()