diff --git a/docs/ota_nrf52_qspi.md b/docs/ota_nrf52_qspi.md index f6cbf924..93e1819f 100644 --- a/docs/ota_nrf52_qspi.md +++ b/docs/ota_nrf52_qspi.md @@ -77,7 +77,7 @@ a hardware chip-select rework. ## One-time prerequisite -Install a QSPI-capable OTAFIX 2.4.1 preview.8 or newer bootloader for the exact +Install a QSPI-capable OTAFIX 2.4.1 preview.9 or newer bootloader for the exact board from the [OTAFIX releases](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases) before using LoRa OTA. The release notes must explicitly list that board's QSPI @@ -174,8 +174,12 @@ download is reopened only when its header and trailer are valid; every claimed block is re-hashed before it is trusted. Between a probe, transfer operation, or checkpoint, firmware puts the NOR into deep power-down, deactivates the nRF QSPI peripheral, and turns off a board-provided flash power-enable pin. The -next operation powers and identifies the chip again, so merely running -`ota self` does not leave QSPI drawing active-mode current. +next operation powers the chip, shifts the NOR's `0xAB` wake command over GPIO, +then activates the nRF QSPI peripheral and identifies the flash. Waking it +before peripheral activation is required because a flash in deep power-down +ignores the activation traffic itself. Merely running `ota self` therefore does +not leave QSPI drawing active-mode current or prevent the following operation +from reactivating it. Installing a QSPI repeater build over a former companion build therefore repurposes the external flash and destroys companion filesystem data as OTA diff --git a/src/helpers/ota/OtaStoreQspiNrf52.cpp b/src/helpers/ota/OtaStoreQspiNrf52.cpp index 93292d96..6a837349 100644 --- a/src/helpers/ota/OtaStoreQspiNrf52.cpp +++ b/src/helpers/ota/OtaStoreQspiNrf52.cpp @@ -162,6 +162,47 @@ static void configure_qspi_gpio(const nrf_qspi_pins_t& pins) { NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE); } +static void wake_qspi_flash_before_activate(const nrf_qspi_pins_t& pins) { + // nRF52840 TASKS_ACTIVATE communicates with the NOR before CINSTR is + // available. A flash put to sleep by our preceding 0xB9 ignores that + // activation traffic, so issue the only command it accepts (0xAB) with a + // short mode-0 GPIO transaction first. IO2/WP# and IO3/HOLD# stay high. + nrf_gpio_pin_clear(pins.sck_pin); + nrf_gpio_pin_set(pins.csn_pin); + nrf_gpio_pin_set(pins.io0_pin); + if (pins.io2_pin != NRF_QSPI_PIN_NOT_CONNECTED) nrf_gpio_pin_set(pins.io2_pin); + if (pins.io3_pin != NRF_QSPI_PIN_NOT_CONNECTED) nrf_gpio_pin_set(pins.io3_pin); + + nrf_gpio_cfg(pins.sck_pin, NRF_GPIO_PIN_DIR_OUTPUT, 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_OUTPUT, 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_OUTPUT, 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_OUTPUT, 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_OUTPUT, NRF_GPIO_PIN_INPUT_DISCONNECT, + NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE); + + delayMicroseconds(1); + nrf_gpio_pin_clear(pins.csn_pin); + delayMicroseconds(1); + for (uint8_t bit = 0; bit < 8u; bit++) { + nrf_gpio_pin_write(pins.io0_pin, mota_qspi_release_from_dpd_bit(bit)); + delayMicroseconds(1); + nrf_gpio_pin_set(pins.sck_pin); + delayMicroseconds(1); + nrf_gpio_pin_clear(pins.sck_pin); + delayMicroseconds(1); + } + nrf_gpio_pin_set(pins.csn_pin); + delayMicroseconds(MOTA_QSPI_DPD_WAKE_GUARD_US); +} + } // namespace OtaStoreQspiNrf52::OtaStoreQspiNrf52() { @@ -281,6 +322,12 @@ bool OtaStoreQspiNrf52::ensureFlash() { pins.io1_pin = qspi_io1_pin(); pins.io2_pin = qspi_io2_pin(); pins.io3_pin = qspi_io3_pin(); + _stage = OtaQspiStage::WAKE; + // A reset may have interrupted a NOR program/erase. Until activation and a + // successful RDSR prove WIP clear, every failure path must retain power and + // hold CS# high instead of sending B9 or cutting the flash rail. + _memory_operation_pending = true; + wake_qspi_flash_before_activate(pins); configure_qspi_gpio(pins); nrf_qspi_pins_set(NRF_QSPI, &pins); @@ -307,26 +354,17 @@ bool OtaStoreQspiNrf52::ensureFlash() { _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)) { + // ACTIVATE itself can wait for a previously interrupted NOR erase. Match the + // recovery window used by the explicit status poll below. + if (!waitReady(30000)) { releaseFlash(); fail("QSPI activate timed out"); return false; } - // 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; - } + // The GPIO 0xAB issued before TASKS_ACTIVATE already woke the NOR. CINSTR + // cannot perform that first wake because it is unavailable until activation. _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. diff --git a/src/helpers/ota/OtaStoreQspiNrf52.h b/src/helpers/ota/OtaStoreQspiNrf52.h index 7d402faf..6a746e99 100644 --- a/src/helpers/ota/OtaStoreQspiNrf52.h +++ b/src/helpers/ota/OtaStoreQspiNrf52.h @@ -29,6 +29,17 @@ 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; +static const uint8_t MOTA_QSPI_RELEASE_FROM_DPD_OPCODE = 0xABu; + +// TASKS_ACTIVATE initiates traffic with the external memory. A NOR in deep +// power-down ignores that traffic and the nRF QSPI peripheral can wait forever +// for READY, so the 0xAB wake command has to be shifted over GPIO before QSPI +// owns the pins. Keep the bit order host-testable instead of open-coding it in +// the hardware-only path. +inline bool mota_qspi_release_from_dpd_bit(uint8_t bit_index) { + return bit_index < 8u && + ((MOTA_QSPI_RELEASE_FROM_DPD_OPCODE >> (7u - bit_index)) & 1u) != 0; +} // 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; diff --git a/test/test_ota/test_ota_flashplan.cpp b/test/test_ota/test_ota_flashplan.cpp index d42057b0..61f5c7cd 100644 --- a/test/test_ota/test_ota_flashplan.cpp +++ b/test/test_ota/test_ota_flashplan.cpp @@ -35,6 +35,17 @@ TEST(OtaQspiTiming, PreservesDeepPowerDownEntryAndWakeGuards) { EXPECT_GE(MOTA_QSPI_DPD_WAKE_GUARD_US, 45u); } +TEST(OtaQspiWake, ShiftsReleaseCommandMostSignificantBitFirst) { + // This byte is emitted over GPIO before TASKS_ACTIVATE. If activation is + // attempted first, a sleeping NOR ignores it and READY never arrives. + const bool expected[] = {true, false, true, false, true, false, true, true}; + EXPECT_EQ(MOTA_QSPI_RELEASE_FROM_DPD_OPCODE, 0xABu); + for (uint8_t bit = 0; bit < 8u; bit++) { + EXPECT_EQ(mota_qspi_release_from_dpd_bit(bit), expected[bit]); + } + EXPECT_FALSE(mota_qspi_release_from_dpd_bit(8u)); +} + TEST(OtaQspiStatus, TreatsOnlyTheNorWriteInProgressBitAsBusy) { // Nordic READY is not flash completion. The hardware path polls RDSR until // this predicate clears after every program and erase operation.