From e65549ac974406c0870257eba43f60330e86ef6d Mon Sep 17 00:00:00 2001 From: CValdesS Date: Sun, 20 Sep 2026 22:37:48 +0200 Subject: [PATCH 01/14] Fix tickless idle on the 52-bit GRTC, GPIOTE per port, and the SYSCOUNTER read port_cmsis_systick.c: - grtc_cc_set() wrote CCH = 0. The SYSCOUNTER is 52 bits at 1 MHz and keeps running across soft resets, so it passes 2^32 about 71 minutes after power-on; from then on every compare lands in the past and never fires. The tick only survived while some other interrupt woke the CPU, and the first idle sleep after that never ended. Rebuild the high word from the live counter, carrying when the 32-bit target wrapped. - grtc_counter_get() read SYSCOUNTERL without checking validity. SYSCOUNTERH is latched by the read of SYSCOUNTERL and its reset value already has BUSY set, so re-read the pair until BUSY clears (as nrfx does). A junk read here fed the tick catch-up below. - The tick ISR catch-up had no bound: a bogus counter read stepped the tick by hours. Anything beyond a few seconds is treated as one tick and the tick base is re-anchored; genuine long sleeps are accounted in vPortSuppressTicksAndSleep. WInterrupts.c: - attachInterrupt() only ever used GPIOTE20, but nRF54L routes P0 to GPIOTE30 (4 channels) and P1 to GPIOTE20 (8 channels); P2 has no GPIOTE at all. Pick the instance by port, keep per-instance channel maps, serve both IRQ lines, and return 0 for P2 pins. Verified on an nRF54L15-DK with an SX1262 (Meshtastic): boots cold and warm, tick tracks wall clock, DIO1 interrupts on P0.00. --- cores/nRF5/WInterrupts.c | 152 +++++++++++------- .../CMSIS/nrf54l/port_cmsis_systick.c | 36 ++++- 2 files changed, 128 insertions(+), 60 deletions(-) diff --git a/cores/nRF5/WInterrupts.c b/cores/nRF5/WInterrupts.c index 7b0288b..6c132a6 100644 --- a/cores/nRF5/WInterrupts.c +++ b/cores/nRF5/WInterrupts.c @@ -23,13 +23,17 @@ #include "wiring_private.h" #include "nrf_gpiote.h" -/* The HAL enables interrupts on INTENSET, so the IRQ line and handler must match that group. */ -#define GPIOTE_IRQn NRFX_CONCAT_3(GPIOTE20_, GPIOTE_IRQ_GROUP, _IRQn) -#define GPIOTE_IRQHandler NRFX_CONCAT_3(GPIOTE20_, GPIOTE_IRQ_GROUP, _IRQHandler) - #include -/* nRF54L GPIOTE20 has 8 channels */ +/* nRF54L routes each GPIO port to one GPIOTE instance: P0 -> GPIOTE30 (4 channels), P1 -> GPIOTE20 + * (8 channels). P2, the fast port, has no GPIOTE at all, so no pin on it can raise an interrupt. + * The HAL enables interrupts on INTENSET, so each instance's IRQ line and handler + * must belong to that group. */ +#define GPIOTE20_IRQn_GRP NRFX_CONCAT_3(GPIOTE20_, GPIOTE_IRQ_GROUP, _IRQn) +#define GPIOTE20_IRQHandler_GRP NRFX_CONCAT_3(GPIOTE20_, GPIOTE_IRQ_GROUP, _IRQHandler) +#define GPIOTE30_IRQn_GRP NRFX_CONCAT_3(GPIOTE30_, GPIOTE_IRQ_GROUP, _IRQn) +#define GPIOTE30_IRQHandler_GRP NRFX_CONCAT_3(GPIOTE30_, GPIOTE_IRQ_GROUP, _IRQHandler) + #define NUMBER_OF_GPIO_TE 8 #ifdef GPIOTE_CONFIG_PORT_Msk @@ -38,43 +42,66 @@ #define GPIOTE_CONFIG_PORT_PIN_Msk GPIOTE_CONFIG_PSEL_Msk #endif -static voidFuncPtr callbacksInt[NUMBER_OF_GPIO_TE]; -static bool callbackDeferred[NUMBER_OF_GPIO_TE]; -static int8_t channelMap[NUMBER_OF_GPIO_TE]; -static int enabled = 0; +typedef struct { + NRF_GPIOTE_Type *reg; + IRQn_Type irqn; + uint8_t nChannels; + bool enabled; + voidFuncPtr callbacksInt[NUMBER_OF_GPIO_TE]; + bool callbackDeferred[NUMBER_OF_GPIO_TE]; + int8_t channelMap[NUMBER_OF_GPIO_TE]; +} gpiote_instance_t; + +static gpiote_instance_t gpiote20 = { NRF_GPIOTE20, GPIOTE20_IRQn_GRP, 8, false, {0}, {0}, {0} }; +static gpiote_instance_t gpiote30 = { NRF_GPIOTE30, GPIOTE30_IRQn_GRP, 4, false, {0}, {0}, {0} }; + +/* Pick the GPIOTE that can see this GPIO pin, or NULL when none can (P2). */ +static gpiote_instance_t *instanceForPin(uint32_t pin) +{ + switch (pin >> 5) { + case 0: return &gpiote30; + case 1: return &gpiote20; + default: return NULL; + } +} /* Configure I/O interrupt sources */ -static void __initialize() +static void __initialize(gpiote_instance_t *inst) { - memset(callbacksInt, 0, sizeof(callbacksInt)); - memset(channelMap, -1, sizeof(channelMap)); - memset(callbackDeferred, 0, sizeof(callbackDeferred)); - - NVIC_DisableIRQ(GPIOTE_IRQn); - NVIC_ClearPendingIRQ(GPIOTE_IRQn); - NVIC_SetPriority(GPIOTE_IRQn, 3); - NVIC_EnableIRQ(GPIOTE_IRQn); + memset(inst->callbacksInt, 0, sizeof(inst->callbacksInt)); + memset(inst->channelMap, -1, sizeof(inst->channelMap)); + memset(inst->callbackDeferred, 0, sizeof(inst->callbackDeferred)); + + NVIC_DisableIRQ(inst->irqn); + NVIC_ClearPendingIRQ(inst->irqn); + NVIC_SetPriority(inst->irqn, 3); + NVIC_EnableIRQ(inst->irqn); } /* * \brief Specifies a named Interrupt Service Routine (ISR) to call when an interrupt occurs. * Replaces any previous function that was attached to the interrupt. * - * \return Interrupt Mask + * \return Interrupt Mask, 0 when the pin cannot raise interrupts or no channel is free */ int attachInterrupt(uint32_t pin, voidFuncPtr callback, uint32_t mode) { - if (!enabled) { - __initialize(); - enabled = 1; - } - if (pin >= PINS_COUNT) { return 0; } pin = g_ADigitalPinMap[pin]; + gpiote_instance_t *inst = instanceForPin(pin); + if (inst == NULL) { + return 0; // P2 has no GPIOTE + } + + if (!inst->enabled) { + __initialize(inst); + inst->enabled = true; + } + bool deferred = (mode & ISR_DEFERRED) ? true : false; mode &= ~ISR_DEFERRED; @@ -98,7 +125,7 @@ int attachInterrupt(uint32_t pin, voidFuncPtr callback, uint32_t mode) } // All information for the configuration is known, except the prior values - // of the config register. Pre-compute the mask and new bits for later use. + // of the config register. Pre-compute the mask and new bits for later use: // CONFIG[n] = (CONFIG[n] & oldRegMask) | newRegBits; // // Three fields are configured here: PORT/PIN, POLARITY, MODE @@ -112,17 +139,17 @@ int attachInterrupt(uint32_t pin, voidFuncPtr callback, uint32_t mode) int newChannel = 0; // Find channel where pin is already assigned, if any - for (int i = 0; i < NUMBER_OF_GPIO_TE; i++) { - if ((uint32_t)channelMap[i] != pin) continue; + for (int i = 0; i < inst->nChannels; i++) { + if ((uint32_t)inst->channelMap[i] != pin) continue; ch = i; break; } // else, find one not already mapped and also not in use by others if (ch == -1) { - for (int i = 0; i < NUMBER_OF_GPIO_TE; i++) { - if (channelMap[i] != -1) continue; - if (nrf_gpiote_te_is_enabled(NRF_GPIOTE, i)) continue; - + for (int i = 0; i < inst->nChannels; i++) { + if (inst->channelMap[i] != -1) continue; + if (nrf_gpiote_te_is_enabled(inst->reg, i)) continue; + ch = i; newChannel = 1; break; @@ -133,22 +160,22 @@ int attachInterrupt(uint32_t pin, voidFuncPtr callback, uint32_t mode) return 0; // no channel available } - channelMap[ch] = pin; // harmless for existing channel - callbacksInt[ch] = callback; // caller might be updating this for existing channel - callbackDeferred[ch] = deferred; // caller might be updating this for existing channel + inst->channelMap[ch] = pin; // harmless for existing channel + inst->callbacksInt[ch] = callback; // caller might be updating this for existing channel + inst->callbackDeferred[ch] = deferred; // caller might be updating this for existing channel - uint32_t tmp = NRF_GPIOTE->CONFIG[ch]; + uint32_t tmp = inst->reg->CONFIG[ch]; tmp &= oldRegMask; tmp |= newRegBits; // for existing channel, effectively updates only the polarity - NRF_GPIOTE->CONFIG[ch] = tmp; + inst->reg->CONFIG[ch] = tmp; // For a new channel, additionally ensure no old events existed, and enable the interrupt if (newChannel) { - NRF_GPIOTE->EVENTS_IN[ch] = 0; + inst->reg->EVENTS_IN[ch] = 0; // nRF54L GPIOTE splits INTENSET/INTENCLR per IRQ group (INTENSET0, // INTENSET1, ...). The HAL routes to the right one based on // NRF_GPIOTE_IRQ_GROUP from the MDK interim header. - nrf_gpiote_int_enable(NRF_GPIOTE, (1 << ch)); + nrf_gpiote_int_enable(inst->reg, (1 << ch)); } // Finally, indicate to caller the allocated / updated channel @@ -166,22 +193,27 @@ void detachInterrupt(uint32_t pin) pin = g_ADigitalPinMap[pin]; - for (int ch = 0; ch < NUMBER_OF_GPIO_TE; ch++) { - if ((uint32_t)channelMap[ch] == pin) { - nrf_gpiote_int_disable(NRF_GPIOTE, (1 << ch)); - NRF_GPIOTE->CONFIG[ch] = 0; - NRF_GPIOTE->EVENTS_IN[ch] = 0; // clear any final events + gpiote_instance_t *inst = instanceForPin(pin); + if (inst == NULL || !inst->enabled) { + return; + } + + for (int ch = 0; ch < inst->nChannels; ch++) { + if ((uint32_t)inst->channelMap[ch] == pin) { + nrf_gpiote_int_disable(inst->reg, (1 << ch)); + inst->reg->CONFIG[ch] = 0; + inst->reg->EVENTS_IN[ch] = 0; // clear any final events // now cleanup the rest of the use of the channel - channelMap[ch] = -1; - callbacksInt[ch] = NULL; - callbackDeferred[ch] = false; + inst->channelMap[ch] = -1; + inst->callbacksInt[ch] = NULL; + inst->callbackDeferred[ch] = false; break; } } } -void GPIOTE_IRQHandler() +static void gpiote_irq(gpiote_instance_t *inst) { #if CFG_SYSVIEW SEGGER_SYSVIEW_RecordEnterISR(); @@ -189,28 +221,28 @@ void GPIOTE_IRQHandler() // Read this once (not 8x), as it's a volatile read // across the AHB, which adds up to 3 cycles. - uint32_t const enabledInterruptMask = nrf_gpiote_int_enable_check(NRF_GPIOTE, ~0u); - for (int ch = 0; ch < NUMBER_OF_GPIO_TE; ch++) { + uint32_t const enabledInterruptMask = nrf_gpiote_int_enable_check(inst->reg, ~0u); + for (int ch = 0; ch < inst->nChannels; ch++) { // only process where the interrupt is enabled and the event register is set // check interrupt enabled mask first, as already read that IOM value, to // reduce delays from AHB (16MHz) reads. if ( 0 == (enabledInterruptMask & (1 << ch))) continue; - if ( 0 == NRF_GPIOTE->EVENTS_IN[ch]) continue; + if ( 0 == inst->reg->EVENTS_IN[ch]) continue; // If the event was set and interrupts are enabled, // call the callback function only if it exists, // but ALWAYS clear the event to prevent an interrupt storm. - if (channelMap[ch] != -1 && callbacksInt[ch]) { - if ( callbackDeferred[ch] ) { + if (inst->channelMap[ch] != -1 && inst->callbacksInt[ch]) { + if ( inst->callbackDeferred[ch] ) { // Adafruit defer callback to non-isr if configured so - ada_callback(NULL, 0, callbacksInt[ch]); + ada_callback(NULL, 0, inst->callbacksInt[ch]); } else { - callbacksInt[ch](); + inst->callbacksInt[ch](); } } // clear the event - NRF_GPIOTE->EVENTS_IN[ch] = 0; + inst->reg->EVENTS_IN[ch] = 0; } // Ensure event clear completes before ISR returns __DSB(); __NOP();__NOP();__NOP();__NOP(); @@ -219,3 +251,13 @@ void GPIOTE_IRQHandler() SEGGER_SYSVIEW_RecordExitISR(); #endif } + +void GPIOTE20_IRQHandler_GRP(void) +{ + gpiote_irq(&gpiote20); +} + +void GPIOTE30_IRQHandler_GRP(void) +{ + gpiote_irq(&gpiote30); +} diff --git a/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c b/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c index f166c5f..6af6711 100644 --- a/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c +++ b/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c @@ -55,16 +55,34 @@ * SYSCOUNTER is 52-bit but we only need 32-bit for tick counting. */ static inline uint32_t grtc_counter_get(void) { - /* Reading SYSCOUNTERL latches SYSCOUNTERH for coherent 64-bit read, - * but we only need the low 32 bits. */ - return (uint32_t)(portNRF_GRTC_REG->SYSCOUNTER[0].SYSCOUNTERL); + /* The SYSCOUNTER is only guaranteed readable while it is active: after an idle sleep a read + * before it settles returns junk, which the tick catch-up below turns into thousands of ticks. + * Reading SYSCOUNTERL latches SYSCOUNTERH (whose reset value already has BUSY set), so the + * pair must be re-read until BUSY clears, as nrfx_grtc does; we only need the low 32 bits. */ + uint32_t lo, hi; + do { + lo = portNRF_GRTC_REG->SYSCOUNTER[0].SYSCOUNTERL; /* latches SYSCOUNTERH */ + hi = portNRF_GRTC_REG->SYSCOUNTER[0].SYSCOUNTERH; + } while (hi & (1UL << 30)); /* BUSY: the latched pair is not valid yet */ + return lo; } -/* Set compare channel value */ +/* Set compare channel value. + * The compare is 52-bit and SYSCOUNTER keeps running across soft resets, so it passes + * 2^32 about 71 minutes after power-on. A compare written with CCH = 0 is then already + * in the past and never fires: the tick only survives while some other interrupt wakes + * the CPU, and the first idle sleep after that never ends. val is a 32-bit target derived + * from the low word; rebuild the high word from the live counter, carrying when val + * wrapped past the end of the current 2^32 epoch. */ static inline void grtc_cc_set(uint32_t cc_channel, uint32_t val) { + uint32_t lo = portNRF_GRTC_REG->SYSCOUNTER[0].SYSCOUNTERL; /* latches SYSCOUNTERH */ + uint32_t hi = portNRF_GRTC_REG->SYSCOUNTER[0].SYSCOUNTERH & 0x000FFFFFUL; + if ((val < lo) && ((int32_t)(val - lo) > 0)) { + hi++; /* target lies in the next epoch */ + } portNRF_GRTC_REG->CC[cc_channel].CCL = val; - portNRF_GRTC_REG->CC[cc_channel].CCH = 0; /* High word = 0 for 32-bit compare */ + portNRF_GRTC_REG->CC[cc_channel].CCH = hi; /* writing CCH enables the compare */ } /* Clear compare event */ @@ -119,6 +137,14 @@ void xPortSysTickHandler( void ) { diff = 1; } + /* A bogus counter read must not stall the CPU in this loop nor jump millis() forward by + * hours: anything beyond a few seconds of catch-up is treated as one tick and the tick base is + * re-anchored to the counter. Genuine long sleeps are accounted in vPortSuppressTicksAndSleep. */ + if (diff > (TickType_t)(4 * configTICK_RATE_HZ)) + { + grtc_tick_base = systick_counter - (xTaskGetTickCount() + 1) * portNRF_GRTC_TICKS_PER_SYSTICK; + diff = 1; + } while ((diff--) > 0) { switch_req |= xTaskIncrementTick(); From c43ebeb4ab9c3b0824925e26c9a31964f65d472b Mon Sep 17 00:00:00 2001 From: CValdesS Date: Sun, 20 Sep 2026 22:57:13 +0200 Subject: [PATCH 02/14] Bound the SoftDevice flash-completion wait and cap the tickless idle product flash_nrf5x.c: wait_for_async_flash_op_completion() blocked on the completion semaphore with portMAX_DELAY. A lost NRF_EVT_FLASH_OPERATION_* event (seen on nRF54L15 during a BLE connection) parked the writing task forever and, with every task blocked, the tickless idle put the CPU to sleep until an arbitrary wake-up: the firmware froze, BLE included. Wait in bounded slices, drain sd_evt_get() from the waiting task so the completion cannot get stuck behind a task that is not running, hand other SoC events to a weak application hook, and report NRF_ERROR_TIMEOUT when nothing arrives. port_cmsis_systick.c: clamp xExpectedIdleTime before multiplying by the GRTC ticks per systick; with every task blocked the product wrapped and the wake-up compare landed anywhere. --- .../CMSIS/nrf54l/port_cmsis_systick.c | 6 ++++ .../InternalFileSytem/src/flash/flash_nrf5x.c | 34 +++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c b/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c index 6af6711..fa9ec7d 100644 --- a/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c +++ b/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c @@ -241,6 +241,12 @@ void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime ) { TickType_t xModifiableIdleTime; /* Convert OS ticks to GRTC ticks for wakeup time */ + /* xExpectedIdleTime * TICKS_PER_SYSTICK must fit 32 bits: with every task blocked forever + * FreeRTOS asks for a huge idle and the product wrapped to an arbitrary wake-up time. */ + if (xExpectedIdleTime > (portNRF_GRTC_MAXTICKS / portNRF_GRTC_TICKS_PER_SYSTICK) - 1) + { + xExpectedIdleTime = (portNRF_GRTC_MAXTICKS / portNRF_GRTC_TICKS_PER_SYSTICK) - 1; + } uint32_t wakeupTime = (enterTime + xExpectedIdleTime * portNRF_GRTC_TICKS_PER_SYSTICK) & portNRF_GRTC_MAXTICKS; /* Disable periodic tick interrupt, use compare for wakeup */ diff --git a/libraries/InternalFileSytem/src/flash/flash_nrf5x.c b/libraries/InternalFileSytem/src/flash/flash_nrf5x.c index 1f1487b..ee0c7d2 100644 --- a/libraries/InternalFileSytem/src/flash/flash_nrf5x.c +++ b/libraries/InternalFileSytem/src/flash/flash_nrf5x.c @@ -58,6 +58,14 @@ void flash_nrf5x_event_cb (uint32_t event) // How many retry attempts when performing flash operations #define MAX_RETRY 20 +// Upper bound for one async flash op: slices x slice length (2 s) +#define FLASH_NRF5X_WAIT_SLICES 100 +#define FLASH_NRF5X_WAIT_SLICE_MS 20 + +// Application hook for SoC events drained here that are not flash completions +// (power-failure warning, RNG seed request, ...). Weak: absent, they are dropped. +void flash_nrf5x_soc_event_hook(uint32_t event) __attribute__((weak)); + // When soft device is enabled, flash ops are async // Eventual success is reported via callback, which we await static uint32_t wait_for_async_flash_op_completion(uint32_t initial_result) @@ -69,8 +77,30 @@ static uint32_t wait_for_async_flash_op_completion(uint32_t initial_result) // Operation was queued successfully if (initial_result == NRF_SUCCESS) { - // Wait for result via callback - xSemaphoreTake(_sem, portMAX_DELAY); + // Wait for the result via callback, but never without a bound: the SoC event normally arrives + // through the SoftDevice event task, yet a lost event would otherwise park the calling task + // forever (seen on nRF54L15 during a BLE connection: the whole firmware froze in this take). + // Drain the SoC event queue ourselves while waiting so the completion cannot get stuck behind + // a task that is not running, and hand any other SoC event to the application hook. + bool completed = false; + for (uint32_t slice = 0; slice < FLASH_NRF5X_WAIT_SLICES && !completed; slice++) { + if (xSemaphoreTake(_sem, pdMS_TO_TICKS(FLASH_NRF5X_WAIT_SLICE_MS)) == pdTRUE) { + completed = true; + break; + } + uint32_t evt; + while (sd_evt_get(&evt) == NRF_SUCCESS) { + if (evt == NRF_EVT_FLASH_OPERATION_SUCCESS || evt == NRF_EVT_FLASH_OPERATION_ERROR) { + _flash_op_result = evt; + completed = true; + } else if (flash_nrf5x_soc_event_hook) { + flash_nrf5x_soc_event_hook(evt); + } + } + } + if (!completed) { + return NRF_ERROR_TIMEOUT; + } // If completed successfully if (_flash_op_result == NRF_EVT_FLASH_OPERATION_SUCCESS) { From fb6561530aff0e8457ce22d0b353167341b0a53d Mon Sep 17 00:00:00 2001 From: CValdesS Date: Mon, 21 Sep 2026 11:51:51 +0200 Subject: [PATCH 03/14] Write the InternalFS pages in place on RRAM and report flash failures to LittleFS The page cache flushed every 256-byte LittleFS block as erase-then-program of its 4 KB page: 32 sd_flash_write() calls filling the page with 0xFF (the nRF54L RRAM has no erase, s145 has no sd_flash_page_erase) followed by the whole page again. A hang or reset between the two left the page at 0xFF; when the page was page 0 both LittleFS superblocks vanished and the next boot reformatted the filesystem (seen on an nRF54L15-DK: the node came back unconfigured with a new identity after an overnight freeze). Flush only the 128-byte chunks that differ from the flash, in place: the other blocks sharing the page are never rewritten, so an interrupted flush can only damage the chunk in flight, which is the power-loss model LittleFS is built for. A boot now costs 11 operations instead of 34 per page. Propagate failures instead of ignoring them: flash_cache_flush() returns false, flash_cache_write() -1 when the flush it forced failed, and the LittleFS prog/ sync callbacks return LFS_ERR_IO so the caller learns the write did not happen. Create the completion semaphore on first use (fal_erase() was the only place that did, and it no longer runs before every program). Re-issue BUSY 20 times 5 ms apart and a lost completion 3 times; any other SoftDevice error is final. Keep a flight recorder (flash_nrf5x_stats) of the SoftDevice flash operations: counts of issued, completed, self-drained, timed-out and failed operations, the last destination and the longest wait, and an in_flight flag, so a stuck firmware can be read from the debugger by symbol. --- .../src/InternalFileSystem.cpp | 7 +- .../InternalFileSytem/src/flash/flash_cache.c | 57 ++++++-- .../InternalFileSytem/src/flash/flash_cache.h | 10 +- .../InternalFileSytem/src/flash/flash_nrf5x.c | 130 +++++++++++------- .../InternalFileSytem/src/flash/flash_nrf5x.h | 24 +++- 5 files changed, 166 insertions(+), 62 deletions(-) diff --git a/libraries/InternalFileSytem/src/InternalFileSystem.cpp b/libraries/InternalFileSytem/src/InternalFileSystem.cpp index 2856b2c..32dcefe 100644 --- a/libraries/InternalFileSytem/src/InternalFileSystem.cpp +++ b/libraries/InternalFileSytem/src/InternalFileSystem.cpp @@ -59,7 +59,9 @@ static int _internal_flash_prog (const struct lfs_config *c, lfs_block_t block, (void) c; uint32_t addr = lba2addr(block) + off; - VERIFY( flash_nrf5x_write(addr, buffer, size), -1) + // A short or negative count means a page flush failed: tell LittleFS instead of pretending + // the block was programmed (it then reports the error to the caller and keeps the old state). + if ( flash_nrf5x_write(addr, buffer, size) != (int) size ) return LFS_ERR_IO; return 0; } @@ -90,8 +92,7 @@ static int _internal_flash_erase (const struct lfs_config *c, lfs_block_t block) static int _internal_flash_sync (const struct lfs_config *c) { (void) c; - flash_nrf5x_flush(); - return 0; + return flash_nrf5x_flush() ? 0 : LFS_ERR_IO; } diff --git a/libraries/InternalFileSytem/src/flash/flash_cache.c b/libraries/InternalFileSytem/src/flash/flash_cache.c index 400533f..6a37b57 100644 --- a/libraries/InternalFileSytem/src/flash/flash_cache.c +++ b/libraries/InternalFileSytem/src/flash/flash_cache.c @@ -45,6 +45,7 @@ int flash_cache_write (flash_cache_t* fc, uint32_t dst, void const * src, uint32 { uint8_t const * src8 = (uint8_t const *) src; uint32_t remain = len; + bool flushed_ok = true; // Program up to page boundary each loop while ( remain ) @@ -58,7 +59,7 @@ int flash_cache_write (flash_cache_t* fc, uint32_t dst, void const * src, uint32 // Page changes, flush old and update new cache if ( page_addr != fc->cache_addr ) { - flash_cache_flush(fc); + if ( !flash_cache_flush(fc) ) flushed_ok = false; fc->cache_addr = page_addr; // read a whole page from flash @@ -73,26 +74,60 @@ int flash_cache_write (flash_cache_t* fc, uint32_t dst, void const * src, uint32 dst += wr_bytes; } - return len - remain; + return flushed_ok ? (int) (len - remain) : -1; } -void flash_cache_flush (flash_cache_t* fc) +bool flash_cache_flush (flash_cache_t* fc) { - if ( fc->cache_addr == FLASH_CACHE_INVALID_ADDR ) return; + if ( fc->cache_addr == FLASH_CACHE_INVALID_ADDR ) return true; - // skip erase & program if verify() exists, and memory matches - if ( !(fc->verify && fc->verify(fc->cache_addr, fc->cache_buf, FLASH_CACHE_SIZE)) ) + uint32_t const page = fc->cache_addr; + bool ok = true; + + if ( fc->erase ) + { + // NOR flash: the page has to be erased before it is programmed. + // skip erase & program if verify() exists, and memory matches + if ( !(fc->verify && fc->verify(page, fc->cache_buf, FLASH_CACHE_SIZE)) ) + { + // indicator TODO allow to disable flash indicator + ledOn(LED_BUILTIN); + + ok = fc->erase(page) && (fc->program(page, fc->cache_buf, FLASH_CACHE_SIZE) == FLASH_CACHE_SIZE); + + ledOff(LED_BUILTIN); + } + } + else { - // indicator TODO allow to disable flash indicator - ledOn(LED_BUILTIN); + // Written in place (RRAM): send only the chunks that differ from the flash. The rest of the + // page is never touched, so an interrupted flush can only damage the chunk in flight, not the + // other 15 LittleFS blocks sharing the page (with erase-then-program a hang mid-page left the + // whole page, superblocks included, at 0xFF). + bool led = false; - fc->erase(fc->cache_addr); - fc->program(fc->cache_addr, fc->cache_buf, FLASH_CACHE_SIZE); + for ( uint32_t off = 0; off < FLASH_CACHE_SIZE; off += FLASH_CACHE_WRITE_CHUNK ) + { + if ( fc->verify && fc->verify(page + off, fc->cache_buf + off, FLASH_CACHE_WRITE_CHUNK) ) continue; + + if ( !led ) + { + ledOn(LED_BUILTIN); + led = true; + } + + if ( fc->program(page + off, fc->cache_buf + off, FLASH_CACHE_WRITE_CHUNK) != FLASH_CACHE_WRITE_CHUNK ) + { + ok = false; + break; + } + } - ledOff(LED_BUILTIN); + if ( led ) ledOff(LED_BUILTIN); } fc->cache_addr = FLASH_CACHE_INVALID_ADDR; + return ok; } int flash_cache_read (flash_cache_t* fc, void* dst, uint32_t addr, uint32_t count) diff --git a/libraries/InternalFileSytem/src/flash/flash_cache.h b/libraries/InternalFileSytem/src/flash/flash_cache.h index bb42ba6..d230e0d 100644 --- a/libraries/InternalFileSytem/src/flash/flash_cache.h +++ b/libraries/InternalFileSytem/src/flash/flash_cache.h @@ -31,8 +31,14 @@ #define FLASH_CACHE_SIZE 4096 // must be a erasable page size #define FLASH_CACHE_INVALID_ADDR 0xffffffff +// Granularity of an in-place flush (erase == NULL): only the chunks of the cached page that +// differ from the flash are written. Must be a multiple of 4 and divide FLASH_CACHE_SIZE. +#define FLASH_CACHE_WRITE_CHUNK 128 + typedef struct { + // NULL when the memory is written in place (RRAM): the flush then programs only the chunks + // that changed and never wipes the page first. Non-NULL keeps the NOR erase-then-program flush. bool (*erase) (uint32_t addr); uint32_t (*program) (uint32_t dst, void const * src, uint32_t len); uint32_t (*read) (void* dst, uint32_t src, uint32_t len); @@ -46,8 +52,10 @@ typedef struct extern "C" { #endif +// Returns count, or -1 when a page flush forced by this write failed (the data is still cached). int flash_cache_write (flash_cache_t* fc, uint32_t dst, void const *src, uint32_t count); -void flash_cache_flush (flash_cache_t* fc); +// Returns false when the page could not be written completely; the cache is dropped either way. +bool flash_cache_flush (flash_cache_t* fc); int flash_cache_read (flash_cache_t* fc, void* dst, uint32_t addr, uint32_t count); #ifdef __cplusplus diff --git a/libraries/InternalFileSytem/src/flash/flash_nrf5x.c b/libraries/InternalFileSytem/src/flash/flash_nrf5x.c index ee0c7d2..09f580b 100644 --- a/libraries/InternalFileSytem/src/flash/flash_nrf5x.c +++ b/libraries/InternalFileSytem/src/flash/flash_nrf5x.c @@ -43,6 +43,18 @@ extern uint32_t __flash_arduino_end[]; static SemaphoreHandle_t _sem = NULL; static uint32_t _flash_op_result = NRF_EVT_FLASH_OPERATION_SUCCESS; +flash_nrf5x_stats_t flash_nrf5x_stats; + +// The completion semaphore used to be created by fal_erase() only, which always ran before +// fal_program(); with in-place writes a program can be the first operation, so create it here. +static bool ensure_sem (void) +{ + if ( _sem == NULL ) { + _sem = xSemaphoreCreateBinary(); + } + return _sem != NULL; +} + void flash_nrf5x_event_cb (uint32_t event) { if ( _sem ) { @@ -55,8 +67,10 @@ void flash_nrf5x_event_cb (uint32_t event) } } -// How many retry attempts when performing flash operations +// How many times an operation the SoftDevice reports BUSY is re-issued (5 ms apart) #define MAX_RETRY 20 +// How many times an operation whose completion never arrived is re-issued +#define MAX_TIMEOUT_RETRY 3 // Upper bound for one async flash op: slices x slice length (2 s) #define FLASH_NRF5X_WAIT_SLICES 100 @@ -83,9 +97,11 @@ static uint32_t wait_for_async_flash_op_completion(uint32_t initial_result) // Drain the SoC event queue ourselves while waiting so the completion cannot get stuck behind // a task that is not running, and hand any other SoC event to the application hook. bool completed = false; + flash_nrf5x_stats.in_flight = 1; for (uint32_t slice = 0; slice < FLASH_NRF5X_WAIT_SLICES && !completed; slice++) { if (xSemaphoreTake(_sem, pdMS_TO_TICKS(FLASH_NRF5X_WAIT_SLICE_MS)) == pdTRUE) { completed = true; + flash_nrf5x_stats.completed++; break; } uint32_t evt; @@ -93,14 +109,21 @@ static uint32_t wait_for_async_flash_op_completion(uint32_t initial_result) if (evt == NRF_EVT_FLASH_OPERATION_SUCCESS || evt == NRF_EVT_FLASH_OPERATION_ERROR) { _flash_op_result = evt; completed = true; + flash_nrf5x_stats.drained++; } else if (flash_nrf5x_soc_event_hook) { flash_nrf5x_soc_event_hook(evt); } } } + flash_nrf5x_stats.in_flight = 0; if (!completed) { + flash_nrf5x_stats.timeouts++; return NRF_ERROR_TIMEOUT; } + flash_nrf5x_stats.last_ticks = xTaskGetTickCount() - flash_nrf5x_stats.last_start; + if (flash_nrf5x_stats.last_ticks > flash_nrf5x_stats.max_ticks) { + flash_nrf5x_stats.max_ticks = flash_nrf5x_stats.last_ticks; + } // If completed successfully if (_flash_op_result == NRF_EVT_FLASH_OPERATION_SUCCESS) { @@ -139,8 +162,43 @@ static uint32_t rram_write(uint32_t dst, uint32_t const * src, uint32_t n_words) static uint32_t flash_words_write(bool sd_en, uint32_t dst, uint32_t const * src, uint32_t n_words) { - if ( !sd_en ) return rram_write(dst, src, n_words); - return wait_for_async_flash_op_completion(sd_flash_write((uint32_t*) dst, src, n_words)); + flash_nrf5x_stats.ops++; + flash_nrf5x_stats.last_addr = dst; + flash_nrf5x_stats.last_words = n_words; + flash_nrf5x_stats.last_start = xTaskGetTickCount(); + + uint32_t result; + if ( !sd_en ) { + result = rram_write(dst, src, n_words); + } else { + result = wait_for_async_flash_op_completion(sd_flash_write((uint32_t*) dst, src, n_words)); + } + + flash_nrf5x_stats.last_result = result; + if ( result != NRF_SUCCESS && result != NRF_ERROR_TIMEOUT ) { + flash_nrf5x_stats.errors++; + } + return result; +} + +// One flash operation with the retry policy: BUSY is re-issued after a short pause (a previous +// operation may still be running inside the SoftDevice), a lost completion is re-issued a few +// times, any other error is final because it will not fix itself (bad address, forbidden area). +static uint32_t flash_words_write_retry(bool sd_en, uint32_t dst, uint32_t const * src, uint32_t n_words) +{ + uint32_t err; + uint8_t busy = 0, timeouts = 0; + + for (;;) { + err = flash_words_write(sd_en, dst, src, n_words); + if ( err == NRF_SUCCESS ) return err; + if ( err == NRF_ERROR_BUSY && ++busy < MAX_RETRY ) { + delay(5); + continue; + } + if ( err == NRF_ERROR_TIMEOUT && ++timeouts < MAX_TIMEOUT_RETRY ) continue; + return err; + } } // Flash Abstraction Layer @@ -153,7 +211,9 @@ static uint8_t _cache_buffer[FLASH_CACHE_SIZE] __attribute__((aligned(4))); static flash_cache_t _cache = { - .erase = fal_erase, + // RRAM is written in place: no erase, the cache flushes only the chunks that changed. + // fal_erase stays available through flash_nrf5x_erase() for the filesystem format path. + .erase = NULL, .program = fal_program, .read = fal_read, .verify = fal_verify, @@ -165,9 +225,11 @@ static flash_cache_t _cache = //--------------------------------------------------------------------+ // Application API //--------------------------------------------------------------------+ -void flash_nrf5x_flush (void) +bool flash_nrf5x_flush (void) { - flash_cache_flush(&_cache); + bool ok = flash_cache_flush(&_cache); + if ( !ok ) flash_nrf5x_stats.flush_failed++; + return ok; } int flash_nrf5x_write (uint32_t dst, void const * src, uint32_t len) @@ -201,11 +263,7 @@ bool flash_nrf5x_erase(uint32_t addr) // writing 0xFF to the entire page via sd_flash_write. static bool fal_erase (uint32_t addr) { - // Init semaphore for first call - if ( _sem == NULL ) { - _sem = xSemaphoreCreateBinary(); - VERIFY(_sem); - } + VERIFY(ensure_sem()); uint8_t sd_en = 0; (void) sd_softdevice_is_enabled(&sd_en); @@ -222,18 +280,8 @@ static bool fal_erase (uint32_t addr) while (remaining > 0) { uint32_t wr_bytes = (remaining < chunk_bytes) ? remaining : chunk_bytes; - uint32_t err; - for (uint8_t attempt = 0; attempt < MAX_RETRY; ++attempt) { - err = flash_words_write(sd_en, dst, ff_buf, wr_bytes / 4); - if (err == NRF_SUCCESS) { - break; - } - if (err == NRF_ERROR_BUSY) { - delay(1); - } - } - VERIFY_STATUS(err, false); + VERIFY_STATUS(flash_words_write_retry(sd_en, dst, ff_buf, wr_bytes / 4), false); dst += wr_bytes; remaining -= wr_bytes; @@ -244,38 +292,28 @@ static bool fal_erase (uint32_t addr) static uint32_t fal_program (uint32_t dst, void const * src, uint32_t len) { + VERIFY(ensure_sem(), 0); + // wait for async event if SD is enabled uint8_t sd_en = 0; (void) sd_softdevice_is_enabled(&sd_en); - uint32_t err; + // Written in slices no larger than the RRAMC write buffer; the cache normally hands over one + // FLASH_CACHE_WRITE_CHUNK at a time, the format path a whole page. + uint8_t const * src8 = (uint8_t const *) src; + uint32_t const chunk_bytes = FLASH_CACHE_WRITE_CHUNK; + uint32_t written = 0; - // Write in two halves to avoid potential SoftDevice issues with large writes - // Write first half - for (uint8_t attempt = 0; attempt < MAX_RETRY; ++attempt) { - err = flash_words_write(sd_en, dst, (uint32_t const *) src, len/8); - if (err == NRF_SUCCESS) { - break; - } - if (err == NRF_ERROR_BUSY) { - delay(1); - } - } - VERIFY_STATUS(err, 0); + while (written < len) + { + uint32_t wr_bytes = (len - written < chunk_bytes) ? (len - written) : chunk_bytes; - // Write second half - for (uint8_t attempt = 0; attempt < MAX_RETRY; ++attempt) { - err = flash_words_write(sd_en, dst + len/2, (uint32_t const *) (src + len/2), len/8); - if (err == NRF_SUCCESS) { - break; - } - if (err == NRF_ERROR_BUSY) { - delay(1); - } + VERIFY_STATUS(flash_words_write_retry(sd_en, dst + written, (uint32_t const *) (src8 + written), wr_bytes / 4), written); + + written += wr_bytes; } - VERIFY_STATUS(err, 0); - return len; + return written; } static uint32_t fal_read (void* dst, uint32_t src, uint32_t len) diff --git a/libraries/InternalFileSytem/src/flash/flash_nrf5x.h b/libraries/InternalFileSytem/src/flash/flash_nrf5x.h index 9fc25a4..b0beb92 100644 --- a/libraries/InternalFileSytem/src/flash/flash_nrf5x.h +++ b/libraries/InternalFileSytem/src/flash/flash_nrf5x.h @@ -33,9 +33,31 @@ extern "C" { #endif -void flash_nrf5x_flush (void); +// Returns false when the cached page could not be written completely. +bool flash_nrf5x_flush (void); bool flash_nrf5x_erase(uint32_t addr); +// Flight recorder of the SoftDevice flash operations, meant to be read from a debugger while the +// firmware is stuck (the counters live in RAM and survive a hang; a debugger reads them by symbol). +typedef struct +{ + uint32_t ops; // operations handed to the SoftDevice (or the RRAMC without it) + uint32_t completed; // completions received through the SoftDevice event path + uint32_t drained; // completions this driver had to pull with sd_evt_get() itself + uint32_t timeouts; // waits that hit the bound without any completion + uint32_t errors; // operations the SoftDevice refused or reported as failed + uint32_t flush_failed; // page flushes reported to the filesystem as an I/O error + uint32_t in_flight; // 1 while a task waits for a completion: a hang shows here + uint32_t last_addr; // destination of the last operation + uint32_t last_words; // its length, 32-bit words + uint32_t last_start; // RTOS tick when it was issued + uint32_t last_ticks; // duration of the last completed operation, RTOS ticks + uint32_t max_ticks; // longest completed operation + uint32_t last_result; // NRF_SUCCESS or the last error code +} flash_nrf5x_stats_t; + +extern flash_nrf5x_stats_t flash_nrf5x_stats; + int flash_nrf5x_write (uint32_t dst, void const * src, uint32_t len); int flash_nrf5x_read (void* dst, uint32_t src, uint32_t len); From f9024cdecd03525bca5c75b61278e2be77cce02e Mon Sep 17 00:00:00 2001 From: CValdesS Date: Mon, 21 Sep 2026 22:26:21 +0200 Subject: [PATCH 04/14] Sleep the tickless idle with WFI instead of a WFE/ISPR loop WFI completes on any enabled pending interrupt regardless of PRIMASK, so the idle sleep no longer depends on SEVONPEND and on the event latch being in the right state when the loop re-checks ISPR. Same pattern Zephyr uses on this part. --- .../portable/CMSIS/nrf54l/port_cmsis_systick.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c b/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c index fa9ec7d..7967f39 100644 --- a/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c +++ b/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c @@ -278,12 +278,15 @@ void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime ) #endif { - /* S145 does not provide sd_app_evt_wait(); use WFE directly. - * BASEPRI cannot be used because it would prevent WFE from waking up. */ - do{ - __WFE(); - } while (0 == (NVIC->ISPR[0] | NVIC->ISPR[1] | NVIC->ISPR[2] | NVIC->ISPR[3] - | NVIC->ISPR[4] | NVIC->ISPR[5] | NVIC->ISPR[6] | NVIC->ISPR[7])); + /* S145 does not provide sd_app_evt_wait(), so the idle task sleeps on its own with + * PRIMASK set. Use WFI, not WFE: WFI completes as soon as an enabled interrupt is + * pending, PRIMASK or not, which is exactly the wake-up this sleep needs. WFE only + * wakes through SEVONPEND turning the pending transition into an event, and on the + * nRF54L15 the core has been found asleep for hours in that WFE with the GRTC tick and + * SoftDevice interrupts pending and SEVONPEND set. Zephyr sleeps the same way on this + * part (arch_cpu_idle: cpsid i / BASEPRI 0 / wfi / cpsie i). BASEPRI still cannot be + * used for the masking because it would keep WFI from waking. */ + __WFI(); } } configPOST_SLEEP_PROCESSING( xExpectedIdleTime ); From b7bf4ac2f61db6fe9738a69f9c32afa3d43e6a6c Mon Sep 17 00:00:00 2001 From: CValdesS Date: Sat, 26 Sep 2026 01:45:02 +0200 Subject: [PATCH 05/14] Run the FreeRTOS tick on the application core's GRTC domain The tick used SYSCOUNTER[0], INTEN0 and GRTC_0_IRQn, which belong to domain 0, the FLPR's. The secure application core is domain 2 (GRTC_IRQ_GROUP in nrf54l15_interim.h), and the SoftDevice already uses group 3 for its own compares. Use the SYSCOUNTER index, INTEN group and IRQ of domain 2, and fail the build if the MDK's GRTC_IRQ_GROUP ever disagrees. This is a correctness fix on its own: it did not cure the idle hang seen on the nRF54L15-DK, whose cause was the GRTC sleep timing (next commit). --- cores/nRF5/freertos/config/FreeRTOSConfig.h | 4 ++-- .../portable/CMSIS/nrf54l/port_cmsis_systick.c | 12 ++++++------ .../portable/CMSIS/nrf54l/portmacro_cmsis.h | 14 ++++++++++++-- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/cores/nRF5/freertos/config/FreeRTOSConfig.h b/cores/nRF5/freertos/config/FreeRTOSConfig.h index 7f11150..492e50b 100644 --- a/cores/nRF5/freertos/config/FreeRTOSConfig.h +++ b/cores/nRF5/freertos/config/FreeRTOSConfig.h @@ -182,8 +182,8 @@ standard names - or at least those used in the unmodified vector table. */ /* GRTC SYSCOUNTER runs at 1 MHz */ #define configSYSTICK_CLOCK_HZ ( 1000000UL ) -/* The tick uses GRTC IRQ group 0 (INTENSET0 in port_cmsis_systick.c); the SoftDevice owns group 3. */ -#define xPortSysTickHandler GRTC_0_IRQHandler +/* The tick uses the application core's GRTC group 2 (portNRF_GRTC_DOMAIN); the SoftDevice owns group 3. */ +#define xPortSysTickHandler GRTC_2_IRQHandler /* CM33 TrustZone / MPU not used */ #define configENABLE_TRUSTZONE 0 diff --git a/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c b/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c index 7967f39..158161c 100644 --- a/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c +++ b/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c @@ -61,8 +61,8 @@ static inline uint32_t grtc_counter_get(void) * pair must be re-read until BUSY clears, as nrfx_grtc does; we only need the low 32 bits. */ uint32_t lo, hi; do { - lo = portNRF_GRTC_REG->SYSCOUNTER[0].SYSCOUNTERL; /* latches SYSCOUNTERH */ - hi = portNRF_GRTC_REG->SYSCOUNTER[0].SYSCOUNTERH; + lo = portNRF_GRTC_REG->SYSCOUNTER[portNRF_GRTC_DOMAIN].SYSCOUNTERL; /* latches SYSCOUNTERH */ + hi = portNRF_GRTC_REG->SYSCOUNTER[portNRF_GRTC_DOMAIN].SYSCOUNTERH; } while (hi & (1UL << 30)); /* BUSY: the latched pair is not valid yet */ return lo; } @@ -76,8 +76,8 @@ static inline uint32_t grtc_counter_get(void) * wrapped past the end of the current 2^32 epoch. */ static inline void grtc_cc_set(uint32_t cc_channel, uint32_t val) { - uint32_t lo = portNRF_GRTC_REG->SYSCOUNTER[0].SYSCOUNTERL; /* latches SYSCOUNTERH */ - uint32_t hi = portNRF_GRTC_REG->SYSCOUNTER[0].SYSCOUNTERH & 0x000FFFFFUL; + uint32_t lo = portNRF_GRTC_REG->SYSCOUNTER[portNRF_GRTC_DOMAIN].SYSCOUNTERL; /* latches SYSCOUNTERH */ + uint32_t hi = portNRF_GRTC_REG->SYSCOUNTER[portNRF_GRTC_DOMAIN].SYSCOUNTERH & 0x000FFFFFUL; if ((val < lo) && ((int32_t)(val - lo) > 0)) { hi++; /* target lies in the next epoch */ } @@ -97,13 +97,13 @@ static inline void grtc_event_compare_clear(uint32_t cc_channel) /* Enable compare interrupt for channel */ static inline void grtc_int_compare_enable(uint32_t cc_channel) { - portNRF_GRTC_REG->INTENSET0 = (1UL << cc_channel); + portNRF_GRTC_REG->portNRF_GRTC_INTENSET = (1UL << cc_channel); } /* Disable compare interrupt for channel */ static inline void grtc_int_compare_disable(uint32_t cc_channel) { - portNRF_GRTC_REG->INTENCLR0 = (1UL << cc_channel); + portNRF_GRTC_REG->portNRF_GRTC_INTENCLR = (1UL << cc_channel); } /*-----------------------------------------------------------*/ diff --git a/cores/nRF5/freertos/portable/CMSIS/nrf54l/portmacro_cmsis.h b/cores/nRF5/freertos/portable/CMSIS/nrf54l/portmacro_cmsis.h index 9a809d3..6ea8938 100644 --- a/cores/nRF5/freertos/portable/CMSIS/nrf54l/portmacro_cmsis.h +++ b/cores/nRF5/freertos/portable/CMSIS/nrf54l/portmacro_cmsis.h @@ -78,8 +78,18 @@ typedef unsigned long UBaseType_t; /* GRTC configuration for nRF54L FreeRTOS tick */ #define portNRF_GRTC_REG NRF_GRTC #define portNRF_GRTC_CC_CH 4 -/* IRQ group 0, matching the INTENSET0/INTENCLR0 writes in port_cmsis_systick.c */ -#define portNRF_GRTC_IRQn GRTC_0_IRQn +/* The tick must use the GRTC domain of the CPU it runs on: SYSCOUNTER index, INTEN group and IRQ. + * The SYSCOUNTER sleeps along with the CPU and only wakes ahead of a compare armed in the sleeping + * domain's own group. With group 0 (the FLPR's) the tick compare did not wake it: the core stayed + * in the idle WFI with the counter stopped until a debugger read woke it. The secure application + * core is domain 2 (GRTC_IRQ_GROUP in nrf54l15_interim.h); the SoftDevice owns group 3. */ +#define portNRF_GRTC_DOMAIN 2 +#define portNRF_GRTC_INTENSET INTENSET2 +#define portNRF_GRTC_INTENCLR INTENCLR2 +#define portNRF_GRTC_IRQn GRTC_2_IRQn +#if defined(GRTC_IRQ_GROUP) && (GRTC_IRQ_GROUP != portNRF_GRTC_DOMAIN) +#error "portNRF_GRTC_DOMAIN does not match this core's GRTC_IRQ_GROUP" +#endif /* GRTC SYSCOUNTER runs at 1 MHz (not LFCLK). configSYSTICK_CLOCK_HZ = 1000000 */ #define portNRF_GRTC_TICKS_PER_SYSTICK ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) /* 32-bit compare window */ From a91802bfc0ac4acd3fac6e246f4cd53f2324dbb7 Mon Sep 17 00:00:00 2001 From: CValdesS Date: Sat, 26 Sep 2026 12:28:01 +0200 Subject: [PATCH 06/14] Set the GRTC sleep timing that nrfx_grtc_init() would Nothing on this core calls nrfx_grtc_init(), so TIMEOUT and WAKETIME kept their reset values, 0 and 1: the SYSCOUNTER stopped the moment the CPU slept and had a single 32 kHz cycle to wake up before a compare. On the nRF54L15-DK the core hung in the tickless idle WFI with the SYSCOUNTER stopped, every 40 min to 3.5 h. In all five hangs caught live, a SoftDevice compare sat 1.5-2 of those cycles past the stop and never fired, and neither did the tick compare 19.5 ms later. Write the values of NRFX_GRTC_SLEEP_DEFAULT_CONFIG (TIMEOUT 5, WAKETIME 4), which is what Zephyr applies on this part. Do it outside the start path, like AUTOEN, since the GRTC survives soft resets. --- .../freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c b/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c index 158161c..eccb9d8 100644 --- a/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c +++ b/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c @@ -196,6 +196,13 @@ void vPortSetupTimerInterrupt( void ) } /* The GRTC survives soft resets, so set this outside the start path: sd_softdevice_enable() requires AUTOEN. */ nrf_grtc_sys_counter_auto_mode_set(NRF_GRTC, true); + /* Nothing else writes the sleep timing either (nrfx_grtc_init() would, but nothing calls it). At the + * reset values, TIMEOUT 0 and WAKETIME 1, the SYSCOUNTER stops as soon as the CPU sleeps and gets a + * single 32 kHz cycle to wake up before a compare. Every idle hang caught on the DK had a SoftDevice + * compare 1.5-2 of those cycles past the stop that never fired, and no later one fired either, so the + * CPU slept until the watchdog. Use the values nrfx_grtc_init() applies (NRFX_GRTC_SLEEP_DEFAULT_CONFIG). */ + nrf_grtc_timeout_set(NRF_GRTC, 5); + nrf_grtc_waketime_set(NRF_GRTC, 4); /* Clear any pending event */ grtc_event_compare_clear(portNRF_GRTC_CC_CH); From 091a5eb08c158de6da842eba69fdd6e07460fa86 Mon Sep 17 00:00:00 2001 From: CValdesS Date: Sat, 3 Oct 2026 16:13:38 +0200 Subject: [PATCH 07/14] Cap the tickless idle at half the GRTC compare range grtc_cc_set() places a 32-bit target in the right 2^32 epoch of the 52-bit compare by carrying into the high word when the target wrapped, and it decides that from (int32_t)(val - lo) > 0. That only holds for targets less than 2^31 GRTC ticks ahead. The tickless idle allowed up to 2^32 ticks, about 71 min at 1 MHz, so with every task blocked forever a wrapped target further than ~35 min out kept the current high word, sat in the past, and the CPU slept until some other interrupt woke it. Cap the idle at 0x7FFFFFFF GRTC ticks instead of 0xFFFFFFFF. --- .../portable/CMSIS/nrf54l/port_cmsis_systick.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c b/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c index eccb9d8..23eb663 100644 --- a/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c +++ b/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c @@ -248,11 +248,13 @@ void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime ) { TickType_t xModifiableIdleTime; /* Convert OS ticks to GRTC ticks for wakeup time */ - /* xExpectedIdleTime * TICKS_PER_SYSTICK must fit 32 bits: with every task blocked forever - * FreeRTOS asks for a huge idle and the product wrapped to an arbitrary wake-up time. */ - if (xExpectedIdleTime > (portNRF_GRTC_MAXTICKS / portNRF_GRTC_TICKS_PER_SYSTICK) - 1) + /* With every task blocked forever FreeRTOS asks for a huge idle. The wake-up must stay less + * than 2^31 GRTC ticks (~35 min) ahead: grtc_cc_set only carries into the next 2^32 epoch + * when (int32_t)(val - lo) is positive, so a target further out lands in the past and the + * compare never fires. That also keeps xExpectedIdleTime * TICKS_PER_SYSTICK within 32 bits. */ + if (xExpectedIdleTime > (0x7FFFFFFFUL / portNRF_GRTC_TICKS_PER_SYSTICK) - 1) { - xExpectedIdleTime = (portNRF_GRTC_MAXTICKS / portNRF_GRTC_TICKS_PER_SYSTICK) - 1; + xExpectedIdleTime = (0x7FFFFFFFUL / portNRF_GRTC_TICKS_PER_SYSTICK) - 1; } uint32_t wakeupTime = (enterTime + xExpectedIdleTime * portNRF_GRTC_TICKS_PER_SYSTICK) & portNRF_GRTC_MAXTICKS; From 828481db7fff4a3c73a6857617545b0bf6c4f874 Mon Sep 17 00:00:00 2001 From: CValdesS Date: Sat, 3 Oct 2026 16:14:02 +0200 Subject: [PATCH 08/14] Keep a page that failed to flush in the InternalFS cache When the flush forced by a page switch failed, flash_cache_flush() dropped the cached page and flash_cache_write() loaded the next one over it. That page held writes LittleFS had already been told were done, so they were lost, while the write that triggered the flush was cached anyway and could reach flash later despite having returned LFS_ERR_IO. Leave the page cached when its flush fails and return -1 before taking the new write, so the next flush (or sync) writes it again; on RRAM only the chunks that still differ are sent. With that, a failed write in the erase callback no longer stores its 0xFF, so the erase now reports LFS_ERR_IO like prog instead of claiming the block was erased. --- libraries/InternalFileSytem/src/InternalFileSystem.cpp | 3 ++- libraries/InternalFileSytem/src/flash/flash_cache.c | 10 ++++++---- libraries/InternalFileSytem/src/flash/flash_cache.h | 5 +++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/libraries/InternalFileSytem/src/InternalFileSystem.cpp b/libraries/InternalFileSytem/src/InternalFileSystem.cpp index 32dcefe..6f462e0 100644 --- a/libraries/InternalFileSytem/src/InternalFileSystem.cpp +++ b/libraries/InternalFileSytem/src/InternalFileSystem.cpp @@ -79,7 +79,8 @@ static int _internal_flash_erase (const struct lfs_config *c, lfs_block_t block) // implement as write 0xff to whole block address for(int i=0; i cache_addr ) { - if ( !flash_cache_flush(fc) ) flushed_ok = false; + // The old page holds writes already reported as done: keep it cached rather than replace it + if ( !flash_cache_flush(fc) ) return -1; fc->cache_addr = page_addr; // read a whole page from flash @@ -74,7 +74,7 @@ int flash_cache_write (flash_cache_t* fc, uint32_t dst, void const * src, uint32 dst += wr_bytes; } - return flushed_ok ? (int) (len - remain) : -1; + return (int) (len - remain); } bool flash_cache_flush (flash_cache_t* fc) @@ -126,7 +126,9 @@ bool flash_cache_flush (flash_cache_t* fc) if ( led ) ledOff(LED_BUILTIN); } - fc->cache_addr = FLASH_CACHE_INVALID_ADDR; + // On failure the page stays cached, so its data is neither lost nor read back stale, and the + // next flush writes it again (on RRAM, only the chunks that still differ). + if ( ok ) fc->cache_addr = FLASH_CACHE_INVALID_ADDR; return ok; } diff --git a/libraries/InternalFileSytem/src/flash/flash_cache.h b/libraries/InternalFileSytem/src/flash/flash_cache.h index d230e0d..319d02b 100644 --- a/libraries/InternalFileSytem/src/flash/flash_cache.h +++ b/libraries/InternalFileSytem/src/flash/flash_cache.h @@ -52,9 +52,10 @@ typedef struct extern "C" { #endif -// Returns count, or -1 when a page flush forced by this write failed (the data is still cached). +// Returns count, or -1 when a page flush forced by this write failed. The page that could not be +// flushed stays cached, and the data of this write is not taken (part of it may be, when it spans pages). int flash_cache_write (flash_cache_t* fc, uint32_t dst, void const *src, uint32_t count); -// Returns false when the page could not be written completely; the cache is dropped either way. +// Returns false when the page could not be written completely; it then stays cached for the next flush. bool flash_cache_flush (flash_cache_t* fc); int flash_cache_read (flash_cache_t* fc, void* dst, uint32_t addr, uint32_t count); From 1d26b405004da588091123f6dd7ce2fbeda44136 Mon Sep 17 00:00:00 2001 From: CValdesS Date: Sat, 3 Oct 2026 16:14:29 +0200 Subject: [PATCH 09/14] Answer RNG seed requests the flash driver drains While it waits for a flash completion, the InternalFS driver drains the SoC event queue itself and passes every non-flash event to flash_nrf5x_soc_event_hook(), which nothing defined. A seed request pulled out there was dropped and never reached adafruit_soc_task(), so sd_rand_seed_set() was not called. Give Bluefruit54Lib a weak default for the hook that seeds the RNG through the same function as the SOC task. It is weak so that an application that reads the SoC events itself (Meshtastic does) can still define its own without a duplicate symbol. --- libraries/Bluefruit54Lib/src/bluefruit.cpp | 33 ++++++++++++------- .../InternalFileSytem/src/flash/flash_nrf5x.c | 3 +- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/libraries/Bluefruit54Lib/src/bluefruit.cpp b/libraries/Bluefruit54Lib/src/bluefruit.cpp index 182abad..e26ada0 100644 --- a/libraries/Bluefruit54Lib/src/bluefruit.cpp +++ b/libraries/Bluefruit54Lib/src/bluefruit.cpp @@ -665,6 +665,27 @@ extern "C" void SD_EVT_IRQHandler(void) #endif } +// S145 requires the application to seed the RNG +static void seed_softdevice_rng(void) +{ + uint8_t seed[SD_RAND_SEED_SIZE]; + // Use FICR device ID and GRTC counter as entropy source + uint32_t* seed32 = (uint32_t*)seed; + for (uint32_t i = 0; i < SD_RAND_SEED_SIZE / 4; i++) + { + seed32[i] = NRF_FICR->INFO.DEVICEID[i & 1] ^ (uint32_t)(NRF_GRTC->SYSCOUNTER[0].SYSCOUNTERL + i); + } + sd_rand_seed_set(seed); +} + +// The InternalFS flash driver drains the SoC event queue while it waits for a flash completion and +// hands every other event to this hook, so a seed request it pulls out never reaches the SOC task +// below. Weak: an application that reads the SoC events itself can take them over. +extern "C" __attribute__((weak)) void flash_nrf5x_soc_event_hook(uint32_t soc_evt) +{ + if ( soc_evt == NRF_EVT_RAND_SEED_REQUEST ) seed_softdevice_rng(); +} + /** * Handle SOC event such as FLASH operation */ @@ -694,17 +715,7 @@ void adafruit_soc_task(void* arg) break; case NRF_EVT_RAND_SEED_REQUEST: - { - // S145 requires the application to seed the RNG - uint8_t seed[SD_RAND_SEED_SIZE]; - // Use FICR device ID and GRTC counter as entropy source - uint32_t* seed32 = (uint32_t*)seed; - for (uint32_t i = 0; i < SD_RAND_SEED_SIZE / 4; i++) - { - seed32[i] = NRF_FICR->INFO.DEVICEID[i & 1] ^ (uint32_t)(NRF_GRTC->SYSCOUNTER[0].SYSCOUNTERL + i); - } - sd_rand_seed_set(seed); - } + seed_softdevice_rng(); break; default: break; diff --git a/libraries/InternalFileSytem/src/flash/flash_nrf5x.c b/libraries/InternalFileSytem/src/flash/flash_nrf5x.c index 09f580b..c0f8e42 100644 --- a/libraries/InternalFileSytem/src/flash/flash_nrf5x.c +++ b/libraries/InternalFileSytem/src/flash/flash_nrf5x.c @@ -77,7 +77,8 @@ void flash_nrf5x_event_cb (uint32_t event) #define FLASH_NRF5X_WAIT_SLICE_MS 20 // Application hook for SoC events drained here that are not flash completions -// (power-failure warning, RNG seed request, ...). Weak: absent, they are dropped. +// (power-failure warning, RNG seed request, ...). Bluefruit54Lib provides a weak default that +// answers the RNG seed request; an application can override it. Absent, they are dropped. void flash_nrf5x_soc_event_hook(uint32_t event) __attribute__((weak)); // When soft device is enabled, flash ops are async From a676905c22f51af2764b242969844bbdf613acab Mon Sep 17 00:00:00 2001 From: CValdesS Date: Sat, 3 Oct 2026 16:17:23 +0200 Subject: [PATCH 10/14] Settle a timed-out flash write before issuing the next one When the wait for a SoftDevice flash completion timed out, the write could still be running. Its completion then arrived later, as a give on the semaphore or as an event in the SoC queue, and the next write (the retry, or any later one) took it for its own and reported success while it was still being written. Count accepted operations and completions instead of taking the semaphore once per operation. The SoftDevice runs one operation at a time and reports them in order, so all accepted operations are finished once the counts match, and the last completion is the current write's. Before issuing, wait (with the same 2 s bound) for any operation left outstanding. If its completion still does not come, issue anyway: BUSY means it is still running and goes through the usual BUSY retries, which no longer wait again; acceptance means it finished and another reader of the SoC queue took its completion without passing it on, so it is written off and counted in flash_nrf5x_stats.written_off. The one case left is a completion counted in the instant between that last check and sd_flash_write(), after it had been missing for more than 2 s. It can end one wait early; the surplus count is then dropped. --- .../InternalFileSytem/src/flash/flash_nrf5x.c | 178 ++++++++++++------ .../InternalFileSytem/src/flash/flash_nrf5x.h | 1 + 2 files changed, 124 insertions(+), 55 deletions(-) diff --git a/libraries/InternalFileSytem/src/flash/flash_nrf5x.c b/libraries/InternalFileSytem/src/flash/flash_nrf5x.c index c0f8e42..0b189f4 100644 --- a/libraries/InternalFileSytem/src/flash/flash_nrf5x.c +++ b/libraries/InternalFileSytem/src/flash/flash_nrf5x.c @@ -43,6 +43,14 @@ extern uint32_t __flash_arduino_end[]; static SemaphoreHandle_t _sem = NULL; static uint32_t _flash_op_result = NRF_EVT_FLASH_OPERATION_SUCCESS; +// SoftDevice flash operations accepted, and completions seen for them. The SoftDevice runs one +// operation at a time and reports each one in order, so every accepted operation has finished once +// the two counts match; the semaphore only wakes the waiting task. Counting, instead of taking the +// semaphore once per operation, keeps the completion of a write whose wait timed out from being +// taken for the completion of the next one. +static volatile uint32_t _ops_issued = 0; +static volatile uint32_t _ops_completed = 0; + flash_nrf5x_stats_t flash_nrf5x_stats; // The completion semaphore used to be created by fal_erase() only, which always ran before @@ -55,18 +63,39 @@ static bool ensure_sem (void) return _sem != NULL; } +static void flash_op_completed (uint32_t event) +{ + // Record the result, for consumption by fal_erase or fal_program + // Used to reattempt failed operations + _flash_op_result = event; + __atomic_fetch_add(&_ops_completed, 1, __ATOMIC_SEQ_CST); +} + void flash_nrf5x_event_cb (uint32_t event) { if ( _sem ) { - // Record the result, for consumption by fal_erase or fal_program - // Used to reattempt failed operations - _flash_op_result = event; + flash_op_completed(event); + flash_nrf5x_stats.completed++; // Signal to fal_erase or fal_program that our async flash op is now complete xSemaphoreGive(_sem); } } +// Accepted operations whose completion has not been seen yet +static uint32_t flash_ops_outstanding (void) +{ + taskENTER_CRITICAL(); + int32_t n = (int32_t) (_ops_issued - _ops_completed); + // More completions than operations: one that sd_flash_write_wait() wrote off did report after all + if ( n < 0 ) { + _ops_completed = _ops_issued; + n = 0; + } + taskEXIT_CRITICAL(); + return (uint32_t) n; +} + // How many times an operation the SoftDevice reports BUSY is re-issued (5 ms apart) #define MAX_RETRY 20 // How many times an operation whose completion never arrived is re-issued @@ -81,65 +110,104 @@ void flash_nrf5x_event_cb (uint32_t event) // answers the RNG seed request; an application can override it. Absent, they are dropped. void flash_nrf5x_soc_event_hook(uint32_t event) __attribute__((weak)); +// Pull the pending SoC events ourselves: flash completions are counted, the rest go to the hook +static void drain_soc_events (void) +{ + uint32_t evt; + while (sd_evt_get(&evt) == NRF_SUCCESS) { + if (evt == NRF_EVT_FLASH_OPERATION_SUCCESS || evt == NRF_EVT_FLASH_OPERATION_ERROR) { + flash_op_completed(evt); + flash_nrf5x_stats.drained++; + } else if (flash_nrf5x_soc_event_hook) { + flash_nrf5x_soc_event_hook(evt); + } + } +} + +// Wait until every accepted operation has reported its completion, but never without a bound: the +// SoC event normally arrives through the SoftDevice event task, yet a lost event would otherwise park +// the calling task forever (seen on nRF54L15 during a BLE connection: the whole firmware froze in an +// unbounded take here). Drain the SoC event queue ourselves while waiting so the completion cannot +// get stuck behind a task that is not running, and hand any other SoC event to the application hook. +static bool wait_flash_ops (void) +{ + for (uint32_t slice = 0; slice < FLASH_NRF5X_WAIT_SLICES; slice++) { + if ( !flash_ops_outstanding() ) return true; + // Only a wake-up: a give left by a completion counted earlier costs one more pass + if (xSemaphoreTake(_sem, pdMS_TO_TICKS(FLASH_NRF5X_WAIT_SLICE_MS)) == pdTRUE) continue; + drain_soc_events(); + } + return !flash_ops_outstanding(); +} + +// Set once settling an outstanding write timed out, so the retries after BUSY do not wait again +static bool _settle_given_up = false; + // When soft device is enabled, flash ops are async // Eventual success is reported via callback, which we await -static uint32_t wait_for_async_flash_op_completion(uint32_t initial_result) +static uint32_t sd_flash_write_wait (uint32_t dst, uint32_t const * src, uint32_t n_words) { - // If initial result not NRF_SUCCESS, no need to await callback - // We will pass the initial result (failure) straight through - int32_t result = initial_result; - - // Operation was queued successfully - if (initial_result == NRF_SUCCESS) { - - // Wait for the result via callback, but never without a bound: the SoC event normally arrives - // through the SoftDevice event task, yet a lost event would otherwise park the calling task - // forever (seen on nRF54L15 during a BLE connection: the whole firmware froze in this take). - // Drain the SoC event queue ourselves while waiting so the completion cannot get stuck behind - // a task that is not running, and hand any other SoC event to the application hook. - bool completed = false; - flash_nrf5x_stats.in_flight = 1; - for (uint32_t slice = 0; slice < FLASH_NRF5X_WAIT_SLICES && !completed; slice++) { - if (xSemaphoreTake(_sem, pdMS_TO_TICKS(FLASH_NRF5X_WAIT_SLICE_MS)) == pdTRUE) { - completed = true; - flash_nrf5x_stats.completed++; - break; - } - uint32_t evt; - while (sd_evt_get(&evt) == NRF_SUCCESS) { - if (evt == NRF_EVT_FLASH_OPERATION_SUCCESS || evt == NRF_EVT_FLASH_OPERATION_ERROR) { - _flash_op_result = evt; - completed = true; - flash_nrf5x_stats.drained++; - } else if (flash_nrf5x_soc_event_hook) { - flash_nrf5x_soc_event_hook(evt); - } - } + flash_nrf5x_stats.in_flight = 1; + + // A write whose wait timed out may still be running, or its completion may still be on its way: + // settle it first, so that completion cannot be taken for the one of the write issued here. + uint32_t owed = flash_ops_outstanding(); + if (owed) { + if (!_settle_given_up && wait_flash_ops()) { + owed = 0; + } else { + // Settling timed out, now or before a BUSY retry: take only what is already queued + drain_soc_events(); + owed = flash_ops_outstanding(); + _settle_given_up = true; } + } + + // Counted before the call: the completion can arrive before sd_flash_write() returns + _ops_issued++; + uint32_t result = sd_flash_write((uint32_t*) dst, src, n_words); + + // Not queued (BUSY while an earlier write still runs, or a real error): no completion will follow + if (result != NRF_SUCCESS) { + _ops_issued--; flash_nrf5x_stats.in_flight = 0; - if (!completed) { - flash_nrf5x_stats.timeouts++; - return NRF_ERROR_TIMEOUT; - } - flash_nrf5x_stats.last_ticks = xTaskGetTickCount() - flash_nrf5x_stats.last_start; - if (flash_nrf5x_stats.last_ticks > flash_nrf5x_stats.max_ticks) { - flash_nrf5x_stats.max_ticks = flash_nrf5x_stats.last_ticks; - } + return result; + } - // If completed successfully - if (_flash_op_result == NRF_EVT_FLASH_OPERATION_SUCCESS) { - result = NRF_SUCCESS; - } + _settle_given_up = false; + if (owed) { + // Accepted, so the earlier writes are finished, since the SoftDevice runs one at a time: another + // reader of the SoC event queue took their completions without passing them on. Write them off. + __atomic_fetch_add(&_ops_completed, owed, __ATOMIC_SEQ_CST); + flash_nrf5x_stats.written_off += owed; + } - // If general failure. - else if (_flash_op_result == NRF_EVT_FLASH_OPERATION_ERROR) { - result = NRF_ERROR_TIMEOUT; - } + bool const completed = wait_flash_ops(); + flash_nrf5x_stats.in_flight = 0; + if (!completed) { + // Left outstanding: the next write settles it before it is issued + flash_nrf5x_stats.timeouts++; + return NRF_ERROR_TIMEOUT; + } + flash_nrf5x_stats.last_ticks = xTaskGetTickCount() - flash_nrf5x_stats.last_start; + if (flash_nrf5x_stats.last_ticks > flash_nrf5x_stats.max_ticks) { + flash_nrf5x_stats.max_ticks = flash_nrf5x_stats.last_ticks; + } - // If this assert triggers, we need to implement a new NRF_SOC_EVTS value - else { - assert(false); - } + // Completions arrive in order, so the last one is this write's. + // If completed successfully + if (_flash_op_result == NRF_EVT_FLASH_OPERATION_SUCCESS) { + result = NRF_SUCCESS; + } + + // If general failure. + else if (_flash_op_result == NRF_EVT_FLASH_OPERATION_ERROR) { + result = NRF_ERROR_TIMEOUT; + } + + // If this assert triggers, we need to implement a new NRF_SOC_EVTS value + else { + assert(false); } return result; @@ -172,7 +240,7 @@ static uint32_t flash_words_write(bool sd_en, uint32_t dst, uint32_t const * src if ( !sd_en ) { result = rram_write(dst, src, n_words); } else { - result = wait_for_async_flash_op_completion(sd_flash_write((uint32_t*) dst, src, n_words)); + result = sd_flash_write_wait(dst, src, n_words); } flash_nrf5x_stats.last_result = result; diff --git a/libraries/InternalFileSytem/src/flash/flash_nrf5x.h b/libraries/InternalFileSytem/src/flash/flash_nrf5x.h index b0beb92..0c0fc1d 100644 --- a/libraries/InternalFileSytem/src/flash/flash_nrf5x.h +++ b/libraries/InternalFileSytem/src/flash/flash_nrf5x.h @@ -54,6 +54,7 @@ typedef struct uint32_t last_ticks; // duration of the last completed operation, RTOS ticks uint32_t max_ticks; // longest completed operation uint32_t last_result; // NRF_SUCCESS or the last error code + uint32_t written_off; // completions given up as lost once the SoftDevice accepted a later operation } flash_nrf5x_stats_t; extern flash_nrf5x_stats_t flash_nrf5x_stats; From 2603f4fc203d01780969f2588e70fcb8517407e1 Mon Sep 17 00:00:00 2001 From: CValdesS Date: Sat, 3 Oct 2026 17:35:32 +0200 Subject: [PATCH 11/14] Count a flash write as done only once its data has landed After a completion was written off as lost, it could still arrive: the SoftDevice accepting the next write proves the earlier one finished, not that another reader consumed its completion. Counted late, it made the counts match while the new write was still running, so the driver returned success early. The cache could then load another page into the buffer the SoftDevice was still reading from, and write that data at the wrong address. Also require the destination to read back as the source before the wait for a write succeeds. The cache only programs chunks that differ from the flash, so a match means the SoftDevice has written them and is done reading the buffer. A stray completion can now only make the wait look again, never end it early. If every completion is in, the last one reported a failure and the data is not there, the write fails and is retried as before. The only write a match cannot vouch for is an erase chunk over flash that is already 0xFF; its source is a constant, so finishing it later cannot write anything else. --- .../InternalFileSytem/src/flash/flash_nrf5x.c | 72 +++++++++++-------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/libraries/InternalFileSytem/src/flash/flash_nrf5x.c b/libraries/InternalFileSytem/src/flash/flash_nrf5x.c index 0b189f4..a8cdeed 100644 --- a/libraries/InternalFileSytem/src/flash/flash_nrf5x.c +++ b/libraries/InternalFileSytem/src/flash/flash_nrf5x.c @@ -124,20 +124,41 @@ static void drain_soc_events (void) } } -// Wait until every accepted operation has reported its completion, but never without a bound: the -// SoC event normally arrives through the SoftDevice event task, yet a lost event would otherwise park -// the calling task forever (seen on nRF54L15 during a BLE connection: the whole firmware froze in an -// unbounded take here). Drain the SoC event queue ourselves while waiting so the completion cannot -// get stuck behind a task that is not running, and hand any other SoC event to the application hook. -static bool wait_flash_ops (void) +// The write just issued has landed once dst reads back as src. The cache only programs chunks that +// differ from the flash, so a match means the SoftDevice has written them and is done reading src, +// which the caller is about to reuse. The counts alone cannot prove it: a completion that +// sd_flash_write_wait() wrote off as lost may still arrive and make them match early. +static bool flash_write_landed (uint32_t dst, uint32_t const * src, uint32_t n_words) +{ + return src == NULL || memcmp((void const *) dst, src, n_words * 4) == 0; +} + +typedef enum +{ + FLASH_WAIT_DONE, + FLASH_WAIT_FAILED, // every completion is in, the last one reported a failure and the data is not there + FLASH_WAIT_TIMEOUT, +} flash_wait_t; + +// Wait until every accepted operation has reported its completion and, when src is given, the write +// just issued has landed; but never without a bound: the SoC event normally arrives through the +// SoftDevice event task, yet a lost event would otherwise park the calling task forever (seen on +// nRF54L15 during a BLE connection: the whole firmware froze in an unbounded take here). Drain the +// SoC event queue ourselves while waiting so the completion cannot get stuck behind a task that is +// not running, and hand any other SoC event to the application hook. +static flash_wait_t wait_flash_ops (uint32_t dst, uint32_t const * src, uint32_t n_words) { for (uint32_t slice = 0; slice < FLASH_NRF5X_WAIT_SLICES; slice++) { - if ( !flash_ops_outstanding() ) return true; + if ( !flash_ops_outstanding() ) { + if ( flash_write_landed(dst, src, n_words) ) return FLASH_WAIT_DONE; + if ( _flash_op_result == NRF_EVT_FLASH_OPERATION_ERROR ) return FLASH_WAIT_FAILED; + } // Only a wake-up: a give left by a completion counted earlier costs one more pass if (xSemaphoreTake(_sem, pdMS_TO_TICKS(FLASH_NRF5X_WAIT_SLICE_MS)) == pdTRUE) continue; drain_soc_events(); } - return !flash_ops_outstanding(); + if ( !flash_ops_outstanding() && flash_write_landed(dst, src, n_words) ) return FLASH_WAIT_DONE; + return FLASH_WAIT_TIMEOUT; } // Set once settling an outstanding write timed out, so the retries after BUSY do not wait again @@ -153,7 +174,7 @@ static uint32_t sd_flash_write_wait (uint32_t dst, uint32_t const * src, uint32_ // settle it first, so that completion cannot be taken for the one of the write issued here. uint32_t owed = flash_ops_outstanding(); if (owed) { - if (!_settle_given_up && wait_flash_ops()) { + if (!_settle_given_up && wait_flash_ops(0, NULL, 0) == FLASH_WAIT_DONE) { owed = 0; } else { // Settling timed out, now or before a BUSY retry: take only what is already queued @@ -177,40 +198,31 @@ static uint32_t sd_flash_write_wait (uint32_t dst, uint32_t const * src, uint32_ _settle_given_up = false; if (owed) { // Accepted, so the earlier writes are finished, since the SoftDevice runs one at a time: another - // reader of the SoC event queue took their completions without passing them on. Write them off. + // reader of the SoC event queue most likely took their completions without passing them on. + // Write them off. One that was only late may still arrive and make the counts match early, + // which is why the wait below also requires the data to have landed. __atomic_fetch_add(&_ops_completed, owed, __ATOMIC_SEQ_CST); flash_nrf5x_stats.written_off += owed; } - bool const completed = wait_flash_ops(); + flash_wait_t const waited = wait_flash_ops(dst, src, n_words); flash_nrf5x_stats.in_flight = 0; - if (!completed) { + if (waited == FLASH_WAIT_TIMEOUT) { // Left outstanding: the next write settles it before it is issued flash_nrf5x_stats.timeouts++; return NRF_ERROR_TIMEOUT; } - flash_nrf5x_stats.last_ticks = xTaskGetTickCount() - flash_nrf5x_stats.last_start; - if (flash_nrf5x_stats.last_ticks > flash_nrf5x_stats.max_ticks) { - flash_nrf5x_stats.max_ticks = flash_nrf5x_stats.last_ticks; - } - // Completions arrive in order, so the last one is this write's. - // If completed successfully - if (_flash_op_result == NRF_EVT_FLASH_OPERATION_SUCCESS) { - result = NRF_SUCCESS; - } - - // If general failure. - else if (_flash_op_result == NRF_EVT_FLASH_OPERATION_ERROR) { - result = NRF_ERROR_TIMEOUT; + // General failure: retried like a lost completion + if (waited == FLASH_WAIT_FAILED) { + return NRF_ERROR_TIMEOUT; } - // If this assert triggers, we need to implement a new NRF_SOC_EVTS value - else { - assert(false); + flash_nrf5x_stats.last_ticks = xTaskGetTickCount() - flash_nrf5x_stats.last_start; + if (flash_nrf5x_stats.last_ticks > flash_nrf5x_stats.max_ticks) { + flash_nrf5x_stats.max_ticks = flash_nrf5x_stats.last_ticks; } - - return result; + return NRF_SUCCESS; } // sd_flash_write() is a SoftDevice SVC; without the SoftDevice the RRAM controller is driven directly. From f0da2b4cb9faf5e1a311d0c4f623e56ac30ac201 Mon Sep 17 00:00:00 2001 From: CValdesS Date: Sun, 4 Oct 2026 00:35:57 +0200 Subject: [PATCH 12/14] Run the GRTC tick on 64-bit counter and compare values grtc_cc_set() rebuilt the high word of a 32-bit target from the live counter. It only ever carried into the next epoch, never borrowed: a target slightly behind the counter when the low word had just wrapped (the tick ISR held off for more than a period by a SoftDevice interrupt) landed about 2^32 us, 71.6 min, ahead, and the tick stopped. Its SYSCOUNTERH read also ignored BUSY and OVERFLOW. Read the counter with nrfy_grtc_sys_counter_get(), which retries on both and reads this core's domain, and arm compares with the full value through nrfy_grtc_sys_counter_cc_set(). A target already in the past now fires at once; the spurious event that writing CCL before CCH can raise is dropped while the target is still ahead, as nrfx does. That removes the epoch reconstruction, the hand-rolled counter read and the 2^31 cap on the tickless idle. Ticks now stay on a grid (grtc_next_tick) instead of being re-armed one period after the ISR ran. A backlog beyond 4 s is no longer dropped by re-anchoring the tick base: it is caught up 4 s per interrupt and counted in grtc_tick_backlogs. The tickless idle wakes on the grid and never steps past the unblock tick; ticks beyond it are caught up by the tick interrupt. Write the sleep configuration the way nrfx_grtc_sleep_configure() does, with the SYSCOUNTER stopped, using NRFX_GRTC_SLEEP_DEFAULT_CONFIG instead of literal 5/4. The nrfx GRTC driver itself is not enabled in this core. --- .../CMSIS/nrf54l/port_cmsis_systick.c | 183 +++++++++--------- .../portable/CMSIS/nrf54l/portmacro_cmsis.h | 2 - 2 files changed, 95 insertions(+), 90 deletions(-) diff --git a/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c b/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c index 23eb663..3170da5 100644 --- a/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c +++ b/cores/nRF5/freertos/portable/CMSIS/nrf54l/port_cmsis_systick.c @@ -28,6 +28,7 @@ /* Scheduler includes. */ #include "nrfy_grtc.h" +#include "nrfx_grtc.h" #include "FreeRTOS.h" #include "task.h" #include "nrf_nvic.h" @@ -51,38 +52,27 @@ /*-----------------------------------------------------------*/ -/* Read the low 32 bits of the GRTC SYSCOUNTER. - * SYSCOUNTER is 52-bit but we only need 32-bit for tick counting. */ -static inline uint32_t grtc_counter_get(void) +/* Read the 52-bit GRTC SYSCOUNTER of this core's domain (NRF_GRTC_DOMAIN_INDEX is GRTC_IRQ_GROUP, + * which the #error in portmacro_cmsis.h ties to portNRF_GRTC_DOMAIN). nrfy re-reads until BUSY and + * OVERFLOW clear: after an idle sleep the counter is not readable until it settles. Working in + * 64 bits leaves no 2^32 epoch to reconstruct, although the counter passes 2^32 about 71 min after + * power-on and keeps running across soft resets. */ +static inline uint64_t grtc_counter_get(void) { - /* The SYSCOUNTER is only guaranteed readable while it is active: after an idle sleep a read - * before it settles returns junk, which the tick catch-up below turns into thousands of ticks. - * Reading SYSCOUNTERL latches SYSCOUNTERH (whose reset value already has BUSY set), so the - * pair must be re-read until BUSY clears, as nrfx_grtc does; we only need the low 32 bits. */ - uint32_t lo, hi; - do { - lo = portNRF_GRTC_REG->SYSCOUNTER[portNRF_GRTC_DOMAIN].SYSCOUNTERL; /* latches SYSCOUNTERH */ - hi = portNRF_GRTC_REG->SYSCOUNTER[portNRF_GRTC_DOMAIN].SYSCOUNTERH; - } while (hi & (1UL << 30)); /* BUSY: the latched pair is not valid yet */ - return lo; + return nrfy_grtc_sys_counter_get(portNRF_GRTC_REG); } -/* Set compare channel value. - * The compare is 52-bit and SYSCOUNTER keeps running across soft resets, so it passes - * 2^32 about 71 minutes after power-on. A compare written with CCH = 0 is then already - * in the past and never fires: the tick only survives while some other interrupt wakes - * the CPU, and the first idle sleep after that never ends. val is a 32-bit target derived - * from the low word; rebuild the high word from the live counter, carrying when val - * wrapped past the end of the current 2^32 epoch. */ -static inline void grtc_cc_set(uint32_t cc_channel, uint32_t val) +/* Arm a compare channel at an absolute SYSCOUNTER value. A target already in the past raises the + * event at once, which is what a late tick needs. Writing CCL before CCH can also form a value in + * the past for a moment; drop that spurious event while the target itself is still ahead, as + * nrfx_grtc_syscounter_cc_abs_set(..., safe_setting = true) does. */ +static inline void grtc_cc_set(uint32_t cc_channel, uint64_t val) { - uint32_t lo = portNRF_GRTC_REG->SYSCOUNTER[portNRF_GRTC_DOMAIN].SYSCOUNTERL; /* latches SYSCOUNTERH */ - uint32_t hi = portNRF_GRTC_REG->SYSCOUNTER[portNRF_GRTC_DOMAIN].SYSCOUNTERH & 0x000FFFFFUL; - if ((val < lo) && ((int32_t)(val - lo) > 0)) { - hi++; /* target lies in the next epoch */ + nrfy_grtc_sys_counter_cc_set(portNRF_GRTC_REG, cc_channel, val); + if (nrfy_grtc_sys_counter_compare_event_check(portNRF_GRTC_REG, cc_channel) && (val > grtc_counter_get())) + { + nrfy_grtc_sys_counter_compare_event_clear(portNRF_GRTC_REG, cc_channel); } - portNRF_GRTC_REG->CC[cc_channel].CCL = val; - portNRF_GRTC_REG->CC[cc_channel].CCH = hi; /* writing CCH enables the compare */ } /* Clear compare event */ @@ -108,8 +98,15 @@ static inline void grtc_int_compare_disable(uint32_t cc_channel) /*-----------------------------------------------------------*/ -// SYSCOUNTER value at scheduler start; the counter survives resets, so ticks count from here. -static uint32_t grtc_tick_base; +/* SYSCOUNTER value at which the next OS tick falls due. Ticks stay on this grid, which starts with + * the scheduler, instead of being re-armed one period after whenever the interrupt happened to run. */ +static uint64_t grtc_next_tick; + +/* A backlog (core halted in a debugger, a long critical section) is caught up at most this many + * ticks per interrupt, so the ISR stays short; the compare is then left behind and fires again. */ +#define portNRF_GRTC_CATCHUP_MAX ((TickType_t)(4 * configTICK_RATE_HZ)) +/* Interrupts that found more than portNRF_GRTC_CATCHUP_MAX ticks due; readable from a debugger. */ +static volatile uint32_t grtc_tick_backlogs; void xPortSysTickHandler( void ) { @@ -121,30 +118,31 @@ void xPortSysTickHandler( void ) BaseType_t switch_req = pdFALSE; uint32_t isrstate = portSET_INTERRUPT_MASK_FROM_ISR(); - uint32_t systick_counter = grtc_counter_get(); + uint64_t const now = grtc_counter_get(); if (configUSE_DISABLE_TICK_AUTO_CORRECTION_DEBUG == 0) { - /* Auto-correct missed ticks. + /* Auto-correct missed ticks: every grid tick that has fallen due. * GRTC runs at configSYSTICK_CLOCK_HZ (1 MHz). * Each OS tick = portNRF_GRTC_TICKS_PER_SYSTICK GRTC ticks. */ - TickType_t diff; - uint32_t expected_counter = grtc_tick_base + xTaskGetTickCount() * portNRF_GRTC_TICKS_PER_SYSTICK; - diff = (systick_counter - expected_counter) / portNRF_GRTC_TICKS_PER_SYSTICK; + TickType_t diff = 0; + if (now >= grtc_next_tick) + { + diff = (TickType_t)((now - grtc_next_tick) / portNRF_GRTC_TICKS_PER_SYSTICK) + 1; + } /* At most 1 step if scheduler is suspended */ if ((diff > 1) && (xTaskGetSchedulerState() != taskSCHEDULER_RUNNING)) { diff = 1; } - /* A bogus counter read must not stall the CPU in this loop nor jump millis() forward by - * hours: anything beyond a few seconds of catch-up is treated as one tick and the tick base is - * re-anchored to the counter. Genuine long sleeps are accounted in vPortSuppressTicksAndSleep. */ - if (diff > (TickType_t)(4 * configTICK_RATE_HZ)) + else if (diff > portNRF_GRTC_CATCHUP_MAX) { - grtc_tick_base = systick_counter - (xTaskGetTickCount() + 1) * portNRF_GRTC_TICKS_PER_SYSTICK; - diff = 1; + grtc_tick_backlogs++; + diff = portNRF_GRTC_CATCHUP_MAX; } + + grtc_next_tick += (uint64_t)diff * portNRF_GRTC_TICKS_PER_SYSTICK; while ((diff--) > 0) { switch_req |= xTaskIncrementTick(); @@ -153,11 +151,18 @@ void xPortSysTickHandler( void ) else { switch_req = xTaskIncrementTick(); + grtc_next_tick = now + portNRF_GRTC_TICKS_PER_SYSTICK; } - /* Schedule next compare */ + /* Schedule next compare: the next tick on the grid. When ticks are still due it lies in the past + * and fires at once, except while the scheduler is suspended, which would only spin through here: + * then wait one tick period. */ { - uint32_t next_cc = grtc_counter_get() + portNRF_GRTC_TICKS_PER_SYSTICK; + uint64_t next_cc = grtc_next_tick; + if ((next_cc <= now) && (xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED)) + { + next_cc = now + portNRF_GRTC_TICKS_PER_SYSTICK; + } grtc_cc_set(portNRF_GRTC_CC_CH, next_cc); } @@ -194,23 +199,36 @@ void vPortSetupTimerInterrupt( void ) nrfy_grtc_prepare(NRF_GRTC, true); nrfy_grtc_sys_counter_start(NRF_GRTC, true); } - /* The GRTC survives soft resets, so set this outside the start path: sd_softdevice_enable() requires AUTOEN. */ - nrf_grtc_sys_counter_auto_mode_set(NRF_GRTC, true); - /* Nothing else writes the sleep timing either (nrfx_grtc_init() would, but nothing calls it). At the + /* The GRTC survives soft resets, so configure its sleep outside the start path. Nothing else does: + * nrfx_grtc_init() would, but nothing calls it and the nrfx GRTC driver is not enabled here. At the * reset values, TIMEOUT 0 and WAKETIME 1, the SYSCOUNTER stops as soon as the CPU sleeps and gets a * single 32 kHz cycle to wake up before a compare. Every idle hang caught on the DK had a SoftDevice * compare 1.5-2 of those cycles past the stop that never fired, and no later one fired either, so the - * CPU slept until the watchdog. Use the values nrfx_grtc_init() applies (NRFX_GRTC_SLEEP_DEFAULT_CONFIG). */ - nrf_grtc_timeout_set(NRF_GRTC, 5); - nrf_grtc_waketime_set(NRF_GRTC, 4); + * CPU slept until the watchdog. Apply NRFX_GRTC_SLEEP_DEFAULT_CONFIG (TIMEOUT 5, WAKETIME 4, and + * AUTOEN, which sd_softdevice_enable() requires) the way nrfx_grtc_sleep_configure() does, with the + * SYSCOUNTER stopped for the write. The SoftDevice is not enabled yet when the scheduler starts. */ + { + nrfx_grtc_sleep_config_t const sleep_cfg = NRFX_GRTC_SLEEP_DEFAULT_CONFIG; + bool const active = nrfy_grtc_sys_counter_check(NRF_GRTC); + if (active) + { + nrfy_grtc_sys_counter_set(NRF_GRTC, false); + } + nrfy_grtc_sys_counter_auto_mode_set(NRF_GRTC, sleep_cfg.auto_mode); + nrfy_grtc_timeout_set(NRF_GRTC, sleep_cfg.timeout); + nrfy_grtc_waketime_set(NRF_GRTC, sleep_cfg.waketime); + if (active) + { + nrfy_grtc_sys_counter_set(NRF_GRTC, true); + } + } /* Clear any pending event */ grtc_event_compare_clear(portNRF_GRTC_CC_CH); - /* Set first compare value */ - uint32_t now = grtc_counter_get(); - grtc_tick_base = now; - grtc_cc_set(portNRF_GRTC_CC_CH, now + portNRF_GRTC_TICKS_PER_SYSTICK); + /* First tick one period from now: the grid starts here */ + grtc_next_tick = grtc_counter_get() + portNRF_GRTC_TICKS_PER_SYSTICK; + grtc_cc_set(portNRF_GRTC_CC_CH, grtc_next_tick); /* Enable compare interrupt */ grtc_int_compare_enable(portNRF_GRTC_CC_CH); @@ -223,13 +241,7 @@ void vPortSetupTimerInterrupt( void ) void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime ) { - TickType_t enterTime; - - /* Make sure the expected idle time does not overflow the counter. */ - if ( xExpectedIdleTime > portNRF_GRTC_MAXTICKS - configEXPECTED_IDLE_TIME_BEFORE_SLEEP ) - { - xExpectedIdleTime = portNRF_GRTC_MAXTICKS - configEXPECTED_IDLE_TIME_BEFORE_SLEEP; - } + /* No cap on xExpectedIdleTime: in 64 bits even portMAX_DELAY ticks (~49 days) fit the 52-bit compare. */ /* Block all the interrupts globally */ #ifdef SOFTDEVICE_PRESENT @@ -242,28 +254,19 @@ void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime ) __disable_irq(); #endif - enterTime = grtc_counter_get(); - if ( eTaskConfirmSleepModeStatus() != eAbortSleep ) { TickType_t xModifiableIdleTime; - /* Convert OS ticks to GRTC ticks for wakeup time */ - /* With every task blocked forever FreeRTOS asks for a huge idle. The wake-up must stay less - * than 2^31 GRTC ticks (~35 min) ahead: grtc_cc_set only carries into the next 2^32 epoch - * when (int32_t)(val - lo) is positive, so a target further out lands in the past and the - * compare never fires. That also keeps xExpectedIdleTime * TICKS_PER_SYSTICK within 32 bits. */ - if (xExpectedIdleTime > (0x7FFFFFFFUL / portNRF_GRTC_TICKS_PER_SYSTICK) - 1) - { - xExpectedIdleTime = (0x7FFFFFFFUL / portNRF_GRTC_TICKS_PER_SYSTICK) - 1; - } - uint32_t wakeupTime = (enterTime + xExpectedIdleTime * portNRF_GRTC_TICKS_PER_SYSTICK) & portNRF_GRTC_MAXTICKS; + /* Wake on the grid, when the tick that unblocks a task falls due: the next tick is due at + * grtc_next_tick and xExpectedIdleTime counts it. */ + uint64_t const wakeupTime = grtc_next_tick + (uint64_t)(xExpectedIdleTime - 1) * portNRF_GRTC_TICKS_PER_SYSTICK; /* Disable periodic tick interrupt, use compare for wakeup */ grtc_int_compare_disable(portNRF_GRTC_CC_CH); - /* Configure compare for wakeup */ - grtc_cc_set(portNRF_GRTC_CC_CH, wakeupTime); + /* Configure compare for wakeup. Clear first: grtc_cc_set() keeps the event of a target already passed. */ grtc_event_compare_clear(portNRF_GRTC_CC_CH); + grtc_cc_set(portNRF_GRTC_CC_CH, wakeupTime); grtc_int_compare_enable(portNRF_GRTC_CC_CH); __DSB(); @@ -305,26 +308,30 @@ void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime ) /* Correct the system ticks */ { - TickType_t diff; - TickType_t exitTime; - - exitTime = grtc_counter_get(); - /* Convert GRTC ticks back to OS ticks */ - diff = ((exitTime - enterTime) & portNRF_GRTC_MAXTICKS) / portNRF_GRTC_TICKS_PER_SYSTICK; - - /* Re-enable periodic tick via compare */ - uint32_t next_cc = grtc_counter_get() + portNRF_GRTC_TICKS_PER_SYSTICK; - grtc_cc_set(portNRF_GRTC_CC_CH, next_cc); - grtc_event_compare_clear(portNRF_GRTC_CC_CH); - grtc_int_compare_enable(portNRF_GRTC_CC_CH); - - /* It is important that we clear pending here so that our corrections are latest and in sync with tick_interrupt handler */ - NVIC_ClearPendingIRQ(portNRF_GRTC_IRQn); + /* Whole grid ticks that fell due while asleep */ + TickType_t diff = 0; + uint64_t const exitTime = grtc_counter_get(); + if (exitTime >= grtc_next_tick) + { + diff = (TickType_t)((exitTime - grtc_next_tick) / portNRF_GRTC_TICKS_PER_SYSTICK) + 1; + } + /* vTaskStepTick() must not pass the unblock tick. Ticks beyond it stay due, and the tick + * compare armed below in the past catches them up at once. */ if ((configUSE_TICKLESS_IDLE_SIMPLE_DEBUG) && (diff > xExpectedIdleTime)) { diff = xExpectedIdleTime; } + grtc_next_tick += (uint64_t)diff * portNRF_GRTC_TICKS_PER_SYSTICK; + + /* It is important that we clear pending here so that our corrections are latest and in sync with + * tick_interrupt handler. Done before re-arming: a compare armed in the past must still fire. */ + grtc_event_compare_clear(portNRF_GRTC_CC_CH); + NVIC_ClearPendingIRQ(portNRF_GRTC_IRQn); + + /* Re-enable periodic tick via compare */ + grtc_cc_set(portNRF_GRTC_CC_CH, grtc_next_tick); + grtc_int_compare_enable(portNRF_GRTC_CC_CH); BaseType_t switch_req = pdFALSE; diff --git a/cores/nRF5/freertos/portable/CMSIS/nrf54l/portmacro_cmsis.h b/cores/nRF5/freertos/portable/CMSIS/nrf54l/portmacro_cmsis.h index 6ea8938..32d8289 100644 --- a/cores/nRF5/freertos/portable/CMSIS/nrf54l/portmacro_cmsis.h +++ b/cores/nRF5/freertos/portable/CMSIS/nrf54l/portmacro_cmsis.h @@ -92,8 +92,6 @@ typedef unsigned long UBaseType_t; #endif /* GRTC SYSCOUNTER runs at 1 MHz (not LFCLK). configSYSTICK_CLOCK_HZ = 1000000 */ #define portNRF_GRTC_TICKS_PER_SYSTICK ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) -/* 32-bit compare window */ -#define portNRF_GRTC_MAXTICKS (0xFFFFFFFFUL) /*-----------------------------------------------------------*/ /* Scheduler utilities. */ From 67e6484d11614db60fb33482cf0a2718b2884045 Mon Sep 17 00:00:00 2001 From: CValdesS Date: Sun, 4 Oct 2026 00:35:57 +0200 Subject: [PATCH 13/14] Mask SoftwareSerial's RX interrupt on the GPIOTE that serves its pin attachInterrupt() returns a channel mask of GPIOTE30 for P0 pins and of GPIOTE20 for P1 pins, but SoftwareSerial applied it to NRF_GPIOTE, which nrf54l_compat.h aliases to GPIOTE20. With the RX pin on P0, write() masked GPIOTE20 channel N instead of the RX channel: the RX edge interrupt kept firing during TX, and whatever P1 pin owned that channel was masked during every byte. Add digitalPinToGpiote() to WInterrupts, using the same pin mapping as attachInterrupt(), and have SoftwareSerial keep the instance its RX pin uses. --- cores/nRF5/WInterrupts.c | 9 +++++++++ cores/nRF5/WInterrupts.h | 8 ++++++++ libraries/SoftwareSerial/SoftwareSerial.cpp | 16 ++++++++++------ libraries/SoftwareSerial/SoftwareSerial.h | 2 ++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/cores/nRF5/WInterrupts.c b/cores/nRF5/WInterrupts.c index 6c132a6..47cf799 100644 --- a/cores/nRF5/WInterrupts.c +++ b/cores/nRF5/WInterrupts.c @@ -182,6 +182,15 @@ int attachInterrupt(uint32_t pin, voidFuncPtr callback, uint32_t mode) return (1 << ch); } +NRF_GPIOTE_Type *digitalPinToGpiote(uint32_t pin) +{ + if (pin >= PINS_COUNT) { + return NULL; + } + gpiote_instance_t *inst = instanceForPin(g_ADigitalPinMap[pin]); + return inst ? inst->reg : NULL; +} + /* * \brief Turns off the given interrupt. */ diff --git a/cores/nRF5/WInterrupts.h b/cores/nRF5/WInterrupts.h index 3e93d17..2ca7e3c 100644 --- a/cores/nRF5/WInterrupts.h +++ b/cores/nRF5/WInterrupts.h @@ -20,6 +20,7 @@ #define _WIRING_INTERRUPTS_ #include +#include "nrf.h" #ifdef __cplusplus extern "C" { @@ -46,6 +47,13 @@ typedef void (*voidFuncPtr)(void); */ int attachInterrupt(uint32_t pin, voidFuncPtr callback, uint32_t mode); +/* + * \brief The GPIOTE instance that serves the given pin, or NULL when none does: P0 pins use + * GPIOTE30, P1 pins GPIOTE20, and P2 has no GPIOTE. The mask returned by + * attachInterrupt() refers to this instance's channels. + */ +NRF_GPIOTE_Type *digitalPinToGpiote(uint32_t pin); + /* * \brief Turns off the given interrupt. */ diff --git a/libraries/SoftwareSerial/SoftwareSerial.cpp b/libraries/SoftwareSerial/SoftwareSerial.cpp index a72c645..7eba812 100644 --- a/libraries/SoftwareSerial/SoftwareSerial.cpp +++ b/libraries/SoftwareSerial/SoftwareSerial.cpp @@ -40,6 +40,8 @@ SoftwareSerial::SoftwareSerial(uint8_t receivePin, uint8_t transmitPin, bool inv { _receivePin = receivePin; _transmitPin = transmitPin; + _intMask = 0; + _gpiote = NULL; } @@ -85,6 +87,8 @@ bool SoftwareSerial::listen() _receive_buffer_head = _receive_buffer_tail = 0; active_object = this; + // attachInterrupt() returns a channel mask of the GPIOTE that serves the RX pin's port + _gpiote = digitalPinToGpiote(_receivePin); if(_inverse_logic) //Start bit high _intMask = attachInterrupt(_receivePin, handle_interrupt, RISING); @@ -157,7 +161,7 @@ size_t SoftwareSerial::write(uint8_t b) if (inv) b = ~b; // turn off interrupts for a clean txmit - nrf_gpiote_int_disable(NRF_GPIOTE, _intMask); + if (_gpiote) nrf_gpiote_int_disable(_gpiote, _intMask); // Write the start bit if (inv) *reg |= reg_mask; @@ -185,7 +189,7 @@ size_t SoftwareSerial::write(uint8_t b) else *reg |= reg_mask; - nrf_gpiote_int_enable(NRF_GPIOTE, _intMask); + if (_gpiote) nrf_gpiote_int_enable(_gpiote, _intMask); delayMicroseconds(delay); @@ -197,11 +201,11 @@ void SoftwareSerial::flush() if (!isListening()) return; - nrf_gpiote_int_disable(NRF_GPIOTE, _intMask); + if (_gpiote) nrf_gpiote_int_disable(_gpiote, _intMask); _receive_buffer_head = _receive_buffer_tail = 0; - nrf_gpiote_int_enable(NRF_GPIOTE, _intMask); + if (_gpiote) nrf_gpiote_int_enable(_gpiote, _intMask); } int SoftwareSerial::peek() @@ -229,7 +233,7 @@ void SoftwareSerial::recv() if (_inverse_logic ? rx_pin_read() : !rx_pin_read()) { - nrf_gpiote_int_disable(NRF_GPIOTE, _intMask); + if (_gpiote) nrf_gpiote_int_disable(_gpiote, _intMask); // Wait approximately 1/2 of a bit width to "center" the sample delayMicroseconds(_rx_delay_centering); @@ -287,7 +291,7 @@ void SoftwareSerial::recv() // skip the stop bit delayMicroseconds(_rx_delay_stopbit); - nrf_gpiote_int_enable(NRF_GPIOTE, _intMask); + if (_gpiote) nrf_gpiote_int_enable(_gpiote, _intMask); } } diff --git a/libraries/SoftwareSerial/SoftwareSerial.h b/libraries/SoftwareSerial/SoftwareSerial.h index e6ad923..1a69486 100644 --- a/libraries/SoftwareSerial/SoftwareSerial.h +++ b/libraries/SoftwareSerial/SoftwareSerial.h @@ -24,6 +24,7 @@ #include #include #include +#include /****************************************************************************** * Definitions @@ -42,6 +43,7 @@ class SoftwareSerial : public Stream uint32_t _transmitBitMask; volatile uint32_t* _transmitPortRegister; volatile uint32_t _intMask; + NRF_GPIOTE_Type* _gpiote; // the instance _intMask refers to (by the RX pin's port), NULL on P2 // Expressed as 4-cycle delays (must never be 0!) uint16_t _rx_delay_centering; From 1530bc406aee5b928fb57e8f7aa1fe1a4d833e8a Mon Sep 17 00:00:00 2001 From: CValdesS Date: Sun, 4 Oct 2026 00:39:34 +0200 Subject: [PATCH 14/14] Tighten the InternalFS flash retry policy and bound a failing page Write only the words of a chunk that differ from the flash, first to last. A chunk ending in words that already matched could otherwise read back equal while the SoftDevice still had those words to write and was still reading the buffer, so a late completion could end the wait before it was done and the cache could reload the buffer under it. A range already in place is now not written at all, which also covers erasing flash that is already 0xFF. Give each chunk a total budget of 2 s, every attempt and wait included, instead of 2 s per wait: with retries one failing chunk could hold the filesystem lock for about 10 s. Settling an operation left outstanding by a timeout may use up to 0.5 s of it. A failure reported by the SoftDevice (NRF_EVT_FLASH_OPERATION_ERROR, which it raises when it cannot fit the write around radio activity) is now retried like BUSY, up to MAX_RETRY times with a pause, instead of three times back to back, and it counts in flash_nrf5x_stats.errors. A page whose flush keeps failing is dropped after three failures in a row. Kept for ever, it refused every write to any other page, including the erases of InternalFS.format(), and its data does not survive a reboot anyway. Drop the NOR erase-then-program flush: the only flash_cache_t writes in place. --- .../InternalFileSytem/src/flash/flash_cache.c | 62 ++++++------- .../InternalFileSytem/src/flash/flash_cache.h | 11 ++- .../InternalFileSytem/src/flash/flash_nrf5x.c | 86 +++++++++++++------ 3 files changed, 90 insertions(+), 69 deletions(-) diff --git a/libraries/InternalFileSytem/src/flash/flash_cache.c b/libraries/InternalFileSytem/src/flash/flash_cache.c index 5c3afa9..dbe288d 100644 --- a/libraries/InternalFileSytem/src/flash/flash_cache.c +++ b/libraries/InternalFileSytem/src/flash/flash_cache.c @@ -31,6 +31,9 @@ //--------------------------------------------------------------------+ // MACRO TYPEDEF CONSTANT ENUM DECLARATION //--------------------------------------------------------------------+ +// Failed flushes in a row after which the cached page is dropped +#define FLASH_CACHE_MAX_FLUSH_FAILURES 3 + static inline uint32_t page_addr_of (uint32_t addr) { return addr & ~(FLASH_CACHE_SIZE - 1); @@ -84,51 +87,40 @@ bool flash_cache_flush (flash_cache_t* fc) uint32_t const page = fc->cache_addr; bool ok = true; - if ( fc->erase ) + // Written in place (RRAM): send only the chunks that differ from the flash. The rest of the + // page is never touched, so an interrupted flush can only damage the chunk in flight, not the + // other 15 LittleFS blocks sharing the page (with erase-then-program a hang mid-page left the + // whole page, superblocks included, at 0xFF). + bool led = false; + + for ( uint32_t off = 0; off < FLASH_CACHE_SIZE; off += FLASH_CACHE_WRITE_CHUNK ) { - // NOR flash: the page has to be erased before it is programmed. - // skip erase & program if verify() exists, and memory matches - if ( !(fc->verify && fc->verify(page, fc->cache_buf, FLASH_CACHE_SIZE)) ) + if ( fc->verify && fc->verify(page + off, fc->cache_buf + off, FLASH_CACHE_WRITE_CHUNK) ) continue; + + if ( !led ) { - // indicator TODO allow to disable flash indicator ledOn(LED_BUILTIN); - - ok = fc->erase(page) && (fc->program(page, fc->cache_buf, FLASH_CACHE_SIZE) == FLASH_CACHE_SIZE); - - ledOff(LED_BUILTIN); + led = true; } - } - else - { - // Written in place (RRAM): send only the chunks that differ from the flash. The rest of the - // page is never touched, so an interrupted flush can only damage the chunk in flight, not the - // other 15 LittleFS blocks sharing the page (with erase-then-program a hang mid-page left the - // whole page, superblocks included, at 0xFF). - bool led = false; - for ( uint32_t off = 0; off < FLASH_CACHE_SIZE; off += FLASH_CACHE_WRITE_CHUNK ) + if ( fc->program(page + off, fc->cache_buf + off, FLASH_CACHE_WRITE_CHUNK) != FLASH_CACHE_WRITE_CHUNK ) { - if ( fc->verify && fc->verify(page + off, fc->cache_buf + off, FLASH_CACHE_WRITE_CHUNK) ) continue; - - if ( !led ) - { - ledOn(LED_BUILTIN); - led = true; - } - - if ( fc->program(page + off, fc->cache_buf + off, FLASH_CACHE_WRITE_CHUNK) != FLASH_CACHE_WRITE_CHUNK ) - { - ok = false; - break; - } + ok = false; + break; } - - if ( led ) ledOff(LED_BUILTIN); } + if ( led ) ledOff(LED_BUILTIN); + // On failure the page stays cached, so its data is neither lost nor read back stale, and the - // next flush writes it again (on RRAM, only the chunks that still differ). - if ( ok ) fc->cache_addr = FLASH_CACHE_INVALID_ADDR; + // next flush writes it again (only the chunks that still differ). A page that keeps failing is + // given up, though: it would refuse every write to any other page, InternalFS.format()'s erases + // included, and its data does not survive a reboot anyway. + if ( ok || ++fc->flush_failures >= FLASH_CACHE_MAX_FLUSH_FAILURES ) + { + fc->cache_addr = FLASH_CACHE_INVALID_ADDR; + fc->flush_failures = 0; + } return ok; } diff --git a/libraries/InternalFileSytem/src/flash/flash_cache.h b/libraries/InternalFileSytem/src/flash/flash_cache.h index 319d02b..fa6a0ca 100644 --- a/libraries/InternalFileSytem/src/flash/flash_cache.h +++ b/libraries/InternalFileSytem/src/flash/flash_cache.h @@ -31,21 +31,19 @@ #define FLASH_CACHE_SIZE 4096 // must be a erasable page size #define FLASH_CACHE_INVALID_ADDR 0xffffffff -// Granularity of an in-place flush (erase == NULL): only the chunks of the cached page that -// differ from the flash are written. Must be a multiple of 4 and divide FLASH_CACHE_SIZE. +// Granularity of a flush: only the chunks of the cached page that differ from the flash are +// written, in place. Must be a multiple of 4 and divide FLASH_CACHE_SIZE. #define FLASH_CACHE_WRITE_CHUNK 128 typedef struct { - // NULL when the memory is written in place (RRAM): the flush then programs only the chunks - // that changed and never wipes the page first. Non-NULL keeps the NOR erase-then-program flush. - bool (*erase) (uint32_t addr); uint32_t (*program) (uint32_t dst, void const * src, uint32_t len); uint32_t (*read) (void* dst, uint32_t src, uint32_t len); bool (*verify) (uint32_t addr, void const * buf, uint32_t len); uint32_t cache_addr; uint8_t* cache_buf; + uint8_t flush_failures; // consecutive failed flushes of the cached page } flash_cache_t; #ifdef __cplusplus @@ -55,7 +53,8 @@ extern "C" { // Returns count, or -1 when a page flush forced by this write failed. The page that could not be // flushed stays cached, and the data of this write is not taken (part of it may be, when it spans pages). int flash_cache_write (flash_cache_t* fc, uint32_t dst, void const *src, uint32_t count); -// Returns false when the page could not be written completely; it then stays cached for the next flush. +// Returns false when the page could not be written completely. It then stays cached for the next +// flush, up to FLASH_CACHE_MAX_FLUSH_FAILURES failures in a row, after which it is dropped. bool flash_cache_flush (flash_cache_t* fc); int flash_cache_read (flash_cache_t* fc, void* dst, uint32_t addr, uint32_t count); diff --git a/libraries/InternalFileSytem/src/flash/flash_nrf5x.c b/libraries/InternalFileSytem/src/flash/flash_nrf5x.c index a8cdeed..30c5d46 100644 --- a/libraries/InternalFileSytem/src/flash/flash_nrf5x.c +++ b/libraries/InternalFileSytem/src/flash/flash_nrf5x.c @@ -96,14 +96,27 @@ static uint32_t flash_ops_outstanding (void) return (uint32_t) n; } -// How many times an operation the SoftDevice reports BUSY is re-issued (5 ms apart) +// How many times an operation the SoftDevice reports BUSY, or could not fit around the radio, is +// re-issued (5 ms apart) #define MAX_RETRY 20 // How many times an operation whose completion never arrived is re-issued #define MAX_TIMEOUT_RETRY 3 -// Upper bound for one async flash op: slices x slice length (2 s) -#define FLASH_NRF5X_WAIT_SLICES 100 -#define FLASH_NRF5X_WAIT_SLICE_MS 20 +// Total time one chunk may take, every attempt and wait included. The filesystem lock is held all +// along, so this, not the number of attempts, is what bounds the stall. +#define FLASH_NRF5X_CHUNK_BUDGET_MS 2000 +// Part of it that settling an operation left outstanding by a timeout may use +#define FLASH_NRF5X_SETTLE_MS 500 +#define FLASH_NRF5X_WAIT_SLICE_MS 20 + +// The SoftDevice reported NRF_EVT_FLASH_OPERATION_ERROR: it could not fit the write around radio +// activity. Retried like BUSY, after a pause. +#define FLASH_NRF5X_ERR_SD_FAILED NRF_ERROR_INTERNAL + +static bool deadline_passed (TickType_t deadline) +{ + return (int32_t) (xTaskGetTickCount() - deadline) >= 0; +} // Application hook for SoC events drained here that are not flash completions // (power-failure warning, RNG seed request, ...). Bluefruit54Lib provides a weak default that @@ -124,8 +137,9 @@ static void drain_soc_events (void) } } -// The write just issued has landed once dst reads back as src. The cache only programs chunks that -// differ from the flash, so a match means the SoftDevice has written them and is done reading src, +// The write just issued has landed once dst reads back as src. flash_words_program() trims every +// write to start and end on a word that differs from the flash, so a match means the SoftDevice has +// written the range through its last word (it writes in address order) and is done reading src, // which the caller is about to reuse. The counts alone cannot prove it: a completion that // sd_flash_write_wait() wrote off as lost may still arrive and make them match early. static bool flash_write_landed (uint32_t dst, uint32_t const * src, uint32_t n_words) @@ -141,24 +155,23 @@ typedef enum } flash_wait_t; // Wait until every accepted operation has reported its completion and, when src is given, the write -// just issued has landed; but never without a bound: the SoC event normally arrives through the +// just issued has landed; but never past the deadline: the SoC event normally arrives through the // SoftDevice event task, yet a lost event would otherwise park the calling task forever (seen on // nRF54L15 during a BLE connection: the whole firmware froze in an unbounded take here). Drain the // SoC event queue ourselves while waiting so the completion cannot get stuck behind a task that is // not running, and hand any other SoC event to the application hook. -static flash_wait_t wait_flash_ops (uint32_t dst, uint32_t const * src, uint32_t n_words) +static flash_wait_t wait_flash_ops (TickType_t deadline, uint32_t dst, uint32_t const * src, uint32_t n_words) { - for (uint32_t slice = 0; slice < FLASH_NRF5X_WAIT_SLICES; slice++) { + for (;;) { if ( !flash_ops_outstanding() ) { if ( flash_write_landed(dst, src, n_words) ) return FLASH_WAIT_DONE; if ( _flash_op_result == NRF_EVT_FLASH_OPERATION_ERROR ) return FLASH_WAIT_FAILED; } + if ( deadline_passed(deadline) ) return FLASH_WAIT_TIMEOUT; // Only a wake-up: a give left by a completion counted earlier costs one more pass if (xSemaphoreTake(_sem, pdMS_TO_TICKS(FLASH_NRF5X_WAIT_SLICE_MS)) == pdTRUE) continue; drain_soc_events(); } - if ( !flash_ops_outstanding() && flash_write_landed(dst, src, n_words) ) return FLASH_WAIT_DONE; - return FLASH_WAIT_TIMEOUT; } // Set once settling an outstanding write timed out, so the retries after BUSY do not wait again @@ -166,7 +179,7 @@ static bool _settle_given_up = false; // When soft device is enabled, flash ops are async // Eventual success is reported via callback, which we await -static uint32_t sd_flash_write_wait (uint32_t dst, uint32_t const * src, uint32_t n_words) +static uint32_t sd_flash_write_wait (TickType_t deadline, uint32_t dst, uint32_t const * src, uint32_t n_words) { flash_nrf5x_stats.in_flight = 1; @@ -174,7 +187,9 @@ static uint32_t sd_flash_write_wait (uint32_t dst, uint32_t const * src, uint32_ // settle it first, so that completion cannot be taken for the one of the write issued here. uint32_t owed = flash_ops_outstanding(); if (owed) { - if (!_settle_given_up && wait_flash_ops(0, NULL, 0) == FLASH_WAIT_DONE) { + TickType_t settle_deadline = xTaskGetTickCount() + pdMS_TO_TICKS(FLASH_NRF5X_SETTLE_MS); + if ((int32_t) (settle_deadline - deadline) > 0) settle_deadline = deadline; + if (!_settle_given_up && wait_flash_ops(settle_deadline, 0, NULL, 0) == FLASH_WAIT_DONE) { owed = 0; } else { // Settling timed out, now or before a BUSY retry: take only what is already queued @@ -205,7 +220,7 @@ static uint32_t sd_flash_write_wait (uint32_t dst, uint32_t const * src, uint32_ flash_nrf5x_stats.written_off += owed; } - flash_wait_t const waited = wait_flash_ops(dst, src, n_words); + flash_wait_t const waited = wait_flash_ops(deadline, dst, src, n_words); flash_nrf5x_stats.in_flight = 0; if (waited == FLASH_WAIT_TIMEOUT) { // Left outstanding: the next write settles it before it is issued @@ -213,9 +228,8 @@ static uint32_t sd_flash_write_wait (uint32_t dst, uint32_t const * src, uint32_ return NRF_ERROR_TIMEOUT; } - // General failure: retried like a lost completion if (waited == FLASH_WAIT_FAILED) { - return NRF_ERROR_TIMEOUT; + return FLASH_NRF5X_ERR_SD_FAILED; } flash_nrf5x_stats.last_ticks = xTaskGetTickCount() - flash_nrf5x_stats.last_start; @@ -241,7 +255,7 @@ static uint32_t rram_write(uint32_t dst, uint32_t const * src, uint32_t n_words) return NRF_SUCCESS; } -static uint32_t flash_words_write(bool sd_en, uint32_t dst, uint32_t const * src, uint32_t n_words) +static uint32_t flash_words_write(bool sd_en, uint32_t dst, uint32_t const * src, uint32_t n_words, TickType_t deadline) { flash_nrf5x_stats.ops++; flash_nrf5x_stats.last_addr = dst; @@ -252,7 +266,7 @@ static uint32_t flash_words_write(bool sd_en, uint32_t dst, uint32_t const * src if ( !sd_en ) { result = rram_write(dst, src, n_words); } else { - result = sd_flash_write_wait(dst, src, n_words); + result = sd_flash_write_wait(deadline, dst, src, n_words); } flash_nrf5x_stats.last_result = result; @@ -262,18 +276,20 @@ static uint32_t flash_words_write(bool sd_en, uint32_t dst, uint32_t const * src return result; } -// One flash operation with the retry policy: BUSY is re-issued after a short pause (a previous -// operation may still be running inside the SoftDevice), a lost completion is re-issued a few +// One flash operation with the retry policy, within FLASH_NRF5X_CHUNK_BUDGET_MS in total: BUSY (a +// previous operation may still be running inside the SoftDevice) and a SoftDevice-reported failure +// (no room around the radio) are re-issued after a short pause, a lost completion is re-issued a few // times, any other error is final because it will not fix itself (bad address, forbidden area). static uint32_t flash_words_write_retry(bool sd_en, uint32_t dst, uint32_t const * src, uint32_t n_words) { + TickType_t const deadline = xTaskGetTickCount() + pdMS_TO_TICKS(FLASH_NRF5X_CHUNK_BUDGET_MS); uint32_t err; - uint8_t busy = 0, timeouts = 0; + uint8_t retries = 0, timeouts = 0; for (;;) { - err = flash_words_write(sd_en, dst, src, n_words); - if ( err == NRF_SUCCESS ) return err; - if ( err == NRF_ERROR_BUSY && ++busy < MAX_RETRY ) { + err = flash_words_write(sd_en, dst, src, n_words, deadline); + if ( err == NRF_SUCCESS || deadline_passed(deadline) ) return err; + if ( (err == NRF_ERROR_BUSY || err == FLASH_NRF5X_ERR_SD_FAILED) && ++retries < MAX_RETRY ) { delay(5); continue; } @@ -282,6 +298,21 @@ static uint32_t flash_words_write_retry(bool sd_en, uint32_t dst, uint32_t const } } +// Write only the words that differ from the flash, from the first to the last one. Besides sparing +// the RRAM, every write then starts and ends on a word it changes, which is what lets the data check +// in wait_flash_ops() vouch for it; a range already in place is not written at all. +static uint32_t flash_words_program(bool sd_en, uint32_t dst, uint32_t const * src, uint32_t n_words) +{ + uint32_t const * flash = (uint32_t const *) dst; + uint32_t first = 0, last = n_words; + + while ( first < n_words && flash[first] == src[first] ) first++; + if ( first == n_words ) return NRF_SUCCESS; + while ( flash[last - 1] == src[last - 1] ) last--; + + return flash_words_write_retry(sd_en, dst + 4 * first, src + first, last - first); +} + // Flash Abstraction Layer static bool fal_erase (uint32_t addr); static uint32_t fal_program (uint32_t dst, void const * src, uint32_t len); @@ -292,9 +323,8 @@ static uint8_t _cache_buffer[FLASH_CACHE_SIZE] __attribute__((aligned(4))); static flash_cache_t _cache = { - // RRAM is written in place: no erase, the cache flushes only the chunks that changed. + // RRAM is written in place: the cache flushes only the chunks that changed. // fal_erase stays available through flash_nrf5x_erase() for the filesystem format path. - .erase = NULL, .program = fal_program, .read = fal_read, .verify = fal_verify, @@ -362,7 +392,7 @@ static bool fal_erase (uint32_t addr) { uint32_t wr_bytes = (remaining < chunk_bytes) ? remaining : chunk_bytes; - VERIFY_STATUS(flash_words_write_retry(sd_en, dst, ff_buf, wr_bytes / 4), false); + VERIFY_STATUS(flash_words_program(sd_en, dst, ff_buf, wr_bytes / 4), false); dst += wr_bytes; remaining -= wr_bytes; @@ -389,7 +419,7 @@ static uint32_t fal_program (uint32_t dst, void const * src, uint32_t len) { uint32_t wr_bytes = (len - written < chunk_bytes) ? (len - written) : chunk_bytes; - VERIFY_STATUS(flash_words_write_retry(sd_en, dst + written, (uint32_t const *) (src8 + written), wr_bytes / 4), written); + VERIFY_STATUS(flash_words_program(sd_en, dst + written, (uint32_t const *) (src8 + written), wr_bytes / 4), written); written += wr_bytes; }