diff --git a/cores/nRF5/WInterrupts.c b/cores/nRF5/WInterrupts.c index 7b0288b..47cf799 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,28 +160,37 @@ 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 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. */ @@ -166,22 +202,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 +230,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 +260,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/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/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 f166c5f..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,20 +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) { - /* 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); + return nrfy_grtc_sys_counter_get(portNRF_GRTC_REG); } -/* Set compare channel value */ -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) { - portNRF_GRTC_REG->CC[cc_channel].CCL = val; - portNRF_GRTC_REG->CC[cc_channel].CCH = 0; /* High word = 0 for 32-bit compare */ + 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); + } } /* Clear compare event */ @@ -79,19 +87,26 @@ 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); } /*-----------------------------------------------------------*/ -// 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 ) { @@ -103,22 +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; } + else if (diff > portNRF_GRTC_CATCHUP_MAX) + { + 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(); @@ -127,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); } @@ -168,16 +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); + /* 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. 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); @@ -190,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 @@ -209,20 +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 */ - 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(); @@ -246,12 +290,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 ); @@ -261,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 9a809d3..32d8289 100644 --- a/cores/nRF5/freertos/portable/CMSIS/nrf54l/portmacro_cmsis.h +++ b/cores/nRF5/freertos/portable/CMSIS/nrf54l/portmacro_cmsis.h @@ -78,12 +78,20 @@ 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 */ -#define portNRF_GRTC_MAXTICKS (0xFFFFFFFFUL) /*-----------------------------------------------------------*/ /* Scheduler utilities. */ diff --git a/libraries/Bluefruit54Lib/src/bluefruit.cpp b/libraries/Bluefruit54Lib/src/bluefruit.cpp index 3b05fcc..6408faa 100644 --- a/libraries/Bluefruit54Lib/src/bluefruit.cpp +++ b/libraries/Bluefruit54Lib/src/bluefruit.cpp @@ -666,6 +666,21 @@ extern "C" void SD_EVT_IRQHandler(void) #endif } +// An unanswered seed request leaves the SoftDevice RNG unseeded, so the SOC task retries a failed seed +// between event batches rather than inline, where it would hold up flash completions. +static volatile bool _seed_pending = false; + +// 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: pass it on, waking the task through SD_EVT_IRQn. 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 ) return; + _seed_pending = true; + NVIC_SetPendingIRQ(SD_EVT_IRQn); +} + /** * Handle SOC event such as FLASH operation */ @@ -673,15 +688,16 @@ void adafruit_soc_task(void* arg) { (void) arg; - // An unanswered seed request leaves the SoftDevice RNG unseeded, so a failed seed is retried between - // event batches rather than inline, where it would hold up flash completions. - bool seed_pending = false; - while (1) { - if ( seed_pending ) seed_pending = !seed_softdevice_rng(); + // Cleared first, so a request the flash hook passes on meanwhile is not lost + if ( _seed_pending ) + { + _seed_pending = false; + if ( !seed_softdevice_rng() ) _seed_pending = true; + } - if ( xSemaphoreTake(Bluefruit._soc_event_sem, seed_pending ? pdMS_TO_TICKS(10) : portMAX_DELAY) ) + if ( xSemaphoreTake(Bluefruit._soc_event_sem, _seed_pending ? pdMS_TO_TICKS(10) : portMAX_DELAY) ) { uint32_t soc_evt; uint32_t err = ERROR_NONE; @@ -701,7 +717,7 @@ void adafruit_soc_task(void* arg) break; case NRF_EVT_RAND_SEED_REQUEST: - seed_pending = true; + _seed_pending = true; break; default: break; diff --git a/libraries/InternalFileSytem/src/InternalFileSystem.cpp b/libraries/InternalFileSytem/src/InternalFileSystem.cpp index 2856b2c..6f462e0 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; } @@ -77,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 ) { - flash_cache_flush(fc); + // 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 @@ -73,26 +77,51 @@ int flash_cache_write (flash_cache_t* fc, uint32_t dst, void const * src, uint32 dst += wr_bytes; } - return len - remain; + return (int) (len - remain); } -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; + + uint32_t const page = fc->cache_addr; + bool ok = true; + + // 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; - // skip erase & program if verify() exists, and memory matches - if ( !(fc->verify && fc->verify(fc->cache_addr, fc->cache_buf, FLASH_CACHE_SIZE)) ) + for ( uint32_t off = 0; off < FLASH_CACHE_SIZE; off += FLASH_CACHE_WRITE_CHUNK ) { - // indicator TODO allow to disable flash indicator - ledOn(LED_BUILTIN); + if ( fc->verify && fc->verify(page + off, fc->cache_buf + off, FLASH_CACHE_WRITE_CHUNK) ) continue; - fc->erase(fc->cache_addr); - fc->program(fc->cache_addr, fc->cache_buf, FLASH_CACHE_SIZE); + if ( !led ) + { + ledOn(LED_BUILTIN); + led = true; + } - ledOff(LED_BUILTIN); + if ( fc->program(page + off, fc->cache_buf + off, FLASH_CACHE_WRITE_CHUNK) != FLASH_CACHE_WRITE_CHUNK ) + { + ok = false; + break; + } } - fc->cache_addr = FLASH_CACHE_INVALID_ADDR; + 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 (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; } 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..fa6a0ca 100644 --- a/libraries/InternalFileSytem/src/flash/flash_cache.h +++ b/libraries/InternalFileSytem/src/flash/flash_cache.h @@ -31,23 +31,31 @@ #define FLASH_CACHE_SIZE 4096 // must be a erasable page size #define FLASH_CACHE_INVALID_ADDR 0xffffffff +// 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 { - 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 extern "C" { #endif +// 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); -void flash_cache_flush (flash_cache_t* fc); +// 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); #ifdef __cplusplus diff --git a/libraries/InternalFileSytem/src/flash/flash_nrf5x.c b/libraries/InternalFileSytem/src/flash/flash_nrf5x.c index 1f1487b..30c5d46 100644 --- a/libraries/InternalFileSytem/src/flash/flash_nrf5x.c +++ b/libraries/InternalFileSytem/src/flash/flash_nrf5x.c @@ -43,52 +43,200 @@ 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 +// 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; +} + +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); } } -// How many retry attempts when performing flash operations +// 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, 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 -// 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) -{ - // 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; +// 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 - // Operation was queued successfully - if (initial_result == NRF_SUCCESS) { +static bool deadline_passed (TickType_t deadline) +{ + return (int32_t) (xTaskGetTickCount() - deadline) >= 0; +} - // Wait for result via callback - xSemaphoreTake(_sem, portMAX_DELAY); +// Application hook for SoC events drained here that are not flash completions +// (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)); - // If completed successfully - if (_flash_op_result == NRF_EVT_FLASH_OPERATION_SUCCESS) { - result = NRF_SUCCESS; +// 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); } + } +} + +// 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) +{ + return src == NULL || memcmp((void const *) dst, src, n_words * 4) == 0; +} - // If general failure. - else if (_flash_op_result == NRF_EVT_FLASH_OPERATION_ERROR) { - result = NRF_ERROR_TIMEOUT; +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 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 (TickType_t deadline, uint32_t dst, uint32_t const * src, uint32_t n_words) +{ + 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 this assert triggers, we need to implement a new NRF_SOC_EVTS value - else { - assert(false); +// 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 sd_flash_write_wait (TickType_t deadline, uint32_t dst, uint32_t const * src, uint32_t n_words) +{ + 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) { + 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 + drain_soc_events(); + owed = flash_ops_outstanding(); + _settle_given_up = true; } } - return result; + // 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; + return result; + } + + _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 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; + } + + 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 + flash_nrf5x_stats.timeouts++; + return NRF_ERROR_TIMEOUT; + } + + if (waited == FLASH_WAIT_FAILED) { + return FLASH_NRF5X_ERR_SD_FAILED; + } + + 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 NRF_SUCCESS; } // sd_flash_write() is a SoftDevice SVC; without the SoftDevice the RRAM controller is driven directly. @@ -107,10 +255,62 @@ 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) { - 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 = sd_flash_write_wait(deadline, 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, 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 retries = 0, timeouts = 0; + + for (;;) { + 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; + } + if ( err == NRF_ERROR_TIMEOUT && ++timeouts < MAX_TIMEOUT_RETRY ) continue; + return err; + } +} + +// 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 @@ -123,7 +323,8 @@ static uint8_t _cache_buffer[FLASH_CACHE_SIZE] __attribute__((aligned(4))); static flash_cache_t _cache = { - .erase = fal_erase, + // 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. .program = fal_program, .read = fal_read, .verify = fal_verify, @@ -135,9 +336,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) @@ -171,11 +374,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); @@ -192,18 +391,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_program(sd_en, dst, ff_buf, wr_bytes / 4), false); dst += wr_bytes; remaining -= wr_bytes; @@ -214,38 +403,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_program(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..0c0fc1d 100644 --- a/libraries/InternalFileSytem/src/flash/flash_nrf5x.h +++ b/libraries/InternalFileSytem/src/flash/flash_nrf5x.h @@ -33,9 +33,32 @@ 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 + 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; + 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); 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;