Fix nRF52 flash completion and preserve recoverable ExtraFS
This commit is contained in:
1 parent
a9d7692bf3
commit
3ce499a700
20 files changed
+970
-33
No files matched your search
@@ -210,7 +210,7 @@ void DataStore::begin() {
|
||||
#if defined(QSPIFLASH)
|
||||
#include <CustomLFS_QSPIFlash.h>
|
||||
#elif defined(EXTRAFS)
|
||||
#include <CustomLFS.h>
|
||||
#include "ResilientInternalExtraFS.h"
|
||||
#else
|
||||
#include <InternalFileSystem.h>
|
||||
#endif
|
||||
@@ -280,8 +280,10 @@ static void cleanupAtomicTempFiles(FILESYSTEM* fs) {
|
||||
bool DataStore::recoverInternalExtraFSOnBoot() {
|
||||
if (_configuredFsExtra == nullptr) return false;
|
||||
|
||||
CustomLFS* extra = static_cast<CustomLFS*>(_configuredFsExtra);
|
||||
ResilientInternalExtraFS* extra =
|
||||
static_cast<ResilientInternalExtraFS*>(_configuredFsExtra);
|
||||
if (_primary_storage_unavailable
|
||||
|| !extra->pageMapReady()
|
||||
|| !mesh::storage::isExpectedInternalExtraFsGeometry(
|
||||
extra->getFlashAddr(), extra->getFlashSize(), extra->getBlockSize())
|
||||
|| !mesh::storage::isInternalExtraFsReservedByApplication(
|
||||
@@ -291,6 +293,19 @@ bool DataStore::recoverInternalExtraFSOnBoot() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (extra->recoveryPending()) {
|
||||
MESH_DEBUG_PRINTLN("DataStore: running requested boot-time ExtraFS scan");
|
||||
if (!reinitializeInternalExtraFS(true)) {
|
||||
disableSecondaryFS(true);
|
||||
return false;
|
||||
}
|
||||
_secondary_authority_unknown = false;
|
||||
_contact_load_incomplete = false;
|
||||
_identity_creation_blocked = false;
|
||||
_prefs_load_incomplete = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const mesh::storage::InternalSecondaryFsRecoveryResult result =
|
||||
mesh::storage::recoverInternalSecondaryFilesystem(
|
||||
[this]() -> bool {
|
||||
@@ -306,14 +321,16 @@ bool DataStore::recoverInternalExtraFSOnBoot() {
|
||||
return true;
|
||||
},
|
||||
[this]() -> bool {
|
||||
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS remains unusable; rebuilding 100 KiB (secondary-only data may be lost)");
|
||||
return reinitializeInternalExtraFS();
|
||||
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS remains unusable after mount retry; scanning and rebuilding (secondary-only data may be lost)");
|
||||
return reinitializeInternalExtraFS(true);
|
||||
});
|
||||
if (result == mesh::storage::InternalSecondaryFsRecoveryResult::Failed) {
|
||||
disableSecondaryFS(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
extra->acknowledgeRecoveredBootHint();
|
||||
|
||||
// This runs only before migration and user-data loading. Clear the initial
|
||||
// mount quarantine, not errors from a later incomplete contact/prefs load.
|
||||
// Migration below establishes authority again and can re-latch any error.
|
||||
@@ -324,13 +341,15 @@ bool DataStore::recoverInternalExtraFSOnBoot() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DataStore::reinitializeInternalExtraFS() {
|
||||
bool DataStore::reinitializeInternalExtraFS(bool scan_physical_pages) {
|
||||
if (_configuredFsExtra == nullptr) return false;
|
||||
|
||||
CustomLFS* extra = static_cast<CustomLFS*>(_configuredFsExtra);
|
||||
ResilientInternalExtraFS* extra =
|
||||
static_cast<ResilientInternalExtraFS*>(_configuredFsExtra);
|
||||
if (!mesh::storage::isExpectedInternalExtraFsGeometry(
|
||||
extra->getFlashAddr(), extra->getFlashSize(),
|
||||
extra->getBlockSize())
|
||||
|| !extra->pageMapReady()
|
||||
|| !mesh::storage::isInternalExtraFsReservedByApplication(
|
||||
(uint32_t)(uintptr_t)__flash_arduino_end)) {
|
||||
_fsExtra = nullptr;
|
||||
@@ -338,6 +357,16 @@ bool DataStore::reinitializeInternalExtraFS() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A physical test is destructive and must run only during early boot, before
|
||||
// radio/UI tasks can issue competing SoftDevice flash operations. Ordinary
|
||||
// USB repair and factory-reset paths retain the already-verified page map.
|
||||
if ((scan_physical_pages && !extra->scanAndRetireBadPages())
|
||||
|| (extra->pageMapNeedsSave() && !extra->savePageMap(*_fs))) {
|
||||
_fsExtra = nullptr;
|
||||
MESH_DEBUG_PRINTLN("DataStore: ExtraFS physical scan or bad-page map save failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
const mesh::storage::InternalSecondaryFsRepairResult result =
|
||||
mesh::storage::repairInternalSecondaryFilesystem(
|
||||
[extra]() -> bool {
|
||||
@@ -359,12 +388,15 @@ bool DataStore::reinitializeInternalExtraFS() {
|
||||
_fsExtra = nullptr;
|
||||
switch (result) {
|
||||
case mesh::storage::InternalSecondaryFsRepairResult::FormatFailed:
|
||||
extra->setStage(ResilientInternalExtraFS::Stage::FormatFailed);
|
||||
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS repair format failed");
|
||||
break;
|
||||
case mesh::storage::InternalSecondaryFsRepairResult::MountFailed:
|
||||
extra->setStage(ResilientInternalExtraFS::Stage::MountFailed);
|
||||
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS repair mount failed");
|
||||
break;
|
||||
case mesh::storage::InternalSecondaryFsRepairResult::ValidationFailed:
|
||||
extra->setStage(ResilientInternalExtraFS::Stage::ValidationFailed);
|
||||
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS repair validation failed");
|
||||
break;
|
||||
default:
|
||||
@@ -374,6 +406,7 @@ bool DataStore::reinitializeInternalExtraFS() {
|
||||
}
|
||||
|
||||
_fsExtra = _configuredFsExtra;
|
||||
extra->setStage(ResilientInternalExtraFS::Stage::Repaired);
|
||||
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS repaired and reactivated");
|
||||
return true;
|
||||
}
|
||||
@@ -648,6 +681,38 @@ bool DataStore::repairInternalExtraFS() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
bool DataStore::requestInternalExtraFSBootScan() {
|
||||
if (_primary_storage_unavailable || _configuredFsExtra == nullptr) {
|
||||
return false;
|
||||
}
|
||||
ResilientInternalExtraFS* extra =
|
||||
static_cast<ResilientInternalExtraFS*>(_configuredFsExtra);
|
||||
return extra->requestBootScan();
|
||||
}
|
||||
|
||||
bool DataStore::formatInternalExtraFSHealth(char* reply,
|
||||
size_t reply_size) const {
|
||||
if (reply == nullptr || reply_size == 0 || _configuredFsExtra == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const ResilientInternalExtraFS* extra =
|
||||
static_cast<const ResilientInternalExtraFS*>(_configuredFsExtra);
|
||||
snprintf(reply, reply_size,
|
||||
"ExtraFS %s; bad=0x%07lX; detected=0x%07lX; pending=0x%07lX; usable=%luK; mount=%s; last=%s; scanned=%u; boot=0x%02X",
|
||||
extra->pageMapReady() ? "mapped" : "map-unreadable",
|
||||
(unsigned long)extra->badPages(),
|
||||
(unsigned long)extra->detectedBadPages(),
|
||||
(unsigned long)extra->pendingPages(),
|
||||
(unsigned long)(extra->usableBytes() / 1024),
|
||||
_fsExtra == _configuredFsExtra ? "active" : "unavailable",
|
||||
ResilientInternalExtraFS::stageName(extra->stage()),
|
||||
(unsigned)extra->scannedPages(),
|
||||
(unsigned)extra->bootMarkerAtInit());
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool DataStore::loadMainIdentity(mesh::LocalIdentity &identity) {
|
||||
#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
|
||||
if (!identity_store.recover("_main")) {
|
||||
|
||||
@@ -90,7 +90,7 @@ class DataStore
|
||||
void resetContactPageState(bool clear_incomplete = false);
|
||||
#if defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
bool recoverInternalExtraFSOnBoot();
|
||||
bool reinitializeInternalExtraFS();
|
||||
bool reinitializeInternalExtraFS(bool scan_physical_pages = false);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -106,6 +106,10 @@ public:
|
||||
void begin();
|
||||
bool formatFileSystem();
|
||||
bool repairInternalExtraFS();
|
||||
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
bool requestInternalExtraFSBootScan();
|
||||
bool formatInternalExtraFSHealth(char* reply, size_t reply_size) const;
|
||||
#endif
|
||||
FILESYSTEM* getPrimaryFS() const { return _fs; }
|
||||
FILESYSTEM* getSecondaryFS() const { return _fsExtra; }
|
||||
void markPrimaryFSUnavailable();
|
||||
|
||||
@@ -2676,6 +2676,12 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
if (strcmp(command, "get flash.health") == 0) {
|
||||
return _store->formatInternalExtraFSHealth(reply, reply_size);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (strncmp(command, "get ", 4) == 0 &&
|
||||
mesh::cli::handleStorageLayoutGet(command + 4, board, reply,
|
||||
reply_size)) {
|
||||
@@ -6036,7 +6042,9 @@ void MyMesh::repairInternalExtraFS(Stream& output) {
|
||||
|
||||
const bool contacts_saved = _store->saveContacts(this, save_filter);
|
||||
const bool channels_saved = saveChannels();
|
||||
const bool capacity_ready = _store->getStorageTotalKb() == 100;
|
||||
const uint32_t capacity_kib = _store->getStorageTotalKb();
|
||||
const bool capacity_ready = capacity_kib >= 84 && capacity_kib <= 100
|
||||
&& (capacity_kib % 4) == 0;
|
||||
if (contacts_saved && channels_saved && capacity_ready
|
||||
&& !_store->hasPendingContactWrites()) {
|
||||
mesh::resetLazyPersistenceAfterSuccess(
|
||||
@@ -6054,6 +6062,24 @@ void MyMesh::repairInternalExtraFS(Stream& output) {
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::scanInternalExtraFS(Stream& output) {
|
||||
if (!_store->requestInternalExtraFSBootScan()) {
|
||||
output.println(" Error: could not record the boot-time ExtraFS scan request");
|
||||
char health[160];
|
||||
if (_store->formatInternalExtraFSHealth(health, sizeof(health))) {
|
||||
output.printf(" > %s\n", health);
|
||||
}
|
||||
return;
|
||||
}
|
||||
output.println(" > boot-time physical scan scheduled; ExtraFS files will be erased; rebooting");
|
||||
// This command deliberately discards the secondary filesystem. The normal
|
||||
// scheduled-reboot path refuses to reset when unsaved contacts exist, which
|
||||
// would strand the durable scan request on a damaged store.
|
||||
output.flush();
|
||||
delay(1000);
|
||||
board.reboot();
|
||||
}
|
||||
|
||||
#if defined(MESHCORE_EXTRAFS_HIL)
|
||||
bool MyMesh::handleExtraFsHilCommand(const char* command, char* reply,
|
||||
size_t reply_size) {
|
||||
@@ -8349,6 +8375,8 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
} else if (strcmp(command, "repair extrafs") == 0) {
|
||||
repairInternalExtraFS(terminalOutput());
|
||||
} else if (strcmp(command, "scan extrafs erase") == 0) {
|
||||
scanInternalExtraFS(terminalOutput());
|
||||
#endif
|
||||
} else if (strcmp(command, "ver") == 0) {
|
||||
terminalOutput().printf("Companion %s (protocol %u, build %s)\r\n",
|
||||
@@ -8362,6 +8390,9 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
terminalOutput().print(" board\r\n");
|
||||
terminalOutput().print(" version\r\n");
|
||||
terminalOutput().print(" get storage.layout\r\n");
|
||||
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
terminalOutput().print(" get flash.health\r\n");
|
||||
#endif
|
||||
#if COMPANION_FEATURE_READER
|
||||
terminalOutput().print(" get reader <chapter>:<verse> (World English Bible, offline)\r\n");
|
||||
#endif
|
||||
@@ -8453,6 +8484,7 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
terminalOutput().print(" channel <name-or-slot> <text>\r\n");
|
||||
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
terminalOutput().print(" repair extrafs (erases/rebuilds internal ExtraFS)\r\n");
|
||||
terminalOutput().print(" scan extrafs erase (USB only; reboot for destructive page test)\r\n");
|
||||
#endif
|
||||
#if COMPANION_FEATURE_TEMP_RADIO || COMPANION_FEATURE_OTA_CLI
|
||||
#if COMPANION_FEATURE_TEMP_RADIO
|
||||
@@ -8899,6 +8931,8 @@ void MyMesh::checkCLIRescueCmd() {
|
||||
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
} else if (strcmp(cli_command, "repair extrafs") == 0) {
|
||||
repairInternalExtraFS(output);
|
||||
} else if (strcmp(cli_command, "scan extrafs erase") == 0) {
|
||||
scanInternalExtraFS(output);
|
||||
#endif
|
||||
} else if (strcmp(cli_command, "rebuild") == 0) {
|
||||
bool success = _store->formatFileSystem();
|
||||
|
||||
@@ -512,6 +512,7 @@ private:
|
||||
uint16_t released_slot);
|
||||
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
void repairInternalExtraFS(Stream& output);
|
||||
void scanInternalExtraFS(Stream& output);
|
||||
#if defined(MESHCORE_EXTRAFS_HIL)
|
||||
bool handleExtraFsHilCommand(const char* command, char* reply,
|
||||
size_t reply_size);
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
#include "ResilientInternalExtraFS.h"
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <helpers/AtomicFileWriter.h>
|
||||
#include <helpers/nrf52/SoftDeviceState.h>
|
||||
#include <nrf_soc.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
constexpr char PAGE_MAP_PATH[] = "/extrafs.badpages";
|
||||
constexpr uint32_t PAGE_MAP_MAGIC = 0x31475042UL; // "BPG1" on nRF52
|
||||
#if defined(NRF52840_XXAA)
|
||||
constexpr uint32_t PRIMARY_START = 0xED000UL;
|
||||
#else
|
||||
constexpr uint32_t PRIMARY_START = 0x6D000UL;
|
||||
#endif
|
||||
constexpr uint32_t PRIMARY_SIZE = 7UL * FLASH_NRF52_PAGE_SIZE;
|
||||
|
||||
StaticSemaphore_t shared_flash_lock_storage;
|
||||
SemaphoreHandle_t shared_flash_lock = nullptr;
|
||||
|
||||
void feedInheritedWatchdog() {
|
||||
if (NRF_WDT->RUNSTATUS == 0) return;
|
||||
const uint32_t enabled = NRF_WDT->RREN & 0xFFUL;
|
||||
for (uint8_t channel = 0; channel < 8; ++channel) {
|
||||
if ((enabled & (1UL << channel)) != 0) {
|
||||
NRF_WDT->RR[channel] = WDT_RR_RR_Reload;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FlashLock {
|
||||
public:
|
||||
FlashLock() {
|
||||
if (shared_flash_lock == nullptr) {
|
||||
shared_flash_lock = xSemaphoreCreateMutexStatic(&shared_flash_lock_storage);
|
||||
}
|
||||
xSemaphoreTake(shared_flash_lock, portMAX_DELAY);
|
||||
}
|
||||
~FlashLock() { xSemaphoreGive(shared_flash_lock); }
|
||||
};
|
||||
|
||||
int primaryRead(const struct lfs_config*, lfs_block_t block, lfs_off_t off,
|
||||
void* buffer, lfs_size_t size) {
|
||||
FlashLock lock;
|
||||
return flash_nrf5x_read(buffer, PRIMARY_START + block * 128 + off, size)
|
||||
== static_cast<int>(size) ? 0 : LFS_ERR_IO;
|
||||
}
|
||||
|
||||
int primaryProg(const struct lfs_config*, lfs_block_t block, lfs_off_t off,
|
||||
const void* buffer, lfs_size_t size) {
|
||||
FlashLock lock;
|
||||
return flash_nrf5x_write(PRIMARY_START + block * 128 + off, buffer, size)
|
||||
== static_cast<int>(size) ? 0 : LFS_ERR_IO;
|
||||
}
|
||||
|
||||
int primaryErase(const struct lfs_config*, lfs_block_t block) {
|
||||
FlashLock lock;
|
||||
const uint32_t address = PRIMARY_START + block * 128;
|
||||
for (uint32_t offset = 0; offset < 128; offset++) {
|
||||
if (flash_nrf5x_write8(address + offset, 0xFF) != 1) return LFS_ERR_IO;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int primarySync(const struct lfs_config*) {
|
||||
FlashLock lock;
|
||||
flash_nrf5x_flush();
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct PageMapRecord {
|
||||
uint32_t magic;
|
||||
uint32_t bad_pages;
|
||||
uint32_t pending_pages;
|
||||
uint32_t check;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
using mesh::storage::countInternalExtraFsBadPages;
|
||||
|
||||
struct lfs_config* ResilientInternalExtraFS::primaryConfig() {
|
||||
static struct lfs_config config = {};
|
||||
config.read = primaryRead;
|
||||
config.prog = primaryProg;
|
||||
config.erase = primaryErase;
|
||||
config.sync = primarySync;
|
||||
config.read_size = 128;
|
||||
config.prog_size = 128;
|
||||
config.block_size = 128;
|
||||
config.block_count = PRIMARY_SIZE / 128;
|
||||
config.lookahead = 128;
|
||||
return &config;
|
||||
}
|
||||
|
||||
ResilientInternalExtraFS::ResilientInternalExtraFS(
|
||||
uint32_t flash_addr, uint32_t flash_size, uint32_t block_size)
|
||||
: CustomLFS(flash_addr, flash_size, block_size) {
|
||||
// Capture this before board power management can consume GPREGRET2. The
|
||||
// bootloader leaves this register alone for an ordinary app reset.
|
||||
const uint8_t retained = static_cast<uint8_t>(NRF_POWER->GPREGRET2);
|
||||
_boot_marker_at_init = retained;
|
||||
if (retained == BOOT_SCAN_ALL) {
|
||||
_boot_scan_requested = true;
|
||||
_boot_scan_forced = true;
|
||||
} else if (retained >= BOOT_SCAN_BAD_PAGE_BASE
|
||||
&& retained < BOOT_SCAN_BAD_PAGE_BASE + PAGE_COUNT) {
|
||||
_retained_bad_page = retained - BOOT_SCAN_BAD_PAGE_BASE;
|
||||
_boot_scan_requested = true;
|
||||
}
|
||||
_configure_lfs();
|
||||
}
|
||||
|
||||
void ResilientInternalExtraFS::_configure_lfs() {
|
||||
CustomLFS::_configure_lfs();
|
||||
_lfs_config.read = readCallback;
|
||||
_lfs_config.prog = progCallback;
|
||||
_lfs_config.erase = eraseCallback;
|
||||
_lfs_config.sync = syncCallback;
|
||||
_lfs_config.block_count =
|
||||
(PAGE_COUNT - countInternalExtraFsBadPages(_bad_pages)) * BLOCKS_PER_PAGE;
|
||||
}
|
||||
|
||||
uint32_t ResilientInternalExtraFS::usableBytes() const {
|
||||
return (PAGE_COUNT - countInternalExtraFsBadPages(_bad_pages)) * PAGE_SIZE;
|
||||
}
|
||||
|
||||
bool ResilientInternalExtraFS::loadPageMap(Adafruit_LittleFS& primary) {
|
||||
_map_ready = false;
|
||||
_bad_pages = 0;
|
||||
_pending_pages = 0;
|
||||
struct lfs_info info;
|
||||
const int stat_result = lfs_stat(primary._getFS(), PAGE_MAP_PATH, &info);
|
||||
if (stat_result == LFS_ERR_NOENT) {
|
||||
_configure_lfs();
|
||||
_map_ready = true;
|
||||
return true;
|
||||
}
|
||||
if (stat_result != LFS_ERR_OK || info.type != LFS_TYPE_REG
|
||||
|| info.size != sizeof(PageMapRecord)) return false;
|
||||
|
||||
File file = primary.open(PAGE_MAP_PATH, FILE_O_READ);
|
||||
if (!file) return false;
|
||||
PageMapRecord record = {};
|
||||
const int read_count = file.read(reinterpret_cast<uint8_t*>(&record),
|
||||
sizeof(record));
|
||||
file.close();
|
||||
if (read_count != sizeof(record) || record.magic != PAGE_MAP_MAGIC
|
||||
|| record.check != ~(record.magic ^ record.bad_pages
|
||||
^ record.pending_pages)
|
||||
|| (record.bad_pages & ~PAGE_MASK) != 0
|
||||
|| (record.pending_pages & ~(PAGE_MASK | SCAN_REQUEST)) != 0
|
||||
|| (record.bad_pages & record.pending_pages & PAGE_MASK) != 0
|
||||
|| countInternalExtraFsBadPages(
|
||||
record.bad_pages | (record.pending_pages & PAGE_MASK))
|
||||
> MAX_BAD_PAGES) {
|
||||
return false;
|
||||
}
|
||||
_bad_pages = record.bad_pages;
|
||||
_pending_pages = record.pending_pages;
|
||||
_recorded_bad_pages = record.bad_pages;
|
||||
_recorded_pending_pages = record.pending_pages;
|
||||
_configure_lfs();
|
||||
_map_ready = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResilientInternalExtraFS::savePageMap(Adafruit_LittleFS& primary) {
|
||||
if (!_map_ready) return false;
|
||||
const PageMapRecord record = {
|
||||
PAGE_MAP_MAGIC, _bad_pages, _pending_pages,
|
||||
~(PAGE_MAP_MAGIC ^ _bad_pages ^ _pending_pages)};
|
||||
mesh::AtomicFileWriter writer(&primary, PAGE_MAP_PATH);
|
||||
const bool saved = writer
|
||||
&& writer.write(reinterpret_cast<const uint8_t*>(&record),
|
||||
sizeof(record)) == sizeof(record)
|
||||
&& writer.commit();
|
||||
if (saved) {
|
||||
_recorded_bad_pages = _bad_pages;
|
||||
_recorded_pending_pages = _pending_pages;
|
||||
}
|
||||
_stage = saved ? Stage::MapSaved : Stage::MapSaveFailed;
|
||||
return saved;
|
||||
}
|
||||
|
||||
bool ResilientInternalExtraFS::pageMapNeedsSave() const {
|
||||
return _bad_pages != _recorded_bad_pages
|
||||
|| _pending_pages != _recorded_pending_pages;
|
||||
}
|
||||
|
||||
bool ResilientInternalExtraFS::setBootScanRequest(uint8_t value) {
|
||||
uint8_t sd_enabled = 0;
|
||||
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS) return false;
|
||||
if (sd_enabled) {
|
||||
volatile uint32_t readback = 0;
|
||||
return sd_power_gpregret_clr(1, 0xFF) == NRF_SUCCESS
|
||||
&& sd_power_gpregret_set(1, value) == NRF_SUCCESS
|
||||
&& sd_power_gpregret_get(1, (uint32_t*)&readback) == NRF_SUCCESS
|
||||
&& static_cast<uint8_t>(readback) == value;
|
||||
}
|
||||
NRF_POWER->GPREGRET2 = value;
|
||||
return static_cast<uint8_t>(NRF_POWER->GPREGRET2) == value;
|
||||
}
|
||||
|
||||
bool ResilientInternalExtraFS::clearBootScanRequest() {
|
||||
if (!_boot_scan_requested) return true;
|
||||
uint8_t sd_enabled = 0;
|
||||
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS) return false;
|
||||
if (sd_enabled) {
|
||||
if (sd_power_gpregret_clr(1, 0xFF) != NRF_SUCCESS) return false;
|
||||
} else {
|
||||
NRF_POWER->GPREGRET2 = 0;
|
||||
}
|
||||
_boot_scan_requested = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResilientInternalExtraFS::requestBootScan() {
|
||||
if (!_map_ready) return false;
|
||||
// Do not touch the primary filesystem while radio/BLE tasks are running.
|
||||
// The suspect 4 KiB page (if known) fits in one retained byte. A fault hint
|
||||
// first gets a non-destructive mount and traversal at boot; only an explicit
|
||||
// user scan or unrecoverable filesystem triggers the destructive test.
|
||||
uint8_t marker = BOOT_SCAN_ALL;
|
||||
if ((_pending_pages & PAGE_MASK) != 0) {
|
||||
for (uint8_t page = 0; page < PAGE_COUNT; ++page) {
|
||||
if ((_pending_pages & (1UL << page)) != 0) {
|
||||
marker = BOOT_SCAN_BAD_PAGE_BASE + page;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return setBootScanRequest(marker);
|
||||
}
|
||||
|
||||
void ResilientInternalExtraFS::acknowledgeRecoveredBootHint() {
|
||||
// A healthy mount and successful file traversal are enough to preserve
|
||||
// the existing filesystem after a transient runtime flash error. The
|
||||
// hint remains a diagnostic only; it is not a bad-page verdict.
|
||||
if (!_boot_scan_forced) {
|
||||
clearBootScanRequest();
|
||||
_pending_pages &= SCAN_REQUEST;
|
||||
_retained_bad_page = 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
const char* ResilientInternalExtraFS::stageName(Stage stage) {
|
||||
switch (stage) {
|
||||
case Stage::Ready: return "ready";
|
||||
case Stage::Scanning: return "scanning";
|
||||
case Stage::TooManyBadPages: return "too-many-bad-pages";
|
||||
case Stage::Scanned: return "scanned";
|
||||
case Stage::MapSaveFailed: return "map-save-failed";
|
||||
case Stage::MapSaved: return "map-saved";
|
||||
case Stage::FormatFailed: return "format-failed";
|
||||
case Stage::MountFailed: return "mount-failed";
|
||||
case Stage::ValidationFailed: return "validation-failed";
|
||||
case Stage::Repaired: return "repaired";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
uint32_t ResilientInternalExtraFS::physicalPageForBlock(
|
||||
lfs_block_t block) const {
|
||||
if (block >= _lfs_config.block_count) return INVALID_PAGE;
|
||||
return mesh::storage::internalExtraFsPhysicalPage(
|
||||
_bad_pages, block / BLOCKS_PER_PAGE);
|
||||
}
|
||||
|
||||
void ResilientInternalExtraFS::retirePending(uint32_t physical_page) {
|
||||
if (physical_page < PAGE_COUNT) {
|
||||
_pending_pages |= 1UL << physical_page;
|
||||
}
|
||||
_io_failed = true;
|
||||
_dirty = false;
|
||||
_cached_page = INVALID_PAGE;
|
||||
}
|
||||
|
||||
int ResilientInternalExtraFS::commitPage() {
|
||||
if (_io_failed) return LFS_ERR_CORRUPT;
|
||||
if (!_dirty) return 0;
|
||||
const uint32_t address = _flash_addr + _cached_page * PAGE_SIZE;
|
||||
if (flash_nrf5x_write(address, _page_buffer, PAGE_SIZE) != PAGE_SIZE) {
|
||||
retirePending(_cached_page);
|
||||
return LFS_ERR_CORRUPT;
|
||||
}
|
||||
flash_nrf5x_flush();
|
||||
// Read actual flash, not flash_nrf5x_read(): that API may return its RAM
|
||||
// cache even when the physical erase/program silently failed.
|
||||
if (memcmp(reinterpret_cast<const void*>(address), _page_buffer,
|
||||
PAGE_SIZE) != 0) {
|
||||
retirePending(_cached_page);
|
||||
return LFS_ERR_CORRUPT;
|
||||
}
|
||||
_dirty = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ResilientInternalExtraFS::loadPage(uint32_t physical_page) {
|
||||
if (_io_failed || physical_page >= PAGE_COUNT) return LFS_ERR_CORRUPT;
|
||||
if (physical_page == _cached_page) return 0;
|
||||
const int result = commitPage();
|
||||
if (result != 0) return result;
|
||||
flash_nrf5x_flush();
|
||||
memcpy(_page_buffer,
|
||||
reinterpret_cast<const void*>(_flash_addr + physical_page * PAGE_SIZE),
|
||||
PAGE_SIZE);
|
||||
_cached_page = physical_page;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ResilientInternalExtraFS::readCallback(
|
||||
const struct lfs_config* config, lfs_block_t block, lfs_off_t off,
|
||||
void* buffer, lfs_size_t size) {
|
||||
FlashLock lock;
|
||||
auto* fs = static_cast<ResilientInternalExtraFS*>(config->context);
|
||||
const uint32_t page = fs->physicalPageForBlock(block);
|
||||
if (page == INVALID_PAGE || off > fs->_block_size
|
||||
|| size > fs->_block_size - off || fs->loadPage(page) != 0) {
|
||||
return LFS_ERR_CORRUPT;
|
||||
}
|
||||
memcpy(buffer, fs->_page_buffer + (block % BLOCKS_PER_PAGE) * fs->_block_size
|
||||
+ off, size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ResilientInternalExtraFS::progCallback(
|
||||
const struct lfs_config* config, lfs_block_t block, lfs_off_t off,
|
||||
const void* buffer, lfs_size_t size) {
|
||||
FlashLock lock;
|
||||
auto* fs = static_cast<ResilientInternalExtraFS*>(config->context);
|
||||
const uint32_t page = fs->physicalPageForBlock(block);
|
||||
if (page == INVALID_PAGE || off > fs->_block_size
|
||||
|| size > fs->_block_size - off || fs->loadPage(page) != 0) {
|
||||
return LFS_ERR_CORRUPT;
|
||||
}
|
||||
memcpy(fs->_page_buffer + (block % BLOCKS_PER_PAGE) * fs->_block_size + off,
|
||||
buffer, size);
|
||||
fs->_dirty = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ResilientInternalExtraFS::eraseCallback(
|
||||
const struct lfs_config* config, lfs_block_t block) {
|
||||
FlashLock lock;
|
||||
auto* fs = static_cast<ResilientInternalExtraFS*>(config->context);
|
||||
const uint32_t page = fs->physicalPageForBlock(block);
|
||||
if (page == INVALID_PAGE || fs->loadPage(page) != 0) {
|
||||
return LFS_ERR_CORRUPT;
|
||||
}
|
||||
memset(fs->_page_buffer + (block % BLOCKS_PER_PAGE) * fs->_block_size,
|
||||
0xFF, fs->_block_size);
|
||||
fs->_dirty = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ResilientInternalExtraFS::syncCallback(const struct lfs_config* config) {
|
||||
FlashLock lock;
|
||||
auto* fs = static_cast<ResilientInternalExtraFS*>(config->context);
|
||||
return fs->commitPage();
|
||||
}
|
||||
|
||||
bool ResilientInternalExtraFS::rawPageEquals(uint32_t address,
|
||||
uint8_t value) const {
|
||||
const volatile uint8_t* raw =
|
||||
reinterpret_cast<const volatile uint8_t*>(address);
|
||||
for (uint32_t offset = 0; offset < PAGE_SIZE; offset++) {
|
||||
if (raw[offset] != value) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResilientInternalExtraFS::testPhysicalPage(uint32_t physical_page) {
|
||||
const uint32_t address = _flash_addr + physical_page * PAGE_SIZE;
|
||||
for (uint8_t attempt = 0; attempt < 2; attempt++) {
|
||||
feedInheritedWatchdog();
|
||||
flash_nrf5x_flush();
|
||||
if (!flash_nrf5x_erase(address) || !rawPageEquals(address, 0xFF)) continue;
|
||||
feedInheritedWatchdog();
|
||||
memset(_page_buffer, 0, PAGE_SIZE);
|
||||
if (flash_nrf5x_write(address, _page_buffer, PAGE_SIZE) != PAGE_SIZE) {
|
||||
continue;
|
||||
}
|
||||
flash_nrf5x_flush();
|
||||
if (!rawPageEquals(address, 0x00)) continue;
|
||||
feedInheritedWatchdog();
|
||||
if (flash_nrf5x_erase(address) && rawPageEquals(address, 0xFF)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ResilientInternalExtraFS::scanAndRetireBadPages() {
|
||||
if (!_map_ready || _flash_addr != 0xD4000UL || _flash_total_size != 0x19000UL
|
||||
|| _block_size != 128) return false;
|
||||
if (!clearBootScanRequest()) return false;
|
||||
end();
|
||||
_cached_page = INVALID_PAGE;
|
||||
_dirty = false;
|
||||
_io_failed = false;
|
||||
_stage = Stage::Scanning;
|
||||
_scanned_pages = 0;
|
||||
_detected_bad_pages = 0;
|
||||
|
||||
// A runtime readback mismatch is a reason to scan, not proof of a dead
|
||||
// physical page: SoftDevice contention can produce transient failures.
|
||||
// Retire only a page that fails the isolated early-boot erase/write/read
|
||||
// test on both attempts.
|
||||
uint32_t bad = _bad_pages;
|
||||
if (countInternalExtraFsBadPages(bad) > MAX_BAD_PAGES) {
|
||||
_stage = Stage::TooManyBadPages;
|
||||
return false;
|
||||
}
|
||||
for (uint32_t page = 0; page < PAGE_COUNT; page++) {
|
||||
feedInheritedWatchdog();
|
||||
if ((bad & (1UL << page)) != 0) continue;
|
||||
if (!testPhysicalPage(page)) {
|
||||
bad |= 1UL << page;
|
||||
_detected_bad_pages |= 1UL << page;
|
||||
if (countInternalExtraFsBadPages(bad) > MAX_BAD_PAGES) {
|
||||
_stage = Stage::TooManyBadPages;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_scanned_pages++;
|
||||
}
|
||||
_bad_pages = bad;
|
||||
_pending_pages = 0;
|
||||
_configure_lfs();
|
||||
_stage = Stage::Scanned;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
|
||||
#include <CustomLFS.h>
|
||||
#include <helpers/nrf52/InternalExtraFsPageMap.h>
|
||||
|
||||
// The T1000-E and other nRF52 internal-ExtraFS companions use 25 physical
|
||||
// 4 KiB pages. LittleFS sees 128-byte blocks; a failed physical page must be
|
||||
// removed as a whole, not treated as 32 unrelated bad blocks.
|
||||
class ResilientInternalExtraFS : public CustomLFS {
|
||||
public:
|
||||
enum class Stage : uint8_t {
|
||||
Ready, Scanning, TooManyBadPages, Scanned, MapSaveFailed,
|
||||
MapSaved, FormatFailed, MountFailed, ValidationFailed, Repaired
|
||||
};
|
||||
static constexpr uint32_t PAGE_COUNT =
|
||||
mesh::storage::INTERNAL_EXTRAFS_PAGE_COUNT;
|
||||
static constexpr uint32_t PAGE_SIZE = FLASH_NRF52_PAGE_SIZE;
|
||||
static constexpr uint32_t BLOCKS_PER_PAGE = PAGE_SIZE / 128;
|
||||
|
||||
ResilientInternalExtraFS(uint32_t flash_addr, uint32_t flash_size,
|
||||
uint32_t block_size);
|
||||
static struct lfs_config* primaryConfig();
|
||||
|
||||
// Read the per-device page map from primary identity storage before the
|
||||
// first secondary mount. An unreadable map must never be guessed as empty.
|
||||
bool loadPageMap(Adafruit_LittleFS& primary);
|
||||
bool pageMapReady() const { return _map_ready; }
|
||||
bool recoveryPending() const {
|
||||
return _boot_scan_forced || (_pending_pages & SCAN_REQUEST) != 0;
|
||||
}
|
||||
bool ioFailed() const { return _io_failed; }
|
||||
uint32_t badPages() const { return _bad_pages; }
|
||||
uint32_t pendingPages() const { return _pending_pages; }
|
||||
uint32_t usableBytes() const;
|
||||
Stage stage() const { return _stage; }
|
||||
uint8_t scannedPages() const { return _scanned_pages; }
|
||||
uint32_t detectedBadPages() const { return _detected_bad_pages; }
|
||||
uint8_t bootMarkerAtInit() const { return _boot_marker_at_init; }
|
||||
static const char* stageName(Stage stage);
|
||||
void setStage(Stage stage) { _stage = stage; }
|
||||
|
||||
// Only call after ExtraFS has been unmounted and its contents have been
|
||||
// declared unrecoverable. Tests every physical page by erase / program /
|
||||
// direct flash readback / erase, then maps LittleFS around bad pages.
|
||||
bool scanAndRetireBadPages();
|
||||
bool savePageMap(Adafruit_LittleFS& primary);
|
||||
bool pageMapNeedsSave() const;
|
||||
bool requestBootScan();
|
||||
void acknowledgeRecoveredBootHint();
|
||||
|
||||
protected:
|
||||
void _configure_lfs() override;
|
||||
|
||||
private:
|
||||
static constexpr uint32_t INVALID_PAGE = 0xFFFFFFFFUL;
|
||||
static constexpr uint32_t PAGE_MASK =
|
||||
mesh::storage::INTERNAL_EXTRAFS_PAGE_MASK;
|
||||
static constexpr uint32_t SCAN_REQUEST = 1UL << 31;
|
||||
static constexpr uint8_t MAX_BAD_PAGES = 4;
|
||||
// GPREGRET2 is retained across a software reset. These values are distinct
|
||||
// from the OTA/bootloader handoff and power-management reason codes.
|
||||
static constexpr uint8_t BOOT_SCAN_ALL = 0x9F;
|
||||
static constexpr uint8_t BOOT_SCAN_BAD_PAGE_BASE = 0x80;
|
||||
|
||||
uint32_t _bad_pages = 0;
|
||||
uint32_t _pending_pages = 0;
|
||||
bool _boot_scan_requested = false;
|
||||
bool _boot_scan_forced = false;
|
||||
uint8_t _boot_marker_at_init = 0;
|
||||
uint8_t _retained_bad_page = 0xFF;
|
||||
uint32_t _recorded_bad_pages = 0;
|
||||
uint32_t _recorded_pending_pages = 0;
|
||||
uint32_t _cached_page = INVALID_PAGE;
|
||||
bool _dirty = false;
|
||||
bool _io_failed = false;
|
||||
bool _map_ready = false;
|
||||
Stage _stage = Stage::Ready;
|
||||
uint8_t _scanned_pages = 0;
|
||||
uint32_t _detected_bad_pages = 0;
|
||||
alignas(4) uint8_t _page_buffer[PAGE_SIZE];
|
||||
|
||||
uint32_t physicalPageForBlock(lfs_block_t block) const;
|
||||
int loadPage(uint32_t physical_page);
|
||||
int commitPage();
|
||||
bool testPhysicalPage(uint32_t physical_page);
|
||||
bool rawPageEquals(uint32_t address, uint8_t value) const;
|
||||
void retirePending(uint32_t physical_page);
|
||||
bool setBootScanRequest(uint8_t value);
|
||||
bool clearBootScanRequest();
|
||||
|
||||
static int readCallback(const struct lfs_config* config, lfs_block_t block,
|
||||
lfs_off_t off, void* buffer, lfs_size_t size);
|
||||
static int progCallback(const struct lfs_config* config, lfs_block_t block,
|
||||
lfs_off_t off, const void* buffer, lfs_size_t size);
|
||||
static int eraseCallback(const struct lfs_config* config, lfs_block_t block);
|
||||
static int syncCallback(const struct lfs_config* config);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -198,9 +198,9 @@ static bool isNetworkTerminalActive();
|
||||
DataStore store(InternalFS, QSPIFlash, rtc_clock);
|
||||
#else
|
||||
#if defined(EXTRAFS)
|
||||
#include <CustomLFS.h>
|
||||
#include "ResilientInternalExtraFS.h"
|
||||
extern "C" uint32_t __flash_arduino_end[];
|
||||
CustomLFS ExtraFS(0xD4000, 0x19000, 128);
|
||||
ResilientInternalExtraFS ExtraFS(0xD4000, 0x19000, 128);
|
||||
DataStore store(InternalFS, ExtraFS, rtc_clock);
|
||||
#else
|
||||
DataStore store(InternalFS, rtc_clock);
|
||||
@@ -2799,7 +2799,12 @@ void setup() {
|
||||
const mesh::storage::InternalSecondaryFsBootResult primary_fs_boot =
|
||||
mesh::storage::prepareInternalSecondaryFilesystem(
|
||||
[]() -> bool {
|
||||
#if defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
return InternalFS.Adafruit_LittleFS::begin(
|
||||
ResilientInternalExtraFS::primaryConfig());
|
||||
#else
|
||||
return InternalFS.Adafruit_LittleFS::begin();
|
||||
#endif
|
||||
},
|
||||
[]() -> bool {
|
||||
return mesh::storage::isErasedFlashRange(
|
||||
@@ -2848,6 +2853,10 @@ void setup() {
|
||||
}
|
||||
#else
|
||||
#if defined(EXTRAFS)
|
||||
const bool extra_fs_map_ready =
|
||||
(primary_fs_boot == mesh::storage::InternalSecondaryFsBootResult::Mounted
|
||||
|| primary_fs_boot == mesh::storage::InternalSecondaryFsBootResult::InitializedBlank)
|
||||
&& ExtraFS.loadPageMap(InternalFS);
|
||||
const bool extra_fs_geometry_valid =
|
||||
mesh::storage::isExpectedInternalExtraFsGeometry(
|
||||
ExtraFS.getFlashAddr(), ExtraFS.getFlashSize(),
|
||||
@@ -2856,10 +2865,15 @@ void setup() {
|
||||
(uint32_t)(uintptr_t)__flash_arduino_end);
|
||||
// Initial mount is always non-destructive, even for blank media.
|
||||
// DataStore validates primary first, then retries/rebuilds ExtraFS.
|
||||
if (!extra_fs_geometry_valid
|
||||
if (!extra_fs_map_ready || !extra_fs_geometry_valid
|
||||
|| ExtraFS.recoveryPending()
|
||||
|| !ExtraFS.Adafruit_LittleFS::begin()) {
|
||||
if (!extra_fs_geometry_valid) {
|
||||
if (!extra_fs_map_ready) {
|
||||
MESH_DEBUG_PRINTLN("CustomLFS: bad-page map unreadable; preserving ExtraFS and blocking writes");
|
||||
} else if (!extra_fs_geometry_valid) {
|
||||
MESH_DEBUG_PRINTLN("CustomLFS: internal ExtraFS geometry is not 100 KiB; refusing to mount or erase it");
|
||||
} else if (ExtraFS.recoveryPending()) {
|
||||
MESH_DEBUG_PRINTLN("CustomLFS: recorded physical page fault; deferring scan until primary validation");
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("CustomLFS: internal ExtraFS mount failed; deferring automatic recovery until primary validation");
|
||||
}
|
||||
@@ -3185,6 +3199,21 @@ void loop() {
|
||||
expireUsbBinaryStartupProbeBeforeDispatch();
|
||||
#endif
|
||||
the_mesh.loop();
|
||||
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
// The flash-page number is retained across a software reset without trying
|
||||
// to write primary identity storage while radio/BLE tasks are running.
|
||||
// Early boot then verifies and records the page map before remounting.
|
||||
if (ExtraFS.ioFailed()) {
|
||||
static uint32_t last_fault_record_attempt = 0;
|
||||
if (static_cast<uint32_t>(millis() - last_fault_record_attempt) >= 5000) {
|
||||
last_fault_record_attempt = millis();
|
||||
if (ExtraFS.requestBootScan()) {
|
||||
MESH_DEBUG_PRINTLN("ExtraFS: physical write failed; rebooting for isolated page scan");
|
||||
board.reboot();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef RECOVERABLE_EXTERNAL_RADIO
|
||||
serviceCompanionRadioRecovery();
|
||||
#endif
|
||||
|
||||
@@ -154,6 +154,7 @@ platform_packages =
|
||||
platformio/toolchain-gccarmnoneeabi@^1.140201.0
|
||||
extra_scripts = create-uf2.py
|
||||
pre:scripts/nrf52_usb_power_fix.py
|
||||
pre:scripts/nrf52_internal_flash_fix.py
|
||||
pre:scripts/nrf52_internal_bootloader_link.py
|
||||
; Nordic's builder otherwise overwrites the LTO-aware archive tools.
|
||||
pre:scripts/nrf52_lto_toolchain.py
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Build-local fix for the nRF52 InternalFS SoftDevice flash wait.
|
||||
|
||||
GCC 14/LTO can treat the naked SVC wrapper as not writing through its pointer
|
||||
argument. It then folds wait_for_async_flash_op_completion() into an immediate
|
||||
success return, allowing LittleFS to read or rewrite a page before the async
|
||||
erase/program has completed. Keep the SoftDevice state volatile and add a
|
||||
compiler barrier after the SVC call. Do not modify PlatformIO's shared SDK.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
OLD_WAIT = """ uint8_t sd_en = 0;
|
||||
(void) sd_softdevice_is_enabled(&sd_en);
|
||||
|
||||
if (sd_en) {
|
||||
"""
|
||||
FIXED_WAIT = """ volatile uint8_t sd_en = 0;
|
||||
(void) sd_softdevice_is_enabled((uint8_t*) &sd_en);
|
||||
__asm volatile ("" ::: "memory");
|
||||
|
||||
if (sd_en) {
|
||||
"""
|
||||
|
||||
|
||||
def patched_source(source):
|
||||
source = source.replace("\r\n", "\n")
|
||||
if FIXED_WAIT in source and OLD_WAIT not in source:
|
||||
return source
|
||||
if source.count(OLD_WAIT) != 1:
|
||||
raise RuntimeError(
|
||||
"nRF52 flash fix: unrecognized InternalFS flash driver; review "
|
||||
"the framework update before building (shared SDK not modified)"
|
||||
)
|
||||
return source.replace(OLD_WAIT, FIXED_WAIT)
|
||||
|
||||
|
||||
def replace_driver(build_env, node):
|
||||
source = Path(node.srcnode().get_abspath())
|
||||
patched = patched_source(source.read_text(encoding="utf-8"))
|
||||
build_env.AppendUnique(CPPPATH=[str(source.parent)])
|
||||
destination = Path(build_env.subst("$BUILD_DIR")) / "patched-nrf52-flash" / source.name
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not destination.exists() or destination.read_text(encoding="utf-8") != patched:
|
||||
destination.write_text(patched, encoding="utf-8")
|
||||
print("nRF52 flash: build-local SoftDevice completion wait fix enabled")
|
||||
return build_env.File(str(destination))
|
||||
|
||||
|
||||
def install(build_env):
|
||||
build_env.AddBuildMiddleware(
|
||||
replace_driver,
|
||||
"*InternalFileSytem*src*flash*flash_nrf5x.c",
|
||||
)
|
||||
|
||||
|
||||
if "Import" in globals():
|
||||
Import("env")
|
||||
install(env)
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "NRF52Board.h"
|
||||
#include "PowerManagementUtils.h"
|
||||
#include "Nrf52BootloaderVersion.h"
|
||||
#include "nrf52/SoftDeviceState.h"
|
||||
#include <target.h>
|
||||
#ifdef USER_GPIO_CONTROL
|
||||
#include "UserGpioPinPolicy.h"
|
||||
@@ -103,7 +104,7 @@ bool NRF52Board::isUserGpioAvailable(uint8_t pin) const {
|
||||
|
||||
bool NRF52Board::rebootToUf2Bootloader() {
|
||||
uint8_t sd_enabled = 0;
|
||||
if (sd_softdevice_is_enabled(&sd_enabled) != NRF_SUCCESS) return false;
|
||||
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS) return false;
|
||||
|
||||
if (sd_enabled) {
|
||||
if (sd_power_gpregret_clr(0, 0xFF) != NRF_SUCCESS
|
||||
@@ -211,7 +212,7 @@ void NRF52Board::initPowerMgr() {
|
||||
// Clear registers for next boot
|
||||
// Note: At this point SoftDevice may or may not be enabled
|
||||
uint8_t sd_enabled = 0;
|
||||
sd_softdevice_is_enabled(&sd_enabled);
|
||||
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
||||
if (sd_enabled) {
|
||||
sd_power_reset_reason_clr(0xFFFFFFFF);
|
||||
sd_power_gpregret_clr(1, 0xFF);
|
||||
@@ -308,7 +309,7 @@ void NRF52Board::enterSystemOff(uint8_t reason) {
|
||||
|
||||
// Record shutdown reason in GPREGRET2
|
||||
uint8_t sd_enabled = 0;
|
||||
sd_softdevice_is_enabled(&sd_enabled);
|
||||
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
||||
if (sd_enabled) {
|
||||
sd_power_gpregret_clr(1, 0xFF);
|
||||
sd_power_gpregret_set(1, reason);
|
||||
@@ -399,7 +400,7 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
|
||||
void NRF52Board::armVbusWake() {
|
||||
// Configure VBUS (USB power) wake alongside (or instead of) LPCOMP.
|
||||
uint8_t sd_enabled = 0;
|
||||
sd_softdevice_is_enabled(&sd_enabled);
|
||||
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
||||
if (sd_enabled) {
|
||||
sd_power_usbdetected_enable(1);
|
||||
} else {
|
||||
@@ -415,7 +416,7 @@ void NRF52BoardDCDC::begin() {
|
||||
|
||||
// Enable DC/DC converter for improved power efficiency
|
||||
uint8_t sd_enabled = 0;
|
||||
sd_softdevice_is_enabled(&sd_enabled);
|
||||
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
||||
if (sd_enabled) {
|
||||
sd_power_dcdc_mode_set(NRF_POWER_DCDC_ENABLE);
|
||||
} else {
|
||||
@@ -426,7 +427,7 @@ void NRF52BoardDCDC::begin() {
|
||||
bool NRF52Board::isExternalPowered() {
|
||||
// Check if SoftDevice is enabled before using its API
|
||||
uint8_t sd_enabled = 0;
|
||||
sd_softdevice_is_enabled(&sd_enabled);
|
||||
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
||||
|
||||
if (sd_enabled) {
|
||||
uint32_t usb_status;
|
||||
@@ -470,7 +471,7 @@ void NRF52Board::sleep(uint32_t secs) {
|
||||
// On nRF52, we use event-driven sleep instead of timed sleep
|
||||
// The 'secs' parameter is ignored - we wake on any interrupt
|
||||
uint8_t sd_enabled = 0;
|
||||
sd_softdevice_is_enabled(&sd_enabled);
|
||||
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
||||
|
||||
if (sd_enabled) {
|
||||
// A single call is required here. If an interrupt arrived since the last
|
||||
@@ -492,7 +493,7 @@ void NRF52Board::sleep(uint32_t secs) {
|
||||
// Temperature from NRF52 MCU
|
||||
float NRF52Board::getMCUTemperature() {
|
||||
uint8_t sd_enabled = 0;
|
||||
sd_softdevice_is_enabled(&sd_enabled);
|
||||
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
||||
if (sd_enabled) {
|
||||
uint32_t err_code;
|
||||
int32_t temp;
|
||||
@@ -587,7 +588,7 @@ void NRF52Board::powerOff() {
|
||||
|
||||
// Enter SYSTEMOFF
|
||||
uint8_t sd_enabled = 0;
|
||||
sd_softdevice_is_enabled(&sd_enabled);
|
||||
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
||||
if (sd_enabled) { // SoftDevice is enabled
|
||||
sd_power_system_off();
|
||||
} else { // SoftDevice is not enable
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace storage {
|
||||
|
||||
static const uint32_t INTERNAL_EXTRAFS_PAGE_COUNT = 25;
|
||||
static const uint32_t INTERNAL_EXTRAFS_PAGE_MASK =
|
||||
(1UL << INTERNAL_EXTRAFS_PAGE_COUNT) - 1UL;
|
||||
|
||||
inline uint8_t countInternalExtraFsBadPages(uint32_t mask) {
|
||||
uint8_t count = 0;
|
||||
while (mask != 0) {
|
||||
count += static_cast<uint8_t>(mask & 1U);
|
||||
mask >>= 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// The old all-good layout is the identity map. Once a confirmed bad page is
|
||||
// retired, every logical 4 KiB page after it moves to the next good physical
|
||||
// page. A fixed 128-byte LittleFS block never straddles those page boundaries.
|
||||
inline uint32_t internalExtraFsPhysicalPage(uint32_t bad_mask,
|
||||
uint32_t logical_page) {
|
||||
if ((bad_mask & ~INTERNAL_EXTRAFS_PAGE_MASK) != 0
|
||||
|| logical_page >= INTERNAL_EXTRAFS_PAGE_COUNT
|
||||
- countInternalExtraFsBadPages(bad_mask)) {
|
||||
return UINT32_MAX;
|
||||
}
|
||||
for (uint32_t physical = 0; physical < INTERNAL_EXTRAFS_PAGE_COUNT;
|
||||
physical++) {
|
||||
if ((bad_mask & (1UL << physical)) != 0) continue;
|
||||
if (logical_page == 0) return physical;
|
||||
logical_page--;
|
||||
}
|
||||
return UINT32_MAX;
|
||||
}
|
||||
|
||||
} // namespace storage
|
||||
} // namespace mesh
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "SerialBLEInterface.h"
|
||||
#include "../BluetoothMac.h"
|
||||
#include "../CompanionFrameQueue.h"
|
||||
#include "SoftDeviceState.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "ble_gap.h"
|
||||
@@ -439,7 +440,7 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
|
||||
if (!Bluefruit.begin()) {
|
||||
uint8_t softdevice_enabled = 0;
|
||||
const uint32_t softdevice_status =
|
||||
sd_softdevice_is_enabled(&softdevice_enabled);
|
||||
mesh_nrf52::softdeviceIsEnabled(softdevice_enabled);
|
||||
ble_gap_addr_t address = {};
|
||||
const uint32_t gap_status = softdevice_enabled
|
||||
? sd_ble_gap_addr_get(&address) : NRF_ERROR_INVALID_STATE;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <nrf_sdm.h>
|
||||
|
||||
namespace mesh_nrf52 {
|
||||
|
||||
// sd_softdevice_is_enabled is an SVC wrapper. With LTO, its assembly body
|
||||
// does not tell GCC that the output pointer is written by the supervisor.
|
||||
// Load the result from volatile storage after a compiler memory barrier.
|
||||
inline uint32_t softdeviceIsEnabled(uint8_t& enabled) {
|
||||
volatile uint8_t state = 0;
|
||||
const uint32_t result = sd_softdevice_is_enabled((uint8_t*)&state);
|
||||
__asm volatile ("" ::: "memory");
|
||||
enabled = state;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace mesh_nrf52
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "nrf.h"
|
||||
#include "nrf_soc.h"
|
||||
#include "nrf_sdm.h"
|
||||
#include "../nrf52/SoftDeviceState.h"
|
||||
#if defined(OTA_SD_STORE)
|
||||
#include "OtaStoreSdNrf52.h"
|
||||
#endif
|
||||
@@ -467,7 +468,7 @@ static OtaStoreFlashNrf52* g_nrf52_apply_store = nullptr;
|
||||
|
||||
static void ota_nrf52_set_reset_handoff(uint8_t request, uint8_t source) {
|
||||
uint8_t sd_en = 0;
|
||||
sd_softdevice_is_enabled(&sd_en);
|
||||
mesh_nrf52::softdeviceIsEnabled(sd_en);
|
||||
if (sd_en) { // POWER is SD-restricted while the SoftDevice runs
|
||||
sd_power_gpregret_clr(1, 0xFFFFFFFF);
|
||||
sd_power_gpregret_set(1, source);
|
||||
@@ -481,7 +482,7 @@ static void ota_nrf52_set_reset_handoff(uint8_t request, uint8_t source) {
|
||||
|
||||
static bool ota_nrf52_clear_reset_reasons() {
|
||||
uint8_t sd_en = 0;
|
||||
if (sd_softdevice_is_enabled(&sd_en) != NRF_SUCCESS) return false;
|
||||
if (mesh_nrf52::softdeviceIsEnabled(sd_en) != NRF_SUCCESS) return false;
|
||||
if (sd_en) {
|
||||
return sd_power_reset_reason_clr(0xFFFFFFFFu) == NRF_SUCCESS;
|
||||
}
|
||||
@@ -527,7 +528,7 @@ void ota_reboot_to_bootloader_update() {
|
||||
source = GPREGRET2_OTA_STAGE_SD;
|
||||
#endif
|
||||
uint8_t sd_en = 0;
|
||||
sd_softdevice_is_enabled(&sd_en);
|
||||
mesh_nrf52::softdeviceIsEnabled(sd_en);
|
||||
if (sd_en) {
|
||||
sd_power_gpregret_clr(1, 0xFFFFFFFF);
|
||||
sd_power_gpregret_set(1, source);
|
||||
|
||||
@@ -63,6 +63,11 @@ struct ble_gap_conn_params_t { unsigned min_conn_interval, max_conn_interval, sl
|
||||
int sd_ble_gap_addr_set(ble_gap_addr_t*) { return NRF_SUCCESS; }
|
||||
int sd_ble_gap_addr_get(ble_gap_addr_t* a) { memset(a, 0, sizeof(*a)); return NRF_SUCCESS; }
|
||||
int sd_softdevice_is_enabled(uint8_t* enabled) { *enabled = 0; return NRF_SUCCESS; }
|
||||
namespace mesh_nrf52 {
|
||||
int softdeviceIsEnabled(uint8_t& enabled) {
|
||||
return sd_softdevice_is_enabled(&enabled);
|
||||
}
|
||||
}
|
||||
int sd_ble_gap_ppcp_set(ble_gap_conn_params_t*) { return NRF_SUCCESS; }
|
||||
namespace mesh {
|
||||
struct Logging { void println(const char*) {} };
|
||||
|
||||
@@ -363,7 +363,7 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
|
||||
|
||||
main = MAIN.read_text(encoding="utf-8")
|
||||
geometry = re.search(
|
||||
r"CustomLFS\s+ExtraFS\s*\(\s*(0x[0-9A-Fa-f]+)\s*,\s*"
|
||||
r"ResilientInternalExtraFS\s+ExtraFS\s*\(\s*(0x[0-9A-Fa-f]+)\s*,\s*"
|
||||
r"(0x[0-9A-Fa-f]+)\s*,\s*128\s*\)",
|
||||
main,
|
||||
)
|
||||
@@ -463,7 +463,7 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
|
||||
ready_at = recover.index("validateLfsFilesystem(_fsExtra)", policy_at)
|
||||
unmount_at = recover.index("extra->end()", ready_at)
|
||||
remount_at = recover.index("extra->Adafruit_LittleFS::begin()", unmount_at)
|
||||
repair_at = recover.index("reinitializeInternalExtraFS()", remount_at)
|
||||
repair_at = recover.index("reinitializeInternalExtraFS(true)", remount_at)
|
||||
self.assertLess(ready_at, unmount_at)
|
||||
self.assertLess(unmount_at, remount_at)
|
||||
self.assertLess(remount_at, repair_at)
|
||||
@@ -475,7 +475,7 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
|
||||
store = STORE.read_text(encoding="utf-8")
|
||||
recover = function_body(store, "bool DataStore::recoverInternalExtraFSOnBoot()")
|
||||
failure_at = recover.index("InternalSecondaryFsRecoveryResult::Failed")
|
||||
clear_at = recover.index("_secondary_authority_unknown = false;")
|
||||
clear_at = recover.rindex("_secondary_authority_unknown = false;")
|
||||
failure = recover[failure_at:clear_at]
|
||||
self.assertIn("disableSecondaryFS(true);", failure)
|
||||
self.assertIn("return false;", failure)
|
||||
@@ -509,7 +509,7 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
|
||||
self.assertNotIn("format()", active_retry)
|
||||
|
||||
reinitialize = function_body(
|
||||
store, "bool DataStore::reinitializeInternalExtraFS()"
|
||||
store, "bool DataStore::reinitializeInternalExtraFS(bool scan_physical_pages)"
|
||||
)
|
||||
geometry_at = reinitialize.index("isExpectedInternalExtraFsGeometry(")
|
||||
self.assertIn("isInternalExtraFsReservedByApplication(", reinitialize)
|
||||
@@ -944,7 +944,7 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
|
||||
# nonblank filesystem after any mount failure. The sole occurrence is
|
||||
# the separately scoped STM32 fallback.
|
||||
self.assertEqual(setup.count("InternalFS.begin();"), 1)
|
||||
self.assertGreater(setup.index("#else", primary_policy), primary_format)
|
||||
self.assertGreater(setup.index("#else\n InternalFS.begin();", primary_policy), primary_format)
|
||||
self.assertIn("store.disableSecondaryFS(true);", setup)
|
||||
secondary_geometry = setup.index(
|
||||
"const bool extra_fs_geometry_valid ="
|
||||
@@ -980,7 +980,7 @@ class Nrf52ExtraFsContractTest(unittest.TestCase):
|
||||
self.assertIn("_store->repairInternalExtraFS()", repair)
|
||||
self.assertIn("_store->saveContacts(this, save_filter)", repair)
|
||||
self.assertIn("const bool channels_saved = saveChannels();", repair)
|
||||
self.assertIn("_store->getStorageTotalKb() == 100", repair)
|
||||
self.assertIn("capacity_kib >= 84 && capacity_kib <= 100", repair)
|
||||
self.assertIn("getStorageTotalKb()", repair)
|
||||
|
||||
rescue = function_body(mesh, "void MyMesh::checkCLIRescueCmd()")
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Host coverage for physical-page retirement in nRF52 internal ExtraFS."""
|
||||
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class InternalExtraFsPageMapTest(unittest.TestCase):
|
||||
def test_mapping_skips_whole_bad_pages_and_keeps_legacy_identity_layout(self):
|
||||
source = r"""
|
||||
#include <helpers/nrf52/InternalExtraFsPageMap.h>
|
||||
#include <stdint.h>
|
||||
|
||||
using mesh::storage::countInternalExtraFsBadPages;
|
||||
using mesh::storage::internalExtraFsPhysicalPage;
|
||||
|
||||
int main() {
|
||||
for (uint32_t p = 0; p < 25; ++p) {
|
||||
if (internalExtraFsPhysicalPage(0, p) != p) return 1;
|
||||
}
|
||||
if (internalExtraFsPhysicalPage(0, 25) != UINT32_MAX) return 2;
|
||||
|
||||
const uint32_t middle = 1UL << 12;
|
||||
if (countInternalExtraFsBadPages(middle) != 1) return 3;
|
||||
if (internalExtraFsPhysicalPage(middle, 11) != 11) return 4;
|
||||
if (internalExtraFsPhysicalPage(middle, 12) != 13) return 5;
|
||||
if (internalExtraFsPhysicalPage(middle, 23) != 24) return 6;
|
||||
if (internalExtraFsPhysicalPage(middle, 24) != UINT32_MAX) return 7;
|
||||
|
||||
const uint32_t edges = (1UL << 0) | (1UL << 24);
|
||||
if (internalExtraFsPhysicalPage(edges, 0) != 1) return 8;
|
||||
if (internalExtraFsPhysicalPage(edges, 22) != 23) return 9;
|
||||
if (internalExtraFsPhysicalPage(edges, 23) != UINT32_MAX) return 10;
|
||||
|
||||
const uint32_t four = (1UL << 0) | (1UL << 4)
|
||||
| (1UL << 17) | (1UL << 24);
|
||||
if (countInternalExtraFsBadPages(four) != 4) return 11;
|
||||
uint32_t previous = 0;
|
||||
for (uint32_t logical = 0; logical < 21; logical++) {
|
||||
const uint32_t physical = internalExtraFsPhysicalPage(four, logical);
|
||||
if (physical >= 25 || (four & (1UL << physical))) return 12;
|
||||
if (logical && physical <= previous) return 13;
|
||||
previous = physical;
|
||||
}
|
||||
if (internalExtraFsPhysicalPage(four, 21) != UINT32_MAX) return 14;
|
||||
if (internalExtraFsPhysicalPage(1UL << 25, 0) != UINT32_MAX) return 15;
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
executable = Path(temp_dir) / "extrafs_page_map"
|
||||
compiled = subprocess.run(
|
||||
["c++", "-std=c++11", f"-I{ROOT / 'src'}", "-x", "c++",
|
||||
"-", "-o", str(executable)],
|
||||
input=source,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stderr)
|
||||
executed = subprocess.run([str(executable)], capture_output=True,
|
||||
text=True)
|
||||
self.assertEqual(executed.returncode, 0, executed.stderr)
|
||||
|
||||
def test_destructive_scan_only_runs_during_early_boot(self):
|
||||
store = (ROOT / "examples/companion_radio/DataStore.cpp").read_text()
|
||||
mesh = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
||||
device = (ROOT / "examples/companion_radio/ResilientInternalExtraFS.cpp").read_text()
|
||||
self.assertIn("reinitializeInternalExtraFS(true)", store)
|
||||
self.assertIn("scan_physical_pages && !extra->scanAndRetireBadPages()", store)
|
||||
self.assertIn("extra->requestBootScan()", store)
|
||||
self.assertIn("setBootScanRequest(marker)", device)
|
||||
self.assertIn("BOOT_SCAN_ALL", device)
|
||||
self.assertIn("uint32_t bad = _bad_pages;", device)
|
||||
self.assertNotIn("_bad_pages | (_pending_pages & PAGE_MASK)", device)
|
||||
self.assertIn("return _boot_scan_forced ||", (
|
||||
ROOT / "examples/companion_radio/ResilientInternalExtraFS.h"
|
||||
).read_text())
|
||||
self.assertIn("extra->acknowledgeRecoveredBootHint()", store)
|
||||
self.assertIn("extra->pageMapNeedsSave()", store)
|
||||
scan_command = mesh.split("void MyMesh::scanInternalExtraFS(Stream& output)", 1)[1]
|
||||
scan_command = scan_command.split("#if defined(MESHCORE_EXTRAFS_HIL)", 1)[0]
|
||||
self.assertIn("_store->requestInternalExtraFSBootScan()", scan_command)
|
||||
self.assertNotIn("scanAndRetireBadPages", scan_command)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Guard the build-local nRF52 SoftDevice flash-completion repair."""
|
||||
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts/nrf52_internal_flash_fix.py"
|
||||
spec = spec_from_file_location("nrf52_internal_flash_fix", SCRIPT)
|
||||
module = module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
|
||||
class Nrf52InternalFlashFixTest(unittest.TestCase):
|
||||
def test_wait_cannot_be_optimized_into_immediate_success(self):
|
||||
source = "before\n" + module.OLD_WAIT + " do_wait();\n }\nafter\n"
|
||||
patched = module.patched_source(source)
|
||||
self.assertIn("volatile uint8_t sd_en", patched)
|
||||
self.assertIn('''__asm volatile ("" ::: "memory");''', patched)
|
||||
self.assertIn("if (sd_en)", patched)
|
||||
self.assertEqual(module.patched_source(patched), patched)
|
||||
|
||||
def test_unrecognized_framework_fails_closed(self):
|
||||
with self.assertRaises(RuntimeError):
|
||||
module.patched_source("changed SDK layout")
|
||||
|
||||
def test_all_nrf52_builds_load_fix(self):
|
||||
ini = (ROOT / "platformio.ini").read_text(encoding="utf-8")
|
||||
self.assertIn("pre:scripts/nrf52_internal_flash_fix.py", ini)
|
||||
self.assertIn("*InternalFileSytem*src*flash*flash_nrf5x.c", SCRIPT.read_text())
|
||||
|
||||
def test_application_softdevice_checks_use_compiler_safe_wrapper(self):
|
||||
helper = ROOT / "src/helpers/nrf52/SoftDeviceState.h"
|
||||
source = helper.read_text(encoding="utf-8")
|
||||
self.assertIn("volatile uint8_t state", source)
|
||||
self.assertIn('''__asm volatile ("" ::: "memory");''', source)
|
||||
for path in (ROOT / "src", ROOT / "variants", ROOT / "examples/companion_radio"):
|
||||
for source_file in path.rglob("*.cpp"):
|
||||
self.assertNotIn("sd_softdevice_is_enabled(",
|
||||
source_file.read_text(encoding="utf-8"),
|
||||
str(source_file))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -18,7 +18,7 @@ chat = (ROOT / "examples/simple_secure_chat/main.cpp").read_text()
|
||||
assert "virtual bool rebootToUf2Bootloader() { return false; }" in main_board
|
||||
assert "bool rebootToUf2Bootloader() override;" in nrf_board_h
|
||||
assert "bool NRF52Board::rebootToUf2Bootloader()" in nrf_board_cpp
|
||||
assert "sd_softdevice_is_enabled(&sd_enabled) != NRF_SUCCESS" in nrf_board_cpp
|
||||
assert "mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS" in nrf_board_cpp
|
||||
clear = nrf_board_cpp.index("sd_power_gpregret_clr(0, 0xFF)")
|
||||
set_magic = nrf_board_cpp.index(
|
||||
"sd_power_gpregret_set(0, DFU_MAGIC_UF2_RESET)"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "nrf_soc.h"
|
||||
|
||||
#include "RAK4631Board.h"
|
||||
#include "../../src/helpers/nrf52/SoftDeviceState.h"
|
||||
|
||||
#ifdef ETHERNET_ENABLED
|
||||
// Drive WB_IO2 HIGH as early as possible using direct register access.
|
||||
@@ -47,7 +48,7 @@ void RAK4631Board::begin() {
|
||||
// merely skipping NRF52BoardDCDC::begin() would inherit any prior state.
|
||||
NRF52Board::begin();
|
||||
uint8_t sd_enabled = 0;
|
||||
sd_softdevice_is_enabled(&sd_enabled);
|
||||
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
||||
if (sd_enabled) {
|
||||
sd_power_dcdc_mode_set(NRF_POWER_DCDC_DISABLE);
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user