Harden recoverable external radio startup

This commit is contained in:
mikecarper committed 2026-08-24 16:54:41 -07:00
1 parent da74d7de22
commit 4a4a795f52
13 files changed
+244 -39

No files matched your search

+47
View File
@@ -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.
+8 -12
View File
@@ -23,10 +23,6 @@
#include <esp_heap_caps.h>
#endif
#endif
#ifdef TBEAM_1W
#include <esp_random.h>
#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)
+19
View File
@@ -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);
}
+1
View File
@@ -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
+8
View File
@@ -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);
+1
View File
@@ -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();
+77 -14
View File
@@ -1,4 +1,5 @@
#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#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);
}
+3
View File
@@ -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
+5
View File
@@ -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
+63 -4
View File
@@ -1,6 +1,9 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/ArduinoHelpers.h>
#ifdef USE_CC310_HW_CRYPTO
#include <helpers/NRF52Crypto.h>
#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<uint8_t*>(&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<uint8_t>(::random(0, 256));
}
#ifdef USE_CC310_HW_CRYPTO
mesh::mixCC310Random(dest, sz);
#endif
}
} rng;
return mesh::LocalIdentity(&rng);
}
+1 -1
View File
@@ -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();
+9 -1
View File
@@ -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);
}
+2 -7
View File
@@ -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)