From f3125bedd8e103a09f335ee4e1fcc3008b960811 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 25 Sep 2026 13:14:30 -0700 Subject: [PATCH] Connect GPIO MISO during RAK NOR identification --- src/helpers/ota/OtaCli.cpp | 12 +++-- src/helpers/ota/OtaStoreQspiNrf52.cpp | 16 +++++++ src/helpers/ota/OtaStoreQspiNrf52.h | 3 ++ tools/hil/verify_rak_ota_storage.py | 65 +++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 tools/hil/verify_rak_ota_storage.py diff --git a/src/helpers/ota/OtaCli.cpp b/src/helpers/ota/OtaCli.cpp index af2aa03d..9d4d5f61 100644 --- a/src/helpers/ota/OtaCli.cpp +++ b/src/helpers/ota/OtaCli.cpp @@ -742,10 +742,16 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board c.fetch_store.last_stage(), c.fetch_store.last_error()[0] ? " error=" : "", c.fetch_store.last_error()); } else if (c.fetch_store.usesInternal()) { - snprintf(reply, 160, "OTA storage: %s; internal capacity=%luK", - c.fetch_store.selectionReason(), (unsigned long)(c.fetch_store.capacity() / 1024)); + uint32_t rak_id = 0, w25_id = 0; + OtaStoreQspiNrf52::autoProbeIds(rak_id, w25_id); + snprintf(reply, 160, "OTA storage: %s; internal capacity=%luK; probe rak=%06lX w25=%06lX", + c.fetch_store.selectionReason(), (unsigned long)(c.fetch_store.capacity() / 1024), + (unsigned long)rak_id, (unsigned long)w25_id); } else { - snprintf(reply, 160, "OTA storage unsafe: %s", c.fetch_store.selectionReason()); + uint32_t rak_id = 0, w25_id = 0; + OtaStoreQspiNrf52::autoProbeIds(rak_id, w25_id); + snprintf(reply, 160, "OTA storage unsafe: %s; probe rak=%06lX w25=%06lX", + c.fetch_store.selectionReason(), (unsigned long)rak_id, (unsigned long)w25_id); } #elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) // This probe only reads JEDEC/SR1. capacity() deliberately preserves a diff --git a/src/helpers/ota/OtaStoreQspiNrf52.cpp b/src/helpers/ota/OtaStoreQspiNrf52.cpp index 08b5c3a7..caaa6939 100644 --- a/src/helpers/ota/OtaStoreQspiNrf52.cpp +++ b/src/helpers/ota/OtaStoreQspiNrf52.cpp @@ -23,6 +23,8 @@ namespace { static uint8_t auto_cs_pin = NRF_QSPI_PIN_NOT_CONNECTED; static uint32_t auto_jedec_id = 0; static uint8_t auto_detection = 0xFF; +static uint32_t auto_rak_probe_id = 0; +static uint32_t auto_w25_probe_id = 0; #endif #if defined(OTA_QSPI_SCK_PHYSICAL_PIN) && defined(OTA_QSPI_SCK_ARDUINO_PIN) @@ -277,6 +279,10 @@ 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); + // 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. + nrf_gpio_cfg_input(pins.io1_pin, NRF_GPIO_PIN_NOPULL); nrf_gpio_pin_clear(cs); delayMicroseconds(1); (void)gpio_spi_byte(pins, 0x9Fu); @@ -284,6 +290,7 @@ static uint32_t probe_nor(uint8_t cs) { ((uint32_t)gpio_spi_byte(pins, 0xFFu) << 8) | gpio_spi_byte(pins, 0xFFu); nrf_gpio_pin_set(cs); + nrf_gpio_input_disconnect(pins.io1_pin); delayMicroseconds(1); if (id == 0xC84015UL || id == 0xEF4015UL) { // Detection is read-only. Return a recognized NOR to low-power standby; @@ -314,6 +321,7 @@ uint8_t OtaStoreQspiNrf52::autoDetect() { digitalWrite(P_LORA_NSS, HIGH); SPI1.end(); const uint32_t w25_id = probe_nor(w25_cs); + auto_w25_probe_id = w25_id; SPI1.begin(); auto_detection = w25_id == 0xEF4015UL ? 2u : 0u; #else @@ -322,6 +330,8 @@ uint8_t OtaStoreQspiNrf52::autoDetect() { nrf_gpio_cfg_output(rak_cs); const uint32_t rak_id = probe_nor(rak_cs); const uint32_t w25_id = probe_nor(w25_cs); + auto_rak_probe_id = rak_id; + auto_w25_probe_id = w25_id; const bool rak = rak_id == 0xC84015UL; const bool w25 = w25_id == 0xEF4015UL; auto_detection = rak && w25 ? 3u : rak ? 1u : w25 ? 2u : 0u; @@ -335,6 +345,12 @@ uint8_t OtaStoreQspiNrf52::autoDetect() { } return auto_detection; } + +void OtaStoreQspiNrf52::autoProbeIds(uint32_t& rak15001, uint32_t& w25q16) { + autoDetect(); + rak15001 = auto_rak_probe_id; + w25q16 = auto_w25_probe_id; +} #endif OtaStoreQspiNrf52::OtaStoreQspiNrf52() { diff --git a/src/helpers/ota/OtaStoreQspiNrf52.h b/src/helpers/ota/OtaStoreQspiNrf52.h index 09873db9..f32c1d40 100644 --- a/src/helpers/ota/OtaStoreQspiNrf52.h +++ b/src/helpers/ota/OtaStoreQspiNrf52.h @@ -170,6 +170,9 @@ public: // Probe exact supported NOR parts before activating the nRF QSPI block. // 0 = absent/unsupported, 1 = RAK15001 C, 2 = W25Q16, 3 = ambiguous. static uint8_t autoDetect(); + // Raw read-only JEDEC results from the cached startup probe. Useful when a + // connected breakout is not one of the exact supported parts. + static void autoProbeIds(uint32_t& rak15001, uint32_t& w25q16); #endif bool begin(uint32_t total_size) override; diff --git a/tools/hil/verify_rak_ota_storage.py b/tools/hil/verify_rak_ota_storage.py new file mode 100644 index 00000000..6f29251e --- /dev/null +++ b/tools/hil/verify_rak_ota_storage.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Check a real RAK4631/RAK3401 OTA store and its matching bootloader over USB. + +Example: python3 tools/hil/verify_rak_ota_storage.py --port /dev/serial/by-id/... + --serial 9AB3B64C641BA927 --expect w25q16 +""" + +import argparse +import json +import re +import time + +from verified_nrf_flash import verify_port + + +def command(port, text): + port.reset_input_buffer() + port.write((text + "\r").encode("ascii")) + end = time.monotonic() + 5 + data = bytearray() + while time.monotonic() < end: + data.extend(port.read(4096)) + decoded = data.decode("utf-8", "replace") + if re.search(r"(?:^|\r?\n)\s*->\s*[^\r\n]+", decoded): + return decoded + raise RuntimeError(f"no reply to {text!r}: {data.decode('utf-8', 'replace')!r}") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--port", required=True) + parser.add_argument("--serial", required=True) + parser.add_argument("--expect", choices=("internal", "w25q16"), required=True) + args = parser.parse_args() + verify_port(args.port, args.serial) + + import serial + with serial.Serial(args.port, 115200, timeout=0.1, write_timeout=2) as port: + time.sleep(0.3) + replies = {cmd: command(port, cmd) for cmd in + ("ota storage", "ota self", "ota status")} + + if args.expect == "w25q16": + required = { + "ota storage": ("QSPI W25Q16", "jedec=EF4015", "size=2048K"), + "ota self": ("QSPI store:2048K", "bootloader: QSPI apply OK"), + "ota status": ("bl:QSPI",), + } + else: + required = { + "ota storage": ("OTA storage: no external NOR", "internal capacity=256K"), + "ota self": ("internal store:256K", "bootloader: delta apply OK"), + "ota status": ("bl:internal",), + } + failures = [f"{cmd}: missing {needle!r}" for cmd, needles in required.items() + for needle in needles if needle not in replies[cmd]] + print(json.dumps({"serial": args.serial, "expected": args.expect, + "passed": not failures, "replies": replies, + "failures": failures}, indent=2)) + if failures: + raise SystemExit(1) + + +if __name__ == "__main__": + main()