From 9127f8fe251af0cedbbd976e68cbf9f3d25d9608 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 25 Sep 2026 14:20:29 -0700 Subject: [PATCH] Detect header W25Q16 on unified RAK firmware --- docs/ota_easy.md | 8 ++++ docs/ota_nrf52_qspi.md | 24 ++++++++++ docs/ota_user_guide.md | 5 +++ src/helpers/ota/OtaApply.cpp | 5 +++ src/helpers/ota/OtaBlInfo.h | 3 +- src/helpers/ota/OtaFlashLayout_nrf52.h | 1 + src/helpers/ota/OtaStoreAdaptiveNrf52.h | 9 +++- src/helpers/ota/OtaStoreQspiNrf52.cpp | 44 +++++++++++++++++-- src/helpers/ota/OtaStoreQspiNrf52.h | 1 + .../sensors/EnvironmentSensorManager.cpp | 15 +++++++ variants/rak4631/variant.cpp | 4 +- 11 files changed, 111 insertions(+), 8 deletions(-) diff --git a/docs/ota_easy.md b/docs/ota_easy.md index 09a15aab5..6efac2c1c 100644 --- a/docs/ota_easy.md +++ b/docs/ota_easy.md @@ -334,6 +334,14 @@ occupied. See the [nRF52 QSPI guide](ota_nrf52_qspi.md#one-w25q16-wiring-for-rak4631-or-rak3401--rak13302) before installing either image. +For a build that uses only the 2.54 mm headers, use the unified +`RAK_4631_repeater_unified_lora_ota` or +`RAK_3401_repeater_unified_lora_ota` image with its matching adaptive +OTAFIX 2.4.8 bootloader. Wire `CLK` to J10 `TX1`, `DO` to J10 `RX1`, `DI` to +J11 `IO1`, and `CS` to J11 `AIN1`; keep the `CS` pull-up and J12 power wiring. +This gives up UART1 GPS/PPS but leaves I2C sensors and the RAK3401 radio +available. See the [header-only wiring table](ota_nrf52_qspi.md#header-only-w25q16-wiring-for-the-unified-rak-images). + ### 4. Build and check the in-place delta ```bash diff --git a/docs/ota_nrf52_qspi.md b/docs/ota_nrf52_qspi.md index 8dd932580..0322c0787 100644 --- a/docs/ota_nrf52_qspi.md +++ b/docs/ota_nrf52_qspi.md @@ -143,6 +143,30 @@ core: The physical flash wiring is identical, but the two application and bootloader pairs are not interchangeable. +### Header-only W25Q16 wiring for the unified RAK images + +The unified RAK repeater images and matching OTAFIX 2.4.8 adaptive bootloaders +also support a W25Q16 through the RAK19007 2.54 mm headers. This arrangement +does not need an IO-connector interposer or underside SPI solder points: + +| W25Q16 breakout | RAK19007 header | nRF52840 / Arduino pin | +| --- | --- | --- | +| `CLK` | J10 `TX1` | P0.16 / `16` | +| `DO` / MISO | J10 `RX1` | P0.15 / `15` | +| `DI` / MOSI | J11 `IO1` | P0.17 / `17` | +| `CS` | J11 `AIN1` | P0.31 / `31` | +| `VCC` | J12 `VDD` | regulated 3.3 V | +| `GND` | J12 `GND` | ground | + +Keep the approximately 10 kOhm `CS`-to-`VDD` pull-up. This mapping gives up +the UART1 GPS and PPS pins while the flash is fitted. The unified firmware +detects the exact `EF4015` device before GPS discovery and leaves I2C sensor +pins `SDA` and `SCL` available. RAK4631 can still use its UART2 bridge; its +UART1 bridge is unavailable with header flash. RAK3401 retains its 1 W radio +and shared sensor/radio power rail. Do not substitute `SCL` for flash clock or +use `SWCLK`, which is the debug clock. The older dedicated W25Q16 images above +expect the WisBlock SPI connector wiring and do not use this header mapping. + ## One-time prerequisite Install a QSPI-capable OTAFIX 2.4.1 preview.9 or newer bootloader for the exact diff --git a/docs/ota_user_guide.md b/docs/ota_user_guide.md index 5f982ddc3..25910d5bd 100644 --- a/docs/ota_user_guide.md +++ b/docs/ota_user_guide.md @@ -104,6 +104,11 @@ power the flash from VBAT. The three SPI signals require underside pads or an IO-connector interposer; the easy J10/J11/J12 headers do not expose them. The flash consumes no WisBlock slot, so a supported GPS can remain in slot A. +The unified RAK repeater images additionally support a header-only W25Q16 +layout, using J10 `TX1`/`RX1` and J11 `IO1`/`AIN1`. This layout needs the +matching adaptive OTAFIX 2.4.8 bootloader and gives up UART1 GPS/PPS; I2C +sensors remain available. See the [pin table](ota_nrf52_qspi.md#header-only-w25q16-wiring-for-the-unified-rak-images). + The ordinary full-sensor `RAK_4631_repeater` image remains too large for the safe internal in-place update limit. Without external flash, use `RAK_4631_repeater_lora_ota_no_external_sensors`; it trims selected optional diff --git a/src/helpers/ota/OtaApply.cpp b/src/helpers/ota/OtaApply.cpp index d5c91b6c0..abf73f9f2 100644 --- a/src/helpers/ota/OtaApply.cpp +++ b/src/helpers/ota/OtaApply.cpp @@ -1197,6 +1197,11 @@ bool ota_apply_mota_nrf52(OtaStoreQspiNrf52& store, const SignerAllowlist& allow if (!ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_QSPI, "QSPI", false, st, msg)) { return false; } +#if defined(OTA_RAK_AUTO_STORE) + if (OtaStoreQspiNrf52::headerW25Detected()) { + g_nrf52_qspi_handoff = GPREGRET2_OTA_STAGE_HEADER_W25; + } +#endif #if defined(OTA_RAK_AUTO_STORE) && !defined(RAK_3401) if (store.jedec_id() == 0xC84015UL) { OtaBootloaderIdentity identity; diff --git a/src/helpers/ota/OtaBlInfo.h b/src/helpers/ota/OtaBlInfo.h index d025caedc..80199cdf6 100644 --- a/src/helpers/ota/OtaBlInfo.h +++ b/src/helpers/ota/OtaBlInfo.h @@ -84,7 +84,8 @@ static const uint8_t OTA_BL_STORAGE_SD = 0x01; static const uint8_t OTA_BL_STORAGE_STAGE_CEILING = 0x02; static const uint8_t OTA_BL_STORAGE_QSPI = 0x04; static const uint8_t OTA_BL_STORAGE_BOOT_UPDATE = 0x08; -static const uint8_t OTA_BL_STORAGE_KNOWN = 0x0F; +static const uint8_t OTA_BL_STORAGE_HEADER_W25 = 0x10; +static const uint8_t OTA_BL_STORAGE_KNOWN = 0x1F; static const uint8_t OTA_BL_PROFILE_SD_BOOT_UPDATE = OTA_BL_STORAGE_SD | OTA_BL_STORAGE_BOOT_UPDATE; static const uint8_t OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE = diff --git a/src/helpers/ota/OtaFlashLayout_nrf52.h b/src/helpers/ota/OtaFlashLayout_nrf52.h index 6ca53610e..a167bfd51 100644 --- a/src/helpers/ota/OtaFlashLayout_nrf52.h +++ b/src/helpers/ota/OtaFlashLayout_nrf52.h @@ -56,6 +56,7 @@ static const uint8_t GPREGRET2_OTA_STAGE_LEGACY = 0xD4u; static const uint8_t GPREGRET2_OTA_STAGE_EXPANDED = 0xEDu; static const uint8_t GPREGRET2_OTA_STAGE_QSPI = 0x51u; static const uint8_t GPREGRET2_OTA_STAGE_RAK15001 = 0x52u; +static const uint8_t GPREGRET2_OTA_STAGE_HEADER_W25 = 0x54u; static const uint8_t GPREGRET2_OTA_STAGE_SD = 0x53u; static const uint8_t GPREGRET2_OTA_STAGE_HYBRID = 0xA6u; diff --git a/src/helpers/ota/OtaStoreAdaptiveNrf52.h b/src/helpers/ota/OtaStoreAdaptiveNrf52.h index 347616918..47ec2b6ca 100644 --- a/src/helpers/ota/OtaStoreAdaptiveNrf52.h +++ b/src/helpers/ota/OtaStoreAdaptiveNrf52.h @@ -50,6 +50,12 @@ class OtaStoreAdaptiveNrf52 : public OtaStore { } const bool qspi_bootloader = caps.present && (caps.storage_flags & OTA_BL_STORAGE_QSPI) != 0; + if (detected == 2u && OtaStoreQspiNrf52::headerW25Detected() && + (caps.storage_flags & OTA_BL_STORAGE_HEADER_W25) == 0) { + activateInternal(); + _reason = "header W25 needs newer OTAFIX bootloader"; + return; + } OtaBootloaderIdentity identity; const bool identity_valid = qspi_bootloader && ota_installed_bootloader_identity(identity) && identity.crc_ok; @@ -69,7 +75,8 @@ class OtaStoreAdaptiveNrf52 : public OtaStore { } if (choice == RakStorageChoice::Qspi) { activateExternal(); - _reason = detected == 1u ? "RAK15001 C" : "W25Q16"; + _reason = detected == 1u ? "RAK15001 C" : + OtaStoreQspiNrf52::headerW25Detected() ? "W25Q16 header" : "W25Q16 SPI"; return; } _mode = UNSAFE; diff --git a/src/helpers/ota/OtaStoreQspiNrf52.cpp b/src/helpers/ota/OtaStoreQspiNrf52.cpp index caaa69399..05d53204e 100644 --- a/src/helpers/ota/OtaStoreQspiNrf52.cpp +++ b/src/helpers/ota/OtaStoreQspiNrf52.cpp @@ -25,6 +25,12 @@ 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; +// The header-connected W25Q16 uses J10 TX1/RX1 and J11 IO1/AIN1. It is +// mutually exclusive with GPS on this board but leaves both I2C pins free. +static bool auto_header_w25 = false; +static const uint8_t HEADER_SCK_PIN = 16; // J10 TX1, P0.16 +static const uint8_t HEADER_IO0_PIN = 17; // J11 IO1, P0.17, W25 DI +static const uint8_t HEADER_IO1_PIN = 15; // J10 RX1, P0.15, W25 DO #endif #if defined(OTA_QSPI_SCK_PHYSICAL_PIN) && defined(OTA_QSPI_SCK_ARDUINO_PIN) @@ -100,6 +106,9 @@ static uint8_t arduino_to_physical(uint32_t arduino_pin) { } static uint8_t qspi_sck_pin() { +#if defined(OTA_RAK_AUTO_STORE) + if (auto_header_w25) return arduino_to_physical(HEADER_SCK_PIN); +#endif #ifdef OTA_QSPI_SCK_PHYSICAL_PIN return OTA_QSPI_SCK_PHYSICAL_PIN; #else @@ -120,6 +129,9 @@ static uint8_t qspi_cs_pin() { } static uint8_t qspi_io0_pin() { +#if defined(OTA_RAK_AUTO_STORE) + if (auto_header_w25) return arduino_to_physical(HEADER_IO0_PIN); +#endif #ifdef OTA_QSPI_IO0_PHYSICAL_PIN return OTA_QSPI_IO0_PHYSICAL_PIN; #else @@ -128,6 +140,9 @@ static uint8_t qspi_io0_pin() { } static uint8_t qspi_io1_pin() { +#if defined(OTA_RAK_AUTO_STORE) + if (auto_header_w25) return arduino_to_physical(HEADER_IO1_PIN); +#endif #ifdef OTA_QSPI_IO1_PHYSICAL_PIN return OTA_QSPI_IO1_PHYSICAL_PIN; #else @@ -320,18 +335,34 @@ uint8_t OtaStoreQspiNrf52::autoDetect() { pinMode(P_LORA_NSS, OUTPUT); digitalWrite(P_LORA_NSS, HIGH); SPI1.end(); - const uint32_t w25_id = probe_nor(w25_cs); - auto_w25_probe_id = w25_id; + uint32_t w25_id = probe_nor(w25_cs); SPI1.begin(); - auto_detection = w25_id == 0xEF4015UL ? 2u : 0u; #else const uint8_t rak_cs = arduino_to_physical(26); nrf_gpio_pin_set(rak_cs); nrf_gpio_cfg_output(rak_cs); const uint32_t rak_id = probe_nor(rak_cs); - const uint32_t w25_id = probe_nor(w25_cs); + uint32_t w25_id = probe_nor(w25_cs); auto_rak_probe_id = rak_id; +#endif + if (w25_id != 0xEF4015UL) { + // GPS discovery calls this one-time probe before it starts UART1. Ending + // an unopened Adafruit UART can block indefinitely, so leave it alone. + auto_header_w25 = true; + const uint32_t header_id = probe_nor(w25_cs); + if (header_id == 0xEF4015UL) { + w25_id = header_id; + } else { + auto_header_w25 = false; + nrf_gpio_cfg_default(arduino_to_physical(HEADER_SCK_PIN)); + nrf_gpio_cfg_default(arduino_to_physical(HEADER_IO0_PIN)); + nrf_gpio_cfg_default(arduino_to_physical(HEADER_IO1_PIN)); + } + } auto_w25_probe_id = w25_id; +#if defined(RAK_3401) + auto_detection = w25_id == 0xEF4015UL ? 2u : 0u; +#else const bool rak = rak_id == 0xC84015UL; const bool w25 = w25_id == 0xEF4015UL; auto_detection = rak && w25 ? 3u : rak ? 1u : w25 ? 2u : 0u; @@ -346,6 +377,11 @@ uint8_t OtaStoreQspiNrf52::autoDetect() { return auto_detection; } +bool OtaStoreQspiNrf52::headerW25Detected() { + autoDetect(); + return auto_header_w25; +} + void OtaStoreQspiNrf52::autoProbeIds(uint32_t& rak15001, uint32_t& w25q16) { autoDetect(); rak15001 = auto_rak_probe_id; diff --git a/src/helpers/ota/OtaStoreQspiNrf52.h b/src/helpers/ota/OtaStoreQspiNrf52.h index f32c1d402..346f740af 100644 --- a/src/helpers/ota/OtaStoreQspiNrf52.h +++ b/src/helpers/ota/OtaStoreQspiNrf52.h @@ -170,6 +170,7 @@ 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(); + static bool headerW25Detected(); // 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); diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 2d1ac8981..f7c2a11ce 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -2,6 +2,9 @@ #include "I2CAddressClaimPolicy.h" #include "EnvironmentI2CConfig.h" #include "NmeaSentenceProbe.h" +#if defined(OTA_RAK_AUTO_STORE) && defined(RAK_BOARD) +#include "../ota/OtaStoreQspiNrf52.h" +#endif #include @@ -1226,6 +1229,18 @@ void EnvironmentSensorManager::initBasicGPS() { // or make a new location provider ... #ifdef RAK_WISBLOCK_GPS void EnvironmentSensorManager::rakGPSInit() { +#if defined(OTA_RAK_AUTO_STORE) && defined(RAK_BOARD) + // The 2.54 mm header W25Q16 uses the UART1 and PPS pads. Probe before + // starting GPS discovery so UART1 never drives a NOR data/clock wire. + if (mesh::ota::OtaStoreQspiNrf52::headerW25Detected()) { + gps_active = false; + gps_detected = false; + gps_serial_transport = false; + i2cGPSFlag = false; + serialGPSFlag = false; + return; + } +#endif // Preserve the established owner across repeated begin() calls. Clearing // this flag and starting Serial1 here would silently take UART1 back from an // active bridge. diff --git a/variants/rak4631/variant.cpp b/variants/rak4631/variant.cpp index c339b3544..ebd61165f 100644 --- a/variants/rak4631/variant.cpp +++ b/variants/rak4631/variant.cpp @@ -47,8 +47,8 @@ void initVariant() ledOff(PIN_LED2);; #if defined(OTA_RAK_AUTO_STORE) - // Both candidates share SCK/MOSI/MISO. Hold both chip selects high until - // the exact JEDEC probe chooses one; W25Q16 also needs its reset pull-up. + // Keep both chip selects high through startup. RAK15001 and native W25Q16 + // use the WisBlock SPI bus; a header W25Q16 uses TX1/RX1/IO1 instead. pinMode(26, OUTPUT); digitalWrite(26, HIGH); pinMode(31, OUTPUT);