Harden recoverable external radio startup
This commit is contained in:
1 parent
da74d7de22
commit
4a4a795f52
13 files changed
+244
-39
No files matched your search
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user