diff --git a/src/helpers/ota/OtaStoreQspiNrf52.cpp b/src/helpers/ota/OtaStoreQspiNrf52.cpp index 20c2208b..83278dcc 100644 --- a/src/helpers/ota/OtaStoreQspiNrf52.cpp +++ b/src/helpers/ota/OtaStoreQspiNrf52.cpp @@ -266,7 +266,7 @@ bool OtaStoreQspiNrf52::ensureFlash() { return false; } _qspi_awake = true; - delayMicroseconds(50); + delayMicroseconds(MOTA_QSPI_DPD_WAKE_GUARD_US); 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) { @@ -306,7 +306,10 @@ void OtaStoreQspiNrf52::releaseFlash() { // releasing the nRF QSPI peripheral instead of leaving both active for // the rest of the boot. ensureFlash() issues 0xAB on the next operation. (void)customInstruction(0xB9, NRF_QSPI_CINSTR_LEN_1B); - delayMicroseconds(5); + // Do not deactivate or let a following ensureFlash() assert CS until the + // flash has both entered and remained in DPD for its required interval. + // plan_layout() is intentionally followed immediately by begin(). + delayMicroseconds(MOTA_QSPI_DPD_ENTRY_GUARD_US); } // Match nrfx_qspi_uninit(): DEACTIVATE does not require a READY wait before // disabling the peripheral. Trigger it after every successful ENABLE, even diff --git a/src/helpers/ota/OtaStoreQspiNrf52.h b/src/helpers/ota/OtaStoreQspiNrf52.h index cbe11a3d..2b1d8487 100644 --- a/src/helpers/ota/OtaStoreQspiNrf52.h +++ b/src/helpers/ota/OtaStoreQspiNrf52.h @@ -1,5 +1,7 @@ #pragma once +#include + #if defined(OTA_QSPI_STORE) && defined(QSPIFLASH) #error "OTA_QSPI_STORE raw staging cannot share a QSPI chip with QSPIFLASH" #endif @@ -16,6 +18,21 @@ #error "RAK3401's RAK13302 radio already owns the WisBlock SPI bus/chip-select" #endif +namespace mesh { +namespace ota { + +// A status/capacity probe can be followed immediately by begin(). Supported +// NOR parts need time both to enter deep power-down after B9 and to remain +// there before CS may be asserted again. MX25R1635F needs 10 us + 30 us; +// retain margin for scheduling granularity and other matched flashes. +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; + +} // namespace ota +} // namespace mesh + #if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) #include "OtaStore.h" diff --git a/test/test_ota/test_ota_flashplan.cpp b/test/test_ota/test_ota_flashplan.cpp index bdc66b43..e944f42a 100644 --- a/test/test_ota/test_ota_flashplan.cpp +++ b/test/test_ota/test_ota_flashplan.cpp @@ -2,6 +2,7 @@ #include #include "helpers/ota/OtaFlashLayout_nrf52.h" +#include "helpers/ota/OtaStoreQspiNrf52.h" #include "helpers/ota/OtaSdHandoff.h" using namespace mesh::ota; @@ -25,6 +26,15 @@ static constexpr uint32_t CAP_V7 = LEGACY - APP_END_V7; static constexpr uint32_t CAP_V6_EXPANDED = EXPANDED - APP_END_V6; static constexpr uint32_t CAP_V7_EXPANDED = EXPANDED - APP_END_V7; +TEST(OtaQspiTiming, PreservesDeepPowerDownEntryAndWakeGuards) { + // MX25R1635F requires 10 us to enter DPD plus 30 us before another command; + // its release latency can reach 45 us. These constants are consumed by the + // real HAL path, so a future power-saving edit cannot restore the live + // plan_layout()->begin() race without failing the native suite. + EXPECT_GE(MOTA_QSPI_DPD_ENTRY_GUARD_US, 50u); + EXPECT_GE(MOTA_QSPI_DPD_WAKE_GUARD_US, 45u); +} + TEST(OtaFlashPlan, SelectsCeilingFromLinkedLayoutAndStorage) { // Actual internal secondary storage is authoritative regardless of linker selection. EXPECT_EQ(mota_nrf52_stage_ceiling_for_layout(EXPANDED, true), LEGACY);