From 4a4a795f520be70050471454b782b82c1215fc87 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 24 Aug 2026 16:54:41 -0700 Subject: [PATCH] Harden recoverable external radio startup --- docs/rak3401_mota_chain.md | 47 +++++++++++++ examples/companion_radio/main.cpp | 20 +++--- examples/simple_repeater/main.cpp | 19 ++++++ variants/lilygo_tbeam_1w/platformio.ini | 1 + variants/lilygo_tbeam_1w/target.cpp | 8 +++ variants/lilygo_tbeam_1w/target.h | 1 + variants/rak3401/RAK3401Board.cpp | 91 +++++++++++++++++++++---- variants/rak3401/RAK3401Board.h | 3 + variants/rak3401/platformio.ini | 5 ++ variants/rak3401/target.cpp | 67 ++++++++++++++++-- variants/rak3401/target.h | 2 +- variants/rak3401/variant.cpp | 10 ++- variants/rak3401/variant.h | 9 +-- 13 files changed, 244 insertions(+), 39 deletions(-) diff --git a/docs/rak3401_mota_chain.md b/docs/rak3401_mota_chain.md index 56032f3c3..35a2cb015 100644 --- a/docs/rak3401_mota_chain.md +++ b/docs/rak3401_mota_chain.md @@ -389,3 +389,50 @@ OTA reach before every package, requires the exact post-boot EndF hash, and only re-enables the watchdog after step 9. Success requires endpoint body hash `4BB1526BF647547D`, target `2FA509C1`, hardware `RAK_3401`, normal radio `910.525 MHz / 62.5 kHz / SF7 / CR5`, and the watchdog verified on. + +## External-radio startup and manual USB recovery + +RAK3401 is a distinct target from RAK4631. Its RAK13300/RAK13302 radio is on +the WisBlock SPI bus (`SPI1` in the Adafruit nRF52 core), with BUSY and DIO1 on +P0.09/P0.10. Current builds explicitly enable those NFC-capable pads as GPIO +and do not advertise the radio bus as an on-board QSPI flash device. + +The RAK13302's FEM and boost supply use switched `3V3_S`, but the SX1262 core +uses unswitched 3V3. An MCU reset, a 1200-baud DFU touch, or disconnecting only +USB may therefore leave a battery-powered SX1262 in its previous state. At +startup, current firmware quiesces the FEM, reinitializes the dedicated SPI +instance, asserts NRESET, and, if BUSY remains high, sends the Semtech +NSS/GET_STATUS wake sequence without waiting on RadioLib first. The one-time +board startup cold-starts `3V3_S`; later radio retries leave that shared rail +enabled so they do not reset GPS or sensor modules. + +If BUSY still cannot be released, firmware must not enter a reset loop: + +- Companion starts its USB/BLE management services after three bounded probes, + uses nRF52 hardware entropy if it needs a first-boot identity, and retries the + radio every 60 seconds. Radio statistics remain zero until recovery. +- Repeater keeps the same USB CDC session open and prints + `Radio unavailable; retrying in 60 seconds`. It repeats the board-level probe + in place. The normal repeater CLI and mesh transport cannot start until the + radio responds. + +A permanently high BUSY after NRESET and the direct wake transaction indicates +an electrically unavailable radio, loose module, or power-domain fault; an MCU +reboot cannot manufacture a response. If local access is possible, disconnect +the battery as well as USB and reseat the WisBlock module. Remote firmware will +continue retrying without requiring that physical intervention or churning the +USB device. + +For a local Serial DFU recovery with the repository helper: + +```bash +cd /path/to/meshfirmware +MCFIRMWARE_NO_SUDO=1 ./mcfirmware.sh +``` + +Select the serial entry whose USB identity is `WisCore RAK3401 Board`, then +choose **Custom**, **nrf52**, and the exact RAK3401 `.zip`. Use **flash-update** +to preserve InternalFS preferences and identity. Use **flash-wipe + flash** only +when erasing application data is intentional. The helper follows the same USB +device into bootloader mode, so do not substitute another `/dev/ttyACM*` merely +because its number appears first. diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index ea434f330..9f5373760 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -23,10 +23,6 @@ #include #endif #endif -#ifdef TBEAM_1W -#include -#endif - // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -150,7 +146,7 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store /* END GLOBAL OBJECTS */ -#ifdef TBEAM_1W +#ifdef RECOVERABLE_EXTERNAL_RADIO static bool companion_radio_available = true; static unsigned long companion_radio_retry_at = 0; static const unsigned long COMPANION_RADIO_RETRY_MS = 60000UL; @@ -161,7 +157,6 @@ static void serviceCompanionRadioRecovery() { companion_radio_retry_at = millis() + COMPANION_RADIO_RETRY_MS; mesh::usbLoggingPort().println("Radio recovery probe starting"); - board.powerCycleRadio(); if (!radio_init()) { mesh::usbLoggingPort().println( "Radio recovery probe failed; companion services remain available"); @@ -1054,11 +1049,11 @@ void setup() { ++radioinit_attempts; MESH_DEBUG_PRINTLN("Radio init failed! (attempt %d)", radioinit_attempts); if (radioinit_attempts >= 3) { -#if defined(TBEAM_1W) +#if defined(RECOVERABLE_EXTERNAL_RADIO) // Continue into a recovery-capable Companion instead of trapping native // USB in a reboot loop. The main loop retries the radio independently. mesh::usbLoggingPort().println( - "Radio unavailable; starting display, USB, BLE, and WiFi recovery services"); + "Radio unavailable; starting companion management services"); break; #else MESH_DEBUG_PRINTLN("Radio init failed 3x - rebooting"); @@ -1068,15 +1063,16 @@ void setup() { delay(500); } -#ifdef TBEAM_1W +#ifdef RECOVERABLE_EXTERNAL_RADIO companion_radio_available = radio_available; companion_radio_retry_at = millis() + COMPANION_RADIO_RETRY_MS; - fast_rng.begin(radio_available ? radio_driver.getRngSeed() : esp_random()); + fast_rng.begin(radio_available ? radio_driver.getRngSeed() + : radio_fallback_rng_seed()); #ifdef DISPLAY_CLASS if (!radio_available && disp != NULL) { disp->startFrame(); disp->drawTextCentered(disp->width() / 2, 20, "Radio unavailable"); - disp->drawTextCentered(disp->width() / 2, 40, "Starting WiFi..."); + disp->drawTextCentered(disp->width() / 2, 40, "Starting interfaces..."); disp->endFrame(); } #endif @@ -1289,7 +1285,7 @@ void loop() { board.feedWatchdog(); #endif the_mesh.loop(); -#ifdef TBEAM_1W +#ifdef RECOVERABLE_EXTERNAL_RADIO serviceCompanionRadioRecovery(); #endif #if defined(ENABLE_USB_INTERFACE) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index a4c837c4c..b0f667b39 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -98,8 +98,27 @@ void setup() { ++radioinit_attempts; MESH_DEBUG_PRINTLN("Radio init failed! (attempt %d)", radioinit_attempts); if (radioinit_attempts >= 3) { +#ifdef RECOVERABLE_EXTERNAL_RADIO + // A remote external-radio node must not churn USB or require a physical + // power cut merely because its radio is temporarily unavailable. Keep + // the MCU alive and retry in place; target radio_init() performs the + // board-specific regulator/reset/wake recovery on each attempt. + Serial.println("Radio unavailable; retrying in 60 seconds"); + radioinit_attempts = 0; + const uint32_t retry_started = millis(); + while (millis() - retry_started < 60000UL) { +#if defined(NRF52_PLATFORM) + board.feedWatchdog(); +#endif +#ifdef HAS_EXTERNAL_WATCHDOG + external_watchdog.loop(); +#endif + delay(10); + } +#else MESH_DEBUG_PRINTLN("Radio init failed 3x - rebooting"); board.reboot(); +#endif } delay(500); } diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index 41fb91db7..f231673ec 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -5,6 +5,7 @@ build_flags = ${esp32_base.build_flags} -I variants/lilygo_tbeam_1w -D TBEAM_1W + -D RECOVERABLE_EXTERNAL_RADIO ; Radio - SX1262 with high-power PA (32dBm max output) ; Note: Set SX1262 output to 22dBm max, external PA provides additional gain diff --git a/variants/lilygo_tbeam_1w/target.cpp b/variants/lilygo_tbeam_1w/target.cpp index a8de201b1..aa38eea12 100644 --- a/variants/lilygo_tbeam_1w/target.cpp +++ b/variants/lilygo_tbeam_1w/target.cpp @@ -36,6 +36,10 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #endif bool radio_init() { + if (target_peripherals_initialized && !target_radio_available) { + board.powerCycleRadio(); + } + if (!target_peripherals_initialized) { fallback_clock.begin(); rtc_clock.begin(Wire); @@ -66,6 +70,10 @@ bool radio_init() { return success; } +uint32_t radio_fallback_rng_seed() { + return esp_random(); +} + mesh::LocalIdentity radio_new_identity() { if (target_radio_available) { RadioNoiseListener rng(radio); diff --git a/variants/lilygo_tbeam_1w/target.h b/variants/lilygo_tbeam_1w/target.h index b197d098b..2d1c85b57 100644 --- a/variants/lilygo_tbeam_1w/target.h +++ b/variants/lilygo_tbeam_1w/target.h @@ -24,4 +24,5 @@ extern AutoDiscoverRTCClock rtc_clock; extern EnvironmentSensorManager sensors; bool radio_init(); +uint32_t radio_fallback_rng_seed(); mesh::LocalIdentity radio_new_identity(); diff --git a/variants/rak3401/RAK3401Board.cpp b/variants/rak3401/RAK3401Board.cpp index 40bcb9915..7010a5333 100644 --- a/variants/rak3401/RAK3401Board.cpp +++ b/variants/rak3401/RAK3401Board.cpp @@ -1,4 +1,5 @@ #include +#include #include #include "RAK3401Board.h" @@ -45,22 +46,84 @@ void RAK3401Board::begin() { Wire.begin(); - // PIN_3V3_EN (WB_IO2, P0.34) controls the 3V3_S switched peripheral rail - // AND the 5V boost regulator (U5) on the RAK13302 that powers the SKY66122 PA. - // Must stay HIGH during radio operation - do not toggle for power saving. - pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); - - // Enable SKY66122-11 FEM on the RAK13302 module. - // CSD and CPS are tied together on the RAK13302 PCB, routed to IO3 (P0.21). - // HIGH = FEM active (LNA for RX, PA path available for TX). - // TX/RX switching (CTX) is handled by SX1262 DIO2 via SetDIO2AsRfSwitchCtrl. - pinMode(SX126X_POWER_EN, OUTPUT); #ifdef NRF52_POWER_MANAGEMENT // Boot voltage protection check (may not return if voltage too low) - // We need to call this after we configure SX126X_POWER_EN as output but before we pull high checkBootVoltage(&power_config); #endif - digitalWrite(SX126X_POWER_EN, HIGH); - delay(1); // SKY66122 turn-on settling time (tON = 3us typ) + + // Cold-start the switched auxiliary rail once. It powers the RAK13302 FEM + // and any fitted WisBlock peripherals, not the SX1262 core. Runtime radio + // retries must leave this rail up so GPS and sensors are not reset every + // minute. + pinMode(SX126X_POWER_EN, OUTPUT); + digitalWrite(SX126X_POWER_EN, LOW); + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, LOW); + delay(100); + digitalWrite(PIN_3V3_EN, HIGH); + delay(10); + + recoverRadio(); +} + +bool RAK3401Board::recoverRadio() { + // The RAK13302 FEM/boost supply is on 3V3_S, but the SX1262 itself is on the + // unswitched 3V3 rail. A battery-backed radio can therefore remain asleep + // across MCU resets. Quiesce the frontend and bus first, then reset the + // radio and use Semtech's NSS/GET_STATUS wake sequence if BUSY is still + // asserted. Leave 3V3_S enabled here because it is shared with GPS/sensors + // and does not power-cycle the SX1262 core anyway. + pinMode(SX126X_POWER_EN, OUTPUT); + digitalWrite(SX126X_POWER_EN, LOW); + + if (radio_spi_initialized) { + SPI1.end(); + radio_spi_initialized = false; + } + + pinMode(P_LORA_RESET, OUTPUT); + digitalWrite(P_LORA_RESET, LOW); + pinMode(P_LORA_NSS, INPUT); + pinMode(P_LORA_SCLK, INPUT); + pinMode(P_LORA_MOSI, INPUT); + pinMode(P_LORA_MISO, INPUT); + pinMode(P_LORA_BUSY, INPUT); + pinMode(P_LORA_DIO_1, INPUT); + + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); + digitalWrite(P_LORA_RESET, HIGH); + delay(20); + + SPI1.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI1.begin(); + radio_spi_initialized = true; + pinMode(P_LORA_NSS, OUTPUT); + digitalWrite(P_LORA_NSS, HIGH); + pinMode(P_LORA_BUSY, INPUT); + if (digitalRead(P_LORA_BUSY) == LOW) return true; + + // SetSleep is exited by an NSS falling edge. Do not use RadioLib for this + // transaction: its normal pre-transfer BUSY wait is exactly what prevents + // a sleeping/stuck radio from receiving the wake command. + SPI1.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0)); + digitalWrite(P_LORA_NSS, LOW); + SPI1.transfer(0xC0); // SX126x GET_STATUS + SPI1.transfer(0x00); // NOP + digitalWrite(P_LORA_NSS, HIGH); + SPI1.endTransaction(); + + const uint32_t wake_started = millis(); + while (digitalRead(P_LORA_BUSY) == HIGH) { + if (millis() - wake_started >= 1000) return false; + delay(1); + } + return true; +} + +void RAK3401Board::enableRadioFrontend() { + // CSD+CPS enable the SKY66122-11 FEM. SX1262 DIO2 performs the subsequent + // TX/RX path switching in hardware. + digitalWrite(SX126X_POWER_EN, HIGH); + delay(1); } diff --git a/variants/rak3401/RAK3401Board.h b/variants/rak3401/RAK3401Board.h index 3a080d5e2..e74ce847c 100644 --- a/variants/rak3401/RAK3401Board.h +++ b/variants/rak3401/RAK3401Board.h @@ -13,12 +13,15 @@ class RAK3401Board : public NRF52BoardDCDC { protected: + bool radio_spi_initialized = false; #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif public: RAK3401Board() : NRF52Board("RAK3401_OTA") {} void begin(); + bool recoverRadio(); + void enableRadioFrontend(); #define BATTERY_SAMPLES 8 diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index f7467102d..77d5f283b 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -8,6 +8,11 @@ build_flags = ${nrf52_base.build_flags} -I variants/rak3401 -D RAK_3401 -D RAK_BOARD + -D RECOVERABLE_EXTERNAL_RADIO + ; RAK13300/RAK13302 BUSY and DIO1 use nRF52840 P0.09/P0.10. A full + ; chip erase restores those pads to NFC mode, so SystemInit must repair + ; UICR and reset once before the external SX1262 can be reached. + -D CONFIG_NFCT_PINS_AS_GPIOS -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper diff --git a/variants/rak3401/target.cpp b/variants/rak3401/target.cpp index 284e20ffb..0e8d45510 100644 --- a/variants/rak3401/target.cpp +++ b/variants/rak3401/target.cpp @@ -1,6 +1,9 @@ #include #include "target.h" #include +#ifdef USE_CC310_HW_CRYPTO + #include +#endif RAK3401Board board; @@ -17,7 +20,10 @@ RAK3401Board board; #endif #endif -RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +// The external WisBlock radio has its own SPI interface in variant.h. Keep it +// off the generic high-speed SPI/SPIM3 instance so other SPI consumers cannot +// leave RadioLib talking on stale pins after a warm reset or recovery attempt. +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI1); WRAPPER_CLASS radio_driver(radio, board); @@ -32,12 +38,65 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); EnvironmentSensorManager sensors; #endif +static bool target_radio_available = false; + bool radio_init() { + // BUSY is authoritative on SX126x. If reset plus a direct NSS wake cannot + // release it, do not enter RadioLib's much longer pre-transfer timeout; the + // application can remain manageable and retry this inexpensive probe later. + if (!board.recoverRadio()) { + target_radio_available = false; + return false; + } + rtc_clock.begin(Wire); - return radio.std_init(&SPI); + if (radio.std_init(&SPI1)) { + target_radio_available = true; + board.enableRadioFrontend(); + return true; + } + + // A warm MCU reset does not remove power from the external RAK13300/RAK13302 + // SX1262. If its ordinary NRESET probe fails, perform a forced wake before + // letting the application apply its normal retry policy. + if (!board.recoverRadio()) { + target_radio_available = false; + return false; + } + rtc_clock.begin(Wire); + if (!radio.std_init(&SPI1)) { + target_radio_available = false; + return false; + } + target_radio_available = true; + board.enableRadioFrontend(); + return true; +} + +uint32_t radio_fallback_rng_seed() { + uint32_t seed = micros() ^ NRF_FICR->DEVICEID[0] ^ NRF_FICR->DEVICEID[1]; +#ifdef USE_CC310_HW_CRYPTO + mesh::mixCC310Random(reinterpret_cast(&seed), sizeof(seed)); +#endif + return seed; } mesh::LocalIdentity radio_new_identity() { - RadioNoiseListener rng(radio); - return mesh::LocalIdentity(&rng); // create new random identity + if (target_radio_available) { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); + } + + class RAK3401IdentityRNG : public mesh::RNG { + public: + void random(uint8_t* dest, size_t sz) override { + for (size_t i = 0; i < sz; ++i) { + dest[i] = static_cast(::random(0, 256)); + } +#ifdef USE_CC310_HW_CRYPTO + mesh::mixCC310Random(dest, sz); +#endif + } + } rng; + return mesh::LocalIdentity(&rng); } diff --git a/variants/rak3401/target.h b/variants/rak3401/target.h index 00653e90e..9cb872818 100644 --- a/variants/rak3401/target.h +++ b/variants/rak3401/target.h @@ -24,5 +24,5 @@ extern AutoDiscoverRTCClock rtc_clock; extern EnvironmentSensorManager sensors; bool radio_init(); +uint32_t radio_fallback_rng_seed(); mesh::LocalIdentity radio_new_identity(); - diff --git a/variants/rak3401/variant.cpp b/variants/rak3401/variant.cpp index d562189f9..17e619ef2 100644 --- a/variants/rak3401/variant.cpp +++ b/variants/rak3401/variant.cpp @@ -48,5 +48,13 @@ void initVariant() // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); - digitalWrite(PIN_3V3_EN, HIGH); + // Start the auxiliary 3V3_S rail off so the RAK13302 FEM/boost circuitry is + // quiescent while the external SX1262 is reset. The SX1262 core itself uses + // unswitched 3V3, so RAK3401Board::begin() gives this rail one cold-start + // interval and recoverRadio() separately resets/wakes the radio core. + digitalWrite(PIN_3V3_EN, LOW); + + // Keep the RAK13302 FEM disabled while its auxiliary rail is off. + pinMode(SX126X_POWER_EN, OUTPUT); + digitalWrite(SX126X_POWER_EN, LOW); } diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h index d3c4e776e..0bbc87e01 100644 --- a/variants/rak3401/variant.h +++ b/variants/rak3401/variant.h @@ -135,8 +135,8 @@ static const uint8_t AREF = PIN_AREF; #define PIN_WIRE_SDA (WB_I2C1_SDA) #define PIN_WIRE_SCL (WB_I2C1_SCL) -// QSPI Pins -// QSPI occupied by GPIO's +// The RAK3401 has no on-board QSPI flash. These pads are the WisBlock SPI bus +// and are used by the external RAK13300/RAK13302 radio module. #define PIN_QSPI_SCK 3 #define PIN_QSPI_CS 26 #define PIN_QSPI_IO0 30 @@ -144,11 +144,6 @@ static const uint8_t AREF = PIN_AREF; #define PIN_QSPI_IO2 28 #define PIN_QSPI_IO3 2 -// On-board QSPI Flash -// No onboard flash -#define EXTERNAL_FLASH_DEVICES IS25LP080D -#define EXTERNAL_FLASH_USE_QSPI - #define USE_SX1262 #define SX126X_CS (26) #define SX126X_DIO1 (10)