Connect GPIO MISO during RAK NOR identification

This commit is contained in:
mikecarper
2026-09-25 13:14:30 -07:00
parent 9638223138
commit f3125bedd8
4 changed files with 93 additions and 3 deletions
+9 -3
View File
@@ -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
+16
View File
@@ -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() {
+3
View File
@@ -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;
+65
View File
@@ -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()