Wake XIAO QSPI flash and preserve access after failed resets
This commit is contained in:
1 parent
85a786e796
commit
61dc5ca3f1
9 files changed
+450
-55
No files matched your search
@@ -467,6 +467,7 @@ jobs:
|
||||
python3 -B test/test_companion_contact_partial_tail.py -v
|
||||
python3 -B test/test_companion_channel_storage.py -v
|
||||
python3 -B test/test_nrf52_extrafs_contract.py -v
|
||||
python3 -B test/test_xiao_qspi_recovery.py -v
|
||||
python3 -B test/test_companion_ota_config.py -v
|
||||
python3 -B test/test_ota_identity_policy.py -v
|
||||
python3 -B test/test_companion_radio_gain_restore.py -v
|
||||
|
||||
@@ -795,6 +795,16 @@ bool DataStore::removeFile(FILESYSTEM* fs, const char* filename) {
|
||||
}
|
||||
|
||||
bool DataStore::formatFileSystem() {
|
||||
#if defined(NRF52_PLATFORM) && defined(QSPIFLASH)
|
||||
// An explicit erase must cover every configured store. A quarantined QSPI
|
||||
// chip may still hold authoritative contacts/channels, and its disabled
|
||||
// peripheral cannot safely be formatted through the configured pointer.
|
||||
// Refuse before changing primary storage or any in-memory recovery state.
|
||||
if (_configuredFsExtra != nullptr && _fsExtra != _configuredFsExtra) {
|
||||
MESH_DEBUG_PRINTLN("DataStore: erase refused while QSPI storage is unavailable");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#if defined(ESP32_PLATFORM)
|
||||
// A failed format keeps the latest accepted RAM packets available to retry.
|
||||
_advert_write.pause();
|
||||
|
||||
@@ -7474,20 +7474,21 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid stats sub-type
|
||||
}
|
||||
} else if (cmd_frame[0] == CMD_FACTORY_RESET && len >= 6 && memcmp(&cmd_frame[1], "reset", 5) == 0) {
|
||||
if (_serial) {
|
||||
MESH_DEBUG_PRINTLN("Factory reset: disabling serial interface to prevent reconnects (BLE/WiFi)");
|
||||
_serial->disable(); // Phone app disconnects before we can send OK frame so it's safe here
|
||||
}
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
suspendBeacons();
|
||||
#endif
|
||||
BaseSerialInterface* const reply_route = _serial->captureReplyRoute();
|
||||
// Formatting is synchronous in the command-dispatch loop. Keep the
|
||||
// requesting transport available if storage rejects/fails the erase.
|
||||
bool success = _store->formatFileSystem();
|
||||
if (success) {
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
suspendBeacons();
|
||||
#endif
|
||||
MESH_DEBUG_PRINTLN("Factory reset: disabling interfaces before reboot");
|
||||
_serial->disable();
|
||||
writeOKFrame();
|
||||
delay(1000);
|
||||
board.reboot(); // doesn't return
|
||||
} else {
|
||||
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
|
||||
writeErrFrame(ERR_CODE_FILE_IO_ERROR, reply_route);
|
||||
}
|
||||
} else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE_KEY && len >= 2 && cmd_frame[1] == 0) {
|
||||
if (len >= 2 + 16) {
|
||||
|
||||
@@ -230,6 +230,7 @@ static bool isNetworkTerminalActive();
|
||||
#endif
|
||||
#if defined(QSPIFLASH)
|
||||
#include <CustomLFS_QSPIFlash.h>
|
||||
#include <helpers/nrf52/QspiFlashWake.h>
|
||||
DataStore store(InternalFS, QSPIFlash, rtc_clock);
|
||||
#else
|
||||
#if defined(EXTRAFS)
|
||||
@@ -3689,6 +3690,16 @@ void setup() {
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#if defined(QSPIFLASH)
|
||||
#if defined(XIAO_NRF52)
|
||||
// The external NOR can remain asleep across an MCU/DFU reset. Wake it
|
||||
// before CustomLFS activates QSPI, without changing any stored bytes.
|
||||
const nrf_qspi_pins_t qspi_pins = {
|
||||
(uint8_t)g_ADigitalPinMap[PIN_QSPI_SCK], (uint8_t)g_ADigitalPinMap[PIN_QSPI_CS],
|
||||
(uint8_t)g_ADigitalPinMap[PIN_QSPI_IO0], (uint8_t)g_ADigitalPinMap[PIN_QSPI_IO1],
|
||||
(uint8_t)g_ADigitalPinMap[PIN_QSPI_IO2], (uint8_t)g_ADigitalPinMap[PIN_QSPI_IO3]
|
||||
};
|
||||
mesh::nrf52::wakeQspiFlashBeforeActivate(qspi_pins);
|
||||
#endif
|
||||
if (!QSPIFlash.begin()) {
|
||||
// debug output might not be available at this point, might be too early. maybe should fall back to InternalFS here?
|
||||
MESH_DEBUG_PRINTLN("CustomLFS_QSPIFlash: failed to initialize");
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "QspiFlashWakePolicy.h"
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include <Arduino.h>
|
||||
#include <hal/nrf_gpio.h>
|
||||
#include <hal/nrf_qspi.h>
|
||||
#endif
|
||||
|
||||
namespace mesh {
|
||||
namespace nrf52 {
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
inline void wakeQspiFlashBeforeActivate(const nrf_qspi_pins_t& pins) {
|
||||
// TASKS_ACTIVATE talks to the NOR before CINSTR is available. A sleeping
|
||||
// chip ignores that traffic and can leave activation waiting forever.
|
||||
// Send only the non-destructive 0xAB wake byte over mode-0 GPIO first.
|
||||
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, qspiReleaseFromDeepPowerDownBit(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(QSPI_DPD_WAKE_GUARD_US);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace nrf52
|
||||
} // namespace mesh
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace nrf52 {
|
||||
|
||||
// Supported NOR parts need up to 45 us after release from deep power-down.
|
||||
static const uint32_t QSPI_DPD_WAKE_GUARD_US = 50u;
|
||||
static const uint8_t QSPI_RELEASE_FROM_DPD_OPCODE = 0xABu;
|
||||
|
||||
inline bool qspiReleaseFromDeepPowerDownBit(uint8_t bit_index) {
|
||||
return bit_index < 8u &&
|
||||
((QSPI_RELEASE_FROM_DPD_OPCODE >> (7u - bit_index)) & 1u) != 0;
|
||||
}
|
||||
|
||||
} // namespace nrf52
|
||||
} // namespace mesh
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "OtaByteIO.h"
|
||||
#include "OtaFlashLayout_nrf52.h"
|
||||
#include <helpers/nrf52/QspiFlashWake.h>
|
||||
#include "hal/nrf_gpio.h"
|
||||
#include "hal/nrf_qspi.h"
|
||||
#include "nrf.h"
|
||||
@@ -235,47 +236,6 @@ static void release_rak3401_radio_bus() {
|
||||
}
|
||||
#endif
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#if defined(OTA_RAK_AUTO_STORE)
|
||||
static uint8_t gpio_spi_byte(const nrf_qspi_pins_t& pins, uint8_t out) {
|
||||
uint8_t in = 0;
|
||||
@@ -299,7 +259,7 @@ static uint32_t probe_nor(uint8_t cs) {
|
||||
};
|
||||
// A previous reset may have left the NOR in deep power-down. This GPIO
|
||||
// wake precedes the nRF QSPI ACTIVATE for the same reason as ensureFlash().
|
||||
wake_qspi_flash_before_activate(pins);
|
||||
mesh::nrf52::wakeQspiFlashBeforeActivate(pins);
|
||||
// The wake helper leaves GPIO input buffers disconnected for QSPI handoff.
|
||||
// This bit-banged JEDEC read instead samples IO1 through GPIO->IN, which
|
||||
// requires the MISO input buffer to be connected.
|
||||
@@ -552,7 +512,7 @@ bool OtaStoreQspiNrf52::ensureFlash() {
|
||||
// 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);
|
||||
mesh::nrf52::wakeQspiFlashBeforeActivate(pins);
|
||||
configure_qspi_gpio(pins);
|
||||
nrf_qspi_pins_set(NRF_QSPI, &pins);
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include "../nrf52/QspiFlashWakePolicy.h"
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
@@ -52,8 +54,10 @@ namespace ota {
|
||||
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;
|
||||
static const uint32_t MOTA_QSPI_DPD_WAKE_GUARD_US =
|
||||
mesh::nrf52::QSPI_DPD_WAKE_GUARD_US;
|
||||
static const uint8_t MOTA_QSPI_RELEASE_FROM_DPD_OPCODE =
|
||||
mesh::nrf52::QSPI_RELEASE_FROM_DPD_OPCODE;
|
||||
|
||||
// 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
|
||||
@@ -61,8 +65,7 @@ static const uint8_t MOTA_QSPI_RELEASE_FROM_DPD_OPCODE = 0xABu;
|
||||
// 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;
|
||||
return mesh::nrf52::qspiReleaseFromDeepPowerDownBit(bit_index);
|
||||
}
|
||||
|
||||
// The nRF QSPI READY event only says that a program/erase command and its
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
"""Exercise sleeping NOR startup and failed factory-reset recovery natively."""
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_nrf52_extrafs_contract import function_body
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def method(source, signature):
|
||||
start = source.index(signature)
|
||||
return source[start:source.index("{", start) + 1] + function_body(source, signature) + "}"
|
||||
|
||||
|
||||
def run_native(source, headers=None):
|
||||
with tempfile.TemporaryDirectory(prefix="mesh-xiao-qspi-") as temp:
|
||||
work = Path(temp)
|
||||
for name, content in (headers or {}).items():
|
||||
path = work / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="ascii")
|
||||
cpp = work / "check.cpp"
|
||||
cpp.write_text(source, encoding="ascii")
|
||||
binary = work / "check"
|
||||
compiled = subprocess.run([
|
||||
"c++", "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-Werror",
|
||||
"-fsanitize=address,undefined", "-fno-omit-frame-pointer",
|
||||
"-fno-pie", "-no-pie", "-I", str(work), "-I", str(ROOT / "src"),
|
||||
str(cpp), "-o", str(binary),
|
||||
], capture_output=True, text=True)
|
||||
if compiled.returncode:
|
||||
raise AssertionError(compiled.stderr)
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True)
|
||||
if result.returncode:
|
||||
raise AssertionError(result.stdout + result.stderr)
|
||||
|
||||
|
||||
GPIO_DECLARATIONS = r"""#pragma once
|
||||
#include <cstdint>
|
||||
static constexpr uint8_t NRF_QSPI_PIN_NOT_CONNECTED = 0xFFu;
|
||||
struct nrf_qspi_pins_t {
|
||||
uint8_t sck_pin, csn_pin, io0_pin, io1_pin, io2_pin, io3_pin;
|
||||
};
|
||||
enum { NRF_GPIO_PIN_DIR_INPUT, NRF_GPIO_PIN_DIR_OUTPUT,
|
||||
NRF_GPIO_PIN_INPUT_DISCONNECT, NRF_GPIO_PIN_NOPULL,
|
||||
NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE };
|
||||
void nrf_gpio_pin_clear(uint32_t);
|
||||
void nrf_gpio_pin_set(uint32_t);
|
||||
void nrf_gpio_pin_write(uint32_t, bool);
|
||||
void nrf_gpio_cfg(uint32_t, int, int, int, int, int);
|
||||
void delayMicroseconds(uint32_t);
|
||||
"""
|
||||
|
||||
GPIO_RUNTIME = r"""
|
||||
#define NRF52_PLATFORM 1
|
||||
#include <cassert>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <helpers/nrf52/QspiFlashWake.h>
|
||||
|
||||
// Register-level adapter records pin latches/directions and feeds a sleeping
|
||||
// serial NOR model only on clock edges. ACTIVATE cannot complete while asleep.
|
||||
static nrf_qspi_pins_t expected;
|
||||
static std::array<bool, 48> latch;
|
||||
static std::array<int, 48> direction;
|
||||
static std::vector<unsigned> touched;
|
||||
static std::vector<unsigned> commands;
|
||||
static unsigned now_us, wake_at, bits, byte;
|
||||
static bool selected, asleep;
|
||||
static const std::array<uint8_t, 8> contents = {0x17, 0x81, 0x05, 0x99, 1, 2, 3, 4};
|
||||
static std::array<uint8_t, 8> flash;
|
||||
|
||||
static void reset(const nrf_qspi_pins_t& pins, bool sleeping) {
|
||||
expected = pins; latch.fill(false); direction.fill(-1);
|
||||
touched.clear(); commands.clear(); now_us = wake_at = bits = byte = 0;
|
||||
selected = false; asleep = sleeping; flash = contents;
|
||||
}
|
||||
|
||||
static void check_pin(uint32_t pin) {
|
||||
assert(pin < latch.size());
|
||||
assert(pin == expected.sck_pin || pin == expected.csn_pin ||
|
||||
pin == expected.io0_pin || pin == expected.io1_pin ||
|
||||
pin == expected.io2_pin || pin == expected.io3_pin);
|
||||
touched.push_back(pin);
|
||||
}
|
||||
|
||||
void nrf_gpio_pin_write(uint32_t pin, bool level) {
|
||||
check_pin(pin);
|
||||
const bool old = latch[pin];
|
||||
latch[pin] = level;
|
||||
if (pin == expected.csn_pin && old && !level) {
|
||||
assert(!latch[expected.sck_pin]);
|
||||
assert(direction[expected.io1_pin] == NRF_GPIO_PIN_DIR_INPUT);
|
||||
selected = true; bits = byte = 0;
|
||||
}
|
||||
if (pin == expected.sck_pin && !old && level && selected) {
|
||||
if (expected.io2_pin != NRF_QSPI_PIN_NOT_CONNECTED) {
|
||||
assert(latch[expected.io2_pin]);
|
||||
}
|
||||
if (expected.io3_pin != NRF_QSPI_PIN_NOT_CONNECTED) {
|
||||
assert(latch[expected.io3_pin]);
|
||||
}
|
||||
byte = (byte << 1) | latch[expected.io0_pin];
|
||||
++bits;
|
||||
}
|
||||
if (pin == expected.csn_pin && !old && level && selected) {
|
||||
assert(bits == 8 && byte == 0xAB);
|
||||
commands.push_back(byte);
|
||||
selected = false; asleep = false; wake_at = now_us;
|
||||
}
|
||||
}
|
||||
void nrf_gpio_pin_clear(uint32_t pin) { nrf_gpio_pin_write(pin, false); }
|
||||
void nrf_gpio_pin_set(uint32_t pin) { nrf_gpio_pin_write(pin, true); }
|
||||
void nrf_gpio_cfg(uint32_t pin, int mode, int, int, int, int) {
|
||||
check_pin(pin); direction[pin] = mode;
|
||||
}
|
||||
void delayMicroseconds(uint32_t us) { now_us += us; }
|
||||
|
||||
static bool activate() {
|
||||
return !asleep && !selected && latch[expected.csn_pin] &&
|
||||
now_us - wake_at >= mesh::nrf52::QSPI_DPD_WAKE_GUARD_US;
|
||||
}
|
||||
static void check_waveform(const nrf_qspi_pins_t& pins, bool sleeping) {
|
||||
reset(pins, sleeping);
|
||||
assert(!activate()); // The old sleeping-chip path never reaches READY.
|
||||
mesh::nrf52::wakeQspiFlashBeforeActivate(pins);
|
||||
assert(activate());
|
||||
assert(commands == std::vector<unsigned>{0xAB});
|
||||
assert(now_us == 76 && bits == 8 && !selected);
|
||||
assert(!latch[pins.sck_pin] && latch[pins.csn_pin]);
|
||||
assert(direction[pins.io1_pin] == NRF_GPIO_PIN_DIR_INPUT);
|
||||
assert(flash == contents);
|
||||
}
|
||||
|
||||
@VARIANT@
|
||||
static void companion_startup() {
|
||||
@STARTUP@
|
||||
assert(activate()); // Stands for the first CustomLFS QSPI activation.
|
||||
}
|
||||
|
||||
int main() {
|
||||
const nrf_qspi_pins_t xiao = {21, 25, 20, 24, 22, 23};
|
||||
check_waveform(xiao, true);
|
||||
check_waveform(xiao, false); // Already awake NOR also accepts harmless AB.
|
||||
check_waveform({3, 31, 30, 29, NRF_QSPI_PIN_NOT_CONNECTED,
|
||||
NRF_QSPI_PIN_NOT_CONNECTED}, true);
|
||||
reset(xiao, true);
|
||||
companion_startup();
|
||||
assert(commands == std::vector<unsigned>{0xAB} && flash == contents);
|
||||
for (unsigned i = 0; i < 8; ++i) {
|
||||
assert(mesh::nrf52::qspiReleaseFromDeepPowerDownBit(i) == ((0xAB >> (7-i)) & 1));
|
||||
}
|
||||
assert(!mesh::nrf52::qspiReleaseFromDeepPowerDownBit(8));
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class XiaoQspiRecoveryTest(unittest.TestCase):
|
||||
def test_real_gpio_waveform_wakes_nor_before_library_activation(self):
|
||||
main = (ROOT / "examples/companion_radio/main.cpp").read_text()
|
||||
begin = main.index("if (!QSPIFlash.begin())")
|
||||
block_start = main.rindex("#if defined(XIAO_NRF52)", 0, begin)
|
||||
startup = main[block_start:begin]
|
||||
self.assertIn("wakeQspiFlashBeforeActivate(qspi_pins)", startup)
|
||||
startup = startup[startup.index("const nrf_qspi_pins_t"):startup.index("#endif")]
|
||||
|
||||
variant = (ROOT / "variants/xiao_nrf52/variant.cpp").read_text()
|
||||
pin_map = variant[variant.index("g_ADigitalPinMap"):]
|
||||
pin_map = pin_map[pin_map.index("{") + 1:pin_map.index("}")]
|
||||
pin_map = re.sub(r"//[^\n]*", "", pin_map)
|
||||
values = [int(value.strip()) for value in pin_map.split(",") if value.strip()]
|
||||
header = (ROOT / "variants/xiao_nrf52/variant.h").read_text()
|
||||
declarations = "static const uint32_t g_ADigitalPinMap[] = {" + ",".join(map(str, values)) + "};\n"
|
||||
for pin in ("SCK", "CS", "IO0", "IO1", "IO2", "IO3"):
|
||||
value = re.search(r"#define PIN_QSPI_" + pin + r"\s+\((\d+)\)", header).group(1)
|
||||
declarations += "#define PIN_QSPI_" + pin + " " + value + "\n"
|
||||
run_native(GPIO_RUNTIME.replace("@STARTUP@", startup).replace("@VARIANT@", declarations), {
|
||||
"Arduino.h": "#pragma once\n#include <cstdint>\nvoid delayMicroseconds(uint32_t);\n",
|
||||
"hal/nrf_gpio.h": GPIO_DECLARATIONS,
|
||||
"hal/nrf_qspi.h": '#pragma once\n#include "nrf_gpio.h"\n',
|
||||
})
|
||||
|
||||
def test_raw_ota_reuses_same_wake_before_activation(self):
|
||||
source = (ROOT / "src/helpers/ota/OtaStoreQspiNrf52.cpp").read_text()
|
||||
self.assertNotIn("static void wake_qspi_flash_before_activate", source)
|
||||
self.assertEqual(source.count("mesh::nrf52::wakeQspiFlashBeforeActivate(pins);"), 2)
|
||||
ensure = function_body(source, "bool OtaStoreQspiNrf52::ensureFlash(")
|
||||
self.assertLess(ensure.index("wakeQspiFlashBeforeActivate(pins)"),
|
||||
ensure.index("NRF_QSPI_TASK_ACTIVATE"))
|
||||
|
||||
def test_quarantined_qspi_vetoes_erase_before_primary_or_ram_mutation(self):
|
||||
store = (ROOT / "examples/companion_radio/DataStore.cpp").read_text()
|
||||
implementation = method(store, "bool DataStore::formatFileSystem()")
|
||||
run_native(r"""
|
||||
#include <cassert>
|
||||
#define NRF52_PLATFORM 1
|
||||
#define QSPIFLASH 1
|
||||
#define MESH_CONTACT_CACHE 0
|
||||
#define MESH_DEBUG_PRINTLN(...) do {} while(0)
|
||||
struct Fs {
|
||||
unsigned formats = 0;
|
||||
bool ok = true;
|
||||
unsigned retained = 0x1234;
|
||||
bool format() { ++formats; if (!ok) return false; retained = 0; return true; }
|
||||
};
|
||||
struct DataStore {
|
||||
Fs* _fs;
|
||||
Fs* _fsExtra;
|
||||
Fs* _configuredFsExtra;
|
||||
unsigned reset_calls = 0;
|
||||
unsigned _contact_recovery = 9;
|
||||
bool _contact_load_incomplete = true, _channel_load_incomplete = true;
|
||||
bool _identity_creation_blocked = true, _prefs_load_incomplete = true;
|
||||
void resetContactPageState() { ++reset_calls; }
|
||||
bool formatFileSystem();
|
||||
};
|
||||
@IMPLEMENTATION@
|
||||
int main() {
|
||||
Fs primary, extra;
|
||||
DataStore store{&primary, nullptr, &extra};
|
||||
assert(!store.formatFileSystem());
|
||||
assert(primary.formats == 0 && extra.formats == 0 && store.reset_calls == 0);
|
||||
assert(primary.retained == 0x1234 && extra.retained == 0x1234);
|
||||
assert(store._contact_recovery == 9 && store._contact_load_incomplete);
|
||||
assert(store._channel_load_incomplete && store._identity_creation_blocked);
|
||||
assert(store._prefs_load_incomplete);
|
||||
|
||||
store._fsExtra = &extra;
|
||||
extra.ok = false;
|
||||
assert(!store.formatFileSystem());
|
||||
assert(primary.formats == 1 && extra.formats == 1 && store.reset_calls == 1);
|
||||
assert(store._contact_recovery == 9 && store._contact_load_incomplete);
|
||||
extra.ok = true;
|
||||
assert(store.formatFileSystem());
|
||||
assert(primary.formats == 2 && extra.formats == 2);
|
||||
assert(store._contact_recovery == 0 && !store._contact_load_incomplete);
|
||||
assert(!store._channel_load_incomplete && !store._identity_creation_blocked);
|
||||
assert(!store._prefs_load_incomplete && extra.retained == 0);
|
||||
|
||||
Fs lone_primary;
|
||||
DataStore lone{&lone_primary, nullptr, nullptr};
|
||||
assert(lone.formatFileSystem() && lone_primary.formats == 1);
|
||||
}
|
||||
""".replace("@IMPLEMENTATION@", implementation))
|
||||
|
||||
def test_failed_factory_reset_keeps_services_and_routes_error_to_requester(self):
|
||||
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
||||
branch = function_body(source, "cmd_frame[0] == CMD_FACTORY_RESET")
|
||||
run_native(r"""
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#define MESH_ENABLE_TX_BEACONS 1
|
||||
#define MESH_DEBUG_PRINTLN(...) do {} while(0)
|
||||
static std::vector<unsigned> events;
|
||||
static bool beacons_on;
|
||||
enum { RESP_CODE_OK = 0, RESP_CODE_ERR = 1, ERR_CODE_FILE_IO_ERROR = 5 };
|
||||
struct BaseSerialInterface {
|
||||
std::array<bool, 4> enabled = {true, true, false, false};
|
||||
BaseSerialInterface* route = this;
|
||||
BaseSerialInterface* received_route = nullptr;
|
||||
std::vector<uint8_t> reply;
|
||||
BaseSerialInterface* captureReplyRoute() { events.push_back(1); return route; }
|
||||
void disable() { events.push_back(3); enabled.fill(false); route = nullptr; }
|
||||
void writeFrameToRoute(BaseSerialInterface* target, const uint8_t* p, size_t n) {
|
||||
received_route = target; reply.assign(p, p+n); events.push_back(4);
|
||||
}
|
||||
void writeFrame(const uint8_t* p, size_t n) { writeFrameToRoute(route, p, n); }
|
||||
};
|
||||
struct Board { bool rebooted = false; void reboot() { events.push_back(6); rebooted = true; } } board;
|
||||
static void delay(unsigned ms) { assert(ms == 1000); events.push_back(5); }
|
||||
struct Store {
|
||||
BaseSerialInterface* serial;
|
||||
BaseSerialInterface* other;
|
||||
std::array<bool, 4> before;
|
||||
bool ok;
|
||||
bool beacons_before;
|
||||
bool formatFileSystem() {
|
||||
events.push_back(2);
|
||||
assert(serial->enabled == before); // No disabled transport before erase.
|
||||
assert(beacons_on == beacons_before);
|
||||
serial->route = other; // Background route change must not steal failure.
|
||||
return ok;
|
||||
}
|
||||
};
|
||||
struct MyMesh {
|
||||
BaseSerialInterface* _serial;
|
||||
Store* _store;
|
||||
void reset() { @BRANCH@ }
|
||||
void suspendBeacons() { events.push_back(7); beacons_on = false; }
|
||||
void writeOKFrame(BaseSerialInterface* route = nullptr);
|
||||
void writeErrFrame(uint8_t, BaseSerialInterface* route = nullptr);
|
||||
};
|
||||
@REPLIES@
|
||||
int main() {
|
||||
for (unsigned mask = 1; mask < 16; ++mask) {
|
||||
for (unsigned beacon = 0; beacon < 2; ++beacon) {
|
||||
BaseSerialInterface serial, requester, other;
|
||||
for (unsigned i = 0; i < 4; ++i) serial.enabled[i] = (mask & (1u << i)) != 0;
|
||||
serial.route = &requester;
|
||||
beacons_on = beacon != 0;
|
||||
Store store{&serial, &other, serial.enabled, false, beacons_on};
|
||||
MyMesh mesh{&serial, &store};
|
||||
events.clear(); board.rebooted = false;
|
||||
mesh.reset();
|
||||
assert((events == std::vector<unsigned>{1, 2, 4}));
|
||||
assert(serial.enabled == store.before && !board.rebooted);
|
||||
assert(beacons_on == store.beacons_before);
|
||||
assert(serial.received_route == &requester);
|
||||
assert((serial.reply == std::vector<uint8_t>{1, 5}));
|
||||
|
||||
store.ok = true; serial.route = &requester; events.clear();
|
||||
mesh.reset();
|
||||
assert((events == std::vector<unsigned>{1, 2, 7, 3, 4, 5, 6}));
|
||||
assert(board.rebooted);
|
||||
assert(!beacons_on);
|
||||
for (bool on : serial.enabled) assert(!on);
|
||||
}
|
||||
}
|
||||
}
|
||||
""".replace("@BRANCH@", branch).replace("@REPLIES@", "\n".join(
|
||||
method(source, signature) for signature in (
|
||||
"void MyMesh::writeOKFrame(", "void MyMesh::writeErrFrame("))))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user