From 9b5541a44d7e94366c62735518f0c6d856fd8c99 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 25 Aug 2026 10:27:26 -0700 Subject: [PATCH] Harden external radio recovery and power limits --- docs/cli_commands.md | 10 ++- docs/faq.md | 54 +++++++++++++ docs/terminal_chat_cli.md | 4 +- examples/simple_repeater/MyMesh.cpp | 7 +- examples/simple_repeater/MyMesh.h | 2 +- examples/simple_room_server/MyMesh.cpp | 4 +- examples/simple_room_server/MyMesh.h | 2 +- examples/simple_sensor/SensorMesh.cpp | 4 +- examples/simple_sensor/SensorMesh.h | 2 +- platformio.ini | 2 +- src/MeshCore.h | 16 ++++ src/helpers/CLICommandUtils.h | 34 ++++++++ src/helpers/CommonCLI.cpp | 43 +++++++++-- src/helpers/CommonCLI.h | 2 +- src/helpers/radiolib/CustomLLCC68.h | 34 ++++++-- src/helpers/radiolib/CustomLLCC68Wrapper.h | 5 +- src/helpers/radiolib/CustomLR1110Wrapper.h | 7 +- src/helpers/radiolib/CustomLR2021.h | 76 ++++++++++++++---- src/helpers/radiolib/CustomLR2021Wrapper.h | 30 ++++++-- src/helpers/radiolib/CustomSX1262.h | 18 ++--- src/helpers/radiolib/CustomSX1262Wrapper.h | 15 +++- src/helpers/radiolib/CustomSX1268.h | 34 ++++++-- src/helpers/radiolib/CustomSX1268Wrapper.h | 5 +- src/helpers/radiolib/CustomSX1276Wrapper.h | 5 +- src/helpers/radiolib/LR2021Band.h | 16 ++++ src/helpers/radiolib/RadioLibWrappers.cpp | 39 ++++++++-- src/helpers/radiolib/RadioLibWrappers.h | 21 ++++- src/helpers/radiolib/RadioPowerLimits.h | 51 ++++++++++++ src/helpers/sim/SimRadio.h | 2 +- .../test_cli_command_utils.cpp | 38 +++++++++ .../test_radio_liveness.cpp | 8 ++ variants/ebyte_eora_s3/platformio.ini | 5 +- variants/generic-e22/platformio.ini | 3 +- variants/heltec_tracker/platformio.ini | 2 +- variants/heltec_v3/platformio.ini | 2 +- variants/heltec_wireless_paper/platformio.ini | 2 +- variants/lilygo_t_impulse_plus/platformio.ini | 2 + variants/lilygo_tbeam_1w/TBeam1WBoard.h | 7 +- .../platformio.ini | 17 ++-- .../meshnology_w12/MeshnologyW12Board.cpp | 25 ++++-- variants/meshnology_w12/MeshnologyW12Board.h | 4 + variants/meshnology_w12/platformio.ini | 2 +- variants/minewsemi_me25ls01/target.cpp | 38 ++++----- variants/muziworks_r1_neo/variant.h | 2 +- variants/rak3401/RAK3401Board.cpp | 77 ++++++------------- variants/rak3401/RAK3401Board.h | 7 +- variants/rak3401/target.cpp | 23 +++--- variants/rak3401/variant.cpp | 11 ++- variants/tenstar_c3/platformio.ini | 3 + variants/wio_wm1110/platformio.ini | 3 +- variants/wio_wm1110/target.cpp | 38 ++++----- variants/wio_wm1110/variant.h | 2 +- 52 files changed, 645 insertions(+), 220 deletions(-) create mode 100644 src/helpers/radiolib/LR2021Band.h create mode 100644 src/helpers/radiolib/RadioPowerLimits.h diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 270b06e5..15b6a232 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -672,7 +672,8 @@ minimum and return to it exactly when it is safe. - `set tx ` **Parameters:** -- `dbm`: Power level in dBm (1-22) +- `dbm`: Requested radio-chip power in dBm. The valid range depends on the + radio family, selected PA path, and any board-specific external-PA limit. **Set by build flag:** `LORA_TX_POWER` @@ -680,6 +681,13 @@ minimum and return to it exactly when it is safe. **Notes:** This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Refer to the node's manual for the correct setting to use. **Setting a value too high may violate the laws in your country.** +The command strictly rejects malformed values and saves the new preference +only after the active radio driver accepts it. On LR2021, the chip range is +`-9` to `22` dBm below 1500 MHz and `-19` to `12` dBm above 1500 MHz. A lower +board-specific external-PA limit still takes precedence. A saved profile is +clamped to the applicable limit when its frequency changes; temporary radio +settings use a safe effective power without replacing the saved preference. + --- #### Change the radio parameters for a set duration diff --git a/docs/faq.md b/docs/faq.md index dcf250e9..2463467f 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -295,6 +295,60 @@ transmitted. When safe, `RadioLibWrapper::resetAGC()` warm-sleeps the radio, returns the wrapper to idle so receive mode is re-armed, reapplies the cached RX boost setting, and starts a fresh noise-floor calibration. +#### 3.7.1. Q: Can firmware recover an external radio whose BUSY pin is stuck high? +**A:** Current firmware first attempts a non-destructive RX/AGC re-arm and then +uses a physical radio reset when the board exposes one. A normal RadioLib +reinitialization is not reported as a hard reset unless firmware can actually +pulse NRST or cycle the radio's power rail. + +Most supported external-radio boards expose NRST directly. RAK3401 pulses its +dedicated NRST while leaving the shared peripheral rail and SPI controller +intact, and LilyGo T-Beam 1W can cycle the radio regulator. The generic Tenstar +C3 target describes a user-wired radio and has no reset connection by default. +For unattended recovery on that target, wire NRST to a free GPIO and override +`P_LORA_RESET` with that GPIO in the build configuration. + +The SenseCAP T1000-E LR1110 path was hardware-qualified with 50 successful +radio-only resets: 25 in continuous receive and 25 with RX power saving active. +The saved radio tuple, TX power, RX gain, and RX power-saving configuration were +restored after every reset, bidirectional LoRa status traffic still passed, and +the nRF52840 uptime remained continuous. + +An integrated STM32WL radio has no independently resettable RF peripheral, so +it intentionally remains on the bounded soft-recovery path. If its radio core +cannot recover, resetting only the radio is electrically impossible; the MCU +must be restarted. + +#### 3.7.2. Q: Does firmware automatically update LR1110 transceiver firmware? +**A:** No. A transceiver update erases and rewrites the LR1110 itself, so it is +not performed as a side effect of installing ordinary device firmware. Use the +hardware vendor's physical-access maintenance procedure and verify the exact +LR1110 and bootloader versions before changing them. + +Full remediation of +[Semtech SEM-PSA-2026-001](https://www.semtech.com/company/security/security-bulletins/sem-psa-2026-001) +requires LR1110 BL2 0x1001 as well as the applicable transceiver firmware. It +is a separate two-stage maintenance procedure and is not performed +automatically by MeshCore. + +#### 3.7.3. Q: How does firmware handle LR2021 low/high-band changes? +**A:** Firmware first selects the board's matching RF path, retunes the radio, +and then reapplies output power so the LR2021 selects the correct low- or +high-frequency PA. It also uses the matching RF-switch mode when RX or TX is +launched. LR2021 high-band output power is limited to 12 dBm; a lower +board-specific limit still takes precedence. + +On Meshnology W12, the two external FEM supplies are mutually exclusive: +GPIO4 powers the sub-GHz GC1109 path and GPIO3 powers the 2.4 GHz RFX2402E +path. Both supplies are disabled before the selected one is enabled. Temporary +radio settings preserve the saved TX power and safely clamp only the active +band; returning to the saved settings restores its applicable power. A +permanent frequency change clamps the saved TX power to the new band's limit. + +The external SX1262, SX1268, LLCC68, LR1110, and LR2021 drivers also bound the +post-SetTx BUSY wait. A failed PA ramp therefore returns an error and can enter +the physical-reset recovery path instead of hanging the node indefinitely. + ### 3.8. Q: How do I make my repeater an observer on the mesh? **A:** The observer instruction is available here: diff --git a/docs/terminal_chat_cli.md b/docs/terminal_chat_cli.md index 021af986..1089ff38 100644 --- a/docs/terminal_chat_cli.md +++ b/docs/terminal_chat_cli.md @@ -57,7 +57,9 @@ Set the LoRa frequency. Example: set freq 915.8 ``` set tx {tx-power-dbm} ``` -Sets LoRa transmit power in dBm. +Sets LoRa-chip transmit power in dBm. The firmware rejects values outside the +radio/board limit; external-PA boards can have a lower input-power ceiling than +the radio chip itself. ``` set name {name} diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 20e23385..2f001af1 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1,4 +1,5 @@ #include "MyMesh.h" +#include #include #include #include // for qsort() @@ -4268,6 +4269,8 @@ void MyMesh::processScheduledRadioSettings() { // Keep level-derived RX duty-cycle windows synchronized with the newly // persisted SF/BW. Manual RX/sleep timings intentionally remain fixed. CommonCLI::recalculateRxPowerSavingFromLevel(&_prefs); + _prefs.tx_power_dbm = mesh::clampLoRaTxPower( + _prefs.tx_power_dbm, _prefs.freq); savePrefs(); queueSavedRadioApply(); } @@ -4512,8 +4515,8 @@ void MyMesh::dumpLogFile() { } } -void MyMesh::setTxPower(int8_t power_dbm) { - radio_driver.setTxPower(power_dbm); +bool MyMesh::setTxPower(int8_t power_dbm) { + return radio_driver.setTxPower(power_dbm); } bool MyMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) { diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 31bafbb9..237455b0 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -919,7 +919,7 @@ public: } void dumpLogFile() override; - void setTxPower(int8_t power_dbm) override; + bool setTxPower(int8_t power_dbm) override; bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override; bool supportsRxPowerSavingRfRxDisable() const override; bool setRxPowerSavingRfRxDisabled(bool disabled) override; diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 881e8fc5..6ce4674e 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -1511,8 +1511,8 @@ void MyMesh::dumpLogFile() { } } -void MyMesh::setTxPower(int8_t power_dbm) { - radio_driver.setTxPower(power_dbm); +bool MyMesh::setTxPower(int8_t power_dbm) { + return radio_driver.setTxPower(power_dbm); } bool MyMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) { diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index a700d797..4b78b2cd 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -403,7 +403,7 @@ public: } void dumpLogFile() override; - void setTxPower(int8_t power_dbm) override; + bool setTxPower(int8_t power_dbm) override; bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override; void recalibrateNoiseFloor() override { _radio->recalibrateNoiseFloor(); } void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override; diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index d0792ecb..56880994 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -1101,8 +1101,8 @@ void SensorMesh::updateFloodAdvertTimer() { } } -void SensorMesh::setTxPower(int8_t power_dbm) { - radio_driver.setTxPower(power_dbm); +bool SensorMesh::setTxPower(int8_t power_dbm) { + return radio_driver.setTxPower(power_dbm); } bool SensorMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) { diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 3632887c..7abdfdc6 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -79,7 +79,7 @@ public: void setLoggingOn(bool enable) override { } void eraseLogFile() override { } void dumpLogFile() override { } - void setTxPower(int8_t power_dbm) override; + bool setTxPower(int8_t power_dbm) override; bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override; void recalibrateNoiseFloor() override { _radio->recalibrateNoiseFloor(); } void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override; diff --git a/platformio.ini b/platformio.ini index a5df2b5c..324e8176 100644 --- a/platformio.ini +++ b/platformio.ini @@ -20,7 +20,7 @@ lib_deps = SPI Wire ;jgromes/RadioLib @ ^7.7.1 - https://github.com/jgromes/RadioLib.git#6d8934836678d8894e3d556550475b37dce3e2b6 + https://github.com/jgromes/RadioLib.git#187ef24791c3d844939b2be13a68bd890bd04e4c rweather/Crypto @ ^0.4.0 adafruit/RTClib @ ^2.1.3 melopero/Melopero RV3028 @ ^1.1.0 diff --git a/src/MeshCore.h b/src/MeshCore.h index 4b4ca708..e435ae36 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -102,6 +102,22 @@ public: virtual bool setLoRaFemLnaEnabled(bool enable) { return false; } virtual bool canControlLoRaFemLna() const { return false; } virtual bool isLoRaFemLnaEnabled() const { return false; } + // Board-level physical radio reset or power cycle. Most radios expose NRST + // directly and need no override. Boards that route reset through an I/O + // expander or a dedicated regulator use this hook so liveness recovery does + // not claim a hard reset that it cannot actually perform. + virtual bool supportsRadioHardReset() const { return false; } + virtual bool resetRadio() { return false; } + // Select board-level RF hardware for a carrier before the radio is + // configured. Dual-band boards use this to power the matching FEM rail. + virtual bool prepareRadioFrequency(float frequency) { + (void)frequency; + return true; + } + // Restore board-level hardware that must remain quiescent until the radio + // has been reinitialized, such as an external FEM enable. This runs only + // after a successful hard-reset reinitialization. + virtual bool finishRadioHardReset() { return true; } // Software-selectable external FEM transmit gain. This is not a PA power switch. virtual bool setLoRaFemPaGainEnabled(bool enable) { return false; } virtual bool canControlLoRaFemPaGain() const { return false; } diff --git a/src/helpers/CLICommandUtils.h b/src/helpers/CLICommandUtils.h index 6dfbab61..528e8c3c 100644 --- a/src/helpers/CLICommandUtils.h +++ b/src/helpers/CLICommandUtils.h @@ -554,5 +554,39 @@ inline bool parseDecimalStrict(const char* text, float& result) { return true; } +// Parse a signed base-10 integer while rejecting empty input, overflow, +// decimal/exponent syntax, and trailing junk. +inline bool parseIntegerStrict(const char* text, int32_t& result) { + if (text == nullptr) return false; + while (*text == ' ' || *text == '\t') text++; + + bool negative = false; + if (*text == '-' || *text == '+') { + negative = *text == '-'; + text++; + } + + const uint32_t limit = negative ? 0x80000000UL : 0x7FFFFFFFUL; + uint32_t value = 0; + bool saw_digit = false; + while (*text >= '0' && *text <= '9') { + const uint8_t digit = static_cast(*text++ - '0'); + if (value > (limit - digit) / 10UL) return false; + value = value * 10UL + digit; + saw_digit = true; + } + + while (*text == ' ' || *text == '\t') text++; + if (!saw_digit || *text != 0) return false; + if (negative) { + result = value == 0x80000000UL + ? static_cast(-2147483647L - 1L) + : -static_cast(value); + } else { + result = static_cast(value); + } + return true; +} + } // namespace cli } // namespace mesh diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 5f897cab..ad21d1bc 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -1,6 +1,7 @@ #include #include "CommonCLI.h" #include "CLICommandUtils.h" +#include "radiolib/RadioPowerLimits.h" #include "radiolib/LR2021SideDetectorConfig.h" #include "TxtDataHelpers.h" #include "AdvertDataHelpers.h" @@ -1305,7 +1306,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { memset(_prefs->extra_sf, 0, sizeof(_prefs->extra_sf)); _com_prefs_needs_upgrade = true; } - _prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, -9, 30); + _prefs->tx_power_dbm = mesh::clampLoRaTxPower( + _prefs->tx_power_dbm, _prefs->freq); _prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1); _prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f); _prefs->path_hash_mode = constrain(_prefs->path_hash_mode, 0, 2); // NOTE: mode 3 reserved for future @@ -3557,13 +3559,21 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->cr = cr; _prefs->freq = freq; _prefs->bw = bw; + const int8_t previous_power = _prefs->tx_power_dbm; + _prefs->tx_power_dbm = mesh::clampLoRaTxPower( + _prefs->tx_power_dbm, _prefs->freq); // Retune level-based RX powersaving to the new SF/BW. Persist only; the // radio itself is "reboot to apply", and begin() re-arms the timings then. recalcRxPowerSavingFromLevel( _prefs->rx_ps_level, _prefs->sf, _prefs->bw, _prefs->rx_ps_preamble, &_prefs->rx_ps_rx_us, &_prefs->rx_ps_sleep_us); // retune level-based timings to the loaded SF/BW _callbacks->savePrefs(); - strcpy(reply, "OK - reboot to apply"); + if (_prefs->tx_power_dbm != previous_power) { + sprintf(reply, "OK - reboot to apply; TX power limited to %d dBm", + (int)_prefs->tx_power_dbm); + } else { + strcpy(reply, "OK - reboot to apply"); + } appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw); } else { strcpy(reply, "Error, invalid radio params"); @@ -3974,17 +3984,36 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "OK"); } } else if (memcmp(config, "tx ", 3) == 0) { - _prefs->tx_power_dbm = atoi(&config[3]); - savePrefs(); - _callbacks->setTxPower(_prefs->tx_power_dbm); - strcpy(reply, "OK"); + int32_t power = 0; + const int8_t minimum = mesh::minLoRaTxPowerForFrequency(_prefs->freq); + const int8_t maximum = mesh::maxLoRaTxPowerForFrequency(_prefs->freq); + if (!mesh::cli::parseIntegerStrict(&config[3], power)) { + strcpy(reply, "Error: invalid TX power"); + } else if (!mesh::isLoRaTxPowerValid(power, _prefs->freq)) { + sprintf(reply, "Error: TX power range is %d to %d dBm", + (int)minimum, (int)maximum); + } else if (!_callbacks->setTxPower(static_cast(power))) { + strcpy(reply, "Error: radio busy or TX power rejected"); + } else { + _prefs->tx_power_dbm = static_cast(power); + savePrefs(); + strcpy(reply, "OK"); + } } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) { float freq = 0.0f; if (mesh::cli::parseDecimalStrict(&config[5], freq) && freq >= 150.0f && freq <= 2500.0f) { + const int8_t previous_power = _prefs->tx_power_dbm; _prefs->freq = freq; + _prefs->tx_power_dbm = mesh::clampLoRaTxPower( + _prefs->tx_power_dbm, _prefs->freq); savePrefs(); - strcpy(reply, "OK - reboot to apply"); + if (_prefs->tx_power_dbm != previous_power) { + sprintf(reply, "OK - reboot to apply; TX power limited to %d dBm", + (int)_prefs->tx_power_dbm); + } else { + strcpy(reply, "OK - reboot to apply"); + } } else { strcpy(reply, "Error, invalid frequency"); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 4838fed7..7a7cd61c 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -412,7 +412,7 @@ public: virtual void setLoggingOn(bool enable) = 0; virtual void eraseLogFile() = 0; virtual void dumpLogFile() = 0; - virtual void setTxPower(int8_t power_dbm) = 0; + virtual bool setTxPower(int8_t power_dbm) = 0; virtual void formatNeighborsReply(char *reply) = 0; virtual void removeNeighbor(const uint8_t* pubkey, int key_len) { // no op by default diff --git a/src/helpers/radiolib/CustomLLCC68.h b/src/helpers/radiolib/CustomLLCC68.h index 849e664c..09af680a 100644 --- a/src/helpers/radiolib/CustomLLCC68.h +++ b/src/helpers/radiolib/CustomLLCC68.h @@ -2,6 +2,10 @@ #include +#ifndef SX126X_TX_BUSY_TIMEOUT_MS +#define SX126X_TX_BUSY_TIMEOUT_MS 1000UL +#endif + class CustomLLCC68 : public LLCC68 { uint32_t _preambleMillis = 66; uint32_t _maxPayloadMillis = 3934; @@ -45,11 +49,6 @@ class CustomLLCC68 : public LLCC68 { #endif #endif int status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); - // if radio init fails with -707/-706, try again with tcxo voltage set to 0.0f - if (status == RADIOLIB_ERR_SPI_CMD_FAILED || status == RADIOLIB_ERR_SPI_CMD_INVALID) { - tcxo = 0.0f; - status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); - } if (status != RADIOLIB_ERR_NONE) { mesh::usbLoggingPort().print("ERROR: radio init failed: "); mesh::usbLoggingPort().println(status); @@ -80,6 +79,31 @@ class CustomLLCC68 : public LLCC68 { return true; // success } + int16_t launchMode() override { + if (this->stagedMode != RADIOLIB_RADIO_MODE_TX) { + return LLCC68::launchMode(); + } + + this->mod->setRfSwitchState(this->txMode); + int16_t state = this->setTx(RADIOLIB_SX126X_TX_TIMEOUT_NONE); + if (state != RADIOLIB_ERR_NONE) { + this->stagedMode = RADIOLIB_RADIO_MODE_NONE; + return state; + } + + const RadioLibTime_t started = this->mod->hal->millis(); + while (this->mod->hal->digitalRead(this->mod->getGpio())) { + this->mod->hal->yield(); + if (this->mod->hal->millis() - started >= SX126X_TX_BUSY_TIMEOUT_MS) { + this->stagedMode = RADIOLIB_RADIO_MODE_NONE; + return RADIOLIB_ERR_SPI_CMD_TIMEOUT; + } + } + + this->stagedMode = RADIOLIB_RADIO_MODE_NONE; + return RADIOLIB_ERR_NONE; + } + int16_t startReceive() override { return LLCC68::startReceive( RADIOLIB_SX126X_RX_TIMEOUT_INF, diff --git a/src/helpers/radiolib/CustomLLCC68Wrapper.h b/src/helpers/radiolib/CustomLLCC68Wrapper.h index 799b7fb9..6349fe5c 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -46,9 +46,10 @@ public: protected: bool radioDeepInit() override { - return ((CustomLLCC68 *)_radio)->std_init(); + if (!prepareRadioHardReset()) return false; + return ((CustomLLCC68 *)_radio)->std_init() && _board->finishRadioHardReset(); } - bool supportsRadioDeepInit() const override { return true; } + bool supportsRadioDeepInit() const override { return supportsRadioHardResetPath(); } protected: bool applyRxBoostedGainMode(bool en) override { diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 8ee64b71..78b892d0 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -131,9 +131,12 @@ protected: } bool radioDeepInit() override { - return _deep_init != NULL && _deep_init(); + return _deep_init != NULL && prepareRadioHardReset() && _deep_init() + && _board->finishRadioHardReset(); + } + bool supportsRadioDeepInit() const override { + return _deep_init != NULL && supportsRadioHardResetPath(); } - bool supportsRadioDeepInit() const override { return _deep_init != NULL; } public: uint8_t getSpreadingFactor() const override { return ((CustomLR1110 *)_radio)->getSpreadingFactor(); } diff --git a/src/helpers/radiolib/CustomLR2021.h b/src/helpers/radiolib/CustomLR2021.h index 1c014563..403a28c0 100644 --- a/src/helpers/radiolib/CustomLR2021.h +++ b/src/helpers/radiolib/CustomLR2021.h @@ -2,6 +2,11 @@ #include #include "MeshCore.h" +#include "LR2021Band.h" + +#ifndef LR2021_TX_BUSY_TIMEOUT_MS +#define LR2021_TX_BUSY_TIMEOUT_MS 1000UL +#endif class CustomLR2021 : public LR2021 { uint32_t _preambleMillis = 66; @@ -18,22 +23,28 @@ class CustomLR2021 : public LR2021 { } } + uint8_t rfSwitchRxMode() const { + return mesh::lr2021::isHighBand(freqMHz) ? LR2021::MODE_RX_HF : LR2021::MODE_RX; + } + + uint8_t rfSwitchTxMode() const { + return mesh::lr2021::isHighBand(freqMHz) ? LR2021::MODE_TX_HF : LR2021::MODE_TX; + } + public: CustomLR2021(Module *mod) : LR2021(mod) { irqDioNum = LR2021_IRQ_DIO; } - bool std_init(SPIClass* spi = NULL) - { - + bool std_init(SPIClass* spi = NULL) { #ifdef LR2021_TCXO_VOLTAGE - float tcxo = LR2021_TCXO_VOLTAGE; + const float tcxo = LR2021_TCXO_VOLTAGE; #else - float tcxo = 1.6f; + const float tcxo = 1.6f; #endif #ifdef LORA_CR - uint8_t cr = LORA_CR; + const uint8_t cr = LORA_CR; #else - uint8_t cr = 5; + const uint8_t cr = 5; #endif #if defined(P_LORA_SCLK) @@ -51,27 +62,25 @@ class CustomLR2021 : public LR2021 { if (spi) spi->begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); #endif #endif - int status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); - // if radio init fails with -707/-706, try again with tcxo voltage set to 0.0f - if (status == RADIOLIB_ERR_SPI_CMD_FAILED || status == RADIOLIB_ERR_SPI_CMD_INVALID) { - tcxo = 0.0f; - status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); - } + int16_t status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, + RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE, + LORA_TX_POWER, 16, tcxo); if (status != RADIOLIB_ERR_NONE) { mesh::usbLoggingPort().print("ERROR: radio init failed: "); mesh::usbLoggingPort().println(status); return false; // fail } - setCRC(2); - explicitHeader(); + status = setCRC(2); + if (status != RADIOLIB_ERR_NONE) return false; + status = explicitHeader(); + if (status != RADIOLIB_ERR_NONE) return false; #ifdef LR2021_RX_BOOSTED_GAIN setRxBoostedGainMode(LR2021_RX_BOOSTED_GAIN); #endif - // LR2021::begin() performs a hardware reset. Preserve the board-specific - // DIO/FEM routing across watchdog and SPI-timeout recovery resets. + // begin() resets the radio, including its DIO-controlled RF routing. restoreRfSwitchTable(); return true; // success @@ -108,6 +117,39 @@ class CustomLR2021 : public LR2021 { bool getRxBoostedGainMode() const { return _rx_boosted; } float getBandwidthKhz() const { return bandwidthKhz; } + int16_t launchMode() override { + const bool transmitting = this->stagedMode == RADIOLIB_RADIO_MODE_TX; + int16_t state; + if (this->stagedMode == RADIOLIB_RADIO_MODE_RX) { + this->mod->setRfSwitchState(rfSwitchRxMode()); + state = this->setRx(this->rxTimeout); + } else if (transmitting) { + this->mod->setRfSwitchState(rfSwitchTxMode()); + state = this->setTx(RADIOLIB_LR2021_TX_TIMEOUT_NONE); + } else { + return RADIOLIB_ERR_UNSUPPORTED; + } + + if (state != RADIOLIB_ERR_NONE) { + this->stagedMode = RADIOLIB_RADIO_MODE_NONE; + return state; + } + + if (transmitting) { + const RadioLibTime_t started = this->mod->hal->millis(); + while (this->mod->hal->digitalRead(this->mod->getGpio())) { + this->mod->hal->yield(); + if (this->mod->hal->millis() - started >= LR2021_TX_BUSY_TIMEOUT_MS) { + this->stagedMode = RADIOLIB_RADIO_MODE_NONE; + return RADIOLIB_ERR_SPI_CMD_TIMEOUT; + } + } + } + + this->stagedMode = RADIOLIB_RADIO_MODE_NONE; + return RADIOLIB_ERR_NONE; + } + bool isChipBusy() { uint32_t busy = this->mod->getGpio(); return busy != RADIOLIB_NC && this->mod->hal->digitalRead(busy); diff --git a/src/helpers/radiolib/CustomLR2021Wrapper.h b/src/helpers/radiolib/CustomLR2021Wrapper.h index 1e07f80b..46889176 100644 --- a/src/helpers/radiolib/CustomLR2021Wrapper.h +++ b/src/helpers/radiolib/CustomLR2021Wrapper.h @@ -8,6 +8,8 @@ #define USE_LR2021 #endif +#include "RadioPowerLimits.h" + #ifndef LR2021_RX_BOOST_LEVEL #define LR2021_RX_BOOST_LEVEL 7 #endif @@ -20,10 +22,18 @@ public: protected: bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) override { - bool success = ((CustomLR2021 *)_radio)->setFrequency(freq) == RADIOLIB_ERR_NONE - && ((CustomLR2021 *)_radio)->setSpreadingFactor(sf) == RADIOLIB_ERR_NONE - && ((CustomLR2021 *)_radio)->setBandwidth(bw) == RADIOLIB_ERR_NONE - && ((CustomLR2021 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE + CustomLR2021* radio = (CustomLR2021 *)_radio; + if (!_board->prepareRadioFrequency(freq)) return false; + const int8_t requested_power = _dbm_valid ? _cur_dbm : LORA_TX_POWER; + const int8_t effective_power = mesh::clampLoRaTxPower(requested_power, freq); + // LR2021 selects its LF/HF PA from the cached frequency. Reapply power + // immediately after every live retune so a band crossing cannot retain the + // previous PA configuration. + bool success = radio->setFrequency(freq) == RADIOLIB_ERR_NONE + && radio->setOutputPower(effective_power) == RADIOLIB_ERR_NONE + && radio->setSpreadingFactor(sf) == RADIOLIB_ERR_NONE + && radio->setBandwidth(bw) == RADIOLIB_ERR_NONE + && radio->setCodingRate(cr) == RADIOLIB_ERR_NONE && updatePreamble(sf, bw) && applySideDetectorConfig(sf, bw); if (!success) return false; @@ -34,6 +44,12 @@ protected: return true; } + int16_t applyCachedTxPower(int8_t dbm) override { + const float frequency = _params_valid + ? _cur_freq : ((CustomLR2021 *)_radio)->getFreqMHz(); + return _radio->setOutputPower(mesh::clampLoRaTxPower(dbm, frequency)); + } + public: bool setCodingRate(uint8_t cr) override { return ((CustomLR2021 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; @@ -192,9 +208,11 @@ protected: } bool radioDeepInit() override { - return ((CustomLR2021 *)_radio)->std_init(); + if (!prepareRadioHardReset()) return false; + if (!_board->prepareRadioFrequency(LORA_FREQ)) return false; + return ((CustomLR2021 *)_radio)->std_init() && _board->finishRadioHardReset(); } - bool supportsRadioDeepInit() const override { return true; } + bool supportsRadioDeepInit() const override { return supportsRadioHardResetPath(); } bool applyRxBoostedGainMode(bool en) override { return ((CustomLR2021 *)_radio)->setRxBoostedGainMode(en ? LR2021_RX_BOOST_LEVEL : 0) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index e96073d2..3a037801 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -91,12 +91,6 @@ class CustomSX1262 : public SX1262 { #endif #endif int status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo, useRegulatorLDO); - // if radio init fails with -707/-706, try again with tcxo voltage set to 0.0f - if (status == RADIOLIB_ERR_SPI_CMD_FAILED || status == RADIOLIB_ERR_SPI_CMD_INVALID) { - MESH_DEBUG_PRINTLN("SX1262 init failed with error %d, retrying with TCXO at 0.0V", status); - tcxo = 0.0f; - status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo, useRegulatorLDO); - } if (status != RADIOLIB_ERR_NONE) { mesh::usbLoggingPort().print("ERROR: radio init failed: "); mesh::usbLoggingPort().println(status); @@ -105,8 +99,10 @@ class CustomSX1262 : public SX1262 { // RadioLib's default DIO3 TCXO delay is 5 ms and its RX duty-cycle // command adds another 1 ms for sleep/wake transitions. If begin() - // fell back to a crystal, only the fixed 1 ms transition remains. - _rxDutyCycleTransitionUs = tcxo > 0.0f ? 6000UL : 1000UL; + // fell back to a crystal, only the fixed 1 ms transition remains. Use + // RadioLib's resolved oscillator mode: it only falls back after reading + // the SX126x XOSC_START_ERR device flag, not for an arbitrary SPI error. + _rxDutyCycleTransitionUs = this->tcxoVoltage > 0.0f ? 6000UL : 1000UL; setCRC(1); @@ -223,10 +219,10 @@ class CustomSX1262 : public SX1262 { return state; } - const unsigned long started = millis(); + const RadioLibTime_t started = this->mod->hal->millis(); while (isChipBusy()) { - yield(); - if (millis() - started >= SX126X_TX_BUSY_TIMEOUT_MS) { + this->mod->hal->yield(); + if (this->mod->hal->millis() - started >= SX126X_TX_BUSY_TIMEOUT_MS) { this->stagedMode = RADIOLIB_RADIO_MODE_NONE; return RADIOLIB_ERR_SPI_CMD_TIMEOUT; } diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 6a85f10f..bf517a41 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -150,8 +150,13 @@ protected: } bool radioDeepInit() override { - // std_init() re-runs RadioLib begin(), which starts with a hardware reset - // via NRST - the only way out of a hard-locked chip (BUSY stuck high). + // A board hook takes precedence over direct NRST because it may also need + // to recover a regulator, SPI bus, or I/O expander. With neither mechanism + // available, do not report a normal re-init as a successful hard reset. + if (!prepareRadioHardReset()) return false; + + // std_init() re-runs RadioLib begin(). It pulses a direct NRST itself; + // expander-reset boards enter here with the radio already reset. bool success = ((CustomSX1262 *)_radio)->std_init(); #if defined(TBEAM_1W) // std_init() restores RadioLib's default RXEN table. Reapply the T-Beam @@ -161,9 +166,11 @@ protected: _board->isLoRaFemLnaEnabled()); } #endif - return success; + return success && _board->finishRadioHardReset(); + } + bool supportsRadioDeepInit() const override { + return supportsRadioHardResetPath(); } - bool supportsRadioDeepInit() const override { return true; } protected: bool applyRxBoostedGainMode(bool en) override { diff --git a/src/helpers/radiolib/CustomSX1268.h b/src/helpers/radiolib/CustomSX1268.h index 849262c0..18fc8505 100644 --- a/src/helpers/radiolib/CustomSX1268.h +++ b/src/helpers/radiolib/CustomSX1268.h @@ -2,6 +2,10 @@ #include +#ifndef SX126X_TX_BUSY_TIMEOUT_MS +#define SX126X_TX_BUSY_TIMEOUT_MS 1000UL +#endif + class CustomSX1268 : public SX1268 { uint32_t _preambleMillis = 66; uint32_t _maxPayloadMillis = 3934; @@ -45,11 +49,6 @@ class CustomSX1268 : public SX1268 { #endif #endif int status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); - // if radio init fails with -707/-706, try again with tcxo voltage set to 0.0f - if (status == RADIOLIB_ERR_SPI_CMD_FAILED || status == RADIOLIB_ERR_SPI_CMD_INVALID) { - tcxo = 0.0f; - status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo); - } if (status != RADIOLIB_ERR_NONE) { mesh::usbLoggingPort().print("ERROR: radio init failed: "); mesh::usbLoggingPort().println(status); @@ -80,6 +79,31 @@ class CustomSX1268 : public SX1268 { return true; // success } + int16_t launchMode() override { + if (this->stagedMode != RADIOLIB_RADIO_MODE_TX) { + return SX1268::launchMode(); + } + + this->mod->setRfSwitchState(this->txMode); + int16_t state = this->setTx(RADIOLIB_SX126X_TX_TIMEOUT_NONE); + if (state != RADIOLIB_ERR_NONE) { + this->stagedMode = RADIOLIB_RADIO_MODE_NONE; + return state; + } + + const RadioLibTime_t started = this->mod->hal->millis(); + while (this->mod->hal->digitalRead(this->mod->getGpio())) { + this->mod->hal->yield(); + if (this->mod->hal->millis() - started >= SX126X_TX_BUSY_TIMEOUT_MS) { + this->stagedMode = RADIOLIB_RADIO_MODE_NONE; + return RADIOLIB_ERR_SPI_CMD_TIMEOUT; + } + } + + this->stagedMode = RADIOLIB_RADIO_MODE_NONE; + return RADIOLIB_ERR_NONE; + } + int16_t startReceive() override { return SX1268::startReceive( RADIOLIB_SX126X_RX_TIMEOUT_INF, diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index 09a3a2ff..0fd7ee3d 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -48,9 +48,10 @@ public: protected: bool radioDeepInit() override { - return ((CustomSX1268 *)_radio)->std_init(); + if (!prepareRadioHardReset()) return false; + return ((CustomSX1268 *)_radio)->std_init() && _board->finishRadioHardReset(); } - bool supportsRadioDeepInit() const override { return true; } + bool supportsRadioDeepInit() const override { return supportsRadioHardResetPath(); } protected: bool applyRxBoostedGainMode(bool en) override { diff --git a/src/helpers/radiolib/CustomSX1276Wrapper.h b/src/helpers/radiolib/CustomSX1276Wrapper.h index cdfc188b..7ab0ee2a 100644 --- a/src/helpers/radiolib/CustomSX1276Wrapper.h +++ b/src/helpers/radiolib/CustomSX1276Wrapper.h @@ -45,7 +45,8 @@ protected: } bool radioDeepInit() override { - return ((CustomSX1276 *)_radio)->std_init(); + if (!prepareRadioHardReset()) return false; + return ((CustomSX1276 *)_radio)->std_init() && _board->finishRadioHardReset(); } - bool supportsRadioDeepInit() const override { return true; } + bool supportsRadioDeepInit() const override { return supportsRadioHardResetPath(); } }; diff --git a/src/helpers/radiolib/LR2021Band.h b/src/helpers/radiolib/LR2021Band.h new file mode 100644 index 00000000..8006aa03 --- /dev/null +++ b/src/helpers/radiolib/LR2021Band.h @@ -0,0 +1,16 @@ +#pragma once + +namespace mesh { +namespace lr2021 { + +// RadioLib and the LR2021 use separate low- and high-frequency front ends. +// There are no valid channels near this boundary: LF ends at 1090 MHz and HF +// starts at 1900 MHz, so the midpoint is a stable band selector. +static constexpr float LF_HF_CROSSOVER_MHZ = 1500.0f; + +inline bool isHighBand(float frequency) { + return frequency > LF_HF_CROSSOVER_MHZ; +} + +} // namespace lr2021 +} // namespace mesh diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 9efe6157..cac7ca37 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -39,6 +39,22 @@ void RadioLibWrapper::begin() { #endif _radio->setPacketReceivedAction(setFlag); // this is also SentComplete interrupt _radio->setPreambleLength(currentPreambleLength()); // longer preamble for lower SF improves reliability +#if defined(LORA_FREQ) && defined(LORA_BW) && defined(LORA_SF) + if (!_params_valid) { + #ifdef LORA_CR + const uint8_t initial_cr = LORA_CR; + #else + const uint8_t initial_cr = 5; + #endif + cacheParams(LORA_FREQ, LORA_BW, LORA_SF, initial_cr); + } +#endif +#ifdef LORA_TX_POWER + if (!_dbm_valid) { + _cur_dbm = LORA_TX_POWER; + _dbm_valid = true; + } +#endif state = STATE_IDLE; if (_board->getStartupReason() == BD_STARTUP_RX_PACKET) { // received a LoRa packet (while in deep sleep) @@ -69,13 +85,20 @@ uint32_t RadioLibWrapper::getRngSeed() { return _radio->random(0x7FFFFFFF); } -void RadioLibWrapper::setTxPower(int8_t dbm) { - _cur_dbm = dbm; - _dbm_valid = true; -#if defined(USE_LR2021) - idle(); -#endif - _radio->setOutputPower(dbm); +bool RadioLibWrapper::setTxPower(int8_t dbm) { + const uint8_t resume_rx = beginReconfigure(); + if (resume_rx > 1) return false; + + const int16_t status = applyCachedTxPower(dbm); + if (status == RADIOLIB_ERR_NONE) { + _cur_dbm = dbm; + _dbm_valid = true; + } else { + MESH_DEBUG_PRINTLN("RadioLibWrapper: TX power %d rejected (%d)", + (int)dbm, (int)status); + } + endReconfigure(resume_rx); + return status == RADIOLIB_ERR_NONE; } uint8_t RadioLibWrapper::beginReconfigure() { @@ -117,7 +140,7 @@ bool RadioLibWrapper::restoreAfterDeepInit() { == RADIOLIB_ERR_NONE; } if (_dbm_valid) { - restored = _radio->setOutputPower(_cur_dbm) == RADIOLIB_ERR_NONE && restored; + restored = applyCachedTxPower(_cur_dbm) == RADIOLIB_ERR_NONE && restored; } if (_rx_boosted_gain_valid) { restored = applyRxBoostedGainMode(_cur_rx_boosted_gain) && restored; diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 408bac16..549853ae 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -78,6 +78,22 @@ protected: unsigned long _nf_calib_deadline; // abort window if the batch can't complete unsigned long _nf_sample_from; // no samples before this (RX entry settle) + static constexpr bool hasDirectRadioResetPin() { +#if defined(P_LORA_RESET) + return P_LORA_RESET != RADIOLIB_NC; +#else + return false; +#endif + } + bool prepareRadioHardReset() { + if (_board->supportsRadioHardReset()) return _board->resetRadio(); + // A direct NRST is pulsed by the RadioLib begin() call that follows. + return hasDirectRadioResetPin(); + } + bool supportsRadioHardResetPath() const { + return hasDirectRadioResetPin() || _board->supportsRadioHardReset(); + } + void idle() override; void startRecv() override; void rxPsWatchdogCheck(); @@ -107,6 +123,9 @@ protected: virtual bool radioDeepInit() { return false; } virtual bool supportsRadioDeepInit() const { return false; } virtual bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) = 0; + virtual int16_t applyCachedTxPower(int8_t dbm) { + return _radio->setOutputPower(dbm); + } virtual bool applyRxBoostedGainMode(bool) { return false; } // 0 = reconfigure from idle, 1 = resume RX afterwards, 2 = currently busy. uint8_t beginReconfigure(); @@ -165,7 +184,7 @@ public: bool setParams(float freq, float bw, uint8_t sf, uint8_t cr, const uint32_t* rx_ps_timings = NULL); uint32_t getRngSeed(); - void setTxPower(int8_t dbm); + bool setTxPower(int8_t dbm); virtual float getCurrentRSSI() =0; virtual uint8_t getSpreadingFactor() const { return LORA_SF; } diff --git a/src/helpers/radiolib/RadioPowerLimits.h b/src/helpers/radiolib/RadioPowerLimits.h new file mode 100644 index 00000000..c640ebb4 --- /dev/null +++ b/src/helpers/radiolib/RadioPowerLimits.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include "LR2021Band.h" + +// Radio families do not share a universal power range. Keep unspecified +// bounds at the persisted int8_t limits and let the active driver validate the +// request. Boards with an external PA can still impose a stricter input cap. + +namespace mesh { + +inline int8_t minLoRaTxPowerForFrequency(float frequency) { +#if defined(USE_LR2021) + int8_t minimum = lr2021::isHighBand(frequency) ? -19 : -9; +#else + (void)frequency; + int8_t minimum = INT8_MIN; +#endif +#ifdef MIN_LORA_TX_POWER + if (minimum < MIN_LORA_TX_POWER) minimum = MIN_LORA_TX_POWER; +#endif + return minimum; +} + +inline int8_t maxLoRaTxPowerForFrequency(float frequency) { +#if defined(USE_LR2021) + int8_t maximum = lr2021::isHighBand(frequency) ? 12 : 22; +#else + (void)frequency; + int8_t maximum = INT8_MAX; +#endif +#ifdef MAX_LORA_TX_POWER + if (maximum > MAX_LORA_TX_POWER) maximum = MAX_LORA_TX_POWER; +#endif + return maximum; +} + +inline bool isLoRaTxPowerValid(int32_t power, float frequency) { + return power >= minLoRaTxPowerForFrequency(frequency) + && power <= maxLoRaTxPowerForFrequency(frequency); +} + +inline int8_t clampLoRaTxPower(int32_t power, float frequency) { + const int8_t minimum = minLoRaTxPowerForFrequency(frequency); + const int8_t maximum = maxLoRaTxPowerForFrequency(frequency); + if (power < minimum) return minimum; + if (power > maximum) return maximum; + return static_cast(power); +} + +} // namespace mesh diff --git a/src/helpers/sim/SimRadio.h b/src/helpers/sim/SimRadio.h index 44047bd8..9c5f495a 100644 --- a/src/helpers/sim/SimRadio.h +++ b/src/helpers/sim/SimRadio.h @@ -75,7 +75,7 @@ public: == mesh::RadioParamApplyResult::APPLIED; } void powerOff() {} - void setTxPower(int8_t /*dbm*/) {} + bool setTxPower(int8_t /*dbm*/) { return true; } uint32_t getRngSeed() { return _simRandom(); } uint32_t getPacketsRecv() const { return n_recv; } uint32_t getPacketsSent() const { return n_sent; } diff --git a/test/test_cli_command_utils/test_cli_command_utils.cpp b/test/test_cli_command_utils/test_cli_command_utils.cpp index 95f3d272..b97b688a 100644 --- a/test/test_cli_command_utils/test_cli_command_utils.cpp +++ b/test/test_cli_command_utils/test_cli_command_utils.cpp @@ -5,6 +5,7 @@ #include #include #include +#include TEST(ContactListOrder, SortsFavoritesBeforeNewerNonFavorites) { EXPECT_LT(mesh::compareContactListOrder(0x01, 100, 0x00, 200), 0); @@ -385,6 +386,43 @@ TEST(CLICommandUtils, RejectsNonDecimalOrOverflowValues) { EXPECT_FLOAT_EQ(123.0f, value); } +TEST(CLICommandUtils, ParsesSignedIntegersStrictly) { + int32_t value = 0; + + EXPECT_TRUE(mesh::cli::parseIntegerStrict("22", value)); + EXPECT_EQ(22, value); + EXPECT_TRUE(mesh::cli::parseIntegerStrict(" -9 ", value)); + EXPECT_EQ(-9, value); + EXPECT_TRUE(mesh::cli::parseIntegerStrict("+12", value)); + EXPECT_EQ(12, value); + EXPECT_TRUE(mesh::cli::parseIntegerStrict("-2147483648", value)); + EXPECT_EQ(INT32_MIN, value); + EXPECT_TRUE(mesh::cli::parseIntegerStrict("2147483647", value)); + EXPECT_EQ(INT32_MAX, value); +} + +TEST(CLICommandUtils, RejectsInvalidOrOverflowingIntegers) { + int32_t value = 123; + + EXPECT_FALSE(mesh::cli::parseIntegerStrict(nullptr, value)); + EXPECT_FALSE(mesh::cli::parseIntegerStrict("", value)); + EXPECT_FALSE(mesh::cli::parseIntegerStrict("-", value)); + EXPECT_FALSE(mesh::cli::parseIntegerStrict("1.0", value)); + EXPECT_FALSE(mesh::cli::parseIntegerStrict("12dBm", value)); + EXPECT_FALSE(mesh::cli::parseIntegerStrict("2147483648", value)); + EXPECT_FALSE(mesh::cli::parseIntegerStrict("-2147483649", value)); + EXPECT_EQ(123, value); +} + +TEST(RadioPowerLimits, LeavesUnspecifiedBackendRangeToDriver) { + EXPECT_EQ(INT8_MIN, mesh::minLoRaTxPowerForFrequency(915.0f)); + EXPECT_EQ(INT8_MAX, mesh::maxLoRaTxPowerForFrequency(915.0f)); + EXPECT_TRUE(mesh::isLoRaTxPowerValid(INT8_MIN, 915.0f)); + EXPECT_TRUE(mesh::isLoRaTxPowerValid(INT8_MAX, 915.0f)); + EXPECT_FALSE(mesh::isLoRaTxPowerValid(128, 915.0f)); + EXPECT_EQ(INT8_MAX, mesh::clampLoRaTxPower(200, 915.0f)); +} + TEST(CLICommandUtils, ClassifiesStandaloneWiFiGetKeysExactly) { using mesh::cli::StandaloneWiFiKey; diff --git a/test/test_radio_liveness/test_radio_liveness.cpp b/test/test_radio_liveness/test_radio_liveness.cpp index 43ab9b99..9140b9ba 100644 --- a/test/test_radio_liveness/test_radio_liveness.cpp +++ b/test/test_radio_liveness/test_radio_liveness.cpp @@ -1,6 +1,7 @@ #include #include +#include #include using mesh::RadioLivenessTracker; @@ -55,6 +56,13 @@ TEST(RadioLivenessTracker, ElapsedTimeIsRolloverSafe) { EXPECT_EQ(tracker.poll(0x00000100UL, 512, 4096), RadioRecoveryAction::SOFT); } +TEST(LR2021Band, SelectsTheCorrectFrontEnd) { + EXPECT_FALSE(mesh::lr2021::isHighBand(1090.0f)); + EXPECT_FALSE(mesh::lr2021::isHighBand(1500.0f)); + EXPECT_TRUE(mesh::lr2021::isHighBand(1900.0f)); + EXPECT_TRUE(mesh::lr2021::isHighBand(2400.0f)); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/variants/ebyte_eora_s3/platformio.ini b/variants/ebyte_eora_s3/platformio.ini index 1bbcdad5..3853a819 100644 --- a/variants/ebyte_eora_s3/platformio.ini +++ b/variants/ebyte_eora_s3/platformio.ini @@ -10,7 +10,7 @@ build_flags = -D EBYTE_EORA_S3 -D P_LORA_DIO_1=33 -D P_LORA_NSS=7 - -D P_LORA_RESET=8 ; RADIOLIB_NC + -D P_LORA_RESET=8 -D P_LORA_BUSY=34 -D P_LORA_SCLK=5 -D P_LORA_MISO=3 @@ -31,7 +31,8 @@ build_flags = -D PIN_BOARD_SCL=17 -D SX126X_DIO2_AS_RF_SWITCH=true - -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + ; The onboard E22 module uses a passive 32 MHz crystal, not a DIO3 TCXO. + -D SX126X_DIO3_TCXO_VOLTAGE=0.0 -D SX126X_CURRENT_LIMIT=140 -D USE_SX1262 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/generic-e22/platformio.ini b/variants/generic-e22/platformio.ini index e869eef8..4df25bcf 100644 --- a/variants/generic-e22/platformio.ini +++ b/variants/generic-e22/platformio.ini @@ -9,7 +9,7 @@ build_flags = -D PIN_VBAT_READ=35 -D P_LORA_DIO_1=33 -D P_LORA_NSS=18 - -D P_LORA_RESET=RADIOLIB_NC + -D P_LORA_RESET=23 -D P_LORA_BUSY=32 -D P_LORA_SCLK=5 -D P_LORA_MOSI=27 @@ -18,7 +18,6 @@ build_flags = -D SX126X_RXEN=14 -D PIN_BOARD_SDA=21 -D PIN_BOARD_SCL=22 - -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 build_src_filter = ${esp32_base.build_src_filter} diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index 2ea6b1a9..97634487 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -10,7 +10,7 @@ build_flags = -D ESP32_CPU_FREQ=80 -D P_LORA_DIO_1=14 -D P_LORA_NSS=8 - -D P_LORA_RESET=RADIOLIB_NC + -D P_LORA_RESET=12 -D P_LORA_BUSY=13 -D P_LORA_SCLK=9 -D P_LORA_MISO=11 diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 21207e44..bb15548c 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -10,7 +10,7 @@ build_flags = -D ESP32_CPU_FREQ=80 -D P_LORA_DIO_1=14 -D P_LORA_NSS=8 - -D P_LORA_RESET=RADIOLIB_NC + -D P_LORA_RESET=12 -D P_LORA_BUSY=13 -D P_LORA_SCLK=9 -D P_LORA_MISO=11 diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index 9a33ea68..9e9c5ec1 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -8,7 +8,7 @@ build_flags = ;-D ARDUINO_USB_CDC_ON_BOOT=1 ; this breaks Serial -D P_LORA_DIO_1=14 -D P_LORA_NSS=8 - -D P_LORA_RESET=RADIOLIB_NC + -D P_LORA_RESET=12 -D P_LORA_BUSY=13 -D P_LORA_SCLK=9 -D P_LORA_MISO=11 diff --git a/variants/lilygo_t_impulse_plus/platformio.ini b/variants/lilygo_t_impulse_plus/platformio.ini index 5a9b51bb..4c982c78 100644 --- a/variants/lilygo_t_impulse_plus/platformio.ini +++ b/variants/lilygo_t_impulse_plus/platformio.ini @@ -10,6 +10,8 @@ build_flags = ${nrf52_base.build_flags} -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 + ; LilyGo's S62F hardware notes require DIO3 to supply its TCXO at 3.0 V. + -D SX126X_DIO3_TCXO_VOLTAGE=3.0 -D SX126X_RX_BOOSTED_GAIN=1 -D PIN_USER_BTN=-1 ; -D PIN_USER_BTN=TTP223_KEY ; leave button disabled until screen implemented diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.h b/variants/lilygo_tbeam_1w/TBeam1WBoard.h index ef20bb09..84dd3bca 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.h +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.h @@ -20,7 +20,7 @@ class CustomSX1262Wrapper; // // Control signals: // - LDO_EN (GPIO 40): HIGH enables LDO -> powers SX1262 + LNA -// - TCXO_EN (DIO3): HIGH enables TCXO (set to 1.8V per Meshtastic) +// - TCXO_EN (DIO3): 3.0 V (XY16P35 specifies 2.85-3.15 V) // - CTL (GPIO 21): HIGH=RX (LNA on), LOW=TX (LNA off) // - DIO2: AUTO via SX126X_DIO2_AS_RF_SWITCH (TX path) // @@ -44,6 +44,11 @@ public: const char* getManufacturerName() const override; void powerOff() override; void powerCycleRadio(); + bool supportsRadioHardReset() const override { return true; } + bool resetRadio() override { + powerCycleRadio(); + return true; + } void attachRadioDriver(CustomSX1262Wrapper* driver); bool setLoRaFemLnaEnabled(bool enable) override; diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index 3e4c84a4..4d7409ad 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -6,14 +6,16 @@ build_flags = -I variants/lilygo_tbeam_supreme_SX1262 -D TBEAM_SUPREME_SX1262 -D BOARD_HAS_PSRAM=1 - -D P_LORA_DIO_0=26 - -D P_LORA_DIO_1=33 - -D P_LORA_NSS=18 - -D P_LORA_RESET=23 - -D P_LORA_SCLK=5 - -D P_LORA_MISO=19 - -D P_LORA_MOSI=27 + -D P_LORA_DIO_1=1 + -D P_LORA_NSS=10 + -D P_LORA_RESET=5 + -D P_LORA_BUSY=4 + -D P_LORA_SCLK=12 + -D P_LORA_MISO=13 + -D P_LORA_MOSI=11 -D SX126X_CURRENT_LIMIT=140 + ; LilyGo's module is DIO3-controlled and specifies a 1.8 V TCXO supply. + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_RX_BOOSTED_GAIN=1 -D USE_SX1262 -D RADIO_CLASS=CustomSX1262 @@ -22,7 +24,6 @@ build_flags = -D DISPLAY_ADDRESS=0x3D -D DISPLAY_ADDRESS_ALT=0x3C -D LORA_TX_POWER=22 - -D P_LORA_TX_LED=6 -D PIN_BOARD_SDA=17 -D PIN_BOARD_SCL=18 -D PIN_GPS_RX=8 diff --git a/variants/meshnology_w12/MeshnologyW12Board.cpp b/variants/meshnology_w12/MeshnologyW12Board.cpp index e63ba5e1..17b5189a 100644 --- a/variants/meshnology_w12/MeshnologyW12Board.cpp +++ b/variants/meshnology_w12/MeshnologyW12Board.cpp @@ -9,12 +9,10 @@ void MeshnologyW12Board::begin() { digitalWrite(PIN_ADC_CTRL, LOW); // Disable battery sense until required pinMode(P_LORA_LF_PA_POWER, OUTPUT); - digitalWrite(P_LORA_LF_PA_POWER, HIGH); // PA_EN_M — power the GC1109 FEM - pinMode(P_LORA_HF_PA_POWER, OUTPUT); - digitalWrite(P_LORA_HF_PA_POWER, LOW); // PA_EN_G — disable the 2.4GHz FEM - - delay(100); // give the GC1109 some time to start + digitalWrite(P_LORA_LF_PA_POWER, LOW); + digitalWrite(P_LORA_HF_PA_POWER, LOW); + prepareRadioFrequency(LORA_FREQ); periph_power.begin(); esp_reset_reason_t reason = esp_reset_reason(); @@ -37,6 +35,23 @@ void MeshnologyW12Board::begin() { neopixelWrite(NEOPIXEL_LED, 0, 0, 0); // turn TX neopixel off } + bool MeshnologyW12Board::prepareRadioFrequency(float frequency) { + const bool high_band = mesh::lr2021::isHighBand(frequency); + if (radio_band_initialized && high_band == radio_high_band) return true; + + // The W12 schematic connects PA_EN_M (GPIO4) to the GC1109 sub-GHz rail + // and PA_EN_G (GPIO3) to the RFX2402E 2.4 GHz rail. Keep only the selected + // FEM powered, and disable the old path before enabling the new one. + digitalWrite(P_LORA_LF_PA_POWER, LOW); + digitalWrite(P_LORA_HF_PA_POWER, LOW); + digitalWrite(high_band ? P_LORA_HF_PA_POWER : P_LORA_LF_PA_POWER, HIGH); + delay(100); + + radio_high_band = high_band; + radio_band_initialized = true; + return true; + } + void MeshnologyW12Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); diff --git a/variants/meshnology_w12/MeshnologyW12Board.h b/variants/meshnology_w12/MeshnologyW12Board.h index 85bec990..bf87ca11 100644 --- a/variants/meshnology_w12/MeshnologyW12Board.h +++ b/variants/meshnology_w12/MeshnologyW12Board.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #ifndef ADC_MULTIPLIER @@ -13,6 +14,8 @@ class MeshnologyW12Board : public ESP32Board { protected: float adc_mult = ADC_MULTIPLIER; + bool radio_band_initialized = false; + bool radio_high_band = false; public: RefCountedDigitalPin periph_power; @@ -23,6 +26,7 @@ public: void onAfterTransmit(void) override; void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); void powerOff() override; + bool prepareRadioFrequency(float frequency) override; uint16_t getBattMilliVolts() override; bool setAdcMultiplier(float multiplier) override { if (multiplier == 0.0f) { diff --git a/variants/meshnology_w12/platformio.ini b/variants/meshnology_w12/platformio.ini index 69ee04a2..6423c49b 100644 --- a/variants/meshnology_w12/platformio.ini +++ b/variants/meshnology_w12/platformio.ini @@ -21,7 +21,7 @@ build_flags = -D P_LORA_RESET=12 -D P_LORA_BUSY=13 -D LR2021_TCXO_VOLTAGE=0.0f ; this board has no tcxo - -D RF_SWITCH_TABLE ; used below in commented code for the RF switch table. + -D RF_SWITCH_TABLE ; LR2021 DIO-controlled GC1109/RFX2402E routing -D P_LORA_LF_PA_POWER=4 ; PA_EN_M - power enable for the GC1109 868/915 PA -D P_LORA_HF_PA_POWER=3 ; PA_EN_G - power enable for RFX2402E 2.4G PA -D PIN_USER_BTN=0 diff --git a/variants/minewsemi_me25ls01/target.cpp b/variants/minewsemi_me25ls01/target.cpp index 54680b13..58b48d9d 100644 --- a/variants/minewsemi_me25ls01/target.cpp +++ b/variants/minewsemi_me25ls01/target.cpp @@ -27,25 +27,25 @@ extern EnvironmentSensorManager sensors; #endif #ifdef RF_SWITCH_TABLE -static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { - RADIOLIB_LR11X0_DIO5, - RADIOLIB_LR11X0_DIO6, - RADIOLIB_LR11X0_DIO7, - RADIOLIB_LR11X0_DIO8, - RADIOLIB_NC -}; - -static const Module::RfSwitchMode_t rfswitch_table[] = { - // mode DIO5 DIO6 DIO7 DIO8 - { LR11x0::MODE_STBY, {LOW, LOW, LOW, LOW }}, - { LR11x0::MODE_RX, {HIGH, LOW, LOW, HIGH }}, - { LR11x0::MODE_TX, {HIGH, HIGH, LOW, HIGH }}, - { LR11x0::MODE_TX_HP, {LOW, HIGH, LOW, HIGH }}, - { LR11x0::MODE_TX_HF, {LOW, LOW, LOW, LOW }}, - { LR11x0::MODE_GNSS, {LOW, LOW, HIGH, LOW }}, - { LR11x0::MODE_WIFI, {LOW, LOW, LOW, LOW }}, - END_OF_MODE_TABLE, -}; +static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { + RADIOLIB_LR11X0_DIO5, + RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC, + RADIOLIB_NC, + RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, {LOW, LOW }}, + { LR11x0::MODE_RX, {HIGH, LOW }}, + { LR11x0::MODE_TX, {HIGH, HIGH}}, + { LR11x0::MODE_TX_HP, {LOW, HIGH}}, + { LR11x0::MODE_TX_HF, {LOW, LOW }}, + { LR11x0::MODE_GNSS, {LOW, LOW }}, + { LR11x0::MODE_WIFI, {LOW, LOW }}, + END_OF_MODE_TABLE, +}; #endif bool radio_init() { diff --git a/variants/muziworks_r1_neo/variant.h b/variants/muziworks_r1_neo/variant.h index 92046d61..9d89910c 100644 --- a/variants/muziworks_r1_neo/variant.h +++ b/variants/muziworks_r1_neo/variant.h @@ -54,7 +54,7 @@ extern "C" #define P_LORA_DIO_1 (47) // P1.15 (SX) #define P_LORA_NSS (42) // P1.10 (SX) -#define P_LORA_RESET RADIOLIB_NC // P1.06 (SX) -- 38 +#define P_LORA_RESET (38) // P1.06 (SX) #define P_LORA_BUSY (46) // P1.14 (SX) #define P_LORA_SCLK (43) // P1.11 (SX) #define P_LORA_MISO (45) // P1.13 (SX) diff --git a/variants/rak3401/RAK3401Board.cpp b/variants/rak3401/RAK3401Board.cpp index 7010a533..4da55ece 100644 --- a/variants/rak3401/RAK3401Board.cpp +++ b/variants/rak3401/RAK3401Board.cpp @@ -1,5 +1,4 @@ #include -#include #include #include "RAK3401Board.h" @@ -51,71 +50,43 @@ void RAK3401Board::begin() { checkBootVoltage(&power_config); #endif - // 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. + // WB_IO2 controls the shared 3V3_S peripheral rail and the RAK13302 boost + // converter. Keep it enabled during normal operation. Cycling this rail as + // part of radio recovery also resets fitted GPS and sensor modules and does + // not provide a reliable reset of the SX1262 core. + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); + + // CSD and CPS are tied together on the RAK13302 and routed to IO3. Keep the + // FEM quiescent until the radio probe succeeds; enableRadioFrontend() then + // restores it for normal RX/TX operation. 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. + // Quiesce the FEM, leave the shared peripheral rail and SPI controller + // alone, and pulse the SX1262's dedicated NRST pin. BUSY must fall after a + // physical reset before any SPI command is safe. Bounding that wait keeps a + // missing or disconnected RAK13302 from trapping RadioLib in initialization. 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; + pinMode(P_LORA_RESET, OUTPUT); + digitalWrite(P_LORA_RESET, LOW); + // SX126x requires NRST low for at least 100 us. Keep the pulse explicit so + // recovery does not accidentally depend on how long GPIO setup happens to + // take in a particular Arduino core or optimization level. + delay(1); + digitalWrite(P_LORA_RESET, HIGH); - // 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(); + const uint32_t reset_started = millis(); while (digitalRead(P_LORA_BUSY) == HIGH) { - if (millis() - wake_started >= 1000) return false; + serviceWatchdog(); + if (millis() - reset_started >= 100UL) return false; delay(1); } return true; diff --git a/variants/rak3401/RAK3401Board.h b/variants/rak3401/RAK3401Board.h index e74ce847..940a265f 100644 --- a/variants/rak3401/RAK3401Board.h +++ b/variants/rak3401/RAK3401Board.h @@ -13,7 +13,6 @@ class RAK3401Board : public NRF52BoardDCDC { protected: - bool radio_spi_initialized = false; #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif @@ -22,6 +21,12 @@ public: void begin(); bool recoverRadio(); void enableRadioFrontend(); + bool supportsRadioHardReset() const override { return true; } + bool resetRadio() override { return recoverRadio(); } + bool finishRadioHardReset() override { + enableRadioFrontend(); + return true; + } #define BATTERY_SAMPLES 8 diff --git a/variants/rak3401/target.cpp b/variants/rak3401/target.cpp index 0e8d4551..f3346431 100644 --- a/variants/rak3401/target.cpp +++ b/variants/rak3401/target.cpp @@ -20,9 +20,9 @@ RAK3401Board board; #endif #endif -// 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. +// The RAK13302 is wired to the second WisBlock SPI controller. Keep the same +// controller instance for startup and watchdog recovery; resetting the radio +// must not replace or tear down the SPI object used by its Module. 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); @@ -41,29 +41,30 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); 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. + rtc_clock.begin(Wire); + // A successful physical reset proves that the external SX1262 can release + // BUSY before RadioLib is allowed to issue SPI commands. This also prevents + // a missing RAK13302 from turning an initialization retry into a watchdog + // reset loop. if (!board.recoverRadio()) { target_radio_available = false; + mesh::usbLoggingPort().println( + "ERROR: RAK13302 BUSY stayed high after reset; check module seating and power"); return false; } - rtc_clock.begin(Wire); 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. + // Retry once after another physical reset. Keep the shared GPS/sensor rail + // and the SPI controller instance intact throughout recovery. if (!board.recoverRadio()) { target_radio_available = false; return false; } - rtc_clock.begin(Wire); if (!radio.std_init(&SPI1)) { target_radio_available = false; return false; diff --git a/variants/rak3401/variant.cpp b/variants/rak3401/variant.cpp index 17e619ef..9301ecba 100644 --- a/variants/rak3401/variant.cpp +++ b/variants/rak3401/variant.cpp @@ -48,13 +48,12 @@ void initVariant() // 3V3 Power Rail pinMode(PIN_3V3_EN, OUTPUT); - // 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); + // This is a shared peripheral rail as well as the RAK13302 boost supply. + // Keep it on from early startup and never cycle it for radio-only recovery. + digitalWrite(PIN_3V3_EN, HIGH); - // Keep the RAK13302 FEM disabled while its auxiliary rail is off. + // Keep the RAK13302 FEM disabled until board startup has completed its + // voltage check and explicitly enables the frontend. pinMode(SX126X_POWER_EN, OUTPUT); digitalWrite(SX126X_POWER_EN, LOW); } diff --git a/variants/tenstar_c3/platformio.ini b/variants/tenstar_c3/platformio.ini index 54b764f4..20178d83 100644 --- a/variants/tenstar_c3/platformio.ini +++ b/variants/tenstar_c3/platformio.ini @@ -16,6 +16,9 @@ build_flags = -D P_LORA_MOSI=7 -D P_LORA_DIO_1=2 -D P_LORA_NSS=6 + ; This is a user-wired radio, and the base schematic has no SX126x NRST + ; connection. Override P_LORA_RESET with the wired GPIO for unattended hard + ; recovery; the default cannot clear a genuine BUSY lock without one. -D P_LORA_RESET=RADIOLIB_NC -D P_LORA_BUSY=3 ; -D PIN_BOARD_SDA=18 diff --git a/variants/wio_wm1110/platformio.ini b/variants/wio_wm1110/platformio.ini index c061aa3d..5fbee0ee 100644 --- a/variants/wio_wm1110/platformio.ini +++ b/variants/wio_wm1110/platformio.ini @@ -23,7 +23,8 @@ build_flags = ${nrf52_base.build_flags} -D P_LORA_MOSI=46 -D P_LORA_MISO=47 -D LR11X0_DIO_AS_RF_SWITCH=true - -D LR11X0_DIO3_TCXO_VOLTAGE=1.8 + ; The WM1110 module uses a 1.6 V DIO3-controlled TCXO supply. + -D LR11X0_DIO3_TCXO_VOLTAGE=1.6 -D RF_SWITCH_TABLE -D ENV_INCLUDE_GPS=0 build_src_filter = ${nrf52_base.build_src_filter} diff --git a/variants/wio_wm1110/target.cpp b/variants/wio_wm1110/target.cpp index eba4f7b4..764a86ad 100644 --- a/variants/wio_wm1110/target.cpp +++ b/variants/wio_wm1110/target.cpp @@ -16,25 +16,25 @@ EnvironmentSensorManager sensors; #endif #ifdef RF_SWITCH_TABLE -static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { - RADIOLIB_LR11X0_DIO5, - RADIOLIB_LR11X0_DIO6, - RADIOLIB_LR11X0_DIO7, - RADIOLIB_LR11X0_DIO8, - RADIOLIB_NC -}; - -static const Module::RfSwitchMode_t rfswitch_table[] = { - // mode DIO5 DIO6 DIO7 DIO8 - { LR11x0::MODE_STBY, {LOW, LOW, LOW, LOW }}, - { LR11x0::MODE_RX, {HIGH, LOW, LOW, HIGH }}, - { LR11x0::MODE_TX, {HIGH, HIGH, LOW, HIGH }}, - { LR11x0::MODE_TX_HP, {LOW, HIGH, LOW, HIGH }}, - { LR11x0::MODE_TX_HF, {LOW, LOW, LOW, LOW }}, - { LR11x0::MODE_GNSS, {LOW, LOW, HIGH, LOW }}, - { LR11x0::MODE_WIFI, {LOW, LOW, LOW, LOW }}, - END_OF_MODE_TABLE, -}; +static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { + RADIOLIB_LR11X0_DIO5, + RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC, + RADIOLIB_NC, + RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, {LOW, LOW }}, + { LR11x0::MODE_RX, {HIGH, LOW }}, + { LR11x0::MODE_TX, {HIGH, HIGH}}, + { LR11x0::MODE_TX_HP, {LOW, HIGH}}, + { LR11x0::MODE_TX_HF, {LOW, LOW }}, + { LR11x0::MODE_GNSS, {LOW, LOW }}, + { LR11x0::MODE_WIFI, {LOW, LOW }}, + END_OF_MODE_TABLE, +}; #endif bool radio_init() { diff --git a/variants/wio_wm1110/variant.h b/variants/wio_wm1110/variant.h index cc72c328..d2c4f9ac 100644 --- a/variants/wio_wm1110/variant.h +++ b/variants/wio_wm1110/variant.h @@ -101,7 +101,7 @@ // LR1110 specific settings #define LR11X0_DIO_AS_RF_SWITCH true -#define LR11X0_DIO3_TCXO_VOLTAGE 1.8 +#define LR11X0_DIO3_TCXO_VOLTAGE 1.6 #define LR1110_GNSS_ANT_PIN (37) // P1.5 // Pin aliases for LR1110 driver compatibility