Wait for QSPI flash operations to complete

This commit is contained in:
mikecarper
2026-08-20 18:48:03 -07:00
parent eec34b93e5
commit 72a9fd765b
8 changed files with 283 additions and 19 deletions
+2
View File
@@ -169,6 +169,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
| LoRa OTA | [`ota cancel`; `ota drop`; `ota stop`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes |
| LoRa OTA | [`ota announce`; `ota adv`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes |
| LoRa OTA | [`ota self`; `ota id`](ota_protocol.md#11-cli-surface-otaclicpp) | Firmware with EndF trailer | No | No | Yes |
| LoRa OTA | [`ota qspi`; `ota storage`](ota_protocol.md#11-cli-surface-otaclicpp) | nRF52 build with raw-QSPI OTA staging | Feature | Feature | Feature |
| LoRa OTA | [`ota folder`; `ota fold`](ota_protocol.md#11-cli-surface-otaclicpp) | `on` needs compiled folder transport | No | No | Feature |
| LoRa OTA | [`ota config`; `ota cfg`; `ota set`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes |
| LoRa OTA | [`ota key`; `ota keys`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes |
@@ -327,6 +328,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
| LoRa OTA | [`ota cancel`; `ota drop`; `ota stop`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | Yes | Yes |
| LoRa OTA | [`ota announce`; `ota adv`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | Yes | Yes |
| LoRa OTA | [`ota self`; `ota id`](ota_protocol.md#11-cli-surface-otaclicpp) | Firmware with EndF trailer | No | No | Yes | Yes | Yes |
| LoRa OTA | [`ota qspi`; `ota storage`](ota_protocol.md#11-cli-surface-otaclicpp) | nRF52 build with raw-QSPI OTA staging | Feature | Feature | Feature | No | No |
| LoRa OTA | [`ota folder`; `ota fold`](ota_protocol.md#11-cli-surface-otaclicpp) | `on` needs compiled serial or TCP folder transport | No | No | Feature | Feature | Feature |
| LoRa OTA | [`ota config`; `ota cfg`; `ota set`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | Yes | Yes |
| LoRa OTA | [`ota key`; `ota keys`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | Yes | Yes |
+8 -1
View File
@@ -108,14 +108,16 @@ After installing the repeater application, check:
get bootloader.ver
ota self
ota status
ota qspi
```
A ready target reports all of the following:
A ready RAK15001 target reports all of the following:
```text
QSPI store:2048K
bootloader: QSPI apply OK
bl:QSPI
QSPI jedec=C84015 size=2048K sr1=00 stage=jedec
```
Other supported boards can report a capacity different from 2048K; the
@@ -123,6 +125,11 @@ RAK15001 target must report exactly 2048K. `QSPI store:ERR 0K`, `NO QSPI`, or
`bl:NO-QSPI` means the flash wiring, flash power, or bootloader does not match.
Do not start an install in that state.
`ota qspi` is a read-only diagnostic probe available on QSPI OTA builds. It
reports the exact JEDEC ID, status-register byte, last store stage, and the
first latched storage error. Run it after an immediate `storage error` before
starting another pull; later capacity probes preserve that failure detail.
## Capacity and package types
The store reads the JEDEC capacity at runtime and accepts supported 1 MiB
+1
View File
@@ -749,6 +749,7 @@ ota rescue install <base_hash16> internal-flash nRF52 only: recover from failed
ota cancel | drop | stop drop the current fetch session (frees the slot)
ota announce | adv serve self + send a beacon now
ota self | id print this firmware's EndF (body/image size, base_hash)
ota qspi | storage QSPI nRF52 only: JEDEC/SR1/stage/latched storage error (read-only)
ota folder | fold [on|off] attach/detach an external .mota folder (host daemon) ; bare = list
ota config | cfg | set [autofetch|autoinstall|checkpoint] ... show/set persisted policy
ota key | keys [add|rm <hex>] trusted signer allowlist ; bare = list
+19
View File
@@ -151,6 +151,10 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
strcpy(reply,
"OTA: status | stats | ls | get <id> flash [rescue] | install | rescue install <hash16> | "
"cancel | announce | self | folder | config | key");
#elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | qspi | "
"folder | config | key");
#else
snprintf(reply, 160,
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | folder | "
@@ -522,6 +526,21 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
sprintf(reply, "OK beacon sent (serving=%s)", c.serving ? "self fw" : "nothing");
#endif
// ---- raw-QSPI staging diagnostics (read-only probe; preserves a latched fetch failure) ----
} else if (is_cmd(a, "qspi|storage", &rest)) {
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
// This probe only reads JEDEC/SR1. capacity() deliberately preserves a
// latched fetch failure so asking for diagnostics cannot erase its cause.
uint32_t qspi_capacity = c.fetch_store.capacity();
const char *error = c.fetch_store.last_error();
snprintf(reply, 160, "QSPI jedec=%06lX size=%luK sr1=%02X stage=%s%s%s",
(unsigned long)c.fetch_store.jedec_id(),
(unsigned long)(qspi_capacity / 1024), c.fetch_store.status1(),
c.fetch_store.last_stage(), error[0] ? " error=" : "", error);
#else
strcpy(reply, "ERR this build does not use a QSPI OTA store");
#endif
// ---- running firmware identity (compare against a delta's base_hash) ----
} else if (is_cmd(a, "self|id", &rest)) {
SelfFwInfo fi;
+183 -17
View File
@@ -4,6 +4,7 @@
#include "OtaByteIO.h"
#include "OtaFlashLayout_nrf52.h"
#include "hal/nrf_gpio.h"
#include "hal/nrf_qspi.h"
#include "nrf.h"
@@ -139,6 +140,28 @@ static uint8_t qspi_io3_pin() {
#endif
}
static void configure_qspi_gpio(const nrf_qspi_pins_t& pins) {
// The QSPI PSEL registers do not configure GPIO drive strength. Nordic
// initializes every connected pad as input-disconnected/high-drive before
// the peripheral takes ownership and controls each pin's direction.
nrf_gpio_pin_clear(pins.sck_pin); // mode 0 idle level
nrf_gpio_pin_set(pins.csn_pin); // never select the NOR during handoff
nrf_gpio_cfg(pins.sck_pin, NRF_GPIO_PIN_DIR_INPUT, NRF_GPIO_PIN_INPUT_DISCONNECT,
NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE);
nrf_gpio_cfg(pins.csn_pin, NRF_GPIO_PIN_DIR_INPUT, NRF_GPIO_PIN_INPUT_DISCONNECT,
NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE);
nrf_gpio_cfg(pins.io0_pin, NRF_GPIO_PIN_DIR_INPUT, NRF_GPIO_PIN_INPUT_DISCONNECT,
NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE);
nrf_gpio_cfg(pins.io1_pin, NRF_GPIO_PIN_DIR_INPUT, NRF_GPIO_PIN_INPUT_DISCONNECT,
NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE);
if (pins.io2_pin != NRF_QSPI_PIN_NOT_CONNECTED)
nrf_gpio_cfg(pins.io2_pin, NRF_GPIO_PIN_DIR_INPUT, NRF_GPIO_PIN_INPUT_DISCONNECT,
NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE);
if (pins.io3_pin != NRF_QSPI_PIN_NOT_CONNECTED)
nrf_gpio_cfg(pins.io3_pin, NRF_GPIO_PIN_DIR_INPUT, NRF_GPIO_PIN_INPUT_DISCONNECT,
NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE);
}
} // namespace
OtaStoreQspiNrf52::OtaStoreQspiNrf52() {
@@ -151,6 +174,9 @@ OtaStoreQspiNrf52::~OtaStoreQspiNrf52() {
void OtaStoreQspiNrf52::fail(const char *message) {
_io_ok = false;
// Keep the first concrete failure from a manifest attempt. Higher-level
// wrappers must not replace a program/status error with a generic one.
if (_error[0]) return;
strncpy(_error, message ? message : "QSPI error", sizeof(_error) - 1);
_error[sizeof(_error) - 1] = 0;
}
@@ -165,6 +191,7 @@ void OtaStoreQspiNrf52::resetSession() {
memset(_data_page, 0xFF, sizeof(_data_page));
memset(_known_pages, 0, sizeof(_known_pages));
_error[0] = 0;
_stage = OtaQspiStage::IDLE;
}
bool OtaStoreQspiNrf52::pageKnown(uint32_t page) const {
@@ -199,10 +226,35 @@ bool OtaStoreQspiNrf52::customInstruction(uint8_t opcode, uint8_t length, uint8_
return true;
}
bool OtaStoreQspiNrf52::readStatus1() {
uint8_t status = 0;
if (!customInstruction(0x05, NRF_QSPI_CINSTR_LEN_2B, &status)) {
fail("QSPI status read failed");
return false;
}
_status1 = status;
return true;
}
bool OtaStoreQspiNrf52::waitMemoryReady(uint32_t timeout_ms) {
const uint32_t started = millis();
for (;;) {
if (!readStatus1()) return false;
if (!mota_qspi_status_busy(_status1)) {
_memory_operation_pending = false;
return true;
}
// This also covers recovery after a reset during an earlier NOR write:
// once WIP is observed, releaseFlash must not send DPD or cut power unless
// a later status read proves the operation finished.
_memory_operation_pending = true;
if ((uint32_t)(millis() - started) >= timeout_ms) return false;
delay(1);
}
}
bool OtaStoreQspiNrf52::ensureFlash() {
if (_qspi_ready) return true;
_error[0] = 0;
_io_ok = true;
#if defined(OTA_QSPI_POWER_PIN)
pinMode(OTA_QSPI_POWER_PIN, OUTPUT);
@@ -229,6 +281,7 @@ bool OtaStoreQspiNrf52::ensureFlash() {
pins.io1_pin = qspi_io1_pin();
pins.io2_pin = qspi_io2_pin();
pins.io3_pin = qspi_io3_pin();
configure_qspi_gpio(pins);
nrf_qspi_pins_set(NRF_QSPI, &pins);
nrf_qspi_prot_conf_t protocol;
@@ -251,6 +304,7 @@ bool OtaStoreQspiNrf52::ensureFlash() {
nrf_qspi_enable(NRF_QSPI);
_qspi_active = true;
_stage = OtaQspiStage::ACTIVATE;
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_ACTIVATE);
if (!waitReady(1000)) {
@@ -260,23 +314,45 @@ bool OtaStoreQspiNrf52::ensureFlash() {
}
// Release from deep power-down, then identify capacity from the JEDEC byte.
_stage = OtaQspiStage::WAKE;
if (!customInstruction(0xAB, NRF_QSPI_CINSTR_LEN_1B)) {
releaseFlash();
fail("QSPI wake failed");
return false;
}
_qspi_awake = true;
// Until SR1 is read successfully, conservatively assume an operation from
// the preceding boot may still be active. Only waitMemoryReady() may clear
// this guard after observing WIP=0.
_memory_operation_pending = true;
delayMicroseconds(MOTA_QSPI_DPD_WAKE_GUARD_US);
// Recover cleanly if this boot/activation follows an interrupted program or
// erase. RDID is not guaranteed to respond while WIP is set, so prove the
// NOR idle before requesting its identity.
_stage = OtaQspiStage::STATUS;
if (!waitMemoryReady(30000)) {
if (_io_ok) fail("QSPI wake busy timed out");
releaseFlash();
return false;
}
alignas(4) uint8_t jedec[4] = { 0, 0, 0, 0 };
if (!customInstruction(0x9F, NRF_QSPI_CINSTR_LEN_4B, jedec) || jedec[0] == 0 || jedec[0] == 0xFF ||
jedec[2] < 20 || jedec[2] > 24) {
_stage = OtaQspiStage::JEDEC;
_jedec_id = 0;
if (!customInstruction(0x9F, NRF_QSPI_CINSTR_LEN_4B, jedec)) {
releaseFlash();
fail("QSPI JEDEC read failed");
return false;
}
_jedec_id = ((uint32_t)jedec[0] << 16) | ((uint32_t)jedec[1] << 8) | jedec[2];
if (jedec[0] == 0 || jedec[0] == 0xFF || jedec[2] < 20 || jedec[2] > 24) {
releaseFlash();
fail("QSPI JEDEC ID/capacity unsupported");
return false;
}
#ifdef OTA_QSPI_EXPECTED_JEDEC_ID
const uint32_t jedec_id = ((uint32_t)jedec[0] << 16) | ((uint32_t)jedec[1] << 8) | jedec[2];
if (jedec_id != (uint32_t)OTA_QSPI_EXPECTED_JEDEC_ID) {
if (_jedec_id != (uint32_t)OTA_QSPI_EXPECTED_JEDEC_ID) {
releaseFlash();
fail("QSPI JEDEC ID does not match target");
return false;
@@ -300,7 +376,7 @@ bool OtaStoreQspiNrf52::ensureFlash() {
}
void OtaStoreQspiNrf52::releaseFlash() {
if (_qspi_awake) {
if (_qspi_awake && !_memory_operation_pending) {
// The repeater can remain idle for hours after a capacity/status probe or
// a completed checkpoint. Put the NOR into deep power-down before
// releasing the nRF QSPI peripheral instead of leaving both active for
@@ -311,24 +387,45 @@ void OtaStoreQspiNrf52::releaseFlash() {
// plan_layout() is intentionally followed immediately by begin().
delayMicroseconds(MOTA_QSPI_DPD_ENTRY_GUARD_US);
}
// If WIP could not be proven clear, do not send DPD or cut flash power.
// Deactivating the controller leaves CS high while the NOR finishes or
// remains available for a later diagnostic probe.
// Match nrfx_qspi_uninit(): DEACTIVATE does not require a READY wait before
// disabling the peripheral. Trigger it after every successful ENABLE, even
// when wake or JEDEC identification failed before _qspi_ready was set.
if (_qspi_active) {
if (_memory_operation_pending) {
// QSPI owns pin direction while enabled. Preload and configure CS# high
// before releasing the peripheral so an unresolved NOR operation cannot
// see a floating or asserted chip select during the handoff.
const uint8_t cs_pin = qspi_cs_pin();
nrf_gpio_pin_set(cs_pin);
nrf_gpio_cfg(cs_pin, NRF_GPIO_PIN_DIR_OUTPUT, NRF_GPIO_PIN_INPUT_DISCONNECT,
NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE);
}
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_DEACTIVATE);
nrf_qspi_disable(NRF_QSPI);
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
if (_memory_operation_pending) {
const nrf_qspi_pins_t disconnected = {
NRF_QSPI_PIN_NOT_CONNECTED, NRF_QSPI_PIN_NOT_CONNECTED,
NRF_QSPI_PIN_NOT_CONNECTED, NRF_QSPI_PIN_NOT_CONNECTED,
NRF_QSPI_PIN_NOT_CONNECTED, NRF_QSPI_PIN_NOT_CONNECTED
};
nrf_qspi_pins_set(NRF_QSPI, &disconnected);
}
}
_qspi_active = false;
_qspi_awake = false;
_qspi_ready = false;
#if defined(OTA_QSPI_POWER_PIN)
digitalWrite(OTA_QSPI_POWER_PIN, LOW);
if (!_memory_operation_pending) digitalWrite(OTA_QSPI_POWER_PIN, LOW);
#elif defined(PIN_FLASH_EN)
digitalWrite(PIN_FLASH_EN, LOW);
if (!_memory_operation_pending) digitalWrite(PIN_FLASH_EN, LOW);
#elif defined(QSPI_FLASH_EN)
digitalWrite(QSPI_FLASH_EN, LOW);
if (!_memory_operation_pending) digitalWrite(QSPI_FLASH_EN, LOW);
#endif
}
@@ -341,6 +438,7 @@ bool OtaStoreQspiNrf52::dmaReadAligned(uint32_t address, uint32_t length) {
fail("QSPI aligned read invalid");
return false;
}
_stage = OtaQspiStage::READ;
nrf_qspi_read_buffer_set(NRF_QSPI, _bounce, length, address);
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_READSTART);
@@ -358,13 +456,23 @@ bool OtaStoreQspiNrf52::dmaWriteAligned(uint32_t address, uint32_t length) {
fail("QSPI aligned program invalid");
return false;
}
_stage = OtaQspiStage::PROGRAM;
nrf_qspi_write_buffer_set(NRF_QSPI, _bounce, length, address);
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
_memory_operation_pending = true;
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_WRITESTART);
if (!waitReady(10000)) {
fail("QSPI program timed out");
return false;
}
// READY only reports that the page-program command/data were sent. Poll the
// NOR WIP bit before verifying, issuing another operation, or entering DPD.
_stage = OtaQspiStage::PROGRAM_BUSY;
if (!waitMemoryReady(10000)) {
if (_io_ok) fail("QSPI program busy timed out");
return false;
}
_memory_operation_pending = false;
return true;
}
@@ -436,13 +544,22 @@ bool OtaStoreQspiNrf52::rawWrite(uint32_t address, const void *data, uint32_t le
bool OtaStoreQspiNrf52::rawErasePage(uint32_t address) {
if (!ensureFlash() || (address & (PAGE - 1)) != 0 || address > _flash_size - PAGE) return false;
_stage = OtaQspiStage::ERASE;
nrf_qspi_erase_ptr_set(NRF_QSPI, address, NRF_QSPI_ERASE_LEN_4KB);
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
_memory_operation_pending = true;
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_ERASESTART);
if (!waitReady(30000)) {
fail("QSPI erase timed out");
return false;
}
// As with program, READY precedes completion of the flash's internal erase.
_stage = OtaQspiStage::ERASE_BUSY;
if (!waitMemoryReady(30000)) {
if (_io_ok) fail("QSPI erase busy timed out");
return false;
}
_memory_operation_pending = false;
return true;
}
@@ -498,12 +615,36 @@ bool OtaStoreQspiNrf52::useDataPage(uint32_t page) {
uint32_t OtaStoreQspiNrf52::capacity() const {
OtaStoreQspiNrf52 *self = const_cast<OtaStoreQspiNrf52 *>(this);
// A read-only CLI probe must not erase the reason a fetch just failed.
// Preserve the latched failure while still refreshing the JEDEC ID/capacity.
const bool preserve_failure = self->_error[0] != 0;
char saved_error[sizeof(self->_error)];
OtaQspiStage saved_stage = self->_stage;
uint8_t saved_status1 = self->_status1;
bool saved_io_ok = self->_io_ok;
if (preserve_failure) {
strncpy(saved_error, self->_error, sizeof(saved_error));
saved_error[sizeof(saved_error) - 1] = 0;
}
uint32_t result = self->ensureFlash() ? self->_flash_size : 0;
self->releaseFlash();
if (preserve_failure) {
strncpy(self->_error, saved_error, sizeof(self->_error));
self->_error[sizeof(self->_error) - 1] = 0;
self->_stage = saved_stage;
self->_status1 = saved_status1;
self->_io_ok = saved_io_ok;
}
return result;
}
bool OtaStoreQspiNrf52::plan_layout(bool, uint32_t image_size, uint32_t, uint32_t payload_size) {
// This is the start of a new manifest admission attempt. Clear diagnostics
// left by the normal empty-store reopen probe, then latch the first error
// from this attempt until the operator reads it or starts another attempt.
_error[0] = 0;
_io_ok = true;
_stage = OtaQspiStage::IDLE;
const uint32_t app_base = mota_nrf52_app_base();
if (image_size == 0 || payload_size == 0 || app_base >= MOTA_NRF52_APP_END ||
image_size > MOTA_NRF52_APP_END - app_base) {
@@ -517,8 +658,12 @@ bool OtaStoreQspiNrf52::plan_layout(bool, uint32_t image_size, uint32_t, uint32_
}
bool OtaStoreQspiNrf52::begin(uint32_t total_size) {
if (!ensureFlash() || total_size < 13 || total_size > _flash_size) {
if (_io_ok) fail("QSPI lacks space for update");
if (!ensureFlash()) {
releaseFlash();
return false;
}
if (total_size < 13 || total_size > _flash_size) {
fail("QSPI lacks space for update");
releaseFlash();
return false;
}
@@ -527,9 +672,19 @@ bool OtaStoreQspiNrf52::begin(uint32_t total_size) {
// replaced with the new header at the first checkpoint/finalize.
uint8_t zero[4] = { 0, 0, 0, 0 };
uint8_t check[sizeof(zero)];
if (!rawWrite(0, zero, sizeof(zero)) || !rawRead(0, check, sizeof(check)) ||
memcmp(check, zero, sizeof(zero)) != 0) {
fail("QSPI old-container invalidation failed");
if (!rawWrite(0, zero, sizeof(zero))) {
releaseFlash();
return false;
}
if (!rawRead(0, check, sizeof(check))) {
releaseFlash();
return false;
}
if (memcmp(check, zero, sizeof(zero)) != 0) {
_stage = OtaQspiStage::INVALIDATE_VERIFY;
char message[sizeof(_error)];
snprintf(message, sizeof(message), "QSPI invalidation mismatch sr1=%02X", _status1);
fail(message);
releaseFlash();
return false;
}
@@ -540,11 +695,22 @@ bool OtaStoreQspiNrf52::begin(uint32_t total_size) {
}
bool OtaStoreQspiNrf52::set_meta_size(uint32_t meta_bytes) {
return _total >= 13 && meta_bytes <= PAGE;
if (_total < 13 || meta_bytes > PAGE) {
_stage = OtaQspiStage::META_SIZE;
fail("QSPI metadata exceeds first page");
return false;
}
return true;
}
bool OtaStoreQspiNrf52::write(uint32_t offset, const uint8_t *data, uint32_t len) {
if (!_io_ok || !data || (uint64_t)offset + len > _total) {
if (!_io_ok) {
releaseFlash();
return false;
}
if (!data || (uint64_t)offset + len > _total) {
_stage = OtaQspiStage::BUFFER_WRITE;
fail(!data ? "QSPI write buffer missing" : "QSPI write outside container");
releaseFlash();
return false;
}
+52
View File
@@ -30,6 +30,49 @@ static const uint32_t MOTA_QSPI_DPD_ENTRY_GUARD_US = 50u;
// Release-from-deep-power-down latency is as high as 45 us on supported NOR.
static const uint32_t MOTA_QSPI_DPD_WAKE_GUARD_US = 50u;
// The nRF QSPI READY event only says that a program/erase command and its
// data reached the NOR. The memory can remain internally busy afterwards;
// issuing another command or powering it down before WIP clears is unsafe.
static const uint8_t MOTA_QSPI_STATUS_WIP = 0x01u;
inline bool mota_qspi_status_busy(uint8_t status1) {
return (status1 & MOTA_QSPI_STATUS_WIP) != 0;
}
enum class OtaQspiStage : uint8_t {
IDLE = 0,
ACTIVATE,
WAKE,
JEDEC,
STATUS,
READ,
PROGRAM,
PROGRAM_BUSY,
ERASE,
ERASE_BUSY,
INVALIDATE_VERIFY,
META_SIZE,
BUFFER_WRITE
};
inline const char *mota_qspi_stage_name(OtaQspiStage stage) {
switch (stage) {
case OtaQspiStage::ACTIVATE: return "activate";
case OtaQspiStage::WAKE: return "wake";
case OtaQspiStage::JEDEC: return "jedec";
case OtaQspiStage::STATUS: return "status";
case OtaQspiStage::READ: return "read";
case OtaQspiStage::PROGRAM: return "program";
case OtaQspiStage::PROGRAM_BUSY: return "program-busy";
case OtaQspiStage::ERASE: return "erase";
case OtaQspiStage::ERASE_BUSY: return "erase-busy";
case OtaQspiStage::INVALIDATE_VERIFY: return "invalidate-verify";
case OtaQspiStage::META_SIZE: return "meta-size";
case OtaQspiStage::BUFFER_WRITE: return "buffer-write";
default: return "idle";
}
}
} // namespace ota
} // namespace mesh
@@ -53,9 +96,13 @@ class OtaStoreQspiNrf52 : public OtaStore {
uint32_t _total = 0;
uint32_t _flash_size = 0;
uint32_t _jedec_id = 0;
uint8_t _status1 = 0xFF;
OtaQspiStage _stage = OtaQspiStage::IDLE;
bool _qspi_active = false;
bool _qspi_awake = false;
bool _qspi_ready = false;
bool _memory_operation_pending = false;
bool _io_ok = true;
bool _meta_dirty = false;
bool _data_dirty = false;
@@ -72,6 +119,8 @@ class OtaStoreQspiNrf52 : public OtaStore {
void releaseFlash();
bool waitReady(uint32_t timeout_ms);
bool customInstruction(uint8_t opcode, uint8_t length, uint8_t *rx = nullptr);
bool readStatus1();
bool waitMemoryReady(uint32_t timeout_ms);
bool dmaReadAligned(uint32_t address, uint32_t length);
bool dmaWriteAligned(uint32_t address, uint32_t length);
bool rawRead(uint32_t address, void *data, uint32_t length);
@@ -104,6 +153,9 @@ public:
// Sets APRV only after all app-side verification gates have passed.
bool approve_for_bootloader();
const char *last_error() const { return _error; }
const char *last_stage() const { return mota_qspi_stage_name(_stage); }
uint32_t jedec_id() const { return _jedec_id; }
uint8_t status1() const { return _status1; }
};
} // namespace ota
+16
View File
@@ -35,6 +35,22 @@ TEST(OtaQspiTiming, PreservesDeepPowerDownEntryAndWakeGuards) {
EXPECT_GE(MOTA_QSPI_DPD_WAKE_GUARD_US, 45u);
}
TEST(OtaQspiStatus, TreatsOnlyTheNorWriteInProgressBitAsBusy) {
// Nordic READY is not flash completion. The hardware path polls RDSR until
// this predicate clears after every program and erase operation.
EXPECT_FALSE(mota_qspi_status_busy(0x00));
EXPECT_FALSE(mota_qspi_status_busy(0xFC));
EXPECT_TRUE(mota_qspi_status_busy(0x01));
EXPECT_TRUE(mota_qspi_status_busy(0xFF));
}
TEST(OtaQspiDiagnostics, ExposesStableFailureStageNames) {
EXPECT_STREQ(mota_qspi_stage_name(OtaQspiStage::PROGRAM), "program");
EXPECT_STREQ(mota_qspi_stage_name(OtaQspiStage::PROGRAM_BUSY), "program-busy");
EXPECT_STREQ(mota_qspi_stage_name(OtaQspiStage::ERASE_BUSY), "erase-busy");
EXPECT_STREQ(mota_qspi_stage_name(OtaQspiStage::INVALIDATE_VERIFY), "invalidate-verify");
}
TEST(OtaFlashPlan, SelectsCeilingFromLinkedLayoutAndStorage) {
// Actual internal secondary storage is authoritative regardless of linker selection.
EXPECT_EQ(mota_nrf52_stage_ceiling_for_layout(EXPANDED, true), LEGACY);
+2 -1
View File
@@ -728,7 +728,8 @@ class ReliabilityTests(unittest.TestCase):
"OTA | no download | target:1234ABCD hw=Xiao_nrf52 | bl:QSPI blrc:B0",
"> 0.9.2-OTAFIX2.4",
"self body=1 image=2 base_hash=0011223344556677 | "
"bootloader: QSPI apply OK (abi=2 codecs=0x5)",
"QSPI store:2048K | bootloader: QSPI apply OK "
"(abi=2 codecs=0x5)",
"OTA | fw v1.17.0 id=00112233",
])