From f356d08d55477258c02a30a36b992b1e9f58b08a Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:59:16 -0500 Subject: [PATCH 1/4] fix(dfu): retry a timed-out flash operation instead of faulting --- changelog.md | 1 + .../drivers_nrf/pstorage/pstorage_raw.c | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/changelog.md b/changelog.md index d318c0a4..f3aeef9d 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ it and is upstream Adafruit history, kept for provenance. ## OTAFIX 2.6 +- A SoftDevice flash timeout during a transfer or the activation is retried (up to 5 times) instead of resetting the bootloader. About 3 in 10 BLE OTAs on a Wio Tracker L1 validated and then faulted with `Last-Fault: id=0x4001 info=0xd` before the application was marked valid. - `INFO_UF2.TXT` shows a `Last-Fault: id=0x.. pc=0x.. info=0x..` line for a SoftDevice or SDK fault the bootloader recorded since the last UF2 drive session. It appears only if the next boot reaches the drive before an application boots, since the application's RAM use wipes the record. The record lives in a 16-byte RAM region behind a magic word and is cleared once printed; for an SDK error `info` is the error code. - A device with no valid application that is plugged into a USB host comes up as a UF2 drive showing NO FIRMWARE instead of waiting for a phone. On battery (no VBUS) it goes straight to the BLE OTA wait, and with VBUS but no host enumerating within 3 s (a charger or power bank) it falls through to the same wait. A cabled board with no application returns to the drive after an aborted copy, and after an eject on boards built with `UF2_EXIT_ON_EJECT` (`mesh_tracker_x1`, `t1000_e`). When the cable is pulled while the drive is up (VBUS gone for 250 ms), it goes to the BLE OTA wait. Plugging a cable into a board with no application that is waiting for BLE, with no phone connected, brings the drive up (VBUS present for 250 ms); a cable already present when the wait began, or a phone connected, leaves the wait alone. - A UF2 application copy marks `bank_0` invalid when its first application block arrives, so a cable pulled mid-copy leaves the device with no application instead of an old `bank_0` marked valid over a half-written image. The completion path records the real application size as before. diff --git a/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c b/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c index 1c5aab8c..60e4bdf5 100644 --- a/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c +++ b/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c @@ -47,6 +47,7 @@ #error No target defined #endif +#define FLASH_RETRY_LIMIT 5 /**< Timed-out flash operations re-issued before the error is reported. */ #define PENDING_PACKET_SIZE 256 /**< Maximum size of a single pending packet data buffer. */ #define PENDING_PACKET_COUNT 16 /**< Packets buffered during lazy erase; must exceed the largest client PRN interval (Android 10, Apple 8). */ @@ -125,6 +126,7 @@ static uint8_t m_pending_count; /**< Numbe static bool m_lazy_erase_active; /**< True if a lazy erase is in progress. */ static uint32_t m_lazy_erase_page; /**< Page index currently being erased. */ static bool m_processing_pending; /**< True if processing a pending packet store. */ +static uint8_t m_flash_retries; /**< Timeouts retried for the operation at rp. */ /** * @brief Function for processing of commands and issuing flash access request to the SoftDevice. @@ -519,6 +521,7 @@ void pstorage_sys_event_handler(uint32_t sys_evt) case NRF_EVT_FLASH_OPERATION_SUCCESS: { p_cmd = &m_cmd_queue.cmd[m_cmd_queue.rp]; + m_flash_retries = 0; m_round_val++; bool command_finished = ((m_round_val * SOC_MAX_WRITE_SIZE) >= p_cmd->size); @@ -609,6 +612,18 @@ void pstorage_sys_event_handler(uint32_t sys_evt) break; case NRF_EVT_FLASH_OPERATION_ERROR: + // The SoftDevice missed its radio gap; erase and same-word write are safe to repeat. + if (m_flash_retries < FLASH_RETRY_LIMIT) + { + m_flash_retries++; + retval = cmd_process(); + if (retval == NRF_SUCCESS || retval == NRF_ERROR_BUSY) + { + // BUSY leaves the element queued for the next dequeue kick. + break; + } + } + m_flash_retries = 0; m_lazy_erase_active = false; m_processing_pending = false; app_notify(NRF_ERROR_TIMEOUT, &m_cmd_queue.cmd[m_cmd_queue.rp]); @@ -705,6 +720,7 @@ uint32_t pstorage_init(void) m_next_app_instance = 0; m_round_val = 0; + m_flash_retries = 0; for(unsigned int index = 0; index < PSTORAGE_NUM_OF_PAGES; index++) { From 05f36cd5647d3275743abd24825f86b3def08bad Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:01:16 -0500 Subject: [PATCH 2/4] fix(dfu): re-issue a busy flash retry from the scheduler --- changelog.md | 2 +- .../drivers_nrf/pstorage/pstorage_raw.c | 53 ++++++++++++++----- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/changelog.md b/changelog.md index f3aeef9d..61fd5321 100644 --- a/changelog.md +++ b/changelog.md @@ -5,7 +5,7 @@ it and is upstream Adafruit history, kept for provenance. ## OTAFIX 2.6 -- A SoftDevice flash timeout during a transfer or the activation is retried (up to 5 times) instead of resetting the bootloader. About 3 in 10 BLE OTAs on a Wio Tracker L1 validated and then faulted with `Last-Fault: id=0x4001 info=0xd` before the application was marked valid. +- A SoftDevice flash timeout during a BLE or serial transfer, or the activation settings save, is retried up to 5 times instead of resetting the bootloader. - `INFO_UF2.TXT` shows a `Last-Fault: id=0x.. pc=0x.. info=0x..` line for a SoftDevice or SDK fault the bootloader recorded since the last UF2 drive session. It appears only if the next boot reaches the drive before an application boots, since the application's RAM use wipes the record. The record lives in a 16-byte RAM region behind a magic word and is cleared once printed; for an SDK error `info` is the error code. - A device with no valid application that is plugged into a USB host comes up as a UF2 drive showing NO FIRMWARE instead of waiting for a phone. On battery (no VBUS) it goes straight to the BLE OTA wait, and with VBUS but no host enumerating within 3 s (a charger or power bank) it falls through to the same wait. A cabled board with no application returns to the drive after an aborted copy, and after an eject on boards built with `UF2_EXIT_ON_EJECT` (`mesh_tracker_x1`, `t1000_e`). When the cable is pulled while the drive is up (VBUS gone for 250 ms), it goes to the BLE OTA wait. Plugging a cable into a board with no application that is waiting for BLE, with no phone connected, brings the drive up (VBUS present for 250 ms); a cable already present when the wait began, or a phone connected, leaves the wait alone. - A UF2 application copy marks `bank_0` invalid when its first application block arrives, so a cable pulled mid-copy leaves the device with no application instead of an old `bank_0` marked valid over a half-written image. The completion path records the real application size as before. diff --git a/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c b/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c index 60e4bdf5..277b9962 100644 --- a/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c +++ b/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c @@ -19,6 +19,7 @@ #include "nrf.h" #include "nrf_soc.h" #include "app_util.h" +#include "app_scheduler.h" /** @file * @@ -496,6 +497,43 @@ static bool lazy_erase_check_and_trigger(pstorage_handle_t * p_handle, pstorage_ } +static void flash_retry(void); + +static void flash_retry_kick(void * p_event_data, uint16_t event_size) +{ + (void) p_event_data; + (void) event_size; + if (!m_cmd_queue.flash_access && m_cmd_queue.count > 0) + { + flash_retry(); + } +} + +/** + * @brief Re-issue the element at rp after a flash timeout, or report the timeout once retries run out. + */ +static void flash_retry(void) +{ + if (m_flash_retries < FLASH_RETRY_LIMIT) + { + m_flash_retries++; + uint32_t retval = cmd_process(); + if (retval == NRF_SUCCESS) + { + return; + } + // BUSY: no flash event will follow, so re-issue from the scheduler. + if (retval == NRF_ERROR_BUSY && app_sched_event_put(NULL, 0, flash_retry_kick) == NRF_SUCCESS) + { + return; + } + } + m_flash_retries = 0; + m_lazy_erase_active = false; + m_processing_pending = false; + app_notify(NRF_ERROR_TIMEOUT, &m_cmd_queue.cmd[m_cmd_queue.rp]); +} + /** * @brief Function for handling of system events from SoftDevice. * @@ -613,20 +651,7 @@ void pstorage_sys_event_handler(uint32_t sys_evt) case NRF_EVT_FLASH_OPERATION_ERROR: // The SoftDevice missed its radio gap; erase and same-word write are safe to repeat. - if (m_flash_retries < FLASH_RETRY_LIMIT) - { - m_flash_retries++; - retval = cmd_process(); - if (retval == NRF_SUCCESS || retval == NRF_ERROR_BUSY) - { - // BUSY leaves the element queued for the next dequeue kick. - break; - } - } - m_flash_retries = 0; - m_lazy_erase_active = false; - m_processing_pending = false; - app_notify(NRF_ERROR_TIMEOUT, &m_cmd_queue.cmd[m_cmd_queue.rp]); + flash_retry(); break; default: From 2e6757681ab22ffd305db1acaa8f9c8ccfd94aba Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:10:36 -0500 Subject: [PATCH 3/4] fix(dfu): bound busy flash re-issues on their own and report the real error --- .../drivers_nrf/pstorage/pstorage_raw.c | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c b/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c index 277b9962..08511d8a 100644 --- a/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c +++ b/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c @@ -128,6 +128,7 @@ static bool m_lazy_erase_active; /**< True static uint32_t m_lazy_erase_page; /**< Page index currently being erased. */ static bool m_processing_pending; /**< True if processing a pending packet store. */ static uint8_t m_flash_retries; /**< Timeouts retried for the operation at rp. */ +static uint8_t m_flash_busy_kicks; /**< BUSY re-issues scheduled for the operation at rp. */ /** * @brief Function for processing of commands and issuing flash access request to the SoftDevice. @@ -497,7 +498,17 @@ static bool lazy_erase_check_and_trigger(pstorage_handle_t * p_handle, pstorage_ } -static void flash_retry(void); +static void flash_retry_issue(void); + +/** @brief Give up on the element at rp and report the error to its owner. */ +static void flash_retry_fail(uint32_t result) +{ + m_flash_retries = 0; + m_flash_busy_kicks = 0; + m_lazy_erase_active = false; + m_processing_pending = false; + app_notify(result, &m_cmd_queue.cmd[m_cmd_queue.rp]); +} static void flash_retry_kick(void * p_event_data, uint16_t event_size) { @@ -505,33 +516,37 @@ static void flash_retry_kick(void * p_event_data, uint16_t event_size) (void) event_size; if (!m_cmd_queue.flash_access && m_cmd_queue.count > 0) { - flash_retry(); + flash_retry_issue(); } } -/** - * @brief Re-issue the element at rp after a flash timeout, or report the timeout once retries run out. - */ +/** @brief Re-issue the element at rp; BUSY is re-kicked from the scheduler on its own bound. */ +static void flash_retry_issue(void) +{ + uint32_t retval = cmd_process(); + if (retval == NRF_SUCCESS) + { + return; + } + if (retval == NRF_ERROR_BUSY && m_flash_busy_kicks < FLASH_RETRY_LIMIT && + app_sched_event_put(NULL, 0, flash_retry_kick) == NRF_SUCCESS) + { + m_flash_busy_kicks++; + return; + } + flash_retry_fail(retval); +} + +/** @brief After a flash timeout, re-issue the element at rp, or report the timeout once retries run out. */ static void flash_retry(void) { - if (m_flash_retries < FLASH_RETRY_LIMIT) + if (m_flash_retries >= FLASH_RETRY_LIMIT) { - m_flash_retries++; - uint32_t retval = cmd_process(); - if (retval == NRF_SUCCESS) - { - return; - } - // BUSY: no flash event will follow, so re-issue from the scheduler. - if (retval == NRF_ERROR_BUSY && app_sched_event_put(NULL, 0, flash_retry_kick) == NRF_SUCCESS) - { - return; - } + flash_retry_fail(NRF_ERROR_TIMEOUT); + return; } - m_flash_retries = 0; - m_lazy_erase_active = false; - m_processing_pending = false; - app_notify(NRF_ERROR_TIMEOUT, &m_cmd_queue.cmd[m_cmd_queue.rp]); + m_flash_retries++; + flash_retry_issue(); } /** @@ -560,6 +575,7 @@ void pstorage_sys_event_handler(uint32_t sys_evt) { p_cmd = &m_cmd_queue.cmd[m_cmd_queue.rp]; m_flash_retries = 0; + m_flash_busy_kicks = 0; m_round_val++; bool command_finished = ((m_round_val * SOC_MAX_WRITE_SIZE) >= p_cmd->size); @@ -650,7 +666,7 @@ void pstorage_sys_event_handler(uint32_t sys_evt) break; case NRF_EVT_FLASH_OPERATION_ERROR: - // The SoftDevice missed its radio gap; erase and same-word write are safe to repeat. + // The SoftDevice could not start the operation in a radio gap, so nothing was written; re-issue it. flash_retry(); break; @@ -746,6 +762,7 @@ uint32_t pstorage_init(void) m_next_app_instance = 0; m_round_val = 0; m_flash_retries = 0; + m_flash_busy_kicks = 0; for(unsigned int index = 0; index < PSTORAGE_NUM_OF_PAGES; index++) { From f6dba8163f4a2947b8b9708a9f5dcebea6c86540 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 26 Oct 2026 05:35:15 -0500 Subject: [PATCH 4/4] fix(dfu): space flash timeout retries so the radio can clear --- changelog.md | 2 +- .../drivers_nrf/pstorage/pstorage_raw.c | 24 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index 61fd5321..9c4b5bbe 100644 --- a/changelog.md +++ b/changelog.md @@ -5,7 +5,7 @@ it and is upstream Adafruit history, kept for provenance. ## OTAFIX 2.6 -- A SoftDevice flash timeout during a BLE or serial transfer, or the activation settings save, is retried up to 5 times instead of resetting the bootloader. +- A SoftDevice flash timeout during a BLE or serial transfer, or the activation settings save, is retried every 100 ms, up to 20 times, instead of resetting the bootloader. - `INFO_UF2.TXT` shows a `Last-Fault: id=0x.. pc=0x.. info=0x..` line for a SoftDevice or SDK fault the bootloader recorded since the last UF2 drive session. It appears only if the next boot reaches the drive before an application boots, since the application's RAM use wipes the record. The record lives in a 16-byte RAM region behind a magic word and is cleared once printed; for an SDK error `info` is the error code. - A device with no valid application that is plugged into a USB host comes up as a UF2 drive showing NO FIRMWARE instead of waiting for a phone. On battery (no VBUS) it goes straight to the BLE OTA wait, and with VBUS but no host enumerating within 3 s (a charger or power bank) it falls through to the same wait. A cabled board with no application returns to the drive after an aborted copy, and after an eject on boards built with `UF2_EXIT_ON_EJECT` (`mesh_tracker_x1`, `t1000_e`). When the cable is pulled while the drive is up (VBUS gone for 250 ms), it goes to the BLE OTA wait. Plugging a cable into a board with no application that is waiting for BLE, with no phone connected, brings the drive up (VBUS present for 250 ms); a cable already present when the wait began, or a phone connected, leaves the wait alone. - A UF2 application copy marks `bank_0` invalid when its first application block arrives, so a cable pulled mid-copy leaves the device with no application instead of an old `bank_0` marked valid over a half-written image. The completion path records the real application size as before. diff --git a/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c b/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c index 08511d8a..d8aeb52f 100644 --- a/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c +++ b/lib/sdk11/components/drivers_nrf/pstorage/pstorage_raw.c @@ -20,6 +20,7 @@ #include "nrf_soc.h" #include "app_util.h" #include "app_scheduler.h" +#include "app_timer.h" /** @file * @@ -48,7 +49,9 @@ #error No target defined #endif -#define FLASH_RETRY_LIMIT 5 /**< Timed-out flash operations re-issued before the error is reported. */ +#define FLASH_RETRY_LIMIT 20 /**< Timed-out flash operations re-issued before the error is reported. */ +#define FLASH_RETRY_DELAY_MS 100 /**< Wait before re-issuing a timed-out operation; 20 x 100 ms gives the radio 2 s to quiet. */ +#define FLASH_BUSY_KICK_LIMIT 5 /**< BUSY re-issues scheduled for one operation. */ #define PENDING_PACKET_SIZE 256 /**< Maximum size of a single pending packet data buffer. */ #define PENDING_PACKET_COUNT 16 /**< Packets buffered during lazy erase; must exceed the largest client PRN interval (Android 10, Apple 8). */ @@ -128,6 +131,7 @@ static bool m_lazy_erase_active; /**< True static uint32_t m_lazy_erase_page; /**< Page index currently being erased. */ static bool m_processing_pending; /**< True if processing a pending packet store. */ static uint8_t m_flash_retries; /**< Timeouts retried for the operation at rp. */ +APP_TIMER_DEF(m_flash_retry_timer); static uint8_t m_flash_busy_kicks; /**< BUSY re-issues scheduled for the operation at rp. */ /** @@ -528,7 +532,7 @@ static void flash_retry_issue(void) { return; } - if (retval == NRF_ERROR_BUSY && m_flash_busy_kicks < FLASH_RETRY_LIMIT && + if (retval == NRF_ERROR_BUSY && m_flash_busy_kicks < FLASH_BUSY_KICK_LIMIT && app_sched_event_put(NULL, 0, flash_retry_kick) == NRF_SUCCESS) { m_flash_busy_kicks++; @@ -537,16 +541,25 @@ static void flash_retry_issue(void) flash_retry_fail(retval); } -/** @brief After a flash timeout, re-issue the element at rp, or report the timeout once retries run out. */ +static void flash_retry_timer_handler(void * p_context) +{ + (void) p_context; + if (app_sched_event_put(NULL, 0, flash_retry_kick) != NRF_SUCCESS) + { + app_timer_start(m_flash_retry_timer, APP_TIMER_TICKS(FLASH_RETRY_DELAY_MS), NULL); + } +} + +/** @brief After a flash timeout, re-issue the element at rp after a delay, or report the timeout once retries run out. */ static void flash_retry(void) { - if (m_flash_retries >= FLASH_RETRY_LIMIT) + if (m_flash_retries >= FLASH_RETRY_LIMIT || + app_timer_start(m_flash_retry_timer, APP_TIMER_TICKS(FLASH_RETRY_DELAY_MS), NULL) != NRF_SUCCESS) { flash_retry_fail(NRF_ERROR_TIMEOUT); return; } m_flash_retries++; - flash_retry_issue(); } /** @@ -763,6 +776,7 @@ uint32_t pstorage_init(void) m_round_val = 0; m_flash_retries = 0; m_flash_busy_kicks = 0; + (void) app_timer_create(&m_flash_retry_timer, APP_TIMER_MODE_SINGLE_SHOT, flash_retry_timer_handler); for(unsigned int index = 0; index < PSTORAGE_NUM_OF_PAGES; index++) {